HTML Diff
2 added 2 removed
Original 2026-01-01
Modified 2026-02-19
1 Что такое Job и CronJob в Kubernetes, для чего они нужны, а для чего их использовать не стоит.Эта статья - выжимка из <a>лекции</a> вечерней школы "Слёрм Kubernetes".<h2>Job: сущность для разовых задач</h2>
1 Что такое Job и CronJob в Kubernetes, для чего они нужны, а для чего их использовать не стоит.Эта статья - выжимка из <a>лекции</a> вечерней школы "Слёрм Kubernetes".<h2>Job: сущность для разовых задач</h2>
2 Job (работа, задание) - это yaml-манифест, который создаёт под для выполнения разовой задачи. Если запуск задачи завершается с ошибкой, Job перезапускает поды до успешного выполнения или до истечения таймаутов. Когда задача выполнена, Job считается завершённым и больше никогда в кластере не запускается. Job - это сущность для разовых задач.<h2>Когда используют Job</h2>
2 Job (работа, задание) - это yaml-манифест, который создаёт под для выполнения разовой задачи. Если запуск задачи завершается с ошибкой, Job перезапускает поды до успешного выполнения или до истечения таймаутов. Когда задача выполнена, Job считается завершённым и больше никогда в кластере не запускается. Job - это сущность для разовых задач.<h2>Когда используют Job</h2>
3 <strong>При установке и настройке окружения.</strong> Например, мы построили CI/CD, который при создании новой ветки автоматически создаёт для неё окружение для тестирования. Появилась ветка - в неё пошли коммиты - CI/CD создал в кластере отдельный namespace и запустил Job - тот, в свою очередь, создал базу данных, налил туда данные, все конфиги сохранил в Secret и ConfigMap. То есть Job подготовил цельное окружение, на котором можно тестировать и отлаживать новую функциональность.<p><strong>При выкатке helm chart.</strong> После развёртывания helm chart с помощью хуков (hook) запускается Job, чтобы проверить, как раскатилось приложение и работает ли оно.</p>
3 <strong>При установке и настройке окружения.</strong> Например, мы построили CI/CD, который при создании новой ветки автоматически создаёт для неё окружение для тестирования. Появилась ветка - в неё пошли коммиты - CI/CD создал в кластере отдельный namespace и запустил Job - тот, в свою очередь, создал базу данных, налил туда данные, все конфиги сохранил в Secret и ConfigMap. То есть Job подготовил цельное окружение, на котором можно тестировать и отлаживать новую функциональность.<p><strong>При выкатке helm chart.</strong> После развёртывания helm chart с помощью хуков (hook) запускается Job, чтобы проверить, как раскатилось приложение и работает ли оно.</p>
4 <h2>Таймауты, ограничивающие время выполнения Job</h2>
4 <h2>Таймауты, ограничивающие время выполнения Job</h2>
5 Job будет создавать поды до тех пор, пока под не завершится с успешным результатом. Это значит, что если в поде есть ошибка, которая приводит к неуспешному результату (exit code не равен 0), то Job будет пересоздавать этот под до бесконечности. Чтобы ограничить перезапуски, в описании Job есть два таймаута: activeDeadlineSeconds и backoffLimit.activeDeadlineSeconds - это количество секунд, которое отводится всему Job на выполнение. Обратите внимание, это ограничение не для одного пода или одной попытки запуска, а для всего Job.<p>Например, если указать в Job, что activeDeadlineSeconds равен 200 сек., а наше приложение падает с ошибкой через 5 сек., то Job сделает 40 попыток и только после этого остановится.</p>
5 Job будет создавать поды до тех пор, пока под не завершится с успешным результатом. Это значит, что если в поде есть ошибка, которая приводит к неуспешному результату (exit code не равен 0), то Job будет пересоздавать этот под до бесконечности. Чтобы ограничить перезапуски, в описании Job есть два таймаута: activeDeadlineSeconds и backoffLimit.activeDeadlineSeconds - это количество секунд, которое отводится всему Job на выполнение. Обратите внимание, это ограничение не для одного пода или одной попытки запуска, а для всего Job.<p>Например, если указать в Job, что activeDeadlineSeconds равен 200 сек., а наше приложение падает с ошибкой через 5 сек., то Job сделает 40 попыток и только после этого остановится.</p>
6 backoffLimit - это количество попыток. Если указать 2, то Job дважды попробует запустить под и остановится.<p>Параметр backoffLimit очень важен, потому что, если его не задать, контроллер будет создавать поды бесконечно. А ведь чем больше объектов в кластере, тем больше ресурсов API нужно серверам, и что самое главное: каждый такой под - это как минимум два контейнера в остановленном состоянии на узлах кластера. При этом поды в состоянии Completed или Failed не учитываются в ограничении 110 подов на узел, и в итоге, когда на узле будет несколько тысяч контейнеров, докер-демону скорее всего будет очень плохо.</p>
6 backoffLimit - это количество попыток. Если указать 2, то Job дважды попробует запустить под и остановится.<p>Параметр backoffLimit очень важен, потому что, если его не задать, контроллер будет создавать поды бесконечно. А ведь чем больше объектов в кластере, тем больше ресурсов API нужно серверам, и что самое главное: каждый такой под - это как минимум два контейнера в остановленном состоянии на узлах кластера. При этом поды в состоянии Completed или Failed не учитываются в ограничении 110 подов на узел, и в итоге, когда на узле будет несколько тысяч контейнеров, докер-демону скорее всего будет очень плохо.</p>
7 <p>Учитывая, что контроллер постоянно увеличивает время между попытками запуска подов, проблемы могут начаться в ночь с пятницы на понедельник. Особенно, если вы не мониторите количество подов в кластере, которые не находятся в статусе Running.</p>
7 <p>Учитывая, что контроллер постоянно увеличивает время между попытками запуска подов, проблемы могут начаться в ночь с пятницы на понедельник. Особенно, если вы не мониторите количество подов в кластере, которые не находятся в статусе Running.</p>
8 <h2>Удаление Job</h2>
8 <h2>Удаление Job</h2>
9 После успешного завершения задания манифесты Job и подов, созданных им, остаются в кластере навсегда. Все поля Job имеют статус Immutable, то есть "неизменяемый", и поэтому обычно при создании Job из различных автоматических сценариев сначала удаляют Job, который остался от предыдущего запуска. Практика генерации уникальных имен для запуска таких Job может привести к накоплению большого количества ненужных манифестов.<p>В Kubernetes есть специальный TTL Controller, который умеет удалять завершенные Job вместе с подами. Вот только он появился в версии 1.12 и до сих пор находится в статусе alpha, поэтому его необходимо включать с помощью соответствующего <a>feature gate</a> TTLAfterFinished.</p>
9 После успешного завершения задания манифесты Job и подов, созданных им, остаются в кластере навсегда. Все поля Job имеют статус Immutable, то есть "неизменяемый", и поэтому обычно при создании Job из различных автоматических сценариев сначала удаляют Job, который остался от предыдущего запуска. Практика генерации уникальных имен для запуска таких Job может привести к накоплению большого количества ненужных манифестов.<p>В Kubernetes есть специальный TTL Controller, который умеет удалять завершенные Job вместе с подами. Вот только он появился в версии 1.12 и до сих пор находится в статусе alpha, поэтому его необходимо включать с помощью соответствующего <a>feature gate</a> TTLAfterFinished.</p>
10 ttlSecondsAfterFinished - указывает, через сколько секунд специальный TimeToLive контроллер должен удалить завершившийся Job вместе с подами и их логами.<p><em>spec:</em><em>ttlSecondsAfterFinished: 100</em></p>
10 ttlSecondsAfterFinished - указывает, через сколько секунд специальный TimeToLive контроллер должен удалить завершившийся Job вместе с подами и их логами.<p><em>spec:</em><em>ttlSecondsAfterFinished: 100</em></p>
11 <h2>Манифест</h2>
11 <h2>Манифест</h2>
12 Посмотрим на пример Job-манифеста.<p><em>apiVersion: batch/v1</em><em>kind: Job</em><em>metadata:</em><em>name: hello</em><em>spec:</em><em>backoffLimit: 2</em><em>activeDeadlineSeconds: 60</em><em>template:</em><em>spec:</em><em>containers:</em><em>- name: hello</em><em>image: busybox</em><em>args:</em><em>- /bin/sh</em><em>- -c</em><em>- date; echo Hello from the Kubernetes cluster</em><em>restartPolicy: Never</em></p>
12 Посмотрим на пример Job-манифеста.<p><em>apiVersion: batch/v1</em><em>kind: Job</em><em>metadata:</em><em>name: hello</em><em>spec:</em><em>backoffLimit: 2</em><em>activeDeadlineSeconds: 60</em><em>template:</em><em>spec:</em><em>containers:</em><em>- name: hello</em><em>image: busybox</em><em>args:</em><em>- /bin/sh</em><em>- -c</em><em>- date; echo Hello from the Kubernetes cluster</em><em>restartPolicy: Never</em></p>
13 <p>В начале указаны название api-группы, тип сущности, имя и дальше - спецификация.</p>
13 <p>В начале указаны название api-группы, тип сущности, имя и дальше - спецификация.</p>
14 <p>В спецификации указаны таймауты и темплейт пода, который будет запускаться. Опции backoffLimit: 2 и activeDeadlineSeconds: 60 значат, что Job будет пытаться выполнить задачу не более двух раз и в общей сложности не дольше 60 секунд.</p>
14 <p>В спецификации указаны таймауты и темплейт пода, который будет запускаться. Опции backoffLimit: 2 и activeDeadlineSeconds: 60 значат, что Job будет пытаться выполнить задачу не более двух раз и в общей сложности не дольше 60 секунд.</p>
15 <p>template- это описание пода, который будет выполнять задачу; в нашем случае запускается простой контейнер busybox, который выводит текущую дату и передаёт привет из Kubernetes.</p>
15 <p>template- это описание пода, который будет выполнять задачу; в нашем случае запускается простой контейнер busybox, который выводит текущую дату и передаёт привет из Kubernetes.</p>
16 <h2>Практические примеры</h2>
16 <h2>Практические примеры</h2>
17 <em>kubectl apply -f job.yaml</em><p><em>И посмотрим, что получилось.</em></p>
17 <em>kubectl apply -f job.yaml</em><p><em>И посмотрим, что получилось.</em></p>
18 <p><em>k get pod -w</em></p>
18 <p><em>k get pod -w</em></p>
19 <p>Видим, что контейнер поднялся и завершился в статусе Completed. В отличие от приложений, которые всегда работают и имеют статус Running.</p>
19 <p>Видим, что контейнер поднялся и завершился в статусе Completed. В отличие от приложений, которые всегда работают и имеют статус Running.</p>
20 <p>Статистику по Job можно посмотреть следующей командой.</p>
20 <p>Статистику по Job можно посмотреть следующей командой.</p>
21 <p><em>kubectl get job</em></p>
21 <p><em>kubectl get job</em></p>
22 <p>Видим, что завершились все задания, время выполнения - 5 секунд.</p>
22 <p>Видим, что завершились все задания, время выполнения - 5 секунд.</p>
23 <p>Ненужный Job обязательно надо удалять. Потому что, если мы не удалим его руками, Job и под будут висеть в кластере всегда - никакой garbage collector не придёт и не удалит их.</p>
23 <p>Ненужный Job обязательно надо удалять. Потому что, если мы не удалим его руками, Job и под будут висеть в кластере всегда - никакой garbage collector не придёт и не удалит их.</p>
24 <p>Если у вас запускаются по 10-20 заданий в час, и никто их не удаляет, они копятся и в кластере появляется много абстракций, которые никому не нужны, но место занимают. А как я уже говорил выше, каждый под в состоянии Completed - это, как минимум, два остановленных контейнера на узле. А докер демон начинает притормаживать, если на узле оказывается несколько сотен контейнеров, и не важно, работают они или остановлены.</p>
24 <p>Если у вас запускаются по 10-20 заданий в час, и никто их не удаляет, они копятся и в кластере появляется много абстракций, которые никому не нужны, но место занимают. А как я уже говорил выше, каждый под в состоянии Completed - это, как минимум, два остановленных контейнера на узле. А докер демон начинает притормаживать, если на узле оказывается несколько сотен контейнеров, и не важно, работают они или остановлены.</p>
25 <p>Команда для удаления:</p>
25 <p>Команда для удаления:</p>
26 <p><em>kubectl delete job hello</em></p>
26 <p><em>kubectl delete job hello</em></p>
27 <h2>Что будет, если сломать Job</h2>
27 <h2>Что будет, если сломать Job</h2>
28 Job, который выполняется без проблем, не очень интересен. Давайте мы над ним немного поиздеваемся.<p>Поправим yaml: добавим в темплейт контейнера exit 1. То есть скажем Job’у, чтобы он завершался с кодом завершения 1. Для Kubernetes это будет сигналом о том, что Job завершился неуспешно.</p>
28 Job, который выполняется без проблем, не очень интересен. Давайте мы над ним немного поиздеваемся.<p>Поправим yaml: добавим в темплейт контейнера exit 1. То есть скажем Job’у, чтобы он завершался с кодом завершения 1. Для Kubernetes это будет сигналом о том, что Job завершился неуспешно.</p>
29 <p><em>containers:</em><em>- name: hello</em><em>image: busybox</em><em>args:</em><em>- /bin/sh</em><em>- -c</em><em>- date; echo Hello from the Kubernetes cluster; exit 1</em><em>restartPolicy: Never</em></p>
29 <p><em>containers:</em><em>- name: hello</em><em>image: busybox</em><em>args:</em><em>- /bin/sh</em><em>- -c</em><em>- date; echo Hello from the Kubernetes cluster; exit 1</em><em>restartPolicy: Never</em></p>
30 <p>Применяем и смотрим, что происходит: один контейнер создался и упал с ошибкой, затем ещё и ещё один. Больше ничего не создаётся.</p>
30 <p>Применяем и смотрим, что происходит: один контейнер создался и упал с ошибкой, затем ещё и ещё один. Больше ничего не создаётся.</p>
31 <p>В статистике подов видим, что создано три пода, у каждого статус Error. Из статистики Job следует, что у нас создан один Job и он не завершился.</p>
31 <p>В статистике подов видим, что создано три пода, у каждого статус Error. Из статистики Job следует, что у нас создан один Job и он не завершился.</p>
32 Если посмотреть описание, то увидим, что было создано три пода, и Job завершился, потому что был достигнут backoffLimit.<p><strong>Обратите внимание!</strong>В yaml лимит равен 2. То есть, если следовать документации, Job должен был остановиться после двух раз, но мы видим три пода. В данном случае "после выполнения двух раз" значит 3 попытки. Когда мы проделываем то же самое на <a>интенсиве</a> с сотней студентов, то примерно у половины создаётся два пода, а у оставшихся три. Это надо понять и простить.</p>
32 Если посмотреть описание, то увидим, что было создано три пода, и Job завершился, потому что был достигнут backoffLimit.<p><strong>Обратите внимание!</strong>В yaml лимит равен 2. То есть, если следовать документации, Job должен был остановиться после двух раз, но мы видим три пода. В данном случае "после выполнения двух раз" значит 3 попытки. Когда мы проделываем то же самое на <a>интенсиве</a> с сотней студентов, то примерно у половины создаётся два пода, а у оставшихся три. Это надо понять и простить.</p>
33 <h2>Проверка ограничения по времени</h2>
33 <h2>Проверка ограничения по времени</h2>
34 Сделаем бесконечный цикл и посмотрим, как работает ограничение по времени - activeDeadlineSeconds.<p>Ограничения оставим теми же (60 секунд), изменим описание контейнера: сделаем бесконечный цикл.</p>
34 Сделаем бесконечный цикл и посмотрим, как работает ограничение по времени - activeDeadlineSeconds.<p>Ограничения оставим теми же (60 секунд), изменим описание контейнера: сделаем бесконечный цикл.</p>
35 <p><em>containers:</em><em>- name: hello</em><em>image: busybox</em><em>args:</em><em>- /bin/sh</em><em>- -c</em><em>- while true; do date; echo Hello from the Kubernetes cluster; sleep 1; done</em><em>restartPolicy: Never</em></p>
35 <p><em>containers:</em><em>- name: hello</em><em>image: busybox</em><em>args:</em><em>- /bin/sh</em><em>- -c</em><em>- while true; do date; echo Hello from the Kubernetes cluster; sleep 1; done</em><em>restartPolicy: Never</em></p>
36 <p>Под запустился.</p>
36 <p>Под запустился.</p>
37 <p>Если посмотреть в логи, то увидим, что каждую секунду у нас появляется новый "Hello" - всё как надо.</p>
37 <p>Если посмотреть в логи, то увидим, что каждую секунду у нас появляется новый "Hello" - всё как надо.</p>
38 <p>Через 60 секунд под оказывается в статусе Terminating (иногда это происходит через ± 10 сек).</p>
38 <p>Через 60 секунд под оказывается в статусе Terminating (иногда это происходит через ± 10 сек).</p>
39 <p>Вспомним, как в Kubernetes реализована концепция остановки подов. Когда приходит время остановить под, то есть все контейнеры в поде, контейнерам посылается sigterm-сигнал и Kubernetes ждёт определённое время, чтобы приложение внутри контейнера отреагировало на этот сигнал.</p>
39 <p>Вспомним, как в Kubernetes реализована концепция остановки подов. Когда приходит время остановить под, то есть все контейнеры в поде, контейнерам посылается sigterm-сигнал и Kubernetes ждёт определённое время, чтобы приложение внутри контейнера отреагировало на этот сигнал.</p>
40 <p>В нашем случае приложение - это простой bash-скрипт с бесконечным циклом, реагировать на сигнал некому. Kubernetes ждёт время, которое задано в параметре graceful shutdown. По дефолту - 30 секунд. То есть если за 30 секунд приложение на sigterm не среагировало, дальше посылается sigkill и процесс с pid 1 внутри контейнера убивается, контейнер останавливается.</p>
40 <p>В нашем случае приложение - это простой bash-скрипт с бесконечным циклом, реагировать на сигнал некому. Kubernetes ждёт время, которое задано в параметре graceful shutdown. По дефолту - 30 секунд. То есть если за 30 секунд приложение на sigterm не среагировало, дальше посылается sigkill и процесс с pid 1 внутри контейнера убивается, контейнер останавливается.</p>
41 <p>Спустя чуть более 100 секунд под удалился. Причем ничего в кластере не осталось, потому что единственный способ остановить что-то в контейнере - это послать sigterm и sigkill. После этого приходит garbage collector, который удаляет все поды в статусе Terminating, чтобы они не засоряли кластер.</p>
41 <p>Спустя чуть более 100 секунд под удалился. Причем ничего в кластере не осталось, потому что единственный способ остановить что-то в контейнере - это послать sigterm и sigkill. После этого приходит garbage collector, который удаляет все поды в статусе Terminating, чтобы они не засоряли кластер.</p>
42 <p>В описании Job мы увидим, что он был остановлен, так как активность превысила допустимую.</p>
42 <p>В описании Job мы увидим, что он был остановлен, так как активность превысила допустимую.</p>
43 <h2>Поле restartPolicy</h2>
43 <h2>Поле restartPolicy</h2>
44 При проверке backoffLimit поды у нас перезагружались. При этом в манифесте указан параметр restartPolicy: Never. Но когда мы смотрели, как работает опция backoffLimit, поды перезагружались. Здесь нет противоречия: если вы посмотрите на весь yaml-файл, то заметите, что этот параметр относится не к Job, а к спецификации контейнера, который запускается внутри пода.<p><em>a</em><em>piVersion: batch/v1</em><em>kind: Job</em><em>metadata:</em><em>name: hello</em><em>spec:</em><em>backoffLimit: 2</em><em>activeDeadlineSeconds: 60</em><em>template:</em><em>spec:</em><em>containers:</em><em>- name: hello</em><em>image: busybox</em><em>args:</em><em>- /bin/sh</em><em>- -c</em><em>- date; echo Hello from the Kubernetes cluster</em><em>restartPolicy: Never</em></p>
44 При проверке backoffLimit поды у нас перезагружались. При этом в манифесте указан параметр restartPolicy: Never. Но когда мы смотрели, как работает опция backoffLimit, поды перезагружались. Здесь нет противоречия: если вы посмотрите на весь yaml-файл, то заметите, что этот параметр относится не к Job, а к спецификации контейнера, который запускается внутри пода.<p><em>a</em><em>piVersion: batch/v1</em><em>kind: Job</em><em>metadata:</em><em>name: hello</em><em>spec:</em><em>backoffLimit: 2</em><em>activeDeadlineSeconds: 60</em><em>template:</em><em>spec:</em><em>containers:</em><em>- name: hello</em><em>image: busybox</em><em>args:</em><em>- /bin/sh</em><em>- -c</em><em>- date; echo Hello from the Kubernetes cluster</em><em>restartPolicy: Never</em></p>
45 <p>Этот параметр говорит kubelet, что делать с контейнером после того, как он был завершён с ошибкой. По умолчанию стоит политика Always, то есть если у нас контейнер в поде завершился, kubelet этот контейнер перезапускает. Причем, все остальные контейнеры в поде продолжают работать, а перезапускается только упавший контейнер.</p>
45 <p>Этот параметр говорит kubelet, что делать с контейнером после того, как он был завершён с ошибкой. По умолчанию стоит политика Always, то есть если у нас контейнер в поде завершился, kubelet этот контейнер перезапускает. Причем, все остальные контейнеры в поде продолжают работать, а перезапускается только упавший контейнер.</p>
46 <p>Это политика по умолчанию, и если её применить в Job, то Job-контроллер не сможет получить информацию о том, что под был завершён с ошибкой. С его точки зрения под будет очень долго выполняться, а то, что kubelet перезапускает упавший контейнер, Job-контроллер не увидит.</p>
46 <p>Это политика по умолчанию, и если её применить в Job, то Job-контроллер не сможет получить информацию о том, что под был завершён с ошибкой. С его точки зрения под будет очень долго выполняться, а то, что kubelet перезапускает упавший контейнер, Job-контроллер не увидит.</p>
47 Если вы укажете только backoffLimit, но забудете указать restartPolicy, то Job будет выполняться бесконечно.<strong>Поэтому в Job надо всегда указывать:</strong><ul><li>backoffLimit (количество попыток),</li>
47 Если вы укажете только backoffLimit, но забудете указать restartPolicy, то Job будет выполняться бесконечно.<strong>Поэтому в Job надо всегда указывать:</strong><ul><li>backoffLimit (количество попыток),</li>
48 <li>activeDeadlineSeconds (общее время),</li>
48 <li>activeDeadlineSeconds (общее время),</li>
49 <li>restartPolicy: Never (сказать kubelet, чтобы он никогда не перезапускал контейнер в поде; если контейнер в поде упал, то и сам под считается упавшим, то есть завершённым. Пусть Job-контроллер разбирается, что произошло).</li>
49 <li>restartPolicy: Never (сказать kubelet, чтобы он никогда не перезапускал контейнер в поде; если контейнер в поде упал, то и сам под считается упавшим, то есть завершённым. Пусть Job-контроллер разбирается, что произошло).</li>
50 </ul><a>Инструкция по Job’ам в документации Kubernetes</a><h2>CronJob: создание объектов Job по расписанию</h2>
50 </ul><a>Инструкция по Job’ам в документации Kubernetes</a><h2>CronJob: создание объектов Job по расписанию</h2>
51 Job позволяет выполнить разовые задачи, но на практике постоянно возникает потребность выполнять что-то по расписанию. И вот здесь Kubernetes предлагает CronJob.<p>CronJob - это yaml-манифест, на основании которого по расписанию создаются Job’ы, которые в свою очередь создают поды, а те делают полезную работу.</p>
51 Job позволяет выполнить разовые задачи, но на практике постоянно возникает потребность выполнять что-то по расписанию. И вот здесь Kubernetes предлагает CronJob.<p>CronJob - это yaml-манифест, на основании которого по расписанию создаются Job’ы, которые в свою очередь создают поды, а те делают полезную работу.</p>
52 <p>На первый взгляд, всё вроде бы просто, но, как и в предыдущем случае, тут есть куча мест, где можно испытать боль.</p>
52 <p>На первый взгляд, всё вроде бы просто, но, как и в предыдущем случае, тут есть куча мест, где можно испытать боль.</p>
53 <p>В манифесте CronJob указывают расписание и ещё несколько важных параметров.</p>
53 <p>В манифесте CronJob указывают расписание и ещё несколько важных параметров.</p>
54 <ul><li>startingDeadlineSeconds,</li>
54 <ul><li>startingDeadlineSeconds,</li>
55 <li>concurrencyPolicy.</li>
55 <li>concurrencyPolicy.</li>
56 </ul>И два параметра, которые влияют на историю выполнения.<ul><li>successfulJobsHistoryLimit,</li>
56 </ul>И два параметра, которые влияют на историю выполнения.<ul><li>successfulJobsHistoryLimit,</li>
57 <li>failedJobsHistoryLimit.</li>
57 <li>failedJobsHistoryLimit.</li>
58 </ul>Посмотрим на манифест CronJob и поговорим о каждом параметре подробнее.<p><em>apiVersion: batch/v1beta1</em><em>kind: CronJob</em><em>metadata:</em><em>name: hello</em><em>spec:</em><em>schedule: "*/1 * * * *"</em><em>concurrencyPolicy: Allow</em><em>jobTemplate:</em><em>spec:</em><em>backoffLimit: 2</em><em>activeDeadlineSeconds: 100</em><em>template:</em><em>spec:</em><em>containers:</em><em>- name: hello</em><em>image: busybox</em><em>args:</em><em>- /bin/sh</em><em>- -c</em><em>- date; echo Hello from the Kubernetes cluster</em><em>restartPolicy: Never</em></p>
58 </ul>Посмотрим на манифест CronJob и поговорим о каждом параметре подробнее.<p><em>apiVersion: batch/v1beta1</em><em>kind: CronJob</em><em>metadata:</em><em>name: hello</em><em>spec:</em><em>schedule: "*/1 * * * *"</em><em>concurrencyPolicy: Allow</em><em>jobTemplate:</em><em>spec:</em><em>backoffLimit: 2</em><em>activeDeadlineSeconds: 100</em><em>template:</em><em>spec:</em><em>containers:</em><em>- name: hello</em><em>image: busybox</em><em>args:</em><em>- /bin/sh</em><em>- -c</em><em>- date; echo Hello from the Kubernetes cluster</em><em>restartPolicy: Never</em></p>
59 <p>schedule - это расписание в виде строчки, которая имеет обычный cron-формат. Строчка в примере говорит о том, что наш Job должен выполняться раз в минуту.</p>
59 <p>schedule - это расписание в виде строчки, которая имеет обычный cron-формат. Строчка в примере говорит о том, что наш Job должен выполняться раз в минуту.</p>
60 <p>concurrencyPolicy - этот параметр отвечает за одновременное выполнение заданий. Бывает трёх видов: Allow, Forbid, Replace.</p>
60 <p>concurrencyPolicy - этот параметр отвечает за одновременное выполнение заданий. Бывает трёх видов: Allow, Forbid, Replace.</p>
61 <p><strong>Allow</strong> позволяет подам запускаться. Если за минуту Job не отработал, все равно будет создан ещё один. Одновременно могут выполняться несколько Job’ов.</p>
61 <p><strong>Allow</strong> позволяет подам запускаться. Если за минуту Job не отработал, все равно будет создан ещё один. Одновременно могут выполняться несколько Job’ов.</p>
62 <p>Например, если один Job выполняется 100 сек., а Cron выполняется раз в минуту, то запускается Job, выполняется 61 сек., в это время запускается ещё один Job. В итоге в кластере одновременно работают два Job’a, которые выполняют одну и ту же работу. Возникает положительная обратная связь: чем больше Job’ов запущено, тем больше нагрузка на кластер, тем медленнее они работают, тем дольше они работают и тем больше одновременных подов запускается - в итоге всё застывает под бешеной нагрузкой.</p>
62 <p>Например, если один Job выполняется 100 сек., а Cron выполняется раз в минуту, то запускается Job, выполняется 61 сек., в это время запускается ещё один Job. В итоге в кластере одновременно работают два Job’a, которые выполняют одну и ту же работу. Возникает положительная обратная связь: чем больше Job’ов запущено, тем больше нагрузка на кластер, тем медленнее они работают, тем дольше они работают и тем больше одновременных подов запускается - в итоге всё застывает под бешеной нагрузкой.</p>
63 <p><strong>Replace</strong> заменяет запущенную нагрузку: старый Job убивается, запускается новый. На самом деле это не самый лучший вариант, когда прерываем то, что уже выполнялось, и начинаем ту же самую задачу выполнять заново. В каких-то случаях это возможно, в каких-то неприемлемо.</p>
63 <p><strong>Replace</strong> заменяет запущенную нагрузку: старый Job убивается, запускается новый. На самом деле это не самый лучший вариант, когда прерываем то, что уже выполнялось, и начинаем ту же самую задачу выполнять заново. В каких-то случаях это возможно, в каких-то неприемлемо.</p>
64 <p><strong>Forbid</strong> запрещает запуск новых Job’ов, пока не отработает предыдущий. С этой политикой можно быть уверенным, что всегда запускается только один экземпляр задачи. Поэтому Forbid используют наиболее часто.</p>
64 <p><strong>Forbid</strong> запрещает запуск новых Job’ов, пока не отработает предыдущий. С этой политикой можно быть уверенным, что всегда запускается только один экземпляр задачи. Поэтому Forbid используют наиболее часто.</p>
65 <p>jobTemplate - это шаблон, из которого создаётся объект Job. Ну а всё остальное мы уже видели в манифесте Job.</p>
65 <p>jobTemplate - это шаблон, из которого создаётся объект Job. Ну а всё остальное мы уже видели в манифесте Job.</p>
66 <p>Применим манифест.</p>
66 <p>Применим манифест.</p>
67 <p><em>kubectl apply -f cronjob.yaml</em></p>
67 <p><em>kubectl apply -f cronjob.yaml</em></p>
68 <p>Посмотрим, что получилось:</p>
68 <p>Посмотрим, что получилось:</p>
69 <p><em>k get cronjobs.batch</em></p>
69 <p><em>k get cronjobs.batch</em></p>
70 <p>Увидим название CronJob, расписание, параметр запуска, количество активных Job’ов и сколько времени они работают.</p>
70 <p>Увидим название CronJob, расписание, параметр запуска, количество активных Job’ов и сколько времени они работают.</p>
71 <p>Раздел Suspend - временная приостановка CronJob. В данном случае указано значение False. Это значит, что CronJob выполняется. Можно отредактировать манифест и поставить опцию True, и тогда он не будет выполняться, пока мы его снова не запустим.</p>
71 <p>Раздел Suspend - временная приостановка CronJob. В данном случае указано значение False. Это значит, что CronJob выполняется. Можно отредактировать манифест и поставить опцию True, и тогда он не будет выполняться, пока мы его снова не запустим.</p>
72 <p>Active - сколько Job’ов создано, Last Schedule - когда последний раз исполнялся.</p>
72 <p>Active - сколько Job’ов создано, Last Schedule - когда последний раз исполнялся.</p>
73 <p>Теперь можно посмотреть статистику по Job’ам и подам.</p>
73 <p>Теперь можно посмотреть статистику по Job’ам и подам.</p>
74 <p><em>kubectl get job</em></p>
74 <p><em>kubectl get job</em></p>
75 <p>Видно, что создан один Job.</p>
75 <p>Видно, что создан один Job.</p>
76 <p><em>kubectl get pod</em></p>
76 <p><em>kubectl get pod</em></p>
77 <p>Под создан, он выполнил полезную работу.</p>
77 <p>Под создан, он выполнил полезную работу.</p>
78 <p>Что получается: CronJob создал Job, Job создал под, под отработал, завершился - всё здорово.</p>
78 <p>Что получается: CronJob создал Job, Job создал под, под отработал, завершился - всё здорово.</p>
79 <p>Ещё раз посмотрим на CronJob:</p>
79 <p>Ещё раз посмотрим на CronJob:</p>
80 Last Schedule был 19 секунд назад. Если посмотреть на Job, то увидим, что у нас появился следующий Job и следующий под.<p>Возникает вопрос: а что будет, если CronJob отработает хотя бы пару недель? Неужели у нас в кластере будет столько же Job’ов и подов в статусе Completed, сколько в этой паре недель минут?</p>
80 Last Schedule был 19 секунд назад. Если посмотреть на Job, то увидим, что у нас появился следующий Job и следующий под.<p>Возникает вопрос: а что будет, если CronJob отработает хотя бы пару недель? Неужели у нас в кластере будет столько же Job’ов и подов в статусе Completed, сколько в этой паре недель минут?</p>
81 <p>Когда CronJob’ы только появились и были на стадии альфа-тестирования, примерно это и происходило: делали CronJob раз в минуту, смотрели - работает, всё здорово, а через неделю кластер становился неработоспособным, потому что количество остановленных контейнеров на узлах было ошеломляющим. Теперь же ситуация изменилась.</p>
81 <p>Когда CronJob’ы только появились и были на стадии альфа-тестирования, примерно это и происходило: делали CronJob раз в минуту, смотрели - работает, всё здорово, а через неделю кластер становился неработоспособным, потому что количество остановленных контейнеров на узлах было ошеломляющим. Теперь же ситуация изменилась.</p>
82 <p>Снова откроем манифест и посмотрим, что было добавлено:</p>
82 <p>Снова откроем манифест и посмотрим, что было добавлено:</p>
83 <p><em>k</em><em>get cronjobs.batch hello -o yaml</em></p>
83 <p><em>k</em><em>get cronjobs.batch hello -o yaml</em></p>
84 Появились опции failedJobHistorLimit со значением 1 и successfulJobHistoryLimit со значением 3. Они отвечают за количество Job’ов, которые остаются одновременно в кластере. То есть CronJob не только создаёт новые Job’ы, но и удаляет старые.<p>Когда только контроллер CronJob создавался, эти опции не были установлены по умолчанию и CronJob за собой ничего не удалял. Было много возмущений от пользователей, и тогда поставили дефолтные лимиты.</p>
84 Появились опции failedJobHistorLimit со значением 1 и successfulJobHistoryLimit со значением 3. Они отвечают за количество Job’ов, которые остаются одновременно в кластере. То есть CronJob не только создаёт новые Job’ы, но и удаляет старые.<p>Когда только контроллер CronJob создавался, эти опции не были установлены по умолчанию и CronJob за собой ничего не удалял. Было много возмущений от пользователей, и тогда поставили дефолтные лимиты.</p>
85 <h2>И на сладкое - про startingDeadlineSeconds</h2>
85 <h2>И на сладкое - про startingDeadlineSeconds</h2>
86 В параметре startingDeadlineSeconds указывают количество секунд, на которое можно просрочить запуск Job. Если по каким-то причинам Job не создался и с момента, когда его надо было создать, прошло больше секунд, чем указано в этом параметре, то он и <strong>не будет создан</strong>. А если меньше, то хоть и с опозданием, Job будет создан.<p>Тут есть небольшая ловушка, если concurrencyPolicy разрешают одновременное создание Job, то при большом значении параметра startingDeadlineSeconds возможен одновременный запуск десятков пропущенных Job одновременно. Для уменьшения всплеска нагрузки в код Kubernetes захардкожены лимиты и запрещающие процедуры:</p>
86 В параметре startingDeadlineSeconds указывают количество секунд, на которое можно просрочить запуск Job. Если по каким-то причинам Job не создался и с момента, когда его надо было создать, прошло больше секунд, чем указано в этом параметре, то он и <strong>не будет создан</strong>. А если меньше, то хоть и с опозданием, Job будет создан.<p>Тут есть небольшая ловушка, если concurrencyPolicy разрешают одновременное создание Job, то при большом значении параметра startingDeadlineSeconds возможен одновременный запуск десятков пропущенных Job одновременно. Для уменьшения всплеска нагрузки в код Kubernetes захардкожены лимиты и запрещающие процедуры:</p>
87 <p>Если параметр startingDeadlineSeconds не указан в манифесте:</p>
87 <p>Если параметр startingDeadlineSeconds не указан в манифесте:</p>
88 <p>CronJob контроллер при создании Job смотрит на время последнего запуска - значение LastscheduleTime в status: и считает, сколько времени прошло с последнего запуска. И если это время достаточно велико, а точнее за этот промежуток времени CronJob должен был отработать 100 раз или больше, но у нее этого не получилось:</p>
88 <p>CronJob контроллер при создании Job смотрит на время последнего запуска - значение LastscheduleTime в status: и считает, сколько времени прошло с последнего запуска. И если это время достаточно велико, а точнее за этот промежуток времени CronJob должен был отработать 100 раз или больше, но у нее этого не получилось:</p>
89 <ul><li>например конкурент полиси стоит форбид, крон запускается раз в минуту, и один из запусков подзавис и работал 2 часа, то есть 120 минут;</li>
89 <ul><li>например конкурент полиси стоит форбид, крон запускается раз в минуту, и один из запусков подзавис и работал 2 часа, то есть 120 минут;</li>
90 <li>или у нас один мастер в кластере, который упал на пару часов.</li>
90 <li>или у нас один мастер в кластере, который упал на пару часов.</li>
91 </ul>В этом случае происходит нечто странное: CronJob перестает работать, новые Job’ы больше не создаются. Сообщение об этом приходит в Events, но хранится там недолго. Для восстановления работы приходится удалять CronJob и создавать его заново.<p>И еще более странное:</p>
91 </ul>В этом случае происходит нечто странное: CronJob перестает работать, новые Job’ы больше не создаются. Сообщение об этом приходит в Events, но хранится там недолго. Для восстановления работы приходится удалять CronJob и создавать его заново.<p>И еще более странное:</p>
92 <p>Если установлен параметр startingDeadlineSeconds, то поведение немного меняется. 100 пропущенных запусков должны уложиться в количество секунд, указанных в этом параметре. т. е. если в расписании стоит выполняться раз в минуту, а startingDeadlineSeconds меньше 6000 секунд, тогда CronJob будет работать всегда, но в этом случае при политике Allow возможен одновременный запуск множества Job.</p>
92 <p>Если установлен параметр startingDeadlineSeconds, то поведение немного меняется. 100 пропущенных запусков должны уложиться в количество секунд, указанных в этом параметре. т. е. если в расписании стоит выполняться раз в минуту, а startingDeadlineSeconds меньше 6000 секунд, тогда CronJob будет работать всегда, но в этом случае при политике Allow возможен одновременный запуск множества Job.</p>
93 <p>И наконец, любопытный side-эффект:</p>
93 <p>И наконец, любопытный side-эффект:</p>
94 <p>Если установить опцию startingDeadlineSeconds равной нулю, Job’ы вообще перестают создаваться.</p>
94 <p>Если установить опцию startingDeadlineSeconds равной нулю, Job’ы вообще перестают создаваться.</p>
95 Если вы используете опцию startingDeadlineSeconds, указывайте её значение меньше, чем интервал выполнения в расписании, но не ноль.Применяйте политику Forbid. Например, если было пропущено 5 вызовов и наступило очередное время исполнения, то контроллер не будет 5 раз запускать пропущенные задачи, а создаст только один Job. Это логичное поведение.<h2>Особенность работы CronJob</h2>
95 Если вы используете опцию startingDeadlineSeconds, указывайте её значение меньше, чем интервал выполнения в расписании, но не ноль.Применяйте политику Forbid. Например, если было пропущено 5 вызовов и наступило очередное время исполнения, то контроллер не будет 5 раз запускать пропущенные задачи, а создаст только один Job. Это логичное поведение.<h2>Особенность работы CronJob</h2>
96 Цитата из <a>документации Kubernetes</a>:<p><em>A cron job creates a job object about once per execution time of its schedule. We say "about" because there are certain circumstances where two jobs might be created, or no job might be created. We attempt to make these rare, but do not completely prevent them. Therefore, jobs should be idempotent.</em></p>
96 Цитата из <a>документации Kubernetes</a>:<p><em>A cron job creates a job object about once per execution time of its schedule. We say "about" because there are certain circumstances where two jobs might be created, or no job might be created. We attempt to make these rare, but do not completely prevent them. Therefore, jobs should be idempotent.</em></p>
97 - <p>Вольный перевод на русский:</p>
97 + <p>Вольный перевод на руский:</p>
98 - <p><em>CronJob создаёт объект Job примерно один раз на каждое время исполнения по распсанию. Мы говорим "примерно", потому что иногда бывают случаи, когда создаются два Job’а одновременно или ни одного. Мы делаем всё, чтобы сделать подобные случаи как можно более редкими, но полностью избежать этого не получается. Следовательно, Job’ы должны быть идемпотентны.</em></p>
98 + <p><em>CronJob создаёт объект Job примерно один раз на каждое время исполнения по расписанию. Мы говорим "примерно", потому что иногда бывают случаи, когда создаются два Job’а одновременно или ни одного. Мы делаем всё, чтобы сделать подобные случаи как можно более редкими, но полностью избежать этого не получается. Следовательно, Job’ы должны быть идемпотентны.</em></p>
99 <p>Идемпотентны - должны выполняться на одной и той же среде несколько раз и всегда возвращать одинаковый результат.</p>
99 <p>Идемпотентны - должны выполняться на одной и той же среде несколько раз и всегда возвращать одинаковый результат.</p>
100 <p>В общем, используйте CronJob на свой страх и риск.</p>
100 <p>В общем, используйте CronJob на свой страх и риск.</p>
101 <p>В качестве альтернативы CronJob можно использовать под, в котором запущен самый обычный crond. Без выкрутасов. Старый добрый cron работает без проблем, и мы всегда знаем, что задачи будут выполнены один раз. Надо только побеспокоиться, чтобы внутри пода с кроном не выполнялись одновременно несколько задач.</p>
101 <p>В качестве альтернативы CronJob можно использовать под, в котором запущен самый обычный crond. Без выкрутасов. Старый добрый cron работает без проблем, и мы всегда знаем, что задачи будут выполнены один раз. Надо только побеспокоиться, чтобы внутри пода с кроном не выполнялись одновременно несколько задач.</p>
102 <p>Изучить продвинутые абстракции и попрактиковаться в работе с Kubernetes можно с помощью <a>видеокурса Kubernetes База</a>. В октябре 2020 мы обновили курс, подробности <a>здесь</a>.</p>
102 <p>Изучить продвинутые абстракции и попрактиковаться в работе с Kubernetes можно с помощью <a>видеокурса Kubernetes База</a>. В октябре 2020 мы обновили курс, подробности <a>здесь</a>.</p>