Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи? Написать
Дайджесты новостей
Изометрическая сцена серверного центра мониторинга: визуализация сетевых потоков и карты соединений eBPF conntrack в ядре Linux с индикацией тайм-аутов и узких мест маршрутизации в кластере Kubernetes

Inside Cilium CNI: устранение 80-секундных задержек в Kubernetes из-за таблицы conntrack

Переход на сетевые плагины на базе технологии eBPF принято считать надежным способом ускорить сеть в Kubernetes по сравнению с классическим iptables. Однако на высоконагруженных узлах с большим объемом оперативной памяти скрытые алгоритмические особенности могут приводить к масштабным отказам. Инженерная команда компании Adyen столкнулась с ситуацией, когда серверные узлы со 128 ядрами внезапно перестали разворачивать новые контейнеры: плагин Cilium CNI завершал работу с ошибкой тайм-аута API, блокируя сетевую инициализацию подов на десятки секунд.

Загадочные тайм-ауты на мощных узлах

Adyen эксплуатирует более сотни кластеров Kubernetes. Сбой проявился на узлах платформы больших данных, изолированных от контура обработки платежей в реальном времени, поэтому инцидент устранили до нарушения соглашений об уровне обслуживания (SLO).

Симптомы выглядели нетипично. На аналитических узлах под управлением Kubernetes 1.31.7 и Cilium 1.16.5, оснащенных 64 ядрами с 512 ГБ памяти и 128 ядрами с 2 ТБ памяти, создание новых подов зависало в состоянии ContainerCreating. Локальный агент узла kubelet после 90 секунд ожидания прерывал настройку с ошибкой Cilium API client timeout exceeded. При этом процессоры оставались незагруженными, память была свободна, а логи демона Cilium не фиксировали падений.

Жизненный цикл пода в архитектуре Cilium

Интерфейс CNI (Container Network Interface) связывает агент kubelet с сетевым плагином. Cilium использует подсистему eBPF (Extended Berkeley Packet Filter) — виртуальную машину внутри ядра Linux, позволяющую исполнять программы маршрутизации и фильтрации сетевых пакетов на уровне ядра без изменения его исходного кода.

Инициализация сети для каждого пода включает цепочку шагов:

  1. Выделение IP-адреса через встроенный модуль IPAM.
  2. Создание пары виртуальных интерфейсов (veth) между сетевым пространством контейнера и хостом.
  3. Обращение к агенту Cilium Agent (DaemonSet на каждом узле).
  4. Расчет правил безопасности и компиляция программ eBPF для изоляции контейнера.
  5. Очистка устаревших сетевых сессий через процедуру scrubIPsInConntrackTable.

Именно последний шаг стал источником задержек. Подсистема conntrack (Connection Tracking) ядра Linux отслеживает состояние всех активных TCP- и UDP-потоков. Когда контейнер завершает работу, а его IP-адрес переназначается новому поду, Cilium обязан удалить из conntrack записи старого владельца, чтобы исключить коллизии и попадание чужих пакетов новому сервису.

Профилирование рантайма: плотный цикл системных вызовов

Стандартные профили процессора и памяти не выявили аномалий. Причину показала трассировка горутин профилировщиком pprof: десятки потоков Cilium Agent исполняли плотный цикл системных вызовов ядра, поочередно вызывая bpf_map_get_next_key и bpf_map_lookup_elem.

Масштаб нагрузки на системную шину оказался исключительным:

  • За 35 миллисекунд демон совершал 14 916 системных вызовов к ядру, то есть около 426 000 вызовов в секунду.
  • Поскольку для проверки записи требуется два системных вызова (получение ключа и чтение элемента), скорость линейного обхода составляла примерно 213 000 записей в секунду.

Этот обход выполнялся последовательно в пространстве пользователя (userspace). Каждый вызов сопровождался переключением контекста процессора (context switch) между userspace и ядром. Поскольку следующий ключ вычисляется только после получения текущего, распараллелить операцию невозможно.

Ловушка динамических карт и адаптивного сборщика мусора

Причиной разрастания таблицы стала комбинация конфигурации памяти и специфики аналитической нагрузки. Параметр Helm cilium_bpf_map_dynamic_size_ratio: 0.0050 выделяет под структуры eBPF фиксированную долю оперативной памяти хоста. На серверах с 2 ТБ памяти максимальный лимит таблицы TCP conntrack автоматически вырос до 16 777 216 записей.

Команда cilium bpf ct list global показала наличие около 7 миллионов записей, большинство из которых были просроченными (expired). На узлах выполнялись задачи движка Trino и пакетные джобы Apache Spark. Один под Trino при чтении из сотен узлов HDFS создавал в пике до 50 000 соединений в минуту.

Периодический сборщик мусора (GC) Cilium не успевал их удалять из-за адаптивного интервала:

  • Базовый интервал запуска GC равен 5 минутам.
  • Если удалено более 25% записей (maxDeleteRatio > 0.25), интервал уменьшается.
  • Если удалено менее 5% записей (maxDeleteRatio < 0.05), интервал умножается на 1.5.
  • Максимальный лимит интервала (ConntrackGCMaxLRUInterval) составляет 12 часов.

На 16-миллионной таблице для преодоления порога в 5% требовалось удалить сразу более 800 000 записей. На спокойном сервере доля удалений падала ниже порога, интервал GC быстро растягивался до 12 часов, и узел встречал всплеск нагрузки с миллионами накопившихся просроченных записей.

Математика задержек и каскадная блокировка

Расчет времени обхода подтвердил причину тайм-аутов:

  • 7 000 000 записей при скорости 213 000 записей/с требуют около 33–35 секунд.
  • 16 777 216 записей требуют около 80 секунд непрерывного сканирования.

Штатный тайм-аут CNI со стороны kubelet составляет 90 секунд. Одиночный обход укладывался в норматив, но процедура scrubIPsInConntrackTable защищена глобальным мьютексом (mutex).

При одновременном запуске пачки из 50 подов первый под захватывал мьютекс на 35–80 секунд. Второй под вставал в очередь, но его 90-секундный таймер в kubelet уже тикал. Получив мьютекс через 40 секунд, второй под исчерпывал лимит времени до завершения собственного обхода.

Kubelet завершал настройку с ошибкой, а среда containerd инициировала удаление пода (DeleteEndpoint), которое для очистки ресурсов также требовало захвата того же мьютекса. Kubelet повторял попытку создания через 60–90 секунд. Новые запросы поступали быстрее, чем завершался обход, блокируя создание сети на узле.

ПараметрСтандартный узелАналитический узел Adyen
Ресурсы16 ядер, 64 ГБ RAM128 ядер, 2 ТБ RAM
Лимит TCP conntrack~262 000 записей16 777 216 записей
Сетевой потокДесятки запросов/сДо 50 000 соединений/мин (Trino)
Время обхода BPF-картыДо 1–2 секундОт 35 до 80 секунд
Риск конкуренции за мьютексНизкийКаскадный коллапс очереди CNI

Важно разделять два механизма: точечная очистка пода (scrubIPs) ищет записи только своего IP и не удаляет чужой мусор, но вынуждена перебирать всю таблицу. Если периодический сборщик спит часами, точечная очистка перегружается устаревшими записями.

Диагностика и инженерные выводы

Для обнаружения подобных скрытых деградаций инженеры Adyen выделили следующий порядок проверки:

  1. Проверка состояния агента через cilium status --verbose и размера таблиц командой cilium bpf ct list global.
  2. Мониторинг метрик сборщика мусора: cilium_datapath_conntrack_gc_duration_seconds (длительность) и cilium_datapath_conntrack_gc_entries (число записей).
  3. Контроль статуса подов: зависание в состоянии regenerating в выводе kubectl get ciliumendpoint.
  4. Снятие профилей горутин через pprof во время пикового создания подов.

В качестве оперативной меры команда зафиксировала параметр conntrackGCInterval: 60s в Helm. Это отключило адаптивный дрейф к 12 часам и принудило сборщик мусора очищать записи каждую минуту, сократив время обхода до долей секунды.

Главный вывод расследования: аппаратные мощности сервера не нивелируют алгоритмическую сложность O(n) на границе системных вызовов. Масштабирование емкости таблиц ядра всегда требует учета времени их полного перебора.