Примечание «Фланта»: этот перевод поможет вам разобраться, как Kubernetes управляет контейнерами, запуская их как обычные процессы Linux. Вместо того, чтобы в теории рассказывать о пространствах имён, cgroups и внутреннем устройстве ОС, автор статьи развернул под в Kubernetes-кластере и поисследовал, что происходит вокруг него на уровне Linux.
В оригинале использовался K3s, но мы чуть доработали статью со своей стороны и повторили все шаги на Open-source платформе Deckhouse Kubernetes Platform Community Edition.
Введение
Вы можете спросить: зачем при всех достижениях в области оркестрации контейнеров вообще изучать её внутреннее устройство? Ответ может зависеть от того, используете ли вы управляемый или самоуправляемый кластер — иногда это имеет существенное значение. Кроме того, понимание фактических принципов работы Kubernetes является критически важным при управлении кластерами в большом масштабе. Оно помогает в решении проблем, оптимизации производительности и разработке более устойчивых систем.
И наконец, это просто прикольно!
Простейшая модель оркестрации контейнеров
Сначала давайте посмотрим, что представляет собой простейшая система оркестрации контейнеров. Это поможет нам понять, как всё устроено:

Есть машина с Linux (пока без кластеров, извините).
Оператор (то есть вы) задаёт базовую спецификацию (это может быть, например, текстовый или YAML-файл). Она описывает, что нужно запустить. В нашем случае мы запускаем nginx.
Сервис (или контроллер) даёт доступ к API, куда пользователь (вы) может отправить эту спецификацию — с помощью утилиты или API-вызова. Далее она преобразуется во что-то, что понимает менеджер контейнеров (например, Docker).
Менеджер контейнеров (Docker/containerd) использует функции Linux (по изоляции и контролю за потреблением ресурсов), чтобы запустить эти приложения как Linux-процессы.
Разумеется, это очень упрощённая модель — она лишь даёт грубое представление о том, как устроена оркестрация контейнеров. Реальный оркестратор production-уровня вроде Kubernetes куда сложнее — но не потому, что у него другая суть, а потому, что область ответственности гораздо шире: нужно управлять кластеризацией, сетями, планированием, проводить проверки «здоровья», заниматься масштабированием и так далее.
Просто посмотрите на архитектуру Kubernetes и сравните её с нашей умозрительной моделью.
Освежим в памяти: как Linux запускает процессы
Перед тем как погрузиться в Kubernetes, давайте вспомним, как сама Linux запускает процессы. Ведь по сути контейнер — это просто один или несколько процессов Linux.
Когда вы запускаете программу в Linux, ОС создаёт процесс. У этого процесса есть уникальный идентификатор (ID), называемый PID (Process ID). У каждого процесса своя память, файловые дескрипторы и так далее.

Linux организует процессы в виде дерева, где у каждого процесса есть родитель — за исключением самого первого (называемого init или systemd). Вот как это дерево выглядит в выводе команды pstree
:

Изоляция процессов (пространства имён и контрольные группы)
Процессы в системе пользуются общими ресурсами: файловой системой, сетью и так далее. Их нужно как-то изолировать друг от друга. Но почему?
Дело в том, что без изоляции процессы видят друг друга и могут мешать работе соседей. Сбойное или взломанное приложение может подглядывать за другими процессами, заходить в общие файлы или нарушать работу сети. Кроме того, не хочется, чтобы один нехороший процесс «съел» все ценные ресурсы — процессор, память, диск или сеть, — заставив другие процессы голодать.
В Linux для этого есть две основные фичи:
пространства имён (namespaces) — отвечают за то, что процесс видит вокруг себя: сетевые интерфейсы, подмонтированные файловые системы, ID других процессов и прочее;
контрольные группы (cgroups) — определяют, сколько процессорного времени, памяти или дисковых операций может использовать группа процессов.
Вместе пространства имён и cgroups позволяют легковесным способом создавать «песочницы» для процессов — в отличие от запуска полноценной виртуальной машины для каждого приложения.
Погружаемся во внутреннее устройство Kubernetes
Хорошо, теперь, когда мы освежили в памяти, как работают процессы Linux, пора взглянуть на Kubernetes. Я хочу запустить кластер Kubernetes, развернуть под и посмотреть, как он выглядит с точки зрения процессов.
Примечание «Фланта»
Далее в оригинальной статье автор использовал дистрибутив Kubernetes K3s. Но нашему инженеру понравился данный кейс, и он решил повторить его на Open-source платформе Deckhouse Kubernetes Platform Community Edition (DKP CE). Поэтому дальше теория из оригинала будет совмещена с нашей практикой.
Установить DKP CE довольно легко. Следуйте руководству по быстрому старту. Выберите тип инфраструктуры, в которой будет устанавливаться платформа, и следуйте инструкции.
Смотрим, какие процессы активны после установки DKP CE
Запустим ps
и погрепаем по именам процессов, связанных с Kubernetes:
ps aux | grep -E 'kubernetes'

