Cgroups — глубокое погружение в управление ресурсами в Kubernetes

Подробно рассмотрим, что такое cgroups, как Kubernetes использует их для управления ресурсами Node и как мы можем использовать их преимущества помимо настройки запросов ресурсов и ограничений для Pod

Что такое Cgroups

В Kubernetes за управление и распределение ресурсов отвечает механизм cgroups (Control Groups) ядра Linux. Cgroups контролируют ресурсы, такие как процессорное время, память, сетевой трафик и диск.

Они создают иерархию групп процессов, ограничивая ресурсы на уровне родительских и дочерних процессов. Kubernetes использует cgroups для управления ресурсами узлов и выделения их под задачи Pod, обеспечивая оптимальное распределение.

Эта структура хранится в /sys/fs/cgroup/, а для Kubernetes интерес представляет часть под названием kubepods.slice.

/sys/fs/cgroup/
└── kubepods.slice/
    ├── kubepods-besteffort.slice/
    │   └── ...
    ├── kubepods-guaranteed.slice/
    │   └── ...
    └── kubepods-burstable.slice/
        └── kubepods-burstable-pod<SOME_ID>.slice/
            ├── crio-<CONTAINER_ONE_ID>.scope/
            │   ├── cpu.weight
            │   ├── cpu.max
            │   ├── memory.min
            │   └── memory.max
            └── crio-<CONTAINER_TWO_ID>.scope/
                ├── cpu.weight
                ├── cpu.max
                ├── memory.min
                └── memory.max

Все Kubernetes cgroups находятся в подкаталоге kubepods.slice/, который делится на kubepods-besteffort.slice/, kubepods-burstable.slice/ и kubepods-guaranteed.slice/ для каждого типа качества обслуживания (QoS). В этих подкаталогах находятся папки для каждого Pod, а внутри них — папки для каждого контейнера.


На каждом уровне расположены файлы, такие как cpu.weight или cpu.max, указывающие, сколько ресурса (например, CPU) может использовать группа. Эти значения соответствуют запросам и лимитам ресурсов, указанным в манифестах Pod-ов. Однако на практике значения в этих файлах могут отличаться от прямых запросов и лимитов.


Наконец, на «листах» дерева находятся файлы, которые описывают, сколько памяти (memory.min и memory.max), CPU (cpu.weight и cpu.max) или других ресурсов выделено каждому контейнеру. Эти файлы представляют собой прямую интерпретацию запросов и ограничений на ресурсы, указанных в манифестах Pod. Однако, если заглянуть в эти файлы, значения могут показаться не связанными с запросами и ограничениями. Тогда что означают эти значения и откуда они берутся?

Как это работает

Давайте рассмотрим каждый этап, чтобы понять, как запросы и лимиты Pod-а передаются и отображаются в файлах директории /sys/fs/….


Мы начнем с простого определения Pod, в котором указаны запросы и лимиты на память и CPU.

# kubectl apply -f pod.yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: webserver
  name: webserver
spec:
  containers:
  - image: nginx
    name: webserver
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

Когда мы создаем или применяем манифест Pod, Pod назначается на Node. Kubelet на этом Node берет спецификацию Pod (PodSpec) и передает её интерфейсу Container Runtime (CRI), например, containerd или CRI-O. CRI переводит эту спецификацию на низкоуровневый формат OCI JSON, который описывает создаваемый контейнер.

{
  "status": {
    "id": "d94159cf8228addd7a29beaa3f59794799e0f3f65e856af2cb6389704772ffee",
    "metadata": {
      "name": "webserver"
    },
    "image": {
      "image": "docker.io/library/nginx:latest"
    },
    "imageRef": "docker.io/library/nginx@sha256:ab589a3...c332a5f0a8b5e59db"
  },
  "info": {
    "runtimeSpec": {
      "hostname": "webserver",
      "linux": {
        "resources": {
          "memory": {
            "limit": 134217728,
            "swap": 134217728
          },
          "cpu": {
            "shares": 256,
            "quota": 50000,
            "period": 100000
          },
          "pids": { "limit": 0 },
          "hugepageLimits": [{ "pageSize": "2MB", "limit": 0 }],
          "unified":{
            "memory.high": "107374182",
            "memory.min": "67108864"
          }
        },
        "cgroupsPath":
          "kubepods-burstable-pod6910effd_ea14_4f76_a7de_53c333338acb.slice:crio:d94159cf8228addd7a29b...389704772ffee"
      }}}}

Как видно из вышеописанного, спецификация включает cgroupsPath, где будут находиться файлы cgroup. В разделе info.runtimeSpec.linux.resources уже содержатся переведенные запросы и лимиты ресурсов. Затем спецификация передается на более низкий уровень — в OCI-контейнерный рантайм, например, runc, который взаимодействует с драйвером systemd, создавая для контейнера systemd unit и устанавливая значения в файлах на cgroupfs.


