Вопрос или проблема
Я пытаюсь понять, как работают запросы и лимиты памяти с cgroup v2. В манифесте Kubernetes мы можем настроить запросы памяти и лимит памяти. Эти значения затем используются для настройки интерфейса cgroup:
- memory.min устанавливается в запрос памяти
- memory.max устанавливается в лимит памяти
- memory.high устанавливается в лимит памяти * 0.8, если запрос памяти не равен лимиту, в противном случае memory.high остается не установленным
- memory.low всегда не установлен
memory.max довольно самоочевидно: Когда процесс в cgroup пытается выделить страницу, и это приведет к превышению использования памяти над memory.max, а вернуть недостаточно страниц из cgroup, чтобы удовлетворить запрос в пределах memory.max, то вызывается OOM-убийца, чтобы завершить процесс внутри cgroup. memory.high понять сложнее: Документация ядра говорит, что cgroup находится под «высоким давлением на возврат», когда достигается высокий уровень, но что это конкретно означает?
Далее говорится:
Когда это происходит, оно ограничивает выделения, принуждая их к прямому возврату, чтобы избавиться от избыточности, но никогда не вызывает OOM-убийцу.
Правильно ли я предполагаю, что это означает, что когда cgroup пытается выделить страницу за пределами высокого уровня memory.high, она будет синхронно обращаться к lruvecs и пытаться вернуть как можно больше страниц из конца списков, пока не окажется ниже высокого уровня? Или «давление на возврат» — это что-то, что происходит асинхронно (через kswapd)?
Вопрос 2: В чем вообще смысл использования memory.high в Kubernetes? Насколько мне известно, узлы Kubernetes обычно работают без разделяемого пространства. Единственные страницы, которые можно вернуть, — это анонимные страницы (если достаточно доступно свопа) и кэш страниц. Поскольку свопа нет, остается только кэш страниц. Дело в том, что кэш страниц также будет возвращаться при достижении memory.max, прежде чем инициировать OOM-убийцу как последнее средство, если ничего нельзя вернуть. Таким образом, memory.high по сути бесполезен:
-
Пока используется кэш страниц, его всегда можно вернуть, и memory.max тоже это сделает. С memory.high мы просто ограничиваем приложение раньше, чем это нужно. Можно было бы установить memory.max ниже изначально.
-
Если значительный кэш страниц не используется (что, вероятно, является случаем для большинства приложений, работающих на Kubernetes сегодня), то ничего нельзя вернуть, следовательно, нет ограничения (нет выгрузки неиспользуемой анонимной памяти, нет угрожающих сигналов в информации о давлении, которые могли бы нас предостеречь), и мы столкнемся с memory.max, не заметив. Использование memory.high не имеет эффекта.
Ответ или решение
В Kubernetes управление ограничениями памяти осуществляется через интерфейс cgroup v2, который обеспечивает более гибкое управление ресурсами для контейнеризированных приложений. Когда вы настраиваете параметры памяти в манифесте пода (requests и limits), значения как устанавливаются для различных параметров cgroup:
- memory.min – устанавливается в значение memory request.
- memory.max – устанавливается в значение memory limit.
- memory.high – по умолчанию устанавливается в 80% от memory limit, за исключением случая, когда memory request равен memory limit, в таком случае memory.high не устанавливается.
- memory.low – всегда не установлен.
Работа с memory.high
memory.max ограничивает максимальное значение памяти, которое может использовать приложение. Если приложение пытается выделить память, которая превышает значение memory.max, и при этом недостаточно памяти может быть освобождено, то будет инициирован OOM-убийца для завершения процесса внутри cgroup.
memory.high, в свою очередь, является более сложным элементом. Когда cgroup достигает "высокой" границы памяти, это означает, что начинается процесс принудительного "восстановления" или "рейкла ризма". Это не приводит к вызову OOM-убийцы, а заставляет процессы стараться освободить память, чтобы оставаться в пределах заданного лимита.
Как работает "высокая" нагрузка на рефайл
Когда приложение пытается выделить страницу, и это приводит к превышению memory.high, происходит следующее:
- Процессы будут принуждены к "прямому восстановлению", что означает, что они будут пытаться освободить память синхронно, обращаясь к LRU-спискам (Least Recently Used). Это позволит им вернуть выделенные страницы в кучу до тех пор, пока использование памяти не станет ниже high watermark.
- В отличие от работы kswapd, который управляет перераспределением памяти асинхронно, "высокая" нагрузка реагирует мгновенно, что сокращает время, в течение которого приложение может страдать от нехватки памяти.
Зачем использовать memory.high в Kubernetes?
Логично задаться вопросом, какой смысл имеет использование memory.high в среде Kubernetes, особенно если большинство узлов работают без пространства подкачки. В этом случае остаются только страницы анонимной памяти и кеш страниц, которые подлежат восстановлению.
-
При наличии кеша страниц – memory.high может помочь предотвратить острые колебания в использовании памяти и, соответственно, снизить вероятность достижения memory.max. Это позволяет избежать резкого падения производительности приложения и улучшает устойчивость системы.
-
Если кеш страниц незначителен – действительно, Reclaim может не произойти, и приложение может не заметить последствия до достижения memory.max. Однако мониторинг и реакция на использование памяти возможно, могут быть полезны для разработки, позволяя настроить более эффективные политики управления ресурсами и предупредить администратора о проблемах с ресурсами до того, как они станут критичными.
В заключение, использование memory.high в Kubernetes, даже в условиях отсутствия swap, может заранее уведомить вас о состоянии памяти и улучшить производительность системы, предотвращая резкие падения производительности. Правильная настройка этих лимитов может значительно оптимизировать работу приложений в контейнерах.