И это только часть процессов, задействованных в архитектуре Deckhouse. Со всеми возможностями можно ознакомиться в документации.
Cмотрим пространства имён
? Важно. Здесь «пространство имён» — это пространство имён Linux, оно отличается от пространства имён Kubernetes.
Список текущих пространств имён можно вывести с помощью lsns
:

На данный момент почти все эти пространства имён относятся ко всей системе — их создаёт init при старте компьютера. Видно, какой тип у каждого пространства имён — pid, network, mount и другие.
? Примечание. Пока мы не создали под. Вернёмся к этому выводу после того, как запустим его.
Смотрим cgroups
Текущие группы можно посмотреть в /sys/fs/cgroup
:

Пока тут глазу не за что зацепиться, но скоро это изменится, обещаю.
Развёртываем под
Итак, мы посмотрели, что у нас работает в системе, теперь давайте запустим простенький под. Возьмём для примера nginx. Создадим pod.yaml
со следующим содержимым:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:stable-alpine
ports:
- containerPort: 80
И применим его с помощью d8 k apply -f pod.yaml
.
Под работает в Kubernetes в пространстве имён по умолчанию (default):
root@note:~# d8 k get po
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 20s
root@note:~#
Команда d8 k describe pod nginx
выводит подробности о поде:
? Примечание. Удалены некоторые строки, которые не важны в данном контексте.
root@note:~# d8 k describe pod nginx
Name: nginx
Namespace: default
Node: note/192.168.1.33
Status: Running
IP: 10.111.0.57
Containers:
nginx:
Container ID: containerd://86940d0832f169c2f9244d19984a04c2d2355b9da02a2e09cb1d521270050e11
Image: nginx:stable-alpine
Port: 80/TCP
Host Port: 0/TCP
Видим несколько ключевых моментов, таких как IP-адрес, ID контейнера и т. п.
Смотрим pstree
Теперь, когда под работает, давайте с помощью pstree
выведем все процессы на узле, на котором запущен DKP:

Виден процесс nginx
, который работает под процессом containerd-shim
.
Процесс-помощник containerd-shim
управляет жизненным циклом контейнера. Под каждым «шимом» виден реальный процесс приложения — в нашем случае главный процесс nginx и его воркеры.
Также рядом с nginx
работает процесс pause
. Kubernetes запускает этот специальный контейнер, чтобы управлять сетевым пространством имён пода. У каждого пода есть свой pause-контейнер, он как бы «главный» по сети для этого пода.
Изучаем работающие контейнеры
Поскольку DKP использует containerd, вывести список работающих контейнеров можно с помощью crictl
(аналог docker ps
):

Видны контейнеры самого DKP, а сверху — наш контейнер с nginx
. Отлично! Nginx работает в контейнере с ID 86940d0832f16
.
Выполним crictl inspect 86940d0832f16
, чтобы получить подробную информацию о контейнере. Там реально много всего, так что весь вывод показывать не буду.
Давайте узнаем Linux-ID процесса нашего контейнера:
root@note:~# crictl inspect 86940d0832f16 | grep pid
"pid": 182704,
"pid": 1
"type": "pid"
root@note:~#
ID процесса — 182704
(его видно и напрямую):
root@note:~# ps aux|grep 182704
root 182704 0.0 0.0 9524 5504 ? Ss 20:11 0:00 nginx: master process nginx -g daemon off;
root 287859 0.0 0.0 6544 2304 pts/1 S+ 20:45 0:00 grep --color=auto 182704
root@note:~#
Исследуем Linux-пространства имён nginx-контейнера
Снова воспользуемся lsns
, но на этот раз передадим ID процесса, чтобы вывести все пространства имён для него:

И тут есть пара реально интересных моментов. Пространства имён Network (сеть), UTS (имя хоста и доменное имя) и IPC (межпроцессное взаимодействие) создаются на уровне пода.
Этими пространствами имён владеет pause — маленький вспомогательный контейнер, который Kubernetes запускает для каждого пода. Это очень важно: именно благодаря этому контейнеры в одном поде используют общий сетевой стек, имя хоста и пространство IPC.
? Примечание. Если выполнить
exec
в под, вы обнаружите, что все контейнеры разделяют одно и то же пространство процессов, сетевые интерфейсы и доменные имена. Всё выглядит так, будто они находятся внутри одной машины — впрочем, с точки зрения пространств имён так оно и есть.
Исследуем контрольные группы nginx-контейнера
Как говорилось ранее, с помощью контрольных групп (cgroups) Linux управляет использованием ресурсов (CPU, память, ввод-вывод).
Зная PID главного процесса nginx, давайте посмотрим, в какие группы он входит:
root@note:~# cat /proc/182704/cgroup
0::/kubepods/besteffort/podd83ff76a-5ffb-43d5-996c-3572842a5aa0/86940d0832f169c2f9244d19984a04c2d2355b9da02a2e09cb1d521270050e11
root@note:~#
kubepods
— поды Kubernetes объединены под слайсомkubepods
;besteffort
— наш под принадлежит к классу обслуживания (QoS) besteffort;podd83ff76a-5ffb-43d5-996c-3572842a5aa0
— cgroup этого конкретного пода, определяемый его Pod UID;86940d0832f169c2f9244d19984a04c2d2355b9da02a2e09cb1d521270050e11
— фактический ID контейнера внутри пода, управляемый containerd.
Можно даже напрямую посмотреть текущее потребление ресурсов, например памяти:
root@note:~# cat /sys/fs/cgroup/kubepods/besteffort/podd83ff76a-5ffb-43d5-996c-3572842a5aa0/86940d0832f169c2f9244d19984a04c2d2355b9da02a2e09cb1d521270050e11/memory.current
7352320
root@note:~#
Добавим лимиты ресурсов
Теперь давайте добавим запросы и лимиты по CPU и памяти. Измените YAML-файл пода:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:stable-alpine
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
ports:
- containerPort: 80
? Важно. Под придётся удалить и пересоздать, так как меняются настройки ресурсов.
Снова узнаем ID процесса (PID), как делали раньше:
root@note:~# d8 k delete po nginx
pod "nginx" deleted
root@note:~# d8 k apply -f pod.yaml
pod/nginx created
root@note:~# crictl ps | grep nginx
384af860d8347 936a1208f403b 9 seconds ago Running nginx 0 37c469fefa201 nginx
root@note:~# crictl inspect 384af860d8347 | grep pid
"pid": 310913,
"pid": 1
"type": "pid"
root@note:~# cat /proc/310913/cgroup
0::/kubepods/burstable/pod7a838c9f-0304-41d8-a3ce-aefd2d408426/384af860d8347a4ed73e089f54662ab47aaa2598e635b9def0c73f5757dd50fb
root@note:~#
Проверяем cgroup после обновления ресурсов CPU и памяти
Если снова взглянуть на директорию cgroup:

