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.