Примечание «Фланта»: этот перевод поможет вам разобраться, как 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). У каждого процесса своя память, файловые дескрипторы и так далее.

ps -aux — показывает список процессов в Linux
ps -aux — показывает список процессов в Linux

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

Вывод pstree
Вывод 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'
Процессы, имеющие отношение к Kubernetes
Процессы, имеющие отношение к Kubernetes

И это только часть процессов, задействованных в архитектуре Deckhouse. Со всеми возможностями можно ознакомиться в документации

Cмотрим пространства имён

? Важно. Здесь «пространство имён» — это пространство имён Linux, оно отличается от пространства имён Kubernetes.

Список текущих пространств имён можно вывести с помощью lsns:

Вывод lsns
Вывод 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:

Вывод pstree
Вывод pstree

Виден процесс nginx, который работает под процессом containerd-shim.

Процесс-помощник containerd-shim управляет жизненным циклом контейнера. Под каждым «шимом» виден реальный процесс приложения — в нашем случае главный процесс nginx и его воркеры.

Также рядом с nginx работает процесс pause. Kubernetes запускает этот специальный контейнер, чтобы управлять сетевым пространством имён пода. У каждого пода есть свой pause-контейнер, он как бы «главный» по сети для этого пода.

Изучаем работающие контейнеры

Поскольку DKP использует containerd, вывести список работающих контейнеров можно с помощью crictl (аналог docker ps):

Вывод crictl
Вывод crictl

Видны контейнеры самого 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. 

Читайте также в нашем блоге:

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