Для первоначального просмотра systemd-unit контейнера:

# Find the container ID:
crictl ps
CONTAINER      IMAGE  CREATED        STATE    NAME       ATTEMPT  POD ID         POD
029d006435420  ...    6 minutes ago  Running  webserver  0        72d13807f0ab1  webserver

# Find the slice and scope
systemd-cgls --unit kubepods.slice --no-pager
Unit kubepods.slice (/kubepods.slice):
├─kubepods-burstable.slice
│ ├─kubepods-burstable-pod6910effd_ea14_4f76_a7de_53c333338acb.slice
│ │ └─crio-029d0064354201e077d8155f2147907dfe8f18ef2ccead607273d487971df7e0.scope ...
│ │   ├─6166 nginx: master process nginx -g daemon off;
│ │   ├─6212 nginx: worker process
│ │   └─6213 nginx: worker process
│ ├─kubepods-burstable-pod3fee6bda_0ed8_40fa_95c8_deb824f6de93.slice
│ └─ ...
│   └─...
│     └─...
└─...
  └─...
    └─...
      └─...

systemctl show --no-pager crio-029d0064354201e077d8155f2147907dfe8f18ef2ccead607273d487971df7e0.scope
MemoryMin=0
MemoryMin=67108864
MemoryHigh=107374182
MemoryMax=134217728
MemorySwapMax=infinity
MemoryLimit=infinity
CPUWeight=10
CPUQuotaPerSecUSec=500ms
CPUQuotaPeriodUSec=100ms
...
# More info https://github.com/opencontainers/runc/blob/main/docs/systemd.md

Для начала находим ID контейнера с помощью crictl ps, аналогично docker ps для CRI. В выводе команды видим наш Pod webserver и ID контейнера. Далее используем systemd-cgls, чтобы рекурсивно отобразить содержимое контрольных групп. В его выводе находим группу с ID нашего контейнера, например, crio-029d006435420.... Наконец, с помощью systemctl show --no-pager crio-029d006435420... получаем свойства systemd, использованные для установки значений в файлах cgroup.


Для дальнейшего исследования самой файловой системы cgroups:

cd /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/
ls -l kubepods-burstable-pod6910effd_ea14_4f76_a7de_53c333338acb.slice
...
-rw-r--r-- 1 root root 0 Dec  3 11:46 cpu.max
-rw-r--r-- 1 root root 0 Dec  3 11:46 cpu.weight
-r--r--r-- 1 root root 0 Dec  3 11:46 hugetlb.2MB.current
-rw-r--r-- 1 root root 0 Dec  3 11:46 hugetlb.2MB.max
-rw-r--r-- 1 root root 0 Dec  3 11:46 io.max
-rw-r--r-- 1 root root 0 Dec  3 11:46 io.weight
-rw-r--r-- 1 root root 0 Dec  3 11:46 memory.high
-rw-r--r-- 1 root root 0 Dec  3 11:46 memory.low
-rw-r--r-- 1 root root 0 Dec  3 11:46 memory.max
-rw-r--r-- 1 root root 0 Dec  3 11:46 memory.min
...

cat .../cpu.weight  # 10

#                  $MAX  $PERIOD
cat .../cpu.max  # 50000 100000

cat .../memory.min  # 67108864

cat .../memory.max  # 134217728

Мы переходим в директорию kubepods-burstable-pod6910effd_ea14_4f76_a7de_53c333338acb.slice, указанную в выводе systemd-cgls, где находим файлы cgroup для всего Pod webserver. Ключевые файлы и значения:


  • cpu.weight — CPU request, выраженный в весах ("shares"). Указывает долю CPU, выделяемую контейнеру относительно других контейнеров.
  • cpu.max — CPU limit, ограничение на использование CPU за определенный период.
  • memory.min — memory request, минимально гарантируемая память в байтах.
  • memory.max — memory limit, жесткое ограничение памяти. При достижении лимита запускается OOM killer для завершения контейнера.

Дополнительные файлы в cgroup не настраиваются через Pod манифесты.


Если вы хотите разобраться самостоятельно, альтернативным/более быстрым способом найти эти значения может быть получение пути к cgroupfs спецификации среды выполнения контейнера, упомянутой ранее:

POD_ID="$(crictl pods --name webserver -q)"
crictl inspectp -o=json $POD_ID | jq .info.runtimeSpec.linux.cgroupsPath -r
# Output (a path to Pod's pause container):
# kubepods-burstable-pod6910effd_..._53c333338acb.slice:crio:72d13807f0ab1a3860478d6053...745a50e5e296ddd7570e1fa9
# Translates to
ls -l /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6910effd_..._53c333338acb.slice/

