Застрахуй ресурсы в Кубе

image

Управление ресурсами кластера всегда сложная тема. Как объяснить необходимость настройки ресурсов пода пользователю, который деплоит свои приложения в кластер? Может проще это автоматизировать?


Описание проблемы

Управление ресурсами — важная задача в контексте администрирования кластера Kubernetes. Но почему это важно, если Kubernetes делает всю тяжелую работу за вас? Потому что это не так. Kubernetes предоставляет вам удобные инструменты, позволяющие решить множество проблем… если вы будете использовать эти инструменты. Для каждого пода в вашем кластере вы можете указать ресурсы, необходимые для его контейнеров. И Kubernetes будет использовать эту информацию, чтобы распределять экземпляры вашего приложения по нодам кластера.

Мало кто относится к управлению ресурсами в Kubernetes серьезно. Это нормально для слабо загруженного кластера с парой статических приложений. Но что, если у вас очень динамичный кластер? Где приложения приходят и уходят, где неймспейсы создаются и удаляются все время? Кластер с большим количеством пользователей, которые могут создавать свои собственные неймспейсы и деплоить приложения? Что ж, в этом случае вместо стабильной и предсказуемой оркестрации у вас будет куча рандомных сбоев в приложениях, а иногда даже в компонентах самого Kubernetes!

Вот пример такого кластера:


0yar2pe_8-bk-8cwrkd95bz8leu.png

Вы видите 3 пода в состоянии «Terminating». Но тут не обычное удаление подов — они застряли в этом состоянии потому, что демон containerd на их ноде был задет чем-то очень жадным до ресурсов.
Такие проблемы могут быть решены с помощью правильной обработки нехватки ресурсов, но это не тема данной статьи (есть неплохая статья), так же как и не серебряная пуля для решения всех вопросов с ресурсами.

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

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

Также часто можно наблюдать менее критические случаи, когда на некоторые приложения влияют их соседи. Даже если у этих «невинных» приложений были корректно настроены ресурсы, бродячий под может прийти и убить их. Пример такого сценария:


  1. Ваше приложение запрашивает 4 ГБ памяти, но изначально занимает только 1 ГБ.
  2. Бродячий под, без конфигурации ресурсов, назначается на ту же ноду.
  3. Бродячий под потребляет всю доступную память.
  4. Ваше приложение пытается выделить больше памяти и падает, потому что больше нет.

Еще один довольно популярный случай — переоценка. Некоторые разработчики делают огромные запросы в манифестах «на всякий случай» и никогда не используют эти ресурсы. В результате — пустая трата денег.


Теория решения

Ужас! Правда?
К счастью, Kubernetes предлагает способ наложить некоторые ограничения на поды, указав конфигурацию ресурсов по умолчанию, а также минимальные и максимальные значения. Это реализовано с помощью объекта LimitRange. LimitRange очень удобный инструмент, когда у вас ограниченное количество неймспейсов или полный контроль над процессом их создания. Даже без надлежащей конфигурации ресурсов, ваши приложения будут ограничены в их использовании. «Невинные», правильно настроенные поды будут в безопасности и защищены от вредных соседей. Если кто-то развернет жадное приложение без конфигурации ресурсов, это приложение получит дефолтные значения и, вероятно, упадет. И это все! Приложение больше никого не утянет с собой.

Таким образом, у нас есть инструмент для контроля и принудительной конфигурации ресурсов для подов, кажется теперь мы в безопасности. Так? Не совсем. Дело в том, что, как мы описали ранее, наши неймспейсы могут создаваться пользователями, и, следовательно, LimitRange может отсутствовать в таких неймспейсах, поскольку он должна создаваться в каждом неймспейсе отдельно. Поэтому нам нужно что-то не только на уровне неймспейса, но и на уровне кластера. Но такой функции в Kubernetes еще нет.

Вот почему я решил написать свое решение этой задачи. Позвольте вам представить — Limit Operator. Это оператор, созданный на основе фреймворка Operator SDK, который использует кастомный ресурс ClusterLimit и помогает обезопасить все «невинные» приложения в кластере. С помощью этого оператора можно управлять значениями по умолчанию и ограничениями ресурсов для всех неймспейсов, используя минимальный объем конфигурации. Он также позволяет выбирать, где именно применять конфигурацию при помощи namespaceSelector.


Пример ClusterLimit
apiVersion: limit.myafq.com/v1alpha1
kind: ClusterLimit
metadata:
 name: default-limit
