Learning Platform
Глоссарий Troubleshooting
Урок 06.04 · 25 мин
Продвинутый
JobCronJobBatch processingcompletionsparallelismIndexed jobSchedule

Job и CronJob: разовые и периодические задачи

Deployment, ReplicaSet, DaemonSet — это workloads, которые работают бесконечно: их Pod-ы должны быть всегда live. Но в production масса задач — другие: миграция БД при деплое, обработка batch-пачки данных, бэкап раз в сутки, тренировка ML-модели. Это конечные задачи: запускается, отрабатывает, заканчивается с exit code, и больше её делать не нужно (или нужно по расписанию).

Для этого в Kubernetes есть два API-объекта: Job (одноразовая задача) и CronJob (Job по расписанию). На CKAD они входят в обязательный scope и часто попадают в задания.


crontab: расписание задач в Unix-стиле

Job: одноразовая задача

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 3
  activeDeadlineSeconds: 600
  ttlSecondsAfterFinished: 3600
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: migrate
          image: myapp/migrate:1.0
          command: ["./migrate.sh"]

Поля:

  • spec.completions — сколько успешных завершений Pod-ов нужно (default 1). Job завершается когда completions Pod-ов вышли с exit code 0.
  • spec.parallelism — сколько Pod-ов параллельно запускать одновременно (default 1).
  • spec.backoffLimit — после стольких подряд неудачных Pod-ов (failures) Job помечается Failed (default 6).
  • spec.activeDeadlineSeconds — timeout на ВЕСЬ Job. По истечении — Job terminated, все Pods убиты.
  • spec.ttlSecondsAfterFinished — через сколько секунд после finishing (Complete или Failed) удалить Job вместе с Pods.
  • spec.template — Pod template. restartPolicy ОБЯЗАТЕЛЬНО Never или OnFailure.
NOTE

Job-у нельзя restartPolicy: Always. Always означает “контейнер должен работать вечно”, что противоречит самой идее Job. API server отклонит такую спецификацию.


restartPolicy: Never vs OnFailure

Эти две политики дают очень разное поведение при сбое.

Что происходит при failure контейнера
OnFailureЕсли контейнер вышел с non-zero exit code — kubelet рестартит ЕГО в том же Pod-объекте. Pod не пересоздаётся. Счётчик .status.containerStatuses[0].restartCount растёт. Job controller считает это одной попыткой Pod-а, но видит, что Pod ещё жив.
NeverЕсли контейнер вышел с non-zero — kubelet помечает Pod как Failed и НЕ рестартит контейнер. Job controller видит failed Pod, создаёт НОВЫЙ Pod (с новым именем и UID) — это +1 к backoffLimit.

OnFailure:

  • Pros: один Pod, история retries в restartCount контейнера, экономия — нет создания новых Pod-объектов
  • Cons: ресурсы Pod-а (node assignment, volumes) фиксированы — если нода умерла, перезапуск контейнера ничего не даст

Never:

  • Pros: каждый retry — полноценный новый Pod, может попасть на другую ноду
  • Cons: больше нагрузки на API server (каждый retry — new Pod object), volumes пересоздаются

В большинстве случаев лучше Never — даёт более robust retry. OnFailure имеет смысл когда контейнер делает что-то быстрое, что хочется ретраить без нагрузки на scheduler.


backoffLimit и exponential delay

Если Pod падает (контейнер failed, и для Never создан новый Pod, и он тоже failed) — это считается одной неудачей. spec.backoffLimit (default 6) — после стольких неудач Job помечается Failed и больше попыток не будет.

Между retries — exponential backoff: 10s, 20s, 40s, 80s, …, capped at 6 minutes (360s). Это защита от ситуации “сломанный Pod рестартится бесконечно быстро и забивает API server”.

# Смотрим как Job деградирует
kubectl get jobs -w

# NAME           COMPLETIONS   DURATION   AGE
# bad-job        0/1                       3s
# bad-job        0/1           10s         13s  # ← после 10s second retry
# bad-job        0/1           40s         50s  # ← после 30s third retry (10+20)
# ...
# Дальше Job переходит в Failed после backoffLimit неудач

activeDeadlineSeconds: hard timeout

spec:
  activeDeadlineSeconds: 600

