Настройка swap в Kubernetes: параметры ядра Linux

Введение в подкачку Linux

Функция NodeSwap в Kubernetes, которая, вероятно, получит статус стабильной в предстоящем релизе Kubernetes v1.34, разрешает использование подкачки (swap) — это существенный отход от привычной практики отключения свопа ради предсказуемой производительности. Статья целиком посвящена настройке подкачки на Linux-узлах, где эта возможность доступна. Разрешая узлам использовать вторичное хранилище в качестве дополнительной виртуальной памяти при нехватке физической RAM, поддержка свопа на уровне узла стремится повысить утилизацию ресурсов и снизить количество принудительных завершений процессов из-за нехватки памяти (OOM kill).

Однако включение подкачки — не «включи и забудь». Производительность и стабильность узлов под давлением памяти критически зависят от ряда параметров ядра Linux. Неправильная конфигурация способна привести к деградации производительности и нарушить логику вытеснения (eviction) Kubelet.

В этой статье я подробно разберу ключевые параметры ядра Linux, управляющие поведением подкачки. Я покажу, как они влияют на производительность рабочих нагрузок Kubernetes, на использование swap и на механизмы вытеснения. Я представлю результаты различных тестов, демонстрирующих эффект разных конфигураций, и поделюсь выводами о том, как добиться оптимальных настроек для стабильного и высокопроизводительного кластера Kubernetes.

Введение в подкачку Linux

На высоком уровне ядро Linux управляет памятью через страницы (pages), размер которых обычно составляет 4 КиБ. Когда физическая память оказывается под давлением, алгоритм замещения страниц ядра решает, какие страницы перенести в пространство подкачки. Хотя точная логика представляет собой сложную оптимизацию, на этот процесс принятия решений влияют несколько ключевых факторов:

  • Паттерны обращения к страницам (насколько недавно к ним обращались)

  • Грязность страниц (были ли они изменены)

  • Давление памяти (насколько остро системе нужна свободная память)

Анонимная и файловая память

Важно понимать, что не все страницы памяти одинаковы. Ядро различает анонимную (anonymous) и файловую (file-backed) память.

Анонимная память: это память, не привязанная к конкретному файлу на диске, — например, куча (heap) и стек (stack) программы. С точки зрения приложения это приватная память, и когда ядру нужно освободить такие страницы, оно вынуждено записать их в специальное устройство подкачки.

Файловая память: эта память привязана к файлу на файловой системе. Сюда относятся исполняемый код программы, разделяемые библиотеки и кэш файловой системы. Когда ядру нужно освободить такие страницы, оно может просто выбросить их, если они не были изменены («чистые»). Если страница была изменена («грязная»), ядро сначала должно записать изменения обратно в файл, и лишь затем страница может быть выброшена.

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

Ключевые параметры ядра для настройки подкачки

Для эффективной настройки подкачки Linux предоставляет несколько параметров ядра, управляемых через sysctl.

vm.swappiness

Самый известный параметр. Это значение от 0 до 200 (до 100 в старых ядрах), которое определяет склонность ядра к выгрузке анонимных страниц памяти в своп в сравнении с освобождением файловой памяти (page cache).
Высокое значение (например, 90+): ядро агрессивно выгружает редко используемую анонимную память, освобождая место для файлового кэша.
Низкое значение (например, < 10): ядро предпочитает удалять страницы файлового кэша вместо выгрузки анонимной памяти в своп.

vm.min_free_kbytes

Этот параметр указывает ядру держать минимальный объём памяти свободным в качестве буфера. Когда объём свободной памяти опускается ниже этого порога безопасности, ядро начинает более агрессивно освобождать страницы (выгружать в своп и в итоге обрабатывать OOM kill).
Функция: параметр работает как предохранительный клапан, гарантирующий ядру достаточно памяти для критических запросов на выделение, которые нельзя отложить.
Влияние на своп: чем выше min_free_kbytes, тем раньше ядро начинает использовать подкачку при нехватке памяти.

vm.watermark_scale_factor

Этот параметр управляет зазором между отметками уровня памяти (watermarks): min, low и high, которые вычисляются на основе min_free_kbytes.
Что означают отметки:
low: когда свободная память опускается ниже этой отметки, фоновый процесс ядра kswapd просыпается и начинает освобождать страницы в фоне. Именно здесь начинается цикл подкачки.
min: когда свободная память достигает этого минимального уровня, агрессивное освобождение страниц начинает блокировать выделение памяти процессами. Если освободить страницы не удаётся, происходят OOM kill.
high: освобождение памяти прекращается, как только свободная память достигает этого уровня.
Влияние: более высокое значение watermark_scale_factor создаёт больший буфер между отметками low и min, давая kswapd больше времени на постепенное освобождение памяти до того, как система достигнет критического состояния.