spec:
 namespaceSelector:
   matchLabels:
     limit: "limited"
 limitRange:
   limits:
   - type: Container
     max:
       cpu: "800m"
       memory: "1Gi"
     min:
       cpu: "100m"
       memory: "99Mi"
     default:
       cpu: "700m"
       memory: "900Mi"
     defaultRequest:
       cpu: "110m"
       memory: "111Mi"
   - type: Pod
     max:
       cpu: "2"
       memory: "2Gi"

С такой конфигурацией оператор создаст LimitRange только в неймспейсах с лейблом limit: limited. Это будет полезно для обеспечения более строгих ограничений в определенной группе неймспейсов. Если namespaceSelector не указан, оператор будет применять LimitRange ко всем неймспейсам. Если вы хотите настроить LimitRange вручную, для определенного неймспейса, вы можете использовать аннотацию "limit.myafq.com/unlimited": true это скажет оператору пропустить данный неймспейс и не создавать LimitRange автоматически.

Пример сценария использования оператора:


  • Создайте дефолтный ClusterLimit с либеральными ограничениями и без namespaceSelector — он будет применяться везде.
  • Для набора неймспейсов с легковесными приложениями создайте дополнительный, более строгий, ClusterLimit с namespaceSelector. Соответствующим образом поставьте лейблы на эти неймспейсы.
  • На неймспейсе с очень ресурсоемкими приложениями поместите аннотацию «limit.myafq.com/unlimited»: true и настройте LimitRange вручную с гораздо более широкими пределами, чем задали в дефолтном ClusteLimit.


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


Практический пример

Оператор будет отслеживать все изменения во всех неймспейсах, ClusterLimits, дочерних LimitRanges и будет инициировать согласование состояния кластера при любом изменении в отслеживаемых объектах. Давайте посмотрим как это работает на практике.

Для начала создадим под без каких-либо ограничений:


kubectl run/get output
❯() kubectl run --generator=run-pod/v1 --image=bash bash
pod/bash created

❯() kubectl get pod bash -o yaml
apiVersion: v1
kind: Pod
metadata:
 labels:
   run: bash
 name: bash
 namespace: default
spec:
 containers:
 - image: bash
   name: bash
   resources: {}

Примечание: часть вывода команд была опущена, чтобы упростить пример.

Как видите, поле «resources» пустое, значит этот под может быть запущен где угодно.
Теперь мы создадим дефолтный ClusterLimit для всего кластера с достаточно либеральными значениями:


default-limit.yaml
apiVersion: limit.myafq.com/v1alpha1
kind: ClusterLimit
metadata:
 name: default-limit
spec:
 limitRange:
   limits:
   - type: Container
     max:
       cpu: "4"
       memory: "5Gi"
     default:
       cpu: "700m"
       memory: "900Mi"
     defaultRequest:
       cpu: "500m"
       memory: "512Mi"

А также более строгий для подмножества неймспейсов:


restrictive-limit.yaml
apiVersion: limit.myafq.com/v1alpha1
kind: ClusterLimit
metadata:
 name: restrictive-limit
spec:
 namespaceSelector:
   matchLabels:
     limit: "restrictive"
 limitRange:
   limits:
   - type: Container
     max:
       cpu: "800m"
       memory: "1Gi"
     default:
       cpu: "100m"
       memory: "128Mi"
     defaultRequest:
       cpu: "50m"
       memory: "64Mi"
   - type: Pod
     max:
       cpu: "2"
       memory: "2Gi"

Затем создадим неймспейсы и поды в них, чтобы увидеть как это работает.
Обычный неймспейс с ограничением по умолчанию:

apiVersion: v1
kind: Namespace
metadata:
 name: regular

И немного более ограниченный неймспейс, по легенде — для легких приложений:

apiVersion: v1
kind: Namespace
metadata:
 labels:
   limit: "restrictive"
 name: lightweight

Если посмотреть логи оператора сразу после создания неймспейса, то можно обнаружить примерно то, что под спойлером:


логи оператора
{...,"msg":"Reconciling ClusterLimit","Triggered by":"/regular"}
{...,"msg":"Creating new namespace LimitRange.","Namespace":"regular","LimitRange":"default-limit"}
{...,"msg":"Updating namespace LimitRange.","Namespace":"regular","Name":"default-limit"}
{...,"msg":"Reconciling ClusterLimit","Triggered by":"/lightweight"}
{...,"msg":"Creating new namespace LimitRange.","Namespace":"lightweight","LimitRange":"default-limit"}
{...,"msg":"Updating namespace LimitRange.","Namespace":"lightweight","Name":"default-limit"}
{...,"msg":"Creating new namespace LimitRange.","Namespace":"lightweight","LimitRange":"restrictive-limit"}
{...,"msg":"Updating namespace LimitRange.","Namespace":"lightweight","Name":"restrictive-limit"} 