Это абсолютный timeout на весь Job. Через 600 секунд после старта Job (независимо от того, сколько retries осталось по backoffLimit) controller помечает Job Failed с reason DeadlineExceeded и убивает все live Pods.

Используется для long-running задач, которые в норме должны завершиться за известное время — если они зависли, лучше убить и поднять алерт.

activeDeadlineSeconds побеждает над backoffLimit: даже если backoffLimit ещё не исчерпан, deadline режет.


podFailurePolicy: тонкий контроль над failures

Default-поведение Job — любой failed Pod увеличивает счётчик backoffLimit. Это грубо: retry на network blip оправдан, retry на «invalid input» (exit 2) — пустая трата ресурсов.

podFailurePolicy (GA с v1.31) разрешает решать действие по конкретному exit code или Pod condition:

spec:
  backoffLimit: 6
  podFailurePolicy:
    rules:
    - action: FailJob          # сразу пометить Job Failed, не делать retry
      onExitCodes:
        containerName: main
        operator: In
        values: [42]           # exit 42 — bad input, retry бессмыслен
    - action: Ignore           # не считать в backoffLimit
      onPodConditions:
      - type: DisruptionTarget # preemption / node drain — не вина workload

Действия:

  • FailJob — Job сразу Failed, retry прекращается.
  • Ignore — Pod не учитывается в backoffLimit (как будто failure не было).
  • Count — обычный учёт (default-поведение).

Особенно полезно с restartPolicy: Never и spot/preemptible nodes — DisruptionTarget condition освобождает Job от ложных счётчиков.


completions и parallelism: распараллеливание

Эти два параметра вместе определяют шаблон параллельной обработки.

spec:
  completions: 10
  parallelism: 3

Это значит: “нужно 10 успешных Pod-ов, можно запускать одновременно не больше 3-х”. Controller держит 3 активных Pod, при successful completion одного запускает следующий, пока не будет 10 successful.

Шаблоны параллельной обработки Job
completions=1, parallelism=1Простой single-shot Job. Один Pod до успеха. Это default. Use case: DB migration, single batch processing.
completions=N, parallelism=1Sequential — Pods запускаются один за другим, до N успехов. Use case: пошаговая обработка, где порядок важен.
completions=N, parallelism=MParallel с pool size = M. Используется для work queue паттерна: M воркеров параллельно дёргают задачи из очереди, в сумме нужно N успехов.
completions=None, parallelism=MWorker pool без явного N. Pods работают пока сами не решат, что очередь пуста — выходят с exit 0. Job завершается когда последний воркер закончил.

completions: null (не указано вообще) с parallelism > 1 — особый режим: Pods продолжают создаваться пока хотя бы один не вернёт exit 0. Подразумевается, что Pods сами координируются через external queue.


Indexed Job: каждый Pod знает свой индекс

С Kubernetes v1.24 (GA) появился режим completionMode: Indexed — для batch processing с partitioning.

spec:
  completions: 10
  parallelism: 3
  completionMode: Indexed
  template:
    spec:
      containers:
        - name: worker
          image: myapp/worker:1.0
          env:
            - name: JOB_COMPLETION_INDEX
              valueFrom:
                fieldRef:
                  fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']

В этом режиме:

  • Каждый Pod получает уникальный индекс 0..completions-1
  • Индекс доступен в env JOB_COMPLETION_INDEX и в hostname <job-name>-<index>-<random>
  • Controller отслеживает completions ПО ИНДЕКСАМ — для успеха нужно чтобы КАЖДЫЙ индекс 0..N-1 завершился успешно
  • Если индекс 3 упал — будет retry для индекса 3, а не просто очередной random Pod
TIP

Indexed Job — идеален для data parallel вычислений. Если у вас 10 шардов БД и нужно сделать бекап каждого — задаёте completions=10, indexed, и каждый Pod по своему JOB_COMPLETION_INDEX знает, какой шард он обрабатывает. Если упал один — будет retry только этого шарда.


ttlSecondsAfterFinished: автоматическая очистка

spec:
  ttlSecondsAfterFinished: 3600

Через 3600 секунд после того как Job вошёл в финальное состояние (Complete или Failed), специальный TTL controller удалит Job. Cascade удалит и все его Pod-ы.

