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

Kubernetes 1.12: RuntimeClass и gVisor - два runtime в одном кластере

Kubernetes 1.12 вышел в сентябре 2018 с RuntimeClass в alpha. Тестируем gVisor рядом с containerd на одном кластере: что работает, что нет, зачем вообще это нужно.

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

Kubernetes 1.12 GA (сентябрь 2018) - RuntimeClass, TTL Controller для завершённых объектов, улучшенный Audit Logging

Kubernetes 1.12 вышел в конце сентября, и в этот раз самое интересное для нас не в списке stable-фич. Главная тема - RuntimeClass: alpha-фича, которая позволяет запускать разные container runtime в одном кластере и явно указывать в Pod-манифесте, какой runtime использовать для конкретной нагрузки. Мы взяли это на тестирование в рамках managed-инфраструктуры - и провели несколько дней за экспериментами с gVisor рядом с обычными containerd-контейнерами.

Что такое RuntimeClass и зачем это нужно

До 1.12 у каждой ноды был один container runtime - тот, что настроен в kubelet. Если хочешь gVisor или kata-контейнеры на части нод - городи отдельный node pool с нодетейнтами и специальной конфигурацией kubelet, и надейся что поды с нужной нагрузкой прилетят именно туда. Гибкости ноль, операционная сложность растёт.

RuntimeClass меняет логику: runtime-ов на ноде может быть несколько (через разные обработчики containerd), а Pod указывает нужный через поле runtimeClassName. Scheduler ищет ноды где есть нужный runtime и планирует туда. Для планировщика это просто ещё один критерий размещения.

На практике это открывает конкретный сценарий, который нам интересен: запускать недоверенные или изолированные workload через gVisor (runsc), а обычные - через containerd со стандартным runc. В одном кластере, без лишних node pool-ов.

Как поднимали тестовый стенд

Стенд - три ноды: две под обычную нагрузку с containerd/runc, одна с containerd + gVisor-обработчик. На ноде с gVisor в конфиге containerd добавляем runtime-обработчик:

[plugins.cri.containerd.runtimes.runsc]
  runtime_type = "io.containerd.runsc.v1"

В Kubernetes создаём объект RuntimeClass:

apiVersion: node.k8s.io/v1alpha1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc

Дальше в Pod-манифесте:

spec:
  runtimeClassName: gvisor

На бумаге звучит просто. Реально потратили время на то, что gVisor требует специфической версии ядра - 4.14 или 4.9 с патчами. Наши ноды были на Ubuntu 16.04 с ядром 4.4, и runsc отказывался стартовать с малопонятной ошибкой в логах containerd. После апгрейда ядра до 4.15 через HWE-пакеты - заработало.

Что получили при тестировании

Изоляция работает. Процессы внутри gVisor-контейнера видят не хостовое ядро, а ядро-перехватчик (Sentry), который реализует syscall-интерфейс в пространстве пользователя. strace внутри контейнера честно показывает системные вызовы, но они идут к Sentry, а не к хосту. Классические трюки типа записи в /proc/sys хоста - не работают.

Производительность заметно ниже. Каждый системный вызов проходит через дополнительный слой - это не бесплатно. На вычислительно интенсивных задачах накладные расходы могут быть высокими. На I/O-bound нагрузке - заметно, но не критично. Мы не проводили формальный бенчмарк, но субъективно: задачи которые в runc выполняются за секунды, в gVisor растягиваются.

Не все приложения работают. gVisor реализует не весь syscall-интерфейс Linux - некоторые вызовы не поддерживаются или поддерживаются частично. Мы попробовали несколько типичных образов: nginx, python, postgres. Nginx и python - без проблем. Postgres упал при старте с ошибкой при попытке использовать O_DIRECT, что в gVisor не реализовано. Это ожидаемо для alpha, но надо проверять каждое приложение отдельно.

Планировщик работает как ожидается. Pod с runtimeClassName: gvisor планируется только на ноду где настроен runsc. Если такой ноды нет - Pod зависает в Pending с понятной причиной в событиях. Это удобно: нет риска что под с нужными требованиями молча запустится не там где нужно.

TTL Controller и Audit Logging: по делу, коротко

RuntimeClass забрал всё внимание, но 1.12 принёс ещё пару вещей.

TTL Controller (тоже alpha) - механизм автоматической очистки завершённых Job-ов и Pod-ов. Раньше completed Job-ы нужно было чистить вручную или через отдельные скрипты. Теперь у Job можно указать ttlSecondsAfterFinished, и контроллер сам удалит объект через нужное время. Мелочь, но в кластере с большим количеством периодических задач это реальное удобство - объекты не копятся.

Audit Logging получил более гибкую настройку политик. Раньше конфигурация аудита была довольно грубой, сложно было сделать «логируй всё в namespace X, но не надо засорять лог чтением configmap системными компонентами». В 1.12 политики стали точнее: можно фильтровать по resource, verb, namespace, пользователю и сочетаниям. На практике это означает что аудит-лог становится читаемым, а не потоком мусора который надо потом фильтровать в ELK.

Где это применимо прямо сейчас

gVisor в alpha и с ограниченным syscall-покрытием - не то что катишь в продакшн на следующей неделе. Но сценарий уже виден: multi-tenant среда, где разным клиентам или командам нужна разная степень изоляции. Обычные доверенные workload идут через runc - быстро и без накладных расходов. Нагрузка от внешних или менее доверенных источников - через gVisor, с жёсткой изоляцией от хоста.

Альтернатива которую использовали раньше - отдельные кластеры для разных уровней доверия. Это работает, но дорого операционно. RuntimeClass потенциально закрывает этот сценарий одним кластером.

Пока фиксируем: механизм рабочий, ограничения известны, на alpha-статус не обижаемся. Продолжаем смотреть - как раз ещё несколько workload хотим прогнать через runsc.

Контакт

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

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