В типичной серверной нагрузке может существовать долгоживущий процесс с «холодной» памятью. Более высокое значение swappiness позволяет освободить RAM, выгрузив эту холодную память в своп, в пользу других активных процессов, которым выгодно держать файловый кэш.

Настройка min_free_kbytes и watermark_scale_factor для того, чтобы сдвинуть окно подкачки в более ранний момент, даёт kswapd больше пространства для разгрузки памяти на диск и предотвращает OOM kill при внезапных всплесках потребления памяти.

Тесты и результаты подкачки

Чтобы понять реальное влияние этих параметров, я разработал серию нагрузочных тестов.

Конфигурация тестового стенда

Среда: GKE в Google Cloud
Версия Kubernetes: 1.33.2
Конфигурация узла: n2-standard-2 (8 ГиБ RAM, 50 ГБ swap на диске pd-balanced без шифрования), Ubuntu 22.04
Нагрузка: собственное Go-приложение, настроенное на выделение памяти с заданной скоростью, создание давления файлового кэша и имитацию разных паттернов обращения к памяти (случайный и последовательный).
Мониторинг: sidecar-контейнер, снимающий метрики системы каждую секунду.
Защита: критически важные системные компоненты (kubelet, container runtime, sshd) были защищены от подкачки путём установки memory.swap.max=0 в соответствующих cgroup.

Методология тестирования

Я запускал нагрузочный pod на узлах с различными значениями swappiness (0, 60 и 90) и варьировал параметры min_free_kbytes и watermark_scale_factor, наблюдая за результатами при интенсивном выделении памяти и нагрузке на ввод-вывод.

Визуализация работы подкачки

График ниже получен при нагрузочном тесте со скоростью 100 МБ/с и наглядно показывает работу подкачки. По мере уменьшения свободной памяти (в блоке «Memory Usage») растут использование свопа (Swap Used (GiB)) и активность выгрузки (Swap Out (MiB/s)). Важно отметить: по мере того как система всё больше полагается на своп, активность ввода-вывода и соответствующее время ожидания (IO Wait % в блоке «CPU Usage») также растут, что свидетельствует о нагрузке на CPU.

График, показывающий утилизацию CPU, памяти, свопа и активность ввода-вывода на узле Kubernetes

Результаты

Первоначальные тесты с параметрами ядра по умолчанию (swappiness=60, min_free_kbytes=68MB, watermark_scale_factor=10) быстро привели к OOM kill и даже неожиданным перезагрузкам узла под высокой нагрузкой по памяти. При выборе подходящих параметров ядра удалось добиться хорошего баланса между стабильностью и производительностью узла.

Влияние параметра swappiness

Параметр swappiness напрямую определяет выбор ядра между освобождением анонимной памяти (выгрузкой в своп) и удалением страниц файлового кэша. Чтобы это наблюдать, я провёл тест, в котором один pod генерировал и удерживал давление файлового кэша, а затем второй pod начинал выделять анонимную память со скоростью 100 МБ/с — чтобы посмотреть, что предпочтёт освобождать ядро.

Результаты демонстрируют явный компромисс:

swappiness=90: ядро превентивно выгружало неактивную анонимную память, чтобы сохранить файловый кэш. Это привело к высокому и устойчивому использованию свопа и значительной активности ввода-вывода («Blocks Out»), что в свою очередь вызывало всплески времени ожидания ввода-вывода на CPU.

swappiness=0: ядро предпочитало удалять страницы файлового кэша, откладывая расход свопа. Однако важно понимать, что это не отключает подкачку. При высоком давлении памяти ядро всё равно выгружало анонимную память на диск.

Выбор зависит от характера нагрузки. Для рабочих нагрузок, чувствительных к задержкам ввода-вывода, предпочтителен низкий swappiness. Для нагрузок, активно использующих большой файловый кэш, может быть полезен более высокий swappiness — при условии, что диск достаточно быстрый для такой нагрузки.

Настройка watermark для предотвращения вытеснений и OOM kill

Наиболее критичная проблема, с которой я столкнулся, — взаимодействие между быстрым выделением памяти и механизмом вытеснения Kubelet. Когда мой тестовый pod, намеренно настроенный на overcommit памяти, выделял её с высокой скоростью (например, 300–500 МБ/с), система быстро исчерпывала свободную память.

При watermark по умолчанию буфер для освобождения памяти оказался слишком мал. До того как kswapd успевал освободить достаточно памяти через подкачку, узел достигал критического состояния, что приводило к одному из двух исходов:

Вытеснение Kubelet: если менеджер вытеснения kubelet обнаруживал, что memory.available опустилась ниже порогового значения, он вытеснял pod.