Без этого Jobs накапливаются в кластере, забивая etcd, особенно если у вас CronJob — каждые 5 минут создаётся новый Job.

ttlSecondsAfterFinished: 0 — удалить сразу после завершения (лог можно прочитать только если кто-то успел).


Полный пример Job

apiVersion: batch/v1
kind: Job
metadata:
  name: nightly-report
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 2
  activeDeadlineSeconds: 1800   # 30 минут максимум
  ttlSecondsAfterFinished: 86400  # удалить через сутки
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: report
          image: myapp/report-generator:2.5
          command: ["./generate-nightly-report.sh"]
          env:
            - name: REPORT_DATE
              value: "yesterday"
          resources:
            requests:
              cpu: "1"
              memory: "1Gi"
            limits:
              cpu: "2"
              memory: "2Gi"
# Imperative create
kubectl create job nightly-report --image=myapp/report-generator:2.5

# Создать Job из CronJob template (для отладки)
kubectl create job manual-test --from=cronjob/nightly-report

# Статус
kubectl get jobs
# NAME              COMPLETIONS   DURATION   AGE
# nightly-report    1/1           42s        1m

# Логи
kubectl logs -l job-name=nightly-report

# Подробности
kubectl describe job/nightly-report

CronJob: Job по расписанию

CronJob — это шаблон, который создаёт Job-ы по расписанию. API группа та же batch/v1.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-report
spec:
  schedule: "0 2 * * *"
  timeZone: "Europe/Moscow"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  startingDeadlineSeconds: 300
  jobTemplate:
    spec:
      backoffLimit: 2
      ttlSecondsAfterFinished: 86400
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: report
              image: myapp/report-generator:2.5
              command: ["./generate.sh"]

Поля:

  • spec.schedule — cron-выражение в формате минута час день-месяца месяц день-недели
  • spec.timeZone — timezone для расписания (с v1.24+ — раньше всегда UTC)
  • spec.jobTemplate — template Job, который будет создаваться при каждом тике
  • spec.concurrencyPolicy — что делать если предыдущий Job ещё не закончил
  • spec.successfulJobsHistoryLimit (default 3) — сколько completed Jobs хранить
  • spec.failedJobsHistoryLimit (default 1) — сколько failed Jobs хранить
  • spec.startingDeadlineSeconds — за сколько секунд позже scheduled time всё ещё можно стартовать
  • spec.suspend — если true, CronJob не создаёт новые Jobs (но существующие — продолжаются)

Cron syntax: гайд

 ┌───────────── минута (0–59)
 │ ┌───────────── час (0–23)
 │ │ ┌───────────── день месяца (1–31)
 │ │ │ ┌───────────── месяц (1–12)
 │ │ │ │ ┌───────────── день недели (0–6, 0=воскресенье)
 │ │ │ │ │
 * * * * *

Примеры:

"0 2 * * *"      каждый день в 02:00
"*/15 * * * *"   каждые 15 минут
"0 */6 * * *"    каждые 6 часов (00:00, 06:00, 12:00, 18:00)
"0 9 * * 1-5"    в 09:00 в будни (пн-пт)
"30 23 1 * *"    в 23:30 1-го числа каждого месяца
"@hourly"        ярлык — каждый час (=0 * * * *)
"@daily"         каждый день в полночь (=0 0 * * *)
"@weekly"        каждое воскресенье в 00:00

Ярлыки @yearly, @monthly, @weekly, @daily, @hourly поддерживаются.


timeZone: killer момент

До Kubernetes v1.24 все CronJob-ы исполнялись в UTC, независимо от того, какой часовой пояс на нодах. Это была распространённая ловушка: ставишь schedule: "0 2 * * *" ожидая 02:00 Москвы, а получаешь 02:00 UTC = 05:00 Москвы.

С v1.24 (alpha) и v1.27 (GA) есть поле spec.timeZone:

spec:
  schedule: "0 2 * * *"
  timeZone: "Europe/Moscow"

Это запускает в 02:00 МСК независимо от того, где работает control plane. Список валидных TZ — стандартная IANA TZDB: Europe/Moscow, America/New_York, Asia/Tokyo, UTC.

WARNING

