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-кластерах делаем то же самое, но аккуратнее - один кластер, одна нода за раз, с полным наблюдением за логами и метриками между шагами.