image


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


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


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


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


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


image


Вы видите 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.


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

Комментарии (8)


  1. Wesha
    31.10.2019 07:15
    +1

    А как там, на Кубе, дела, как Кастро поживает?


  1. celebrate
    31.10.2019 08:06

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


    1. KIVagant
      31.10.2019 15:58

      Статья как раз про это и сообщает и даже предлагает решение.


      1. celebrate
        31.10.2019 16:12
        +1

        Статья про LimitRange, а я говорю про другую сущность — квоты. Помимо своего прямого назначения квоты также помогают решить проблему BestEffort подов. Если в квоте указаны лимиты, то каждый под в неймспейсе обязан указывать лимиты. Это также может считаться решением проблемы. А в опеншифте это можно применить автоматически ко всем создаваемым неймспейсам (проектам) без установки дополнительных операторов.

        Поэтому предложенное мной решение отличается от описанного в статье.


        1. KIVagant
          31.10.2019 17:00

          Квоты хорошая штука, безусловно. Но с ними усложняется мониторинг. Если ReplicaSet не может создать под из-за квот, то под не виден в списке (возможно в более новых куберах уже улучшили это). В итоге нужно изобретать сложные проверки, чтобы заметить, что есть Replicasets, у которых ожидаемое количество подов не совпадает с желаемым. Тем не менее я также за их установку. Но вот беда в том, что сами resource requests и limits далеко не так просты, как хочется. Например, лимиты на CPU могут существенно снижать производительность кластера. А с лимитами на ephemeral storage вообще крайне сложно, так как невозможно получить реальное использование по этой метрике.
          И дополнительной вишенкой является работа самого планировщика. По-умолчанию он планирует поды по limits, а не по requests. И если установить поду/контейнерам RAM request в 100Mb, а limit в 16Gb, то этот под «зарезервирует» ноду целиком.

          Плюс есть какая-то странность в том, что поды в статусе Completed всё ещё учитываются в квотах по namespace (тут вероятно тоже могут быть отличия в более новых версиях). В итоге квоты приходится раздувать.


          1. celebrate
            31.10.2019 18:01

            Про недостатки лимитов — да, они есть, но что поделаешь, остается лишь ждать пока починят. Альтернативы отсутствуют.

            Про планировщик — он же вроде по реквестам планирует? Откуда информация про планирование по лимитам? Если поду выставить реквест в 100 МБ, а лимит в 16 ГБ, то поду выделится 100 метров. Если под начнет есть память дальше, то при нехватке памяти будет убит. Никакого резервирования 16 гигов я никогда не видел.


            1. KIVagant
              31.10.2019 18:25

              > Откуда информация про планирование по лимитам?

              Это уже у меня смешались в кучу конелюди. Планирует он по requests, это я некорректно сформулировал свою мысль. Была какая-то проблема, с которой я боролся пару месяцев назад. Вспомню, вернусь.


            1. Myafq Автор
              31.10.2019 18:37

              Дефолтный планировщик определенно использует реквесты, вот как раз фраза про это из документации:
              PodFitsResources filter checks whether a candidate Node has enough available resource to meet a Pod’s specific resource requests
              По-моему чтобы планировщик учитывал лимиты, его нужно кастомизировать.


              Квоты в целом очень мощный инструмент, который остался за скоупом статьи как раз по причине своей мощности. Я думаю что применять их надо вместе — LimitRange и ResourceQuota отлично дополняют друг друга. С использованием первого мы позволяем работать любым подам, даже созданным на скорую руку, в рамках второй. Квоты это следующий шаг регулирования кластера после настройки ресурсов и стабилизации работы в целом.