Восемь минут простоя из-за мёртвого 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 (с менеджером безопасности) |
Бесконечно ( |
Закешировала мёртвые IP NLB до перезапуска |
Go net resolver |
Соблюдает TTL записи |
Повторно разрезолвил примерно через 60 с |
Node 18 |
Соблюдает TTL записи |
Повторно разрезолвил примерно через 60 с |
Python |
Кеширования на уровне библиотеки нет |
Разрезолвил заново при обновлении пула соединений |
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 и понаблюдаем за повторной резолюцией в каждом рантайме. Лучше обнаружить вечное кеширование в два часа дня во вторник, чем во время реальной миграции.