Под в Kubernetes редко состоит из одного контейнера. Рядом с основным приложением почти всегда крутится что-то ещё: то контейнер, который заранее накатывает миграции базы, то прокси, через который идёт весь трафик, то агент сбора логов. Долгое время разработчики путали два совершенно разных по природе вспомогательных контейнера и страдали от того, что Kubernetes не давал управлять порядком их завершения. Под уходил в небытие, а спутник умирал раньше основного приложения, обрывая ему связь. Разберём, чем контейнер подготовки отличается от контейнера-спутника, какие задачи решает каждый и как новый встроенный механизм наконец починил порядок завершения.
Чем отличаются обычные, подготовительные и постоянные контейнеры
Сначала надо развести три типа контейнеров, потому что путаница между ними и порождает проблемы. У каждого пода обязан быть хотя бы один обычный контейнер. Когда обычных контейнеров несколько, они стартуют и работают одновременно, и при завершении какого-то из них вступает в силу единая для всего пода политика перезапуска.
Контейнер подготовки устроен иначе. Он выполняется до запуска основных контейнеров и должен отработать до конца, после чего завершается. Контейнеры подготовки запускаются строго последовательно в порядке их объявления: пока один не завершился успешно, следующий не стартует. Это идеально для разовых задач перед стартом приложения: накатить миграции схемы базы, дождаться готовности зависимого сервиса, скачать конфигурацию.
А вот контейнер-спутник нужен для совсем другого. Он должен стартовать до основного приложения, жить ровно столько же, сколько живёт приложение, и завершаться вместе с ним. Классический пример это прокси сервисной сети, через который идёт весь трафик основных контейнеров. Он обязан подняться раньше, чем основные контейнеры начнут принимать трафик, и завершиться позже, чем они допередадут остатки. Эти требования не ложатся ни в модель обычного контейнера, ни в модель контейнера подготовки.
Почему спутники долго были болью
До определённого момента спутники в Kubernetes изображали обычными контейнерами, и это рождало целый ворох проблем. У обычных контейнеров нет гарантии порядка запуска, поэтому нельзя было надёжно сказать, что прокси поднимется раньше приложения. Политика перезапуска одна на весь под, поэтому нельзя было задать спутнику отдельное поведение. А главное, обычный контейнер-спутник не давал поду завершиться: если приложение это разовая задача, которая отработала и закончилась, висящий спутник не давал поду считаться завершённым.
Инженерам приходилось городить костыли и самописные скрипты. Спутник заворачивали в логику ожидания основного контейнера, придумывали общие тома с файлами-флажками, писали обёртки, которые следили за жизнью соседа. Всё это работало нестабильно и плохо переживало граничные случаи. Особенно болезненным был именно порядок завершения: при остановке пода порядок завершения служб оказывался случайным, что ломало предсказуемость, например у обработчиков задач со спутниками.
Как новый встроенный механизм решил проблему
В версии 1.28 появился встроенный механизм спутников, и решение оказалось неожиданно элегантным. Удивительный факт: в эту версию не добавили ни нового типа контейнера, ни отдельного поля для спутников. Так называемые встроенные спутники это те же самые контейнеры подготовки, но с новым свойством, меняющим их поведение до неузнаваемости.
Суть в новом поле политики перезапуска, которое можно задать только контейнеру подготовки, и единственное допустимое значение это постоянный перезапуск. Любой контейнер подготовки превращается в спутник добавлением этого свойства.
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
spec:
initContainers:
- name: init-migrations # обычный контейнер подготовки
image: migrations:1.0
- name: istio-proxy # а это уже спутник
image: istio/proxyv2:1.20
restartPolicy: Always
containers:
- name: myapp
image: myapp:1.0
Здесь контейнер миграций это классический контейнер подготовки, который отработает и завершится перед стартом приложения. А прокси с политикой постоянного перезапуска становится спутником. На первый взгляд решение через переиспользование контейнеров подготовки кажется странным, но за ним стоит продуманный и устойчивый к будущим изменениям выбор.
Что именно меняет постоянный перезапуск
Добавление этого свойства меняет поведение контейнера подготовки кардинально. Контейнер перезапускается, если завершился, что повышает устойчивость: упавший спутник поднимется сам. Все последующие контейнеры подготовки стартуют сразу после успешного прохождения пробы запуска спутника, а не ждут его завершения, ведь он и не завершится, пока живёт приложение. Ресурсы спутника теперь добавляются к сумме запросов основных контейнеров при расчёте потребления пода.
И вот ключевое для нашей темы. Спутник стартует до всех обычных контейнеров, потому что он по своей природе контейнер подготовки, и завершается после того, как завершились все обычные контейнеры. Это и есть починка порядка завершения. Прокси сервисной сети теперь гарантированно поднимается первым и умирает последним, давая приложению допередать трафик перед своей смертью.
При этом завершение пода по-прежнему зависит только от основных контейнеров. Спутник с постоянным перезапуском не мешает поду завершиться после выхода основных контейнеров. Это решает старую боль: разовая задача со спутником теперь корректно завершается, спутник не держит под вечно живым.
Как выглядит реальный порядок завершения
Стоит разобрать порядок завершения по шагам, потому что именно здесь раньше царил хаос. Когда основной контейнер отрабатывает и завершается, запускается процесс завершения пода. Сначала срабатывают хуки перед остановкой у всех спутников. Затем спутники завершаются в порядке, обратном их объявлению: последний объявленный гасится первым.
Порядок завершения пода со спутниками:
1. основной контейнер завершается
2. срабатывают хуки перед остановкой у спутников
3. спутники гасятся в обратном порядке объявления
4. сигнал завершения приходит каждому только после его хука
Этот обратный порядок логичен. Если спутник-2 зависит от спутника-1, и спутник-1 объявлен раньше, то при завершении первым гасится спутник-2, который зависит от первого, а спутник-1 доживает до конца, как и положено зависимости. Сигнал завершения приходит контейнеру только после того, как отработал его хук перед остановкой, что даёт спутнику корректно закрыть соединения и допередать данные.
Когда что выбирать на практике
Теперь к практическим сценариям. Контейнер подготовки выбирают для всего, что нужно сделать один раз перед стартом приложения и что должно гарантированно завершиться: миграции базы, ожидание готовности зависимостей, скачивание секретов или конфигурации, подготовка общих томов. Раз отработал и ушёл, освободив место.
Спутник выбирают для всего, что должно жить параллельно с приложением весь его срок: прокси сервисной сети, агенты сбора логов и метрик, контейнеры обновления конфигурации на лету, локальные кэши. Главные свойства, делающие контейнер подготовки идеальным спутником, складываются в три пункта. У контейнеров подготовки есть чёткий порядок запуска, поэтому спутник гарантированно стартует раньше объявленных после него контейнеров. Спутники не продлевают жизнь пода, поэтому их можно использовать в короткоживущих подах без изменения их жизненного цикла. И спутники перезапускаются при падении, что повышает устойчивость и позволяет основным контейнерам надёжнее на них полагаться.
Стоит, однако, помнить, что шаблон со спутником при всей его полезности обычно не предпочтительный подход по умолчанию: его берут, когда сценарий действительно того требует, а не лепят к каждому поду.
Какая зрелость у механизма и что учесть
Механизм прошёл путь от экспериментального в версии 1.28 до полностью готового к промышленному использованию в версии 1.33. Это значит, что на свежих кластерах им можно пользоваться смело, а на более старых стоит проверить версию и при необходимости включить соответствующий флаг возможности.
Реальные системы уже переезжают на встроенные спутники именно ради порядка завершения. Показательный случай это обработчик задач, у которого спутники-службы при остановке гасились в случайном порядке, ломая предсказуемость планирования. Решение оказалось прямым: перевести службы в контейнеры подготовки со свойством спутника, обеспечив упорядоченное завершение и устранив случайность порядка остановки. Так старая боль превратилась в штатную возможность платформы.
Какой стратегии придерживаться
Различие между двумя типами вспомогательных контейнеров перестаёт путать, как только запоминаешь главное правило. Если задача разовая и должна завершиться до старта приложения, это контейнер подготовки. Если задача должна жить параллельно с приложением и корректно завершиться вместе с ним, это спутник, то есть контейнер подготовки со свойством постоянного перезапуска. Раньше второй случай требовал костылей с обычными контейнерами и страдал от случайного порядка завершения. Теперь встроенный механизм даёт и гарантированный порядок старта, и завершение спутника после основного приложения, и автоматический перезапуск при падении, и отсутствие блокировки завершения пода.
Тот, кто по привычке лепит спутники обычными контейнерами, рискует получить случайный порядок завершения и оборванные соединения в момент остановки пода. А тот, кто пользуется встроенным механизмом, получает предсказуемый жизненный цикл: прокси и агенты поднимаются первыми, верно служат всё время жизни приложения и гасятся последними, аккуратно завершив свою работу. Маленькое свойство в манифесте превращает многолетнюю архитектурную боль в штатное и надёжное поведение.