OOM Killer: в некоторых сценариях с высокой скоростью выделения памяти OOM Killer срабатывал раньше, чем успевало завершиться вытеснение, иногда завершая более приоритетные pod’ы, которые не были источником давления.

Для решения этой проблемы я настроил watermark:

  • Увеличил min_free_kbytes до 512 МиБ: это заставляет ядро начинать освобождать память значительно раньше, обеспечивая больший буфер безопасности.

  • Увеличил watermark_scale_factor до 2000: это расширило зазор между отметками low и high (с примерно 337 МБ до примерно 591 МБ в /proc/zoneinfo моего тестового узла), фактически увеличив окно подкачки.

Эта комбинация дала kswapd более широкую рабочую зону и больше времени для выгрузки страниц на диск во время всплесков потребления памяти, успешно предотвратив как преждевременные вытеснения, так и OOM kill в моих тестовых прогонах.

Таблица сравнивает уровни watermark из /proc/zoneinfo (узел без NUMA):

min_free_kbytes=67584KiB и watermark_scale_factor=10 min_free_kbytes=524288KiB и watermark_scale_factor=2000

Node 0, zone Normal pages free 583273 boost 0 min 10504 low 13130 high 15756 spanned 1310720 present 1310720 managed 1265603

Node 0, zone Normal pages free 470539 min 82109 low 337017 high 591925 spanned 1310720 present 1310720 managed 1274542

График ниже наглядно показывает, что размер буфера ядра и коэффициент масштабирования играют ключевую роль в том, как система реагирует на нагрузку по памяти. При правильном сочетании этих параметров система может эффективно использовать пространство подкачки, избегая вытеснений и сохраняя стабильность.

Сравнение утилизации памяти и свопа при разных значениях min_free_kbytes с указанием влияния на вытеснения

Риски и рекомендации

Включение подкачки в Kubernetes — мощный инструмент, но он несёт риски, которыми необходимо управлять через тщательную настройку.

Риск деградации производительности: подкачка на несколько порядков медленнее обращения к RAM. Если активное рабочее множество приложения окажется в свопе, его производительность резко упадёт из-за высокого времени ожидания ввода-вывода (thrashing). Желательно использовать для свопа хранилище на основе SSD, чтобы повысить производительность.

Риск маскировки утечек памяти: своп может скрывать утечки памяти в приложениях, которые иначе быстро привели бы к OOM kill. При наличии свопа «текущее» приложение может постепенно деградировать производительность узла, делая первопричину сложнее для диагностики.

Риск отключения вытеснений: Kubelet проактивно отслеживает давление памяти на узле и завершает pod’ы для высвобождения ресурсов. Неправильная настройка может приводить к OOM kill прежде, чем Kubelet успеет вытеснить pod’ы корректно. Правильно настроенный min_free_kbytes необходим для того, чтобы механизм вытеснения Kubelet оставался эффективным.

Kubernetes-контекст

Вместе watermark ядра и порог вытеснения Kubelet формируют на узле серию зон давления памяти. Параметры порога вытеснения необходимо настроить так, чтобы управляемые Kubernetes вытеснения происходили раньше OOM kill.

Рекомендуемые пороговые значения для эффективного использования подкачки

Как показывает диаграмма, идеальная конфигурация предполагает создание достаточно большой «зоны подкачки» (между отметками high и min), чтобы ядро могло справляться с давлением памяти через своп раньше, чем доступная память опустится в зону вытеснения и прямого освобождения (Eviction/Direct Reclaim).

Рекомендуемая отправная точка

На основе полученных результатов я рекомендую следующие значения в качестве начальной точки для Linux-узлов с включённой подкачкой. Обязательно проведите бенчмаркинг на собственных рабочих нагрузках.

vm.swappiness=60

Значение по умолчанию в Linux — хорошая отправная точка для нагрузок общего назначения. Однако идеальное значение зависит от нагрузки, и приложениям, чувствительным к свопу, может потребоваться более тщательная настройка.

vm.min_free_kbytes=500000 (500 МБ)

Устанавливайте это значение достаточно высоким (например, 2–3% от общего объёма памяти узла), чтобы обеспечить разумный буфер безопасности.

vm.watermark_scale_factor=2000

Создайте более широкое окно для работы kswapd, предотвращая OOM kill при внезапных всплесках выделения памяти.

Я настоятельно рекомендую запускать бенчмарк-тесты на собственных рабочих нагрузках в тестовых средах при первоначальной настройке свопа в вашем Kubernetes-кластере. Производительность подкачки может существенно варьироваться в зависимости от разных факторов среды: нагрузки на CPU, типа диска (SSD или HDD) и паттернов ввода-вывода.

© 2026 meganuke