-rw-r--r-- 1 root root 0 Dec  3 11:46 cpu.max
-rw-r--r-- 1 root root 0 Dec  3 11:46 cpu.weight
-rw-r--r-- 1 root root 0 Dec  3 11:46 memory.high
-rw-r--r-- 1 root root 0 Dec  3 11:46 memory.low
-rw-r--r-- 1 root root 0 Dec  3 11:46 memory.max
-rw-r--r-- 1 root root 0 Dec  3 11:46 memory.min
...

Мониторинг

Мониторинг использования ресурсов также осуществляется через cgroups с помощью компонента cAdvisor, встроенного в kubelet. cAdvisor собирает метрики потребления ресурсов, которые можно просматривать как значения из файлов cgroups.


Для получения метрик:

# Directly on the node:
curl -sk -X GET  "https://localhost:10250/metrics/cadvisor" \
  --key /etc/kubernetes/pki/apiserver-kubelet-client.key \
  --cacert /etc/kubernetes/pki/ca.crt \
  --cert /etc/kubernetes/pki/apiserver-kubelet-client.crt

# Remotely using "kubectl proxy"
kubectl proxy &
# [1] 2933

kubectl get nodes
NAME        STATUS   ROLES           AGE   VERSION
some-node   Ready    control-plane   7d    v1.25.4

curl http://localhost:8001/api/v1/nodes/some-node/proxy/metrics/cadvisor
  1. Если у вас есть доступ к Node кластера, можно напрямую запросить их через kubelet API.
  2. Также возможно использовать kubectl proxy для доступа к API сервера Kubernetes и выполнения curl-запроса из локальной машины.

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


Вот некоторые из наиболее интересных показателей, которые вы там найдете:

# Memory limit defined for that container (memory.max)
container_spec_memory_limit_bytes{
  container="webserver",
  id="/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod691...38acb.slice/crio-d94159cf...04772ffee.scope",
  image="docker.io/library/nginx:latest",
  name="k8s_webserver_webserver_default_6910effd-ea14-4f76-a7de-53c333338acb_1",
  namespace="default",
  pod="webserver"} 1.34217728e+08

# If the CPU limit is "500m" (500 millicores) for a container and
# the "container_spec_cpu_period" is set to 100,000, this value will be 50,000.
# cpu.max 1st value
container_spec_cpu_quota{...} 50000

# The number of microseconds that the scheduler uses as a window when limiting container processes
# cpu.max 2nd value
container_spec_cpu_period{...} 100000

# CPU share of the container
# cpu.weight
container_spec_cpu_shares{...} 237

В завершение, все настройки и переводы из Pod манифестов можно суммировать в таблице, связывающей параметры манифеста с файлами cgroupfs.

Почему это важно

Почему стоит разбираться в работе cgroups в Kubernetes? Несмотря на то, что Linux и Kubernetes делают большую часть работы за нас, глубокое понимание помогает при отладке и дает доступ к продвинутым функциям. Вот несколько примеров:


  • Memory QoS. Сейчас (до Kubernetes 1.26) запросы на память, заданные в Pod-манифесте, часто игнорируются контейнерным рантаймом. Контейнер не может уменьшить использование памяти, и если достигнут лимит, он просто завершается из-за ошибки OOM (Out of Memory). С функцией Memory QoS (на стадии Alpha) Kubernetes использует дополнительные файлы cgroups — memory.min и memory.high — для ограничения памяти, что позволяет избежать немедленного завершения контейнера.
  • Контейнерно-ориентированный OOM-killer. Например, если у вас есть Pod с побочным контейнером для логирования, и он достиг лимита памяти, основной контейнер может завершиться из-за использования памяти побочным контейнером. С использованием контейнерно-ориентированного OOM-killer можно настроить Pod так, чтобы в первую очередь завершался побочный контейнер.
  • Режим работы без root-доступа (Rootless). Благодаря cgroups можно запускать Kubernetes-компоненты, такие как kubelet или CRI, без root-доступа, что повышает безопасность.
  • Поддержка JVM. Разработчикам на Java полезно понимать работу cgroups, так как JDK использует файлы cgroups для определения доступных CPU и памяти.

Эти примеры лишь отражают часть возможностей, которые открывает использование cgroups. В будущем, вероятно, появится еще больше функций для управления ресурсами в Kubernetes, например, регулировка дисковых операций, сетевого I/O и управления нагрузкой на ресурсы.

Заключение

Хотя работа Kubernetes может показаться "магией", за многими процессами стоит грамотное использование возможностей Linux, как мы видим на примере управления ресурсами. Знание работы cgroups полезно не только для операторов и пользователей, но и помогает эффективно решать сложные задачи и применять продвинутые функции для стабильной работы кластера.

Ответим на все вопросы по переносу, сборке, оркестрации и развертывании.
Настроим ваш Kubernetes