8 минут простоя: JVM кешировала DNS вечно

Восемь минут простоя из-за мёртвого NLB и JVM, кешировавшей DNS вечно

Краткое содержание: мы слили (drain) сетевой балансировщик нагрузки (Network Load Balancer, NLB) во время плановой миграции, а один внутренний сервис продолжал долбить мёртвые IP-адреса ещё 8 минут. Причиной оказалась не сама процедура переключения, а JVM, кешировавшая DNS-запись бесконечно. Исправление уложилось в два параметра — TTL в 30 секунд и небольшая правка таймаута дерегистрации, — а не в перепроектирование системы.

В прошлом месяце я проводила рутинную миграцию одного из наших внутренних сервисов управляющего уровня (control-plane) в Buildkite. Суть задачи: поставить сервис за новый NLB, слить старый, закончить до обеда. Нас в платформенной команде четверо, и это должно было пройти незаметно.

Не прошло.

Когда всё выглядело нормально

Я перевела запись Route 53 на новый NLB, дождалась, пока новые цели (targets) перейдут в состояние healthy, и запустила дренаж старого балансировщика. Трафик на новом пути рос. Трафик на старом… не падал. Один сервис — Java-планировщик, раздающий метаданные сборок, — продолжал ломиться на старые IP-адреса NLB как ни в чём не бывало.

Старые цели уже дерегистрировались. В итоге запросы попадали на бэкенды в процессе завершения работы, зависали на 10 секунд каждый, повторялись, снова зависали. Уровень ошибок этого сервиса подскочил с фактического нуля до 60%. И держался так 8 минут.

Самое раздражающее: все остальные сервисы переключились примерно за 60 секунд. Go-сервисы, Node-воркеры — всё в порядке. Застрял только JVM-сервис.

Почему один рантайм повёл себя иначе

Вот что никто в команде не замечал раньше. Поведение DNS-кеша по умолчанию кардинально отличается в зависимости от того, кто делает запрос.

Рантайм TTL DNS-кеша по умолчанию Что произошло на практике

JVM (без менеджера безопасности)

30 с

Обычно работает нормально

JVM (с менеджером безопасности)

Бесконечно (networkaddress.cache.ttl=-1)

Закешировала мёртвые IP NLB до перезапуска

Go net resolver

Соблюдает TTL записи

Повторно разрезолвил примерно через 60 с

Node 18

Соблюдает TTL записи

Повторно разрезолвил примерно через 60 с

Python requests

Кеширования на уровне библиотеки нет

Разрезолвил заново при обновлении пула соединений

NLB отдаёт IP-адреса за DNS-именем, и эти адреса меняются при перемещении целей. Договорённость такая: клиент повторно резолвит имя и следует актуальной записи. JVM с активным менеджером безопасности устанавливает networkaddress.cache.ttl в -1, что означает: кешировать первый ответ на весь срок жизни процесса. Наш планировщик разрезолвил имя один раз при старте — три недели назад — и больше не спрашивал.

DNS-запись имела TTL в 60 секунд. Это не имело значения: JVM повторно к DNS так и не обратилась.

Исправление

Две строки в конфигурации безопасности JVM, добавленные в базовый образ:

# $JAVA_HOME/conf/security/java.security
networkaddress.cache.ttl=30
networkaddress.cache.negative.ttl=5

Тридцать секунд кеширования и пять секунд для отрицательных ответов (negative lookups) — чтобы временный NXDOMAIN тоже не залипал. Теперь это вшито в базовый образ, и каждый JVM-сервис получает настройку автоматически. Код приложений не менялся.

Вторая часть проблемы: даже при повторной резолюции старый NLB дренировался слишком медленно. Задержка дерегистрации (deregistration delay) по умолчанию — 300 секунд. При плановом переключении это целая вечность, в течение которой полумёртвые цели продолжают принимать соединения. Для этого сервиса мы её уменьшили:

aws elbv2 modify-target-group-attributes \
--target-group-arn "$TG_ARN" \
--attributes Key=deregistration_delay.timeout_seconds,Value=30

Короткий TTL в сочетании с коротким дренажем — и следующее тестовое переключение завершилось менее чем за 90 секунд на всех рантаймах. Никакого всплеска ошибок.

При чём тут LLM

Инцидент особенно задел ещё и потому, что тот же планировщик запускает шаги сборки, которые обращаются к языковой модели (LLM) для классификации нестабильных тестов (flaky-test classification). Эти вызовы идут через AI-шлюз — в нашем случае Bifrost, — который сам занимается переключением между провайдерами и повторной резолюцией на стороне апстрима. Так что этот путь оставался работоспособным всё время. Что поначалу делало происходящее вдвойне запутанным: та часть, которую все считали хрупкой (вызовы модели), держалась уверенно, а горел скучный внутренний HTTP-вызов. Хорошее напоминание: экзотическая зависимость — не всегда та, что укусит.

Компромиссы и ограничения

Короткий DNS TTL — не бесплатный, и притворяться, что это не так, я не буду.

Больше DNS-запросов. Снижение кеша JVM до 30 секунд означает примерно в 120 раз больше резолюций в час на хост по сравнению со старым поведением «разрезолвить один раз». Для нас это шум на фоне Route 53-резолвера, но если вы выполняете тысячи резолюций в секунду — стоит убедиться, что сам резолвер не стал новым узким местом.

30 секунд — это всё равно 30 секунд. Такой TTL сокращает окно переключения, но не устраняет его. Для по-настоящему горячего переключения нужны проверки работоспособности на уровне соединений и активный дренаж, а не только DNS. Мы рассматриваем DNS как грубый регулятор, а задержку дерегистрации — как тонкий.

Компромисс отрицательного кеширования. TTL отрицательных ответов в 5 секунд означает, что кратковременный сбой DNS быстро повторяется, но также — что реально упавшее имя запрашивается агрессивнее. Для внутренних сервисов это приемлемо; для чего-то с rate-limiting стоит подумать дважды.

Это зависит от рантайма. Единого рубильника нет. Исправление для JVM не влияет на Go-бинарник, а поведение Go по умолчанию здесь ни в чём не виновато. Нужно знать особенности каждого рантайма — именно этого пробела нам и не хватало.

Что я сказала бы себе тогда

Тестируй переключение клиентов, а не систему переключения. У нас был аккуратный runbook для смены NLB и никаких тестов того, что клиенты делают в момент смены. «У нас никогда не было проблем с DNS» просто означало, что мы никогда не сливали балансировщик под наблюдением JVM.

На следующем учебном дне (game day) мы намеренно убьём NLB и понаблюдаем за повторной резолюцией в каждом рантайме. Лучше обнаружить вечное кеширование в два часа дня во вторник, чем во время реальной миграции.

© 2026 meganuke