Learning Platform
Глоссарий Troubleshooting
Урок 12.03 · 25 мин
Продвинутый
sidecarv1.33restartPolicy AlwaysJob problemlog shipperservice mesh

Sidecar pattern: классический и native (GA v1.33)

Sidecar — это контейнер, дополняющий main приложение функциональностью, которая полезна, но не входит в core ответственность app. Канонические примеры: log shipper (fluent-bit, fluentd), metrics exporter (node_exporter), service mesh proxy (Envoy, Linkerd-proxy), secret rotator (Vault Agent). Долгое время sidecar в Kubernetes не имел отдельной формы — это был просто container в spec.containers. Эволюция нативной поддержки: alpha v1.28, beta v1.29 (default on), GA v1.33 — через spec.initContainers с restartPolicy: Always. Это разрешило целый класс старых проблем.


Свой systemd-сервис: пишем .service unit для Python ETL daemon

Классический sidecar: до native sidecar API

# Старый, "классический" способ
apiVersion: v1
kind: Pod
metadata:
  name: app-classic
spec:
  containers:
  - name: app
    image: my-app:v1
    volumeMounts:
    - mountPath: /var/log/app
      name: logs
  - name: log-shipper                # sidecar — просто рядом с main
    image: fluent/fluent-bit:3.0
    volumeMounts:
    - mountPath: /var/log/app
      name: logs
  volumes:
  - name: logs
    emptyDir: {}

И app, и log-shipper — это spec.containers. kubelet их стартует одновременно, никакого ordering. Они оба должны быть Ready чтобы Pod был Ready. Если log-shipper падает — он рестартится по restartPolicy Pod (как обычный контейнер).

Звучит просто. Но в этом подходе есть три фундаментальных проблемы.


Проблема 1: Race conditions на старте

main и sidecar стартуют параллельно. Если main критически зависит от sidecar (например, не может работать без Envoy proxy), это race:

  • main стартует, пытается резолвить localhost:15001 → proxy ещё не bind на порт → connect refused → main падает → restart loop.
  • Envoy в service mesh — главный пострадавший. Istio до v1.18 решал это через holdApplicationUntilProxyStarts (специальная аннотация и init container), но это workaround.
Race condition на старте
t=0kubelet стартует ВСЕ containers одновременно. main app и envoy proxy получают команду start.
t=1msmain app быстро инициализируется, делает первый HTTP запрос к localhost:15001 (envoy proxy port). Envoy ещё не закончил bootstrap — не bind на порт.
t=1msEnvoy proxy всё ещё bootstrapping: грузит конфигурацию, dialing pilot, инициализирует listeners. Bind на порт ещё не произошёл.
resultConnection refused. Если app не обрабатывает retry — выходит с ошибкой. CrashLoopBackOff. Если обрабатывает — N секунд пользовательского запроса теряются.

Проблема 2: Job не завершается

Самая болезненная проблема классического sidecar. Возьмём Job, который выполняет batch-задачу и имеет sidecar для отправки метрик/логов:

apiVersion: batch/v1
kind: Job
metadata:
  name: nightly-etl
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: etl
        image: my-etl:v1
        command: ['python', 'etl.py']
      - name: log-shipper
        image: fluent/fluent-bit
        # этот процесс никогда не завершается сам

main etl завершается успешно (exit 0). Но log-shipper — это long-running процесс. Он не знает, что main закончил. Он продолжает работать. И Pod никогда не переходит в Completed, потому что в spec.containers есть running контейнер. Job висит в фазе Running вечно.

Workarounds в старой эре: pre-stop hook на sidecar, который читает sentinel-файл от main; shared process namespace + kill из main; periodic checks через cron. Все они хрупкие.

DANGER

Это главная причина, по которой native sidecar важен. Если у вас есть Job с классическим sidecar (логи, метрики, mesh proxy) — он зависает. С native sidecar — main exits → kubelet shuts down sidecars → Pod Completed. На CKAD точно ожидайте вопрос про это.


Проблема 3: Graceful shutdown без ordering

При delete Pod kubelet шлёт SIGTERM всем containers одновременно. Что происходит:

  • main app получает SIGTERM, начинает доплоштать запросы и логи в файл;
  • log-shipper получает SIGTERM одновременно и решает graceful exit — flush buffers и stop;
  • main пишет последний батч логов в файл → но log-shipper уже остановился → последние логи теряются.

В service mesh симметрично: Envoy умирает раньше, чем main, и последние запросы не уходят через mesh.


Native sidecar containers: решение

История фичи: alpha в v1.28 (декабрь 2023), beta v1.29 — default on (январь 2024), GA v1.33 (апрель 2025). Механизм: контейнер объявляется в spec.initContainers с дополнительным полем restartPolicy: Always. kubelet распознаёт его как sidecar и применяет специальную логику.

apiVersion: v1
kind: Pod
metadata:
  name: app-native-sidecar
spec:
  initContainers:
  - name: log-shipper
    image: fluent/fluent-bit:3.0
    restartPolicy: Always          # это и делает sidecar
    volumeMounts:
    - mountPath: /var/log/app
      name: logs
    startupProbe:
      httpGet:
        path: /healthz
        port: 2020
  containers:
  - name: app
    image: my-app:v1
    volumeMounts:
    - mountPath: /var/log/app
      name: logs
  volumes:
  - name: logs
    emptyDir: {}

Что kubelet делает с native sidecar