...там будет куча всего. Посмотрим на файлы cpu.max
и cpu.stats
:

cpu.max: первое число — это квота, второе — период. Указано
10000 100000
.Квота: 10 000 микросекунд (10 мс): nginx может использовать 10 мс процессорного времени.
Период: 100 000 микросекунд (100 мс): общее окно для измерения времени CPU.
? Примечание. По сути это означает, что nginx разрешено потреблять 10 мс процессорного времени каждые 100 мс.
Полное ядро CPU (в терминах Kubernetes это 1000m) соответствует 100 % времени CPU. Так как лимит для пода nginx равен 100m, Kubernetes переводит это в 10 % одного ядра CPU — что точно совпадает с тем, что мы видим в файле cpu.max
.
cpu.stats: показывает текущую статистику CPU для контейнера. Большинство метрик здесь говорят сами за себя.
throttled_usec 0: количество микросекунд, когда CPU контейнера подвергался троттлингу (0 секунды).
Бонус: входим в пространство имён nginx-контейнера
Выполняя d8 k exec
в под, мы попадаем в пространство имён контейнера (по умолчанию d8 k exec
подключается к первому контейнеру — можно выбрать конкретный контейнер через опцию -c <имя_контейнера>
).
Точно так же можно войти непосредственно в Linux-пространство имён контейнера, чтобы осмотреться и увидеть всё с точки зрения самого процесса контейнера.
Давайте используем ID процесса (PID) с предыдущего шага и войдём в его пространство имен с помощью nsenter
:
nsenter -t 310913 -a /bin/sh
Должны попасть в командную оболочку внутри пространства имён. Посмотрим, что там внутри. Выводим список процессов:
root@note:~# nsenter -t 310913 -a /bin/sh
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 nginx: master process nginx -g daemon off;
30 nginx 0:00 nginx: worker process
31 nginx 0:00 nginx: worker process
32 nginx 0:00 nginx: worker process
33 nginx 0:00 nginx: worker process
34 nginx 0:00 nginx: worker process
35 nginx 0:00 nginx: worker process
36 nginx 0:00 nginx: worker process
37 nginx 0:00 nginx: worker process
38 root 0:00 /bin/sh
39 root 0:00 ps aux
/ #
Мастер-процесс nginx идёт под PID 1 в контейнере. Кроме него, работает ещё несколько процессов: nginx-воркеры, командная оболочка (в неё мы только что вошли) и команда ps aux
, которую мы только что запустили. Процессы хоста не видны — пространство имён обеспечивает полную изоляцию процессов! IP-адрес и имя хоста:
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
317: eth0@if318: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue state UP qlen 1000
link/ether 22:58:e9:b7:de:2a brd ff:ff:ff:ff:ff:ff
inet 10.111.0.27/32 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::2058:e9ff:feb7:de2a/64 scope link
valid_lft forever preferred_lft forever
/ #
/ #
/ # hostname
nginx
/ #
Опять же, видим изолированное сетевое пространство имён для контейнера с его собственными IP-адресами и именем хоста! Круто, правда?
Закругляемся
В этой статье мы немного углубились в то, что на самом деле происходит, когда Kubernetes запускает под. Оказалось, что контейнеры — это просто процессы, к которым применены cgroups и пространства имён для изоляции и управления.
Хотя работа Kubernetes временами может казаться чем-то магическим, в его основе лежат фундаментальные принципы Linux. Понимание этих принципов позволяет эффективнее и быстрее осуществлять масштабирование, а также находить и устранять проблемы.
P. S.
Читайте также в нашем блоге: