CronJob’ы полезны для создания периодических и повторяющихся задач, таких как создание бэкапов или отправка электронных писем. CronJob’ы также могут планировать отдельные Job’ы (задачи, задания - здесь и далее используется английский термин, чтобы избежать путаницы) на конкретное время, например, запланировать Job на то время, когда ваш кластер, скорее всего, будет простаивать. Job’ы Kubernetes в первую очередь предназначены для краткосрочных и пакетных рабочих нагрузок (workloads).
В Kubernetes есть несколько контроллеров для управления подами: ReplicaSets, DaemonSets, Deployments и StatefulSets. У каждого из них есть свой сценарий использования. Однако их всех объединяет гарантия того, что их поды всегда работают. В случае сбоя пода контроллер перезапускает его или переключает его на другой узел (node), чтобы гарантировать постоянную работу программы, которая размещена на подах.
Что, если мы не хотим, чтобы под работал постоянно? Есть сценарии, когда вы не хотите, чтобы процесс продолжался непрерывно. Резервное копирование (backup, создание бэкапа) - это создание копии данных на отдельном носителе, предназначенной для восстановления данных в случае их потери. Данный процесс не должен выполняться постоянно. Напротив, он запускается на выполнение, и после завершения возвращает соответствующий код завершения (exit status), который сообщает, является ли результат успешным или неудачным.
Варианты использования Job’ов Kubernetes
Наиболее подходящие варианты использования Job’ов Kubernetes:
1. Пакетная обработка данных: допустим вы запускаете пакетную задачу с периодичностью один раз в день/неделю/месяц или по определенному расписанию. Примером этого может служить чтение файлов из хранилища или базы данных и передача их в сервис для обработки файлов.
2. Команды/специфические задачи: например, запуск скрипта/кода, который выполняет очистку базы данных.
Job’ы Kubernetes гарантируют, что один или несколько подов выполнят свои команды и успешно завершатся. После завершения всех подов без ошибок, Job завершается. Когда Job удаляется, все созданные поды также удаляются.
Ваш первый Kubernetes Job
Создание Job’а Kubernetes, как и других ресурсов Kubernetes, осуществляется с помощью файла определения (definition file). Откройте новый файл, вы можете назвать его job.yaml. Добавьте в файл следующее:
apiVersion: batch/v1
kind: Job
metadata:
name: hello-world
spec:
template:
metadata:
name: hello-world
spec:
containers:
- name: hello-world
image: busybox
command: ["echo", "Running a hello-world job"]
restartPolicy: OnFailure
Как и в случае с другими ресурсами Kubernetes, мы можем применить это определение к работающему кластеру Kubernetes с помощью kubectl. Выглядит это следующим образом:
$ kubectl apply -f job.yaml
job.batch/hello-world created
Давайте посмотрим, какие поды были созданы:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
hello-world-mstmc 0/1 ContainerCreating 0 1s
Проверим статус job’а с помощью kubectl:
kubectl get jobs hello-world-mstmc
NAME COMPLETIONS DURATION AGE
hello-world-mstmc 1/1 21s 27s
Подождем несколько секунд и снова запустить ту же команду:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
hello-world-mstmc 0/1 Completed 0 25s
Статус пода говорит, что он не запущен. Статус Completed, поскольку job был запущен и выполнен успешно. Job, который мы только что определили, имел простую задачу :) - echo “Running a hello-world job” в стандартный вывод.
Прежде чем двигаться дальше, давайте убедимся, что job действительно выполнил то, что мы от него хотели:
kubectl logs -f hello-world-mstmc
Running a hello-world job
В логах отображается то, что мы задали, т.е. вывести “Running a hello-world job”. Job успешно выполнен.
Запуск CronJob вручную
Бывают ситуации, когда вам нужно выполнить cronjob на разовой основе. Вы можете сделать это, создав job из существующего cronjob’а.
Например, если вы хотите, чтобы cronjob запускался вручную, вы должны сделать следующее.
kubectl create job --from=cronjob/kubernetes-cron-job manual-cron-job
--from=cronjob/kubernetes-cron-job
скопируйте шаблон cronjob’а и создайте job с именем manual-cron-job
Удаление Job’а Kubernetes и очистка (Cleanup)
Когда Job Kubernetes завершается, ни Job, ни созданные им поды не удаляются автоматически. Вы должны удалить их вручную. Эта особенность гарантирует, что вы по-прежнему сможете просматривать логи, Job и его поды в завершенном статусе.
Job можно удалить с помощью kubectl следующим образом:
kubectl delete jobs job_name
Приведенная выше команда удаляет конкретный Job и все его дочерние поды. Как и в случае с другими контроллерами Kubernetes, вы можете удалить Job, только покидая его поды, используя флаг cascade=false
. Например:
kubectl delete jobs job_name cascade=false
Есть еще несколько ключевых параметров, которые вы можете использовать с job’ами/cronjob’ами kubernetes в зависимости от ваших потребностей. Давайте рассмотрим каждый из них.
1. failedJobHistoryLimit и successfulJobsHistoryLimit: Удаления истории успешных и неудавшихся job’ов основано на заданном Вами количестве сохранений. Это очень удобно для отбрасывания всех вхождений, завершенных неудачей, когда вы пытаетесь вывести список job’ов. Например,
failedJobHistoryLimit: 5
successfulJobsHistoryLimit: 10
2. backoffLimit: общее количество повторных попыток в случае сбоя пода.
RestartPolicy Job’а Kubernetes
В параметре restartPolic
y (политика перезапуска) нельзя установить политику “always” (всегда). Job не должен перезапускать под после успешного завершения по определению. Таким образом, для параметра restartPolicy
доступны варианты “Never” (никогда) и “OnFailure” (в случае неудачи).
Ограничение выполнения Job’а Kubernetes по времени
Если вы заинтересованы в выполнении Job’а в течение определенного периода времени, независимо от того, успешно ли завершился процесс, то у Job’ов Kubernetes есть параметр spec.activeDeadlineSeconds
. Установка для этого параметра приведет к немедленному прекращению работы Job’а по истечении заданного количества секунд.
Обратите внимание, что этот параметр переопределяет .spec.backoffLimit
, т.е. если под завершается неудачей и Job достигает своего временного ограничения, неудавшийся под не перезапускается. Все сразу же останавливается.
В следующем примере мы создаем Job как с параметром backoff limit
, так и с deadline
:
apiVersion: batch/v1
kind: Job
metadata:
name: data-consumer
spec:
backoffLimit: 5
activeDeadlineSeconds: 20
template:
spec:
containers:
- name: consumer
image: busybox
command: ["/bin/sh", "-c"]
args: ["echo 'Consuming data'; sleep 1; exit 1"]
restartPolicy: OnFailure
Завершения Job’ов и параллелизм
Мы рассмотрели, как можно выполнить одну задачу, определенную внутри объекта Job, что более известно как шаблон “run-once”. Однако в реальных сценариях используются и другие шаблоны.
Несколько одиночных Job’ов
Например, у нас может быть очередь сообщений, которая требует обработки. Мы должны порождать пользовательские job’ы, которые извлекают сообщения из очереди, пока она не опустеет. Чтобы реализовать этот шаблон с помощью Job’ов Kubernetes, мы устанавливаем в параметр .spec.completions какое-либо число (должно быть ненулевым, а положительным числом). Job начинает создавать поды пока не достигнет заданного числа завершений (completions). Job считается завершенным, когда все поды завершаются с успешным с кодом завершения. Приведем пример. Измените наш файл определения, чтобы он выглядел следующим образом:
apiVersion: batch/v1
kind: Job
metadata:
name: data-consumer
spec:
completions: 5
template:
metadata:
name: data-consumer
spec:
containers:
- name: data-consumer
image: busybox
command: ["/bin/sh","-c"]
args: ["echo 'consuming a message from queue'; sleep 5"]
restartPolicy: OnFailure
Мы указываем параметр completions равным 5.
Несколько параллельно запущенных Job’ов (Work Queue)
Другой шаблон может включать в себя необходимость запуска нескольких job’ов, но вместо того, чтобы запускать их один за другим, нам нужно запускать несколько из них параллельно. Параллельная обработка сокращает общее время выполнения и находит применение во многих областях, например в data science и искусственном интеллекте.
Измените файл определения, чтобы он выглядел следующим образом:
apiVersion: batch/v1
kind: Job
metadata:
name: data-consumer
spec:
parallelism: 5
template:
metadata:
name: data-consumer
spec:
containers:
- name: data-consumer
image: busybox
command: ["/bin/sh","-c"]
args: ["echo 'consuming a message from queue'; sleep $(shuf -i 5-10 -n 1)"]
restartPolicy: OnFailure
В этом примере мы не задали параметр .spec.completions
. Вместо этого мы указали параметр parallelism
. Параметр completions
в нашем случае по умолчанию равен parallelism
(5). Теперь Job делает следующее:
Одновременно будут запущены 5 подов, все они будут выполнять один и то же Job. Когда один из подов завершается успехом, это будет означать, что весь Job готов. Поды больше не создаются, и Job в конечном итоге завершается.
В этом сценарии Kubernetes Job одновременно порождает 5 подов. Знать, завершили ли остальные поды свои задачи, в компетенции самих подов. В нашем примере мы предполагаем, что получаем сообщения из очереди сообщений (например, AWS SQS). Когда сообщений для обработки больше нет, Job получает уведомление о том, что он должен завершиться. После успешного завершения первого пода:
Поды больше не создаются.
Существующие поды завершают свою работу и тоже завершаются.
В приведенном выше примере мы изменили команду, которую выполняет под, чтобы перевести его в спящий режим на случайное количество секунд (от 5 до 10), прежде чем он завершится. Таким образом, мы примерно моделируем, как несколько подов могут работать вместе с внешним источником данных, таким как очередь сообщений или API.
Ограничения CronJob’ов
Cronjob создает объект job примерно по одному разу за сеанс выполнения своего расписания (schedule). Мы говорим «примерно», потому что при определенных обстоятельствах могут быть созданы два job’а или ни одного. Мы пытаемся сделать подобные случаи как можно более редкими, но не можем предотвратить их полностью. Следовательно, job’ы должны быть идемпотентными.
Если параметр startingDeadlineSeconds
установлен на большое значение или не задан (по умолчанию) и параметр concurrencyPolicy
установлен на Allow, job всегда будут запускаться как минимум один раз.
Для каждого CronJob’а контроллер CronJob проверяет, сколько расписаний он пропустил за период с последнего запланированного времени до настоящего момента. Если пропущенных расписаний больше 100, то job не запускается и в логи вносится сообщение об ошибке.
Cannot determine if job needs to be started. Too many missed start time (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew.
Важно отметить, что если параметр startingDeadlineSeconds
установлен (т.е. не nil
), то контроллер считает сколько произошло пропущенных job’ов исходя из значения параметра startingDeadlineSeconds
, начиная с последнего заданного времени до сих пор. Например, если параметр startDeadlineSeconds
установлен на 200
, контроллер подсчитывает, сколько пропущенных job’ов произошло за последние 200 секунд.
CronJob считается пропущенным, если его не удалось создать в установленное время. Например, если для параметра concurrencyPolicy
задано значение Forbid
и была предпринята попытка запланировать CronJob во время выполнения предыдущего расписания, оно будет считаться пропущенным.
Например, предположим, что CronJob настроен запускать новый Job каждую минуту начиная с 08:30:00
, а его параметр startingDeadlineSeconds
не установлен. Если контроллер CronJob не работал с 08:29:00
до 10:21:00
, job не запустится, так как количество пропущенных job’ов в расписании превышает 100.
Чтобы проиллюстрировать эту концепцию с другой стороны, предположим, что CronJob запрограммирован планировать новый Job в расписании каждую минуту начиная с 08:30:00
и его параметр startingDeadlineSeconds
устанавливается на 200 секунд. Если контроллер CronJob не работает в течение того же периода, что и в предыдущем примере (с 08:29:00
до 10:21:00
), Job все равно запустится в 10:22:00. Это происходит, поскольку контроллер теперь проверяет, сколько пропущенных расписаний было за последние 200 секунд (т. е. 3 пропущенных расписания), вместо того, чтобы считать с последнего заданного времени до настоящего момента.
CronJob отвечает только за создание Job’ов, соответствующих его расписанию, а Job, в свою очередь, отвечает за управление представляемыми им подами.
TL;DR (Too Long; Didn’t Read)
Job’ы Kubernetes используются, когда вы хотите создать поды, которые будут выполнять определенную задачу, а затем завершать работу.
Job’ы Kubernetes по умолчанию не нуждаются в селекторах подов; Job автоматически обрабатывает их лейблы и селекторы.
Параметр
restartPolicy
для Job принимает значения «Never» или «OnFailure»
Job’ы используют параметры completions и parallelism для управления шаблонами, которые определяют порядок работы подов. Поды Job’ов могут выполняться как одна задача, несколько последовательных задач или несколько параллельных задач, в которых первая завершенная задача дает указание остальным подам завершиться.
Вы можете контролировать количество попыток Job’а перезапустить неудавшиеся поды, используя параметр
.spec.backoffLimit
. По умолчанию этот лимит равен шести.Вы можете контролировать время работы job’а, используя параметр .
spec.activeDeadlineSeconds
. Этот лимит отменяетbackoffLimit
. Таким образом, Job не пытается перезапустить неудавшийся под, если дедлайн достигнут.Job’ы и их поды не удаляются автоматически после завершения. Вы должны удалить их вручную или использовать контроллер
ttlSecondsAfterFinished
, который на момент написания этой статьи все еще находится в статусе alpha.
Перевод статьи подготовлен в преддверии старта курса "Инфраструктурная платформа на основе Kubernetes". Также приглашаем всех желающих записаться на бесплатный демо-урок по теме: "Работа с NoSQL базами в k8s (на примере Apache Cassandra)"