Если в кластере < v1.27 — timeZone либо игнорируется (старая версия), либо нужен feature gate CronJobTimeZone. Проверьте версию кластера прежде чем полагаться на это поле. Для совместимости со старыми версиями лучше явно считать smещение и писать schedule в UTC.


concurrencyPolicy: что если предыдущий Job ещё работает

CronJob тикает по расписанию. Что если в момент тика предыдущий Job ещё не закончился?

Три политики конкуренции
Allow (default)Создать новый Job независимо от того, что предыдущий ещё работает. Может привести к race condition или дублированной работе. Use case: задачи, которые safe для параллельного выполнения.
ForbidЕсли предыдущий Job ещё активен — пропустить текущий тик. Новый Job НЕ создаётся. Use case: backup, миграции, любая работа которая не должна перекрываться.
ReplaceУбить предыдущий Job (он переходит в Failed) и стартовать новый. Use case: отчёт по последним данным — старая работа уже неактуальна.

Для большинства случаев — Forbid. Это самая безопасная стратегия.


startingDeadlineSeconds: что если controller лагал

Если CronJob controller не работал в момент scheduled time (например, control plane был недоступен), а потом восстановился — CronJob может попытаться “догнать” пропущенные тики.

startingDeadlineSeconds ограничивает это окно. Если scheduled time был N секунд назад, и N > deadline, CronJob НЕ запустит этот пропущенный Job.

spec:
  schedule: "0 * * * *"
  startingDeadlineSeconds: 600   # 10 минут

Если scheduled time был в 14:00, а контроллер пришёл в себя только в 14:15 — этот Job НЕ создастся (опоздание 15 мин > deadline 10 мин). Если бы пришёл в 14:08 — создался бы.

Без startingDeadlineSeconds есть особый дефолт: если controller видит >100 пропущенных тиков подряд, он перестаёт их догонять и логирует error. Это защита от “штормов”.


history limits

spec:
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1

CronJob хранит последние N completed/failed Job-объектов (с их Pods). Это упрощает дебаг — можно посмотреть, что было в прошлый раз.

kubectl get jobs -l app=nightly-report
# NAME                        COMPLETIONS   DURATION   AGE
# nightly-report-28567890     1/1           42s        1d
# nightly-report-28568970     1/1           45s        1h
# nightly-report-28569540     1/1           38s        1m

# Логи последнего Job
kubectl logs -l job-name=nightly-report-28569540

kubectl: imperative создание

CKAD часто требует быстро создать Job/CronJob. Imperative команды экономят время:

# Job из image
kubectl create job hello --image=busybox:1.36 -- echo hello

# Job из CronJob (для ручного запуска)
kubectl create job manual-run --from=cronjob/nightly-report

# CronJob
kubectl create cronjob hello \
  --schedule="*/5 * * * *" \
  --image=busybox:1.36 \
  -- echo "hello at $(date)"

# Сгенерировать YAML без создания
kubectl create cronjob hello \
  --schedule="0 * * * *" \
  --image=busybox \
  -- /bin/sh -c "echo hello" \
  --dry-run=client -o yaml

Проверка знанийKnowledge check
CronJob с schedule '0 2 * * *' без указания timeZone был создан в кластере Kubernetes v1.23. В Европе/Москве администратор ожидает запусков в 02:00 локального времени, но они происходят в 05:00 МСК. Почему и как исправить?
ОтветAnswer
Потому что до v1.24 (и без явного timeZone) CronJob controller всегда интерпретирует schedule в UTC, независимо от часового пояса нод или control plane. В Москве (UTC+3) 02:00 UTC = 05:00 МСК — отсюда расхождение. Поле spec.timeZone появилось в alpha v1.24 и стало GA в v1.27. В v1.23 его нет (или работает только с включённым feature gate CronJobTimeZone). Варианты исправления: (1) обновить кластер до v1.27+ и добавить spec.timeZone: 'Europe/Moscow'; (2) на v1.23 — пересчитать schedule в UTC: для запуска в 02:00 МСК нужно schedule '0 23 * * *' (то есть 23:00 UTC = 02:00 МСК следующего дня). Минус варианта 2 — при переходе на летнее/зимнее время schedule плывёт, нужно вручную пересчитывать.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Почему restartPolicy: Always запрещён в Pod template Job-а, и какие два допустимых значения?

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

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

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

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