Пропущенная часть лога содержит еще 3 поля, не имеющие значения в данный момент

Как вы можете видеть, создание каждого неймспейса запустило создание новых LimitRange. Более ограниченный неймспейс получил два LimitRange — дефолтный и более строгий.

Теперь попробуем создать пару подов в этих неймспейсах.


kubectl run/get output
❯() kubectl run --generator=run-pod/v1 --image=bash bash -n regular
pod/bash created

❯() kubectl get pod bash -o yaml -n regular
apiVersion: v1
kind: Pod
metadata:
 annotations:
   kubernetes.io/limit-ranger: 'LimitRanger plugin set: cpu, memory request for container
     bash; cpu, memory limit for container bash'
 labels:
   run: bash
 name: bash
 namespace: regular
spec:
 containers:
 - image: bash
   name: bash
   resources:
     limits:
       cpu: 700m
       memory: 900Mi
     requests:
       cpu: 500m
       memory: 512Mi

Как видите, не смотря на то, что мы не изменили способ создания пода, поле ресурсов теперь заполнено. Также вы могли заметить аннотацию, автоматически созданную LimitRanger.

Теперь создадим под в облегченном неймспейсе:


kubectl run/get output
❯() kubectl run --generator=run-pod/v1 --image=bash bash -n lightweight
pod/bash created

❯() kubectl get pods -n lightweight bash -o yaml
apiVersion: v1
kind: Pod
metadata:
 annotations:
   kubernetes.io/limit-ranger: 'LimitRanger plugin set: cpu, memory request for container
     bash; cpu, memory limit for container bash'
 labels:
   run: bash
 name: bash
 namespace: lightweight
spec:
 containers:
 - image: bash
   name: bash
   resources:
     limits:
       cpu: 700m
       memory: 900Mi
     requests:
       cpu: 500m
       memory: 512Mi

Обратите внимание, что ресурсы в поде все те же, что и в предыдущем примере. Это потому, что в случае нескольких LimitRange при создании подов будут использованы менее строгие дефолтные значения. Но зачем тогда нам нужен более ограниченный LimitRange? Он будет использован для проверки максимальных и минимальных значений ресурсов. Для демонстрации сделаем наш ограниченный ClusterLimit еще более ограниченным:


restrictive-limit.yaml
apiVersion: limit.myafq.com/v1alpha1
kind: ClusterLimit
metadata:
 name: restrictive-limit
spec:
 namespaceSelector:
   matchLabels:
     limit: "restrictive"
 limitRange:
   limits:
   - type: Container
     max:
       cpu: "200m"
       memory: "250Mi"
     default:
       cpu: "100m"
       memory: "128Mi"
     defaultRequest:
       cpu: "50m"
       memory: "64Mi"
   - type: Pod
     max:
       cpu: "2"
       memory: "2Gi"

Обратите внимание на секцию:

- type: Container
  max:
   cpu: "200m"
   memory: "250Mi"

Теперь мы установили 200m CPU и 250Mi памяти как максимум для контейнера в поде. И теперь снова попробуем создать под:

❯() kubectl run --generator=run-pod/v1 --image=bash bash -n lightweight
Error from server (Forbidden): pods "bash" is forbidden: [maximum cpu usage per Container is 200m, but limit is 700m., maximum memory usage per Container is 250Mi, but limit is 900Mi.]

Наш под имеет большие значения, заданные дефолтным LimitRange и он не смог запуститься потому, что не прошел проверку максимально допустимых ресурсов.


Это был пример использования Limit Operator. Попробуйте это сами и поиграйте с ClusterLimit в своем локальном инстансе Kubernetes.

В GitHub репозитории Limit Operator вы можете найти манифесты для деплоя оператора, а также исходный код. Если хочется расширить функциональность оператора — пулреквесты и фичереквесты приветствуются!


Заключение.

Управление ресурсами в Kubernetes имеет решающее значение для стабильности и надежности ваших приложений. Настраивайте ресурсы своих подов, когда это возможно. И используйте LimitRange чтобы застраховаться от случаев, когда не возможно. Автоматизируйте создание LimitRange с помощью Limit Operator.

Следуйте этим советам, и ваш кластер будет всегда застрахован от ресурсного беспредела бродячих подов.

© Habrahabr.ru