Сервисы в Kubernetes отвечали за 5 секунд ровно. Виноват оказался один бит в ядре Linux

Есть особый тип продовых проблем: они не воспроизводятся, не коррелируют ни с чем и выглядят по-разному в каждом сервисе. У нас такая жила месяца три, пока мы не докопались до причины. Расскажу, как выглядел путь — вдруг кому-то сэкономит эти три месяца.

Симптомы

Микросервисы в кластере периодически отвечали медленно. Не «медленно вообще», а очень характерно: обычное время ответа — 30–50 мс, а иногда — 5 секунд с копейками. Или 2,5 секунды. Почти никогда 1,3 или 3,7 — именно круглые 2,5 и 5.

Частота — доли процента запросов. На дашбордах это выглядело как противный длинный хвост в p99, на который все давно махнули рукой: «ну сеть, ну бывает».

Разные команды объясняли это по-разному. Одни грешили на GC в джаве. Другие — на «шумных соседей» на нодах. Третьи молча подняли таймауты и ретраи, и у них «починилось».

Первый круг: виноват кто угодно

Начали с очевидного. CPU throttling? Проверили — есть немного, но троттлящиеся поды и поды с пятисекундными ответами не совпадают. Перегруз нод? Нет. Проблемный инстанс приложения? Воспроизводится на всех сервисах, на всех языках — джава, го, питон. Это, кстати, был первый полезный факт: когда что-то воспроизводится на всех рантаймах сразу, приложение можно вычёркивать. Остаётся то, что у них общее — ядро и сеть.

Второй полезный факт нашёлся в трейсах: пять секунд терялись не внутри обработки запроса, а до неё. Сервис ещё не начал ничего делать, а время уже ушло.

Второй круг: круглые числа — это улика

Круглые задержки — почти всегда чей-то таймаут. Осталось найти чей. 5 секунд — это дефолтный таймаут DNS-резолвера в glibc (options timeout в resolv.conf). 2,5 — половина. Всё, что «тормозило», на самом деле ждало DNS.

Дальше tcpdump на ноде, фильтр по 53 порту, и картина проявилась: под отправляет DNS-запрос — и для части запросов ответ просто не приходит. Не медленно приходит, а не приходит вообще. Резолвер ждёт свои 5 секунд, шлёт запрос повторно, второй раз всё отрабатывает за миллисекунды.

То есть никакой «медленной сети» не было. Была сеть, которая изредка молча съедает первый UDP-пакет.

Третий круг: кто ест пакеты

А вот дальше начинается то, ради чего я это пишу. Пакеты ел conntrack — механизм ядра, который отслеживает соединения и без которого не работает SNAT (а в кубере через SNAT ходит наружу почти всё).

Механика такая. Резолвер в glibc по умолчанию делает два DNS-запроса параллельно — A и AAAA — с одного сокета, то есть с одинаковой четвёркой src/dst адресов и портов. Оба пакета почти одновременно влетают в netfilter, оба не находят существующей записи conntrack, и ядро создаёт для каждого свою запись-«заготовку». А вставить в таблицу можно только одну — вторая вставка конфликтует, и её пакет молча дропается. Гонка. Insert race, известная в узких кругах как «та самая проблема пятисекундного DNS в Kubernetes».

Никаких ошибок при этом нет нигде: ни в логах приложения, ни у CNI, ни в CoreDNS. Единственный след — счётчик insert_failed в conntrack -S на ноде. У нас он тикал. Месяцами. Просто никто не знал, что туда надо смотреть.

Чем лечится

Вариантов несколько, у всех свои трейдофы:

Самый системный — NodeLocal DNSCache: на каждой ноде поднимается локальный кеш DNS, поды ходят в него без SNAT и без conntrack для UDP, гонка исчезает по построению. Мы в итоге пришли к этому.

Быстрый костыль — опция single-request-reopen в dnsConfig пода: glibc перестаёт слать A и AAAA параллельно с одного сокета. Гонку не убирает, но резко снижает вероятность. Не работает в musl (Alpine), что добавляет веселья в зоопарке образов.

Ну и радикальный путь — DNS по TCP. Работает, но нагрузка на CoreDNS и латентность растут.

Что я из этого вынес

Во-первых, «сеть иногда тупит» — это не диагноз, а отговорка. За каждым таким «иногда» есть конкретный механизм, просто до него долго копать.

Во-вторых, самые злые проблемы живут на стыках. Ни приложение, ни CNI, ни CoreDNS по отдельности не виноваты — виновато их сочетание с дефолтами glibc и поведением conntrack. Такие вещи не находятся, пока в команде нет человека, который понимает, как пакет на самом деле ходит от пода наружу.

В-третьих, conntrack -S должен быть в мониторинге. Это одна метрика, а страданий она снимает на месяцы вперёд.

Такие разборы — как что-то в Linux и Kubernetes работает под капотом и почему оно ломается именно так — я регулярно пишу в телеграм-канале @rootcause_devops. Если тема заходит — забегайте.

А если у вас в проде есть свой красивый хвост в p99, на который все махнули рукой — расскажите в комментариях, почти уверен, что там тоже что-то интересное.

4