ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

Kubernetes CronJob: concurrencyPolicy, startingDeadlineSeconds, timeZone и работа с Job

CronJob в Kubernetes часто «ломается» не из‑за приложения, а из‑за нюансов планирования: параллельные запуски, пропущенные окна, таймзона, пауза suspend и уборка истории. Разбираем ключевые поля и даем команды для диагностики stuck/failed Job и проблем контроллеров.
Kubernetes CronJob: concurrencyPolicy, startingDeadlineSeconds, timeZone и работа с Job

В Kubernetes CronJob выглядит простым: есть расписание — по нему создаются Job, а Job уже поднимают Pod и выполняют задачу. На практике чаще всплывают одни и те же симптомы: запуск «не по времени», дубли, сотни Job в неймспейсе, зависшие или падающие задания, и удивление от того, что «пропущенный» запуск не догоняется.

Ниже — практичный разбор полей CronJob, которые реально решают эксплуатационные проблемы: concurrencyPolicy, startingDeadlineSeconds, suspend, timeZone, а также диагностика stuck jobs / failed jobs и уборка через ttlSecondsAfterFinished. Параллельно разберемся, где в цепочке участвует kube-controller-manager, и как отличать проблемы контроллеров от проблем вашего приложения.

Как CronJob работает изнутри: что важно помнить

Базовая модель такая:

  • CronJob по расписанию создает Job.
  • Job создает Pod(ы) и добивается завершения в рамках настроек Job (backoffLimit, completions, parallelism).
  • После завершения статус Job (Succeeded/Failed) остается в кластере до ручного удаления или автоматической уборки.

Отсюда два практических следствия:

  • Если «CronJob создает слишком много объектов» — обычно речь про накопление Job-объектов, а не Pod.
  • Если «задание не запускается по расписанию» — чаще виноваты таймзона/время, пропущенные окна (startingDeadlineSeconds), политика параллельности (concurrencyPolicy) или пауза (suspend).

concurrencyPolicy: что делать, если расписание быстрее выполнения

concurrencyPolicy определяет, что делать, если наступило время нового запуска, а предыдущий Job (созданный этим CronJob) еще не завершился. Это критично для бэкапов, ETL, отчетов, миграций и любой работы с блокировками и внешними ресурсами.

Варианты concurrencyPolicy

  • Allow (по умолчанию): разрешать параллельные запуски. Если Job выполняется 10 минут, а расписание каждые 2 минуты — получите несколько Job одновременно.
  • Forbid: если предыдущий запуск еще активен — новый пропускается.
  • Replace: новый запуск «заменяет» активный — Kubernetes удалит активный Job и создаст новый.

В эксплуатации чаще всего безопаснее начинать с Forbid для неидемпотентных задач (бэкапы/миграции) и использовать Allow только когда вы уверены, что приложение корректно выдерживает параллельность.

Практические сценарии и подводные камни

Дубли и гонки данных. Если периодически «ломается бэкап» или «таблица внезапно заблокирована», частая причина — параллельные Job при Allow. Для задач с блокировками почти всегда выбирайте Forbid или делайте блокировку внутри задания (например, advisory lock в БД).

Replace опасен для долгих операций. Удаление Job приводит к завершению Pod. Если приложение не обрабатывает SIGTERM корректно, можно получить частично выполненные действия. Используйте Replace только если задача безопасно прерывается и перезапускается без побочных эффектов.

Forbid и «почему пропустило запуск». Это нормальное поведение: новый запуск не состоится, пока старый активен. В мониторинге учитывайте это: «нет Job в текущую минуту» не всегда инцидент.

Пример CronJob с Forbid

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-backup
spec:
  schedule: "0 2 * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 2
  failedJobsHistoryLimit: 5
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: backup
            image: alpine:3.20
            command: ["sh", "-c", "echo backup && sleep 30"]

Если вам нужно запускать такие задачи на выделенных ресурсах, с предсказуемыми лимитами и возможностью тонко контролировать окружение, удобнее держать их на VDS и уже оттуда работать с кластером/CI (вместо запуска «тяжелых» вспомогательных утилит с локальных рабочих станций).

Схема: CronJob создает Job по расписанию и как выглядят пересекающиеся запуски

startingDeadlineSeconds: как Kubernetes «догоняет» пропущенные окна

startingDeadlineSeconds задает окно, в пределах которого CronJob еще имеет смысл запустить, если планировщик пропустил точное время. Пропуски случаются из-за проблем с API, перезапуска компонентов, перегрузки контроллеров, снятия/установки suspend и т. п.

Как это работает

  • Если момент запуска пропущен, контроллер оценивает, насколько мы опоздали.
  • Если опоздание меньше либо равно startingDeadlineSeconds — Job создается (догоняем).
  • Если опоздание больше — запуск считается окончательно пропущенным, и Job не будет создан.

startingDeadlineSeconds не превращает CronJob в очередь заданий. Он задает допустимую задержку старта, а не гарантирует выполнение всех пропущенных запусков.

Рекомендации по значениям

  • Для частых задач (каждые 1–5 минут) часто достаточно 30–120 секунд, чтобы сгладить мелкие задержки.
  • Для редких задач (раз в час/сутки) обычно ставят 10–30 минут: пережить краткий даунтайм контроллеров, но не запускать «вчерашнее» завтра.

Пример: разрешаем запуск при задержке до 10 минут

apiVersion: batch/v1
kind: CronJob
metadata:
  name: hourly-report
spec:
  schedule: "0 * * * *"
  startingDeadlineSeconds: 600
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: report
            image: busybox:1.36
            command: ["sh", "-c", "date; echo generate report"]
FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

suspend: пауза CronJob без удаления и типичные ловушки

Поле suspend позволяет «заморозить» CronJob: новые Job не создаются, но объект CronJob остается, и вы не теряете спецификацию. Это удобно для техработ, аварийной остановки «шумных» задач и временного отключения интеграций.

Что происходит при suspend

  • Новые Job по расписанию перестают создаваться.
  • Уже созданные Job продолжают выполняться как обычно.

Что часто путают

«Я поставил suspend, но Pod еще работает». Ожидаемо: suspend не отменяет активные Job. Если нужно остановить прямо сейчас — разбирайтесь с активным Job/Pod отдельно (обычно удалением Job).

«Снял suspend, и оно не догнало пропущенные запуски». Догон возможен только в пределах startingDeadlineSeconds. Если окно маленькое, а пауза была долгой — пропущенные периоды не воспроизведутся.

Команды для проверки состояния

kubectl get cronjob
kubectl describe cronjob nightly-backup
kubectl get job --sort-by=.metadata.creationTimestamp
kubectl get pod --sort-by=.metadata.creationTimestamp

timeZone в CronJob: почему «не в то время» и как сделать правильно

Симптом «CronJob запускается не в то время» почти всегда сводится к одному из случаев:

  • расписание интерпретируется в неожиданной таймзоне;
  • кластер живет в UTC, а вы ожидаете локальное время;
  • ожидания разъехались из-за сезонных правил времени в вашей зоне.

Поле timeZone

В современных версиях Kubernetes у CronJob есть поле timeZone, позволяющее явно указать IANA-таймзону (например, Europe/Moscow). Тогда таймзона становится частью спецификации, и расписание считается именно в этой зоне.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: local-midnight-task
spec:
  schedule: "0 0 * * *"
  timeZone: "Europe/Moscow"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: task
            image: busybox:1.36
            command: ["sh", "-c", "date; echo do work"]

Если timeZone недоступен в вашем кластере

На старых кластерах или при ограничениях политики приходится жить с UTC и «переводить» расписание вручную. Практичное правило: храните cron-выражение в UTC, а рядом (в описании, в README репозитория, в комментарии к манифесту) фиксируйте локальную привязку, чтобы через полгода никто не гадал, почему стоит именно это выражение.

Не путайте timeZone CronJob и TZ внутри контейнера

Даже если CronJob запускается в «правильное время», приложение внутри контейнера может логировать время в UTC (или наоборот). Это отдельная настройка окружения контейнера (например, переменная TZ) и она не влияет на то, когда контроллер создаст Job.

Если для задач вам нужны «глобальные» логи и удобная диагностика по времени, следите за единым стандартом (часто это UTC) и фиксируйте его в командных утилитах/логгере. А для публичных сервисов не забывайте про TLS: для внешних эндпоинтов и вебхуков удобнее сразу поставить корректные SSL-сертификаты, чтобы не тратить время на нестабильные интеграции.

Иллюстрация: влияние timeZone на расписание CronJob и сравнение локального времени с UTC

Stuck и failed Jobs: диагностика на уровне CronJob, Job и kube-controller-manager

Когда говорят «CronJob сломан», обычно ломается одно из звеньев цепочки. В эксплуатации удобно разделять симптомы:

  • Job не создается — проблема на уровне CronJob/контроллера.
  • Job создается, но Pod не стартует — проблема планирования/ресурсов/образа/политик.
  • Pod стартует, но Job зависает — проблема приложения/внешних зависимостей/блокировок.
  • Job падает (failed) — смотрим логи контейнера и параметры retry.

Быстрый чек-лист команд

kubectl get cronjob local-midnight-task -o wide
kubectl describe cronjob local-midnight-task
kubectl get job --selector=cronjob-name=local-midnight-task
kubectl describe job <job-name>
kubectl get pod --selector=job-name=<job-name>
kubectl logs <pod-name> --all-containers=true
kubectl get events --sort-by=.lastTimestamp

Признаки stuck jobs (зависших Job)

  • Job долго в состоянии Active, а Pod «Running» без прогресса.
  • Pod в Pending из-за нехватки ресурсов или неразрешимых constraints (affinity, nodeSelector, taints/tolerations).
  • Pod в CrashLoopBackOff, и Job остается активным, потому что идут перезапуски/повторы.

Тактика:

  • Проверьте activeDeadlineSeconds в Job — это ограничение на максимальную длительность выполнения. Если задача может зависнуть (ожидание внешнего сервиса), лучше выставить предел и считать таймаут ошибкой.
  • Проверьте backoffLimit — чтобы failed jobs не пытались бесконечно.
  • Проверьте ресурсы Pod: resources.requests и квоты namespace, чтобы не застревать в Pending.

Если вам нужен «одиночный» запуск без параллельности (даже при ручном старте и ретраях), часто выручает файловая или распределенная блокировка. По теме пригодится заметка про блокировку cron-задач: как сделать singleton-задачу с flock/lockfile.

failed jobs: где искать причину

Если Job завершается с ошибкой:

  • сначала смотрим логи Pod: приложение, миграции, доступы к БД, секреты;
  • потом события Pod: образ не скачался, нет прав, проблемы с volume;
  • потом параметры повторов: backoffLimit и поведение приложения на временных ошибках.

Почему CronJob не создает Job и при чем тут kube-controller-manager

За создание Job по расписанию отвечает контроллер CronJob, работающий внутри kube-controller-manager. При системных проблемах (перегрузка API, задержки reconcile, проблемы лидер-элекции в HA) CronJob может пропускать окна запусков или создавать Job с задержкой.

Косвенные признаки проблем на стороне контроллера:

  • много разных CronJob в кластере одновременно «опаздывают»;
  • в событиях CronJob есть ошибки листинга/записи в API;
  • видно, что события/обновления статусов приходят «волнами».

В managed-кластерах доступ к логам управляющего плейна часто ограничен — тогда ориентируйтесь на массовость симптомов и события объектов. В self-hosted кластере проверяйте логи компонента и статус лидер-элекции.

ttlSecondsAfterFinished: как не копить тысячи Job

Даже если CronJob работает идеально, со временем он создаст много Job-объектов. Это:

  • засоряет namespace;
  • увеличивает нагрузку на API при листинге;
  • мешает быстро находить актуальные failed jobs среди «исторических».

Для автоматической уборки используйте ttlSecondsAfterFinished в спецификации Job (обычно через jobTemplate CronJob). После завершения (Succeeded/Failed) Job будет удален через заданный TTL.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: cleanup-demo
spec:
  schedule: "*/15 * * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      ttlSecondsAfterFinished: 3600
      backoffLimit: 1
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: task
            image: busybox:1.36
            command: ["sh", "-c", "date; echo done"]

Связанные поля истории CronJob

Помимо TTL, у CronJob есть:

  • successfulJobsHistoryLimit — сколько успешных Job хранить как историю;
  • failedJobsHistoryLimit — сколько неуспешных Job хранить.

Их можно использовать вместе с TTL: лимиты сокращают визуальный «мусор», а TTL гарантирует уборку по времени, даже если лимиты по какой-то причине не применились так, как вы ожидали.

Набор «боевых» пресетов: подставьте под свои задачи

Пресет 1: бэкап/миграции (важно не параллелить)

  • concurrencyPolicy: Forbid
  • startingDeadlineSeconds: 1800 (30 минут) или меньше
  • activeDeadlineSeconds в Job: ограничить максимальное время
  • ttlSecondsAfterFinished: 86400 (сутки) или меньше

Пресет 2: идемпотентная синхронизация (можно параллелить, но контролируем)

  • concurrencyPolicy: Allow или Forbid (если внешняя система не любит параллельность)
  • startingDeadlineSeconds: 60–120
  • backoffLimit: 2–5
  • ttlSecondsAfterFinished: 3600–21600

Пресет 3: регулярный отчет «строго актуальный» (старые догонять нельзя)

  • startingDeadlineSeconds: небольшой (например, 60–300)
  • concurrencyPolicy: Replace — только если безопасно прерывать
  • timeZone: зафиксировать явно, если критично «по локальному времени»

Частые вопросы и типичные ошибки

Почему CronJob запускается дважды?

Проверьте concurrencyPolicy (возможно, стоит Allow), а также задержки контроллера: если кластер был перегружен, он мог создать Job с опозданием, а затем наступило следующее окно.

Почему Job «висит активным», но Pod уже завершился?

Такое бывает при проблемах с обновлением статуса или при нестабильной связи с API. Смотрите события и общее состояние кластера. Если массово — подозрение на контроллеры/управляющий плейн. Если единично — часто это гонки при удалении Pod или проблемах с нодой.

Почему после включения timeZone время все равно странное?

Убедитесь, что вы сравниваете именно время создания Job (метаданные и события) с ожидаемым. Затем отдельно проверьте, что показывает date внутри контейнера: это может быть другая таймзона, не влияющая на расписание.

Итог: что настроить в первую очередь

Чтобы быстро привести CronJob к «эксплуатационному» виду, обычно достаточно:

  1. Выбрать правильный concurrencyPolicy (часто Forbid для критичных задач).
  2. Задать startingDeadlineSeconds, чтобы понимать, догоняем ли пропуски и насколько.
  3. Явно зафиксировать timeZone, если расписание привязано к локальному времени.
  4. Ограничить «вечные» выполнения через activeDeadlineSeconds (в Job).
  5. Настроить уборку: ttlSecondsAfterFinished плюс адекватные successfulJobsHistoryLimit и failedJobsHistoryLimit.

После этого большинство ситуаций из разряда «suspended cronjob», «stuck jobs», «failed jobs» становятся предсказуемыми: вы быстрее отличаете проблему приложения от проблем кластера и понимаете, что именно произошло в каждом окне расписания.

Поделиться статьей

Вам будет интересно

Kernel panic на VDS: сбор диагностики через kdump и анализ vmcore OpenAI Статья написана AI (GPT 5)

Kernel panic на VDS: сбор диагностики через kdump и анализ vmcore

Kernel panic на VDS случается редко, но приводит к внезапному ребуту и потере контекста. В статье — пошаговый план: быстрый triage ...
SPF, DKIM и DMARC: как читать заголовки письма и SMTP-логи, чтобы повысить доставляемость OpenAI Статья написана AI (GPT 5)

SPF, DKIM и DMARC: как читать заголовки письма и SMTP-логи, чтобы повысить доставляемость

Практическое руководство по диагностике доставляемости: как по заголовкам письма (Authentication-Results, Received-SPF, ARC) прове ...
Kubernetes Service и kube-proxy: ClusterIP, NodePort, LoadBalancer и отладка сетевых проблем OpenAI Статья написана AI (GPT 5)

Kubernetes Service и kube-proxy: ClusterIP, NodePort, LoadBalancer и отладка сетевых проблем

Разбираем, как kube-proxy реализует Kubernetes Service в режиме iptables, чем отличаются ClusterIP/NodePort/LoadBalancer и как быс ...