ADG Оставить заявку
Блог DevOps 5 мин чтения

containerd 1.4: переводим dev-кластер с dockershim на прямой CRI runtime

containerd 1.4 со стабильным CRI plugin - повод убрать dockershim из kubelet и поставить crictl. Разбираем что поменялось в логах и namespace isolation.

Контекст момента

containerd 1.4 вышел со стабильным CRI plugin, который становится основным container runtime для Kubernetes

containerd 1.4 вышел на прошлой неделе, и главное в этом релизе - не набор новых фич, а стабилизация CRI plugin. Это тот момент, когда можно перестать говорить «containerd как CRI экспериментальный» и начать делать что-то руками. Мы взяли dev-кластер и прошли путь от dockershim к прямому containerd runtime - посмотреть что это значит на практике.

Предыстория простая: в Kubernetes kubelet общается с container runtime через CRI (Container Runtime Interface). Исторически Docker не реализует CRI напрямую, поэтому kubelet шёл через dockershim - прослойку, которая переводила CRI-вызовы в Docker API. containerd тоже долгое время использовался через ту же цепочку: Kubernetes -> dockershim -> Docker daemon -> containerd. Это три слоя там, где достаточно одного.

Что меняем и как

Кластер - три ноды, kubeadm, Kubernetes 1.18, Ubuntu 20.04. Docker на нодах стоит и работает - он нам больше не нужен как runtime для kubelet, но docker CLI пока оставляем для удобства разработчиков.

Процедура на каждой ноде примерно такая:

# Дренируем ноду
kubectl drain node-01 --ignore-daemonsets --delete-local-data

# Устанавливаем containerd 1.4
apt-get install containerd=1.4.0~*

# Генерируем дефолтный конфиг
containerd config default > /etc/containerd/config.toml

В /etc/containerd/config.toml включаем SystemdCgroup - без этого kubelet и containerd будут использовать разные cgroup driver-ы, что приводит к тихим проблемам с ресурсами:

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
  SystemdCgroup = true

Дальше - kubelet. В /etc/default/kubelet добавляем:

KUBELET_EXTRA_ARGS=--container-runtime=remote --container-runtime-endpoint=unix:///run/containerd/containerd.sock

И убираем оттуда же старые флаги dockershim если они были прописаны явно. Перезапускаем kubelet, uncordon ноды - поды поднимаются через containerd напрямую.

crictl вместо docker ps

Вот где первое ощутимое изменение в повседневной работе. docker ps больше не показывает контейнеры запущенные через CRI - они существуют в containerd namespace k8s.io, а не в дефолтном moby. Для отладки нужен crictl:

# Список подов
crictl pods

# Список контейнеров
crictl ps

# Логи конкретного контейнера
crictl logs <container-id>

# Exec в контейнер
crictl exec -it <container-id> /bin/sh

Основные команды совпадают с docker CLI по смыслу. crictl images показывает образа в namespace k8s.io. Если нужен образ который туда ещё не попал (например, только что запушили и хочется проверить pull) - через ctr (нижнеуровневый CLI containerd) с явным указанием namespace:

ctr -n k8s.io images ls

Docker CLI при этом продолжает работать для локальной разработки - он общается с Docker daemon который никуда не делся, просто kubelet его больше не использует. Разработчик собирает образ через docker build, пушит в registry, дальше kubelet тянет его уже через containerd. Это не конфликтует.

Логирование: стало по-другому

Второе ощутимое изменение - логи контейнеров. При dockershim kubelet читал логи через Docker log driver и сохранял их в /var/log/pods. containerd пишет туда же, но формат строки немного отличается. Конкретно: добавляется тег потока (stdout/stderr) и признак частичной строки.

Строка в формате dockershim:

2020-08-25T10:23:41.123456789Z app started

Строка в формате containerd CRI:

2020-08-25T10:23:41.123456789Z stdout F app started

F означает full line (строка завершена), P - partial (длинная строка разбита). Для большинства log-shipper-ов типа Filebeat или Fluentd это прозрачно - они умеют оба формата. Но если у вас самописный парсер логов с жёстким regexp - стоит проверить.

У нас в одном кластере был Filebeat с кастомным pipeline для разбора JSON-логов приложения. После перехода логи перестали парситься корректно - именно из-за добавившихся полей в начале строки. Починилось добавлением decode_json_fields с правильным target field, но диагностика заняла время.

Namespace isolation: что стало видно

containerd использует namespace-ы для изоляции ресурсов. Kubernetes-рантайм живёт в k8s.io. Если на ноде запущен Docker daemon - его контейнеры живут в moby. Namespace-ы не пересекаются и ресурсы между ними не видны без явного указания.

Практическое следствие: если вы делаете docker pull образа на ноде, этот образ попадает в namespace moby и kubelet его не видит. kubelet сделает свой pull через containerd в k8s.io. Дублирование образов на диске - реальное явление на нодах, где активно используют и docker CLI, и kubernetes runtime. На dev-ноде с маленьким диском это надо иметь в виду.

Посмотреть что занимает место в каждом namespace:

ctr -n k8s.io images ls
ctr -n moby images ls

Общее впечатление

Переход на dev-кластере прошёл без драм. Основные грабли были не в самом containerd, а в нашем же парсере логов - что, в общем-то, справедливо. Удалённая прослойка dockershim делает поведение системы немного прозрачнее: меньше движущихся частей, проще понять кто что делает при отладке.

crictl освоили за час - кто знает docker CLI, тот разберётся без чтения доки. Единственное, к чему надо привыкнуть: нет привычного docker inspect с полным JSON по контейнеру, crictl inspect выдаёт немного другую структуру.

В managed-сопровождении это пока изменение только для dev. На production-кластерах делаем то же самое, но аккуратнее - один кластер, одна нода за раз, с полным наблюдением за логами и метриками между шагами.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.