Задача
Есть Job, который выгружает данные. Рядом с основным контейнером работает sidecar — прокси к базе данных:
apiVersion: batch/v1
kind: Job
metadata:
name: export
spec:
template:
spec:
restartPolicy: Never
containers:
- name: exporter
image: exporter:1.0 # отработает и завершится
- name: db-proxy
image: cloud-sql-proxy # работает вечноОсновной контейнер успешно отрабатывает за пару минут и завершается. Job при этом никогда не переходит в состояние Completed — под висит в Running, пока его не убьют вручную.
Вопрос: почему и как это правильно решается?
Что кажется очевидным
«Kubernetes должен понять, что главный контейнер закончил, и остановить остальные».
Логично, но исторически Kubernetes так не делал — и понимание почему объясняет целый пласт костылей в чужих манифестах.
Как это работает на самом деле
В поде нет понятия «главный контейнер». Все контейнеры в containers равноправны, и под считается завершённым, только когда завершились все они.
Sidecar-прокси не рассчитан на завершение — он работает бесконечно. Основной контейнер вышел, sidecar продолжает жить, под остаётся Running, Job не завершается. Классическая проблема, из-за которой годами писали костыли: общий том с файлом-флагом, обращение к shared process namespace, вызов эндпоинта завершения у прокси.
Init-контейнеры: другой механизм
initContainers работают иначе:
- запускаются последовательно, строго один за другим;
- каждый должен успешно завершиться, прежде чем стартует следующий;
- только после всех init-контейнеров стартуют обычные контейнеры;
- при падении init-контейнера под перезапускается (согласно
restartPolicy).
Типичное применение — дождаться зависимости, применить миграции, подготовить файлы в общем томе.
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ["sh", "-c", "until nc -z db 5432; do sleep 2; done"]Ограничение классических init-контейнеров: они не могут работать параллельно с основным. Для прокси, который нужен всё время жизни пода, это не подходило.
Нативные sidecar-контейнеры
Именно эту дыру закрыли нативные sidecar’ы. Механизм элегантный: sidecar объявляется как init-контейнер с restartPolicy: Always.
apiVersion: batch/v1
kind: Job
metadata:
name: export
spec:
template:
spec:
restartPolicy: Never
initContainers:
- name: db-proxy
image: cloud-sql-proxy
restartPolicy: Always # ← делает его sidecar'ом
containers:
- name: exporter
image: exporter:1.0Что это даёт:
- sidecar стартует раньше основных контейнеров и продолжает работать параллельно;
- Kubernetes не ждёт его завершения, определяя готовность пода;
- когда основные контейнеры отработали, sidecar останавливается автоматически — Job корректно переходит в
Completed; - при завершении пода sidecar’ы останавливаются после основных контейнеров — то есть прокси доступен до самого конца.
Последний пункт решает и вторую известную боль: раньше при завершении пода прокси мог умереть раньше приложения, и приложение теряло связь с базой в момент shutdown.
Ответ
Под висит потому, что в поде нет главного контейнера — он завершается лишь когда завершились все контейнеры, а sidecar-прокси не завершается никогда.
Правильное решение — объявить прокси нативным sidecar’ом: init-контейнер с restartPolicy: Always. Тогда Kubernetes сам остановит его после отработки основного контейнера.
Что с этим делать
- Для Job с sidecar’ами используйте нативные sidecar’ы. Это единственный способ без костылей. Убедитесь, что версия кластера их поддерживает (механизм появился в относительно недавних релизах и стабилизировался постепенно).
- Порядок запуска теперь управляем. Sidecar стартует до основного контейнера — это важно для прокси, агентов service mesh и сборщиков логов: приложение не начнёт работу раньше, чем инфраструктурный контейнер будет готов.
- Задавайте sidecar’ам probe. У нативных sidecar’ов работают probe: пока sidecar не готов, основные контейнеры не стартуют.
- Init-контейнеры — для подготовки, sidecar’ы — для сопровождения. Если задача должна отработать один раз до старта — это init. Если работать всё время — это sidecar.
- Не забывайте про ресурсы sidecar’ов. Они учитываются в общей заявке пода и влияют на планирование и QoS-класс (см. задачу 3). Sidecar без
requestsиспортит класс всего пода. - Если нативные sidecar’ы недоступны (старый кластер), для Job остаются старые приёмы: общий том с файлом-флагом,
shareProcessNamespaceили обращение к эндпоинту завершения прокси. Все они хрупкие — это аргумент за обновление кластера.
Дальше: Задача 12. Rolling update: maxSurge и maxUnavailable.