Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Job и CronJob: разовые и периодические задачи

Job и CronJob в Kubernetes: миграции, бэкапы и фоновые задачи

Job и CronJob: completions и parallelism, backoffLimit, concurrencyPolicy, часовые пояса и типовые ошибки расписания периодических задач.

Прямой ответ

Job и CronJob — контроллеры для задач, которые должны завершиться, в отличие от Deployment, который поддерживает поды работающими бесконечно.

Job запускает один или несколько подов и гарантирует, что заданное число из них завершится успешно. При сбое создаёт новый под — до исчерпания backoffLimit (по умолчанию 6).

apiVersion: batch/v1
kind: Job
metadata:
  name: migrate-20261021
spec:
  backoffLimit: 3
  activeDeadlineSeconds: 1800      # таймаут всей задачи
  ttlSecondsAfterFinished: 3600    # автоудаление через час
  template:
    spec:
      restartPolicy: Never         # только Never или OnFailure
      containers:
        - name: migrate
          image: registry/app:migrations
          command: ["python", "-m", "app.migrate"]

Ключевые поля Job:

Поле Назначение
completions Сколько успешных завершений требуется
parallelism Сколько подов одновременно
backoffLimit Число повторных попыток
activeDeadlineSeconds Ограничение длительности задачи
podFailurePolicy Разные действия для разных кодов завершения

CronJob создаёт Job по расписанию:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-backup
spec:
  schedule: "30 2 * * *"
  timeZone: "Europe/Moscow"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 600
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: registry/app:backup

Поле concurrencyPolicy определяет поведение, если предыдущий запуск ещё не завершился: Allow (по умолчанию) запускает новый параллельно, Forbid пропускает запуск, Replace завершает текущий и запускает новый. Для резервного копирования и миграций почти всегда нужен Forbid: два одновременных бэкапа одной базы дают повреждённый результат.

Если контроллер был недоступен в момент срабатывания расписания, запуск не теряется: он выполняется, если прошло меньше startingDeadlineSeconds. Без этого поля пропущенные запуски накапливаются и вызывают всплеск одновременных задач.

Практика: надёжные фоновые задачи

1. restartPolicy в шаблоне — только Never или OnFailure. Значение Always не допускается: Job должен уметь отличить успешное завершение от перезапуска.

2. Различайте сбои контейнера и пода. restartPolicy: OnFailure перезапускает контейнер внутри того же пода, что сохраняет IP и привязанные тома. backoffLimit считает попытки на уровне пода. Для задач, которые не должны повторяться частично, удобен Never в сочетании с backoffLimit.

3. Часовой пояс. По умолчанию расписание считается по времени узла, на котором работает контроллер. Явное поле timeZone делает поведение предсказуемым, иначе перевод времени или смена узла сдвигает запуск.

4. Задачи обязаны быть идемпотентными. Повторная попытка после сбоя выполнит часть работы заново. Принцип разобран в статье про идемпотентность.

5. Миграции базы — не CronJob. Миграции должны выполняться один раз и блокировать развёртывание; их запускают отдельным Job в пайплайне. CronJob подходит для периодических операций: бэкапов, очистки, формирования отчётов. Практика миграций — в статье про Alembic.

6. Ограничивайте историю. Без successfulJobsHistoryLimit кластер накапливает тысячи завершённых Job, что замедляет работу API.

7. Используйте podFailurePolicy для различения причин. Код завершения 1 (ошибка данных) и 137 (OOM) требуют разных действий: повторный запуск имеет смысл во втором случае и бесполезен в первом.

Рекомендации:

  1. Всегда задавайте activeDeadlineSeconds. Зависшая задача без таймаута блокирует очередь и потребляет ресурсы бесконечно.
  2. Логируйте начало, завершение и объём работы. Для периодических задач это единственный способ заметить, что запуск перестал выполняться.
  3. Настраивайте алерт на пропуск запуска. Отсутствие метрики о завершении в течение двух периодов — сигнал о проблеме с расписанием или правами доступа.
  4. Задавайте ресурсы. Задача без requests конкурирует с рабочими подами и вытесняется первой — см. базовые объекты Kubernetes.
  5. Проверяйте поведение при нехватке места. Задача резервного копирования, упавшая из-за переполненного тома, не сообщает об этом, если результат не проверяется.

Практический сценарий: бэкап выполнялся дважды и давал повреждённый архив

Контекст: ночной бэкап базы запускался по расписанию. Через несколько недель при попытке восстановления архив оказался нечитаемым.

  1. Диагностика. В списке Job оказалось два запуска в одну и ту же минуту: один длился 40 минут, второй начался, пока первый ещё работал.
  2. Причина. concurrencyPolicy не была задана, то есть действовало значение Allow. Первый запуск затянулся из-за роста объёма данных, и следующий сработал по расписанию.
  3. Усугубляющий фактор. Оба процесса писали в один и тот же путь, поэтому архивы перезаписывали друг друга в середине записи.
  4. Действия. (а) Установили concurrencyPolicy: Forbid; (б) путь архива сделали уникальным с меткой времени и идентификатором Job; (в) добавили шаг проверки целостности архива в конце задачи и ненулевой код возврата при ошибке; (г) ввели activeDeadlineSeconds: 7200; (д) настроили алерт на отсутствие успешного завершения за сутки; (е) добавили еженедельную проверку восстановления на тестовом стенде.
  5. Результат. Повторные одновременные запуски прекратились; повреждённый архив теперь приводит к ненулевому коду завершения и алерту, а не к тихой потере копии.

Вывод: периодическая задача без политики конкурентности и без проверки результата выглядит работающей, но не гарантирует ничего. Проверка целостности и алерт на пропуск важнее самого расписания.

FAQ

Что происходит с Job после завершения? Job и его поды остаются в кластере, пока их не удалить вручную или пока не сработает ttlSecondsAfterFinished. Это удобно для изучения логов, но требует ограничения истории у CronJob.

Как остановить зависший Job? kubectl delete job <имя> завершает поды. Если нужно только остановить запуски CronJob, используют kubectl patch cronjob <имя> -p '{"spec":{"suspend":true}}'.

Почему CronJob не сработал в заданное время? Чаще всего причины три: контроллер был недоступен дольше startingDeadlineSeconds, предыдущий запуск ещё работал при Forbid, либо расписание считается в часовом поясе узла, а не в ожидаемом.

Можно ли запустить Job по событию, а не по расписанию? Да: Job создают через API из пайплайна или обработчика события. Для этого нужен RBAC на создание ресурсов jobs в целевом namespace.

Источники: Kubernetes docs (Job, CronJobs, pod failure policy) — ссылки в шапке.