Перенос очередей сообщений и потоковых платформ из виртуальных машин EC2 в кластеры Kubernetes через оператор Strimzi обычно преследует понятные эксплуатационные цели: декларативное описание брокеров через Custom Resources, автоматизированные обновления и надежное самовосстановление. Однако на практике миграция stateful-сервисов с интенсивным дисковым вводом-выводом нередко вскрывает скрытые системные конфликты: на сопоставимом оборудовании брокеры начинают непрерывно вычитывать данные с физических накопителей, а задержки обработки сообщений лавинообразно растут.
Природа проблемы: чтение с диска как сигнал деградации
Для Apache Kafka чтение с блочного накопителя — это не штатная метрика утилизации диска, а прямой индикатор деградации архитектурного пути исполнения. Kafka спроектирована без собственного тяжелого кэша в памяти JVM: брокер всецело полагается на страничный кэш операционной системы (page cache). Когда продюсеры пишут в хвост журнала (head of the log), а консьюмеры читают данные с минимальным отставанием, сообщения должны отдаваться потребителям напрямую из оперативной памяти ядра через системный вызов sendfile.
В штатном режиме дисковая подсистема брокера занята последовательной записью и фоновым сбросом измененных страниц на диск. Если потребители читают данные вблизи актуального смещения, объем операций дискового чтения должен стремиться к нулю. Появление непрерывного потока дисковых чтений свидетельствует о том, что горячие сегменты журнала преждевременно вытесняются из памяти. Обращение к физическому накопителю вместо мгновенного доступа к RAM приводит к непредсказуемым всплескам задержек (latency spikes) и снижению пропускной способности кластера.
Первичная диагностика: исключение прикладных факторов
При расследовании деградации первым делом проверяются очевидные прикладные гипотезы:
- Отставание потребителей (consumer lag). Если консьюмеры вычитывают старые смещения, обращение к диску неизбежно. Проверка метрик лагов показала полное отсутствие отставания: потребители шли вплотную за свежими записями.
- Конфигурация Kafka. Настройки сжатия, сегментов журнала и брокеров были перенесены без изменений.
- Аппаратные ресурсы. Процессоры, объемы RAM и типы накопителей новой инфраструктуры полностью соответствовали старым инстансам.
Ключевые различия лежали ниже прикладного уровня: вместо Ubuntu 20.04 с cgroup v1 кластер EKS на базе Amazon Linux 2023 работал под управлением cgroup v2.
Системная трассировка: разница между periodic и vmscan
Для выяснения причин сброса страниц в память ядра потребовалось профилирование с помощью bpftrace. Скрипт writeback.bt фиксирует точки входа в подсистему сброса страниц и причину каждой операции.

В исходной среде EC2 подавляющее большинство событий сброса классифицировалось как плановые (periodic) или фоновые (background). Страницы находились в памяти достаточно долго и сбрасывались на накопитель по таймеру или мягким порогам грязных данных.
В Kubernetes под cgroup v2 операции сброса маркировались причиной vmscan. Наличие vmscan доказывает, что ядро находится в режиме активного вытеснения страниц из-за нехватки памяти (reclaim-driven writeback). Операционная система принудительно очищала кэш, вынуждая брокер каждый раз заново считывать данные с накопителя.
Корневая причина: изоляция памяти под cgroup v2
Механизм cgroup v2 изменил логику учета памяти. В первой версии учет анонимных страниц и кэша файлов часто приводил к неравномерному давлению, но ядро отталкивалось от глобального состояния памяти узла. Во второй версии иерархия стала строгой.
Когда для пода задается лимит оперативной памяти (resources.limits.memory), он транслируется в лимит контрольной группы memory.max, а при приближении к нему активируется порог memory.high. Под управлением cgroup v2 ядро агрессивно вытесняет страничный кэш контейнера при достижении внутреннего порога, даже если на физическом сервере свободны десятки гигабайт оперативной памяти.
При этом классическая оптимизация параметров ядра vm.dirty_ratio и vm.dirty_background_ratio не устраняет проблему: они регулируют фоновый сброс грязных данных, но бессильны против локального давления внутри контрольной группы.
Двухэтапное устранение деградации
Для восстановления производительности потребовалось пересмотреть подход к изоляции ресурсов и скорректировать параметры виртуальной памяти узлов.
1. Отказ от жестких лимитов памяти на выделенных нодах
Первым шагом стал отказ от директивы limits.memory в спецификации подов Kafka при изоляции брокеров на выделенных узлах Kubernetes (через nodeSelector или taints/tolerations).
Когда брокер работает с гарантированным запросом памяти (requests.memory), покрывающим кучу JVM и базовые нужды, но не ограничен жесткой планкой limits, ядро Linux получает возможность утилизировать всю свободную оперативную память ноды под страничный кэш. Локальный барьер cgroup v2 перестает провоцировать преждевременный vmscan.
Снятие лимитов памяти допустимо исключительно на выделенных серверах, где брокер является единственным крупным потребителем ресурсов. На общих мультитенантных узлах отказ от лимитов может привести к вытеснению соседних подов при исчерпании глобальной памяти.
2. Корректировка порога vm.min_free_kbytes
Вторым шагом стала калибровка параметра vm.min_free_kbytes. Он задает резерв свободной памяти, на основе которого рассчитываются водяные знаки для пробуждения демона kswapd.
По умолчанию во многих дистрибутивах этот параметр составляет около 64 мегабайт (67584 Кб). При интенсивном потоке дисковой записи резерв исчерпывается мгновенно, и ядро переходит в режим прямой очистки (direct reclaim), блокируя потоки ввода-вывода. Постепенное увеличение vm.min_free_kbytes на ноде (до ~2,5 Гб в конкретном стенде) позволило демону kswapd активироваться раньше и освобождать память плавно.
Итоги и чек-лист диагностики
После применения изменений дисковые чтения снизились практически до нуля, нормализованный load average упал на 50%, а задержки стабилизировались на уровне прежних виртуальных машин.
Чек-лист диагностики дискоемких сервисов в Kubernetes
- Исключите прикладное отставание консьюмеров (consumer lag).
- Оцените сигналы вытеснения через счетчики PSI и метрики
memory.statcgroup v2. - Используйте
bpftraceдля разделения фонового сброса (background) и вытеснения (vmscan). - Изолируйте брокеры на выделенных узлах и снимите искусственные лимиты
memory.max. - Откалибруйте порог
vm.min_free_kbytesна тестовом контуре перед обновлением конфигураций узлов.
Переход на cgroup v2 делает изоляцию памяти предсказуемой для обычных сервисов, но дискоемкие stateful-нагрузки требуют тонкого согласования лимитов контейнеров с механизмами управления кэшем ядра.