Sidecar GA lifecycle
1. Startkubelet стартует sidecar containers ПЕРВЫМИ, в порядке объявления в initContainers. Обычные init containers (без restartPolicy: Always) выполняются sequential. Sidecar — стартует и не блокирует следующий init.
2. Ready gatekubelet ждёт, пока sidecar пройдёт startupProbe (если задан). Только после успеха стартуют main containers. Это гарантирует ordering: Envoy готов до того как app послал первый запрос.
3. Main startПосле Ready всех sidecars kubelet стартует main containers. Они работают параллельно с sidecars весь lifecycle.
4. RuntimeЕсли sidecar падает в течение жизни Pod — kubelet перезапускает его (restartPolicy: Always). Main продолжает работать. Без native sidecar restartPolicy Pod применялся ко всем containers, что давало меньше контроля.
5. TerminationПри завершении main containers (Job completed или Pod delete): kubelet ждёт graceful shutdown main → потом отправляет SIGTERM sidecars. Sidecars завершаются последними. Это решает Job problem и graceful shutdown ordering.

Job problem решён

Native sidecar разрешает Job problem напрямую:

apiVersion: batch/v1
kind: Job
metadata:
  name: nightly-etl
spec:
  template:
    spec:
      restartPolicy: Never
      initContainers:
      - name: log-shipper
        image: fluent/fluent-bit
        restartPolicy: Always          # native sidecar
      containers:
      - name: etl
        image: my-etl:v1
        command: ['python', 'etl.py']

etl завершается → kubelet видит «все main containers Completed» → отправляет SIGTERM в log-shipper → log-shipper flushes и exits → Pod Completed → Job Completed.


YAML differences: классический vs GA

# Классический sidecar (работает, но с lifecycle проблемами)
spec:
  containers:
  - name: app
    image: my-app
  - name: log-shipper                # просто рядом
    image: fluent-bit

# Native sidecar (GA v1.33, рекомендованный способ)
spec:
  initContainers:
  - name: log-shipper                # в initContainers
    image: fluent-bit
    restartPolicy: Always            # ключевое поле
  containers:
  - name: app
    image: my-app

Внешне разница в одном поле. По поведению — это разные категории контейнеров с разным lifecycle.

TIP

Для CKAD v1.35 знайте оба способа и их отличия. На реальных production кластерах используйте native sidecar везде, где возможно, особенно для Job и CronJob.


Канонические sidecar use cases

Use caseSidecarЧто делает
Логиfluent-bit, fluentdчитает app log file, шлёт в Loki/ES
Метрикиprometheus exporterсобирает app metrics, exposes /metrics
Service meshEnvoy, Linkerd-proxyпроксирует весь in/out трафик, mTLS
SecretsVault Agentrefresh секретов из Vault в emptyDir
BackupDB dumperperiodic dump БД в S3
Health/Lifecycletermination handlercatch shutdown signals, drain connections

Resources в sidecar GA

В отличие от обычных init containers, native sidecar считается «работающим параллельно с main». Поэтому его resource requests суммируются с main containers в effective Pod request:

effective = max(
  max(regular_init_i.request),
  sum(sidecar_i.request) + sum(main_container_j.request)
)

Это логично: sidecar реально жрёт ресурсы всё время жизни Pod, scheduler должен это учесть.


Проверка знанийKnowledge check
Чем native sidecar (GA v1.33) синтаксически отличается от обычного init container?
ОтветAnswer
Полем restartPolicy: Always в самом контейнере. Native sidecar объявляется в spec.initContainers (как и обычный init), но имеет restartPolicy: Always — это говорит kubelet, что это long-running контейнер, который должен работать параллельно с main и быть остановлен только когда main завершатся. Обычный init container не указывает restartPolicy и должен завершиться exit 0, прежде чем main стартует.
Проверка знанийKnowledge check
Job с классическим sidecar (fluent-bit в spec.containers) — main завершился. Что происходит?
ОтветAnswer
Pod зависает в Running, Job не Completed. fluent-bit — long-running процесс, он не знает, что main завершился, и продолжает работать. В spec.containers есть Running контейнер — Pod не может перейти в Completed. Это классическая Job problem. Решается через native sidecar (GA v1.33): fluent-bit в spec.initContainers с restartPolicy: Always — при завершении main kubelet шлёт SIGTERM в sidecars.
Проверка знанийKnowledge check
Pod падает в CrashLoopBackOff. main app ругается на refused connection к localhost:15001. Envoy proxy в Pod есть. Что происходит и как решить через native sidecar?
ОтветAnswer
Race condition на старте: kubelet стартует app и envoy параллельно (классический способ), app быстро инициализируется и стучится в localhost до того, как envoy bind на порт → connect refused → app crash. Решение через native sidecar: вынести envoy в spec.initContainers с restartPolicy: Always и задать startupProbe. kubelet дождётся Ready envoy, потом стартует app — гарантированный ordering, race исключён.
Проверка знанийKnowledge check
Какие resource requests эффективно резервируются для Pod, если есть 1 native sidecar (requests memory 200Mi) и 1 main container (300Mi)?
ОтветAnswer
500Mi. В отличие от обычных init containers, native sidecar (restartPolicy: Always в initContainers) считается работающим параллельно с main — его requests суммируются: sum(sidecar) + sum(main) = 200 + 300 = 500Mi. Если бы это был обычный init container — было бы max(200, 300) = 300Mi.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Job с классическим sidecar (fluent-bit в spec.containers). Main 'etl' завершился exit 0. Что происходит с Pod и Job?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5