Что такое джоба в программировании
Создание задания
В Управляемом экземпляре Azure SQL в настоящее время поддерживается большинство функций агента SQL Server (но не все). Подробные сведения см. в статье Различия в T-SQL между Управляемым экземпляром SQL Azure и SQL Server.
В этой статье описывается создание задания агента SQL Server в SQL Server с помощью среды SQL Server Management Studio, Transact-SQL или управляющих объектов SQL Server (SMO).
Чтобы добавить шаги заданий, расписаний, предупреждений и уведомлений, которые можно отправить операторам, см. ссылки на разделы руководства.
Перед началом работы
Для создания задания используется
Перед началом
Ограничения
Назначение задания другому имени входа не гарантирует того, что новый владелец обладает достаточными разрешениями для успешного запуска задания.
безопасность
Чтобы изменить владельца задания, необходимо быть системным администратором.
Permissions
Использование среды SQL Server Management Studio
Создание задания агента SQL Server
В обозревателе объектов щелкните знак «плюс», чтобы развернуть сервер, на котором нужно создать задание агента SQL Server.
Щелкните знак «плюс», чтобы развернуть Агент SQL Server.
Щелкните правой кнопкой мыши папку Задания и выберите пункт Создать задание….
На странице Общие в диалоговом окне Создание задания измените общие свойства задания. Дополнительные сведения о параметрах, доступных на этой странице, см. в разделе Свойства задания — создание задания (страница «Общие»)
На странице Действия задайте шаги задания. Дополнительные сведения о параметрах, доступных на этой странице, см. в разделе Свойства задания — создание задания (страница «Шаги»)
На странице Расписания задайте расписания для задания. Дополнительные сведения о параметрах, доступных на этой странице, см. в разделе Свойства задания — создание задания (страница «Расписания»)
На странице Предупреждения задайте предупреждения для задания. Дополнительные сведения о параметрах, доступных на этой странице, см. в разделе Свойства задания — создание задания (страница «Предупреждения»)
На странице Уведомления задайте действия, которые должен выполнять агент Microsoft SQL Server после завершения задания. Дополнительные сведения о параметрах, доступных на этой странице, см. в разделе Свойства задания — создание задания (страница «Уведомления»).
Страница Цели используется для управления целевыми серверами в задании. Дополнительные сведения о параметрах, доступных на этой странице, см. в разделе Свойства задания — создание задания (страница «Цели»).
После завершения нажмите кнопку ОК.
Использование Transact-SQL
Создание задания агента SQL Server
В обозревателе объектов подключитесь к экземпляру компонента Компонент Database Engine.
На стандартной панели выберите пункт Создать запрос.
Скопируйте следующий пример в окно запроса и нажмите кнопку Выполнить.
Дополнительные сведения см. в разделе:
Использование управляющих объектов SQL Server
Создание задания агента SQL Server
Вызовите метод Create класса Job на любом языке программирования, таком как Visual Basic, Visual C# или PowerShell. Пример кода см. в разделе Планирование автоматических административных задач в агенте SQL Server.
Job System. Обзор с другой стороны
В новой версии unity 2018 года наконец официально добавили новую систему Entity component system или сокращенно ECS которая позволяет вместо привычной работы с компонентами объекта работать только с их данными.
Дополнительная же система задач предлагает вам использовать параллельные вычислительные мощности, чтобы улучшить производительность вашего кода.
Вместе эти две новые системы (ECS и Job System) предлагают новый уровень обработки данных.
Конкретно в этой статье я не буду разбирать всю систему ECS, которая пока что доступна в виде отдельно скачиваемого набора инструментов в unity, а рассмотрю только систему задач и как ее можно использовать вне пакета ECS.
Новая система
Изначально в unity и раньше можно было использовать многопоточные вычисления, но всё это нужно было создавать разработчику самостоятельно, самому решать возникающие проблемы и обходить подводные камни. И если раньше необходимо было работать на прямую с такими вещами как создание потоков, закрытие потоков, пулы, синхронизация, то теперь же вся эта работа легла на плечи движка, а от самого разработчика требуется только создание задач и их выполнение.
Задачи
Чтобы выполнить какие-либо вычисления в новой системе необходимо использовать задачи которые представляют из себя объекты состоящие из методов и данных для вычисления.
Как и любые другие данные в системе ECS, задачи в Job System также представлены в виде структур которые наследует один из трех интерфейсов.
Самый простой интерфейс задачи содержащий в себе один метод Execute который ничего не принимает в виде параметров и ничего не возвращает.
Сама задача выглядит так:
В методе Execute можно выполнять необходимые вычисления.
IJobParallelFor
Еще один интерфейс с таким же методом Execute который уже в свою очередь принимает числовой параметр index.
Этот интерфейс IJobParallelFor, в отличие от интерфейса IJob, предлагает выполнить задачу несколько раз и не просто выполнить, а разбить это выполнение на блоки которые будут распределены между потоками.
Непонятно? Не переживайте об этом я еще расскажу.
IJobParallelForTransform
И последний, особый интерфейс, который, как понятно из названия предназначен для работы с данными транформом объекта. Также содержит в себе метод Execute, с числовым параметром index и параметром TransformAccess где находятся позиция, размер и вращение трансформа.
Из-за того что напрямую в задаче нельзя работать с unity объектами, этот интерфейс может обрабатывать данные трансформа только в виде отдельной структуры TransformAccess.
Готово, теперь вы знаете как создаются структуры задач, можно переходить к практике.
Выполнение задачи
Давайте создадим простую задачу унаследованную от интерфейса IJob и выполним ее. Для этого нам понадобится любой простой MonoBehaviour скрипт и сама структура задачи.
Теперь закиньте это скрипт на какой нибудь объект на сцене. В этом же скрипте (TestJob) ниже напишем структуру задачи и не забывайте импортировать нужные библиотеки.
В методе Execute, для примера, выведем простую строку в консоль.
Теперь перейдем в метод Start скрипта TestJob, где создадим экземпляр задачи после чего выполним его.
Если вы проделали всё как в примере, то после запуска игры получите простое сообщение в консоль как на картинке.
Что здесь происходит: после вызова метода Schedule, планировщик помещает задачу в хэндл и теперь ее можно выполнить вызвав метод Complete.
Это был пример задачи которая просто выводила текст в консоль. Чтобы задача выполняла какие-либо параллельные вычисления нужно наполнить ее данными.
Данные в задаче
Как и в системе ECS в задачах нет доступа к объектам unity, у вас не получиться передать в задачу GameObject и изменить его имя там. Все что вы можете сделать это передать в задачу какие-то отдельные параметры объекта, изменить эти параметры, и после выполнения задачи применить эти изменения обратно объекту.
К самим данным в задаче также есть несколько ограничений: во-первых, это должны быть структуры, во-вторых, это должны быть не преобразуемые типы данных то есть тот же boolean или string вы передать в задачу уже не сможете.
И главное условие: данные не заключенные в контейнер могут быть доступны только внутри задачи!
Контейнеры
При работе с многопоточными вычислениями возникает необходимость как то обмениваться данными между потоками. Чтобы можно было передавать в них данные и считывать их обратно в системе задач для этих целей существуют контейнеры. Эти контейнеры представлены в виде обычных структур и работаю по принципу моста по которому элементарные данные синхронизируются между потоками.
Есть несколько видов контейнеров:
NativeArray. Самый простой и самый часто используемый тип контейнера представлен в виде простого массива с фиксированным размером.
NativeSlice. Еще один контейнер — массив, как понятно из перевода, предназначен для нарезания NativeArray на части.
Это два основных контейнера доступных без подключения системы ECS. В более расширенном варианте существует еще несколько видов контейнеров.
NativeList. Представляет собой обычный список данных.
NativeHashMap. Аналог словаря с ключом и значением.
NativeMultiHashMap. Тот же NativeHashMap только с несколькими значениями под одним ключом.
NativeQueue. Список очереди данных.
Так как мы работает без подключения системы ECS то нам доступны только NativeArray и NativeSlice.
Перед тем как перейти к практической части необходимо разобрать самый главный момент — создание экземпляров.
Создание контейнеров
Как я говорил раньше, эти контейнеры представляют собой мост по которым данные синхронизируются между потоками. Система задач открывает этот мост перед началом работы и закрывает после ее завершения. Процесс открытия называется “аллокация” (Allocation) или еще “выделением памяти”, процесс закрытия — “высвобождением ресурсов”(Dispose).
Именно аллокация определяет то как долго задача сможет использовать данные в контейнере — иначе говоря как долго будет открыт мост.
Для того чтобы лучше понять эти два процесса давайте взглянем на картинку ниже.
Нижняя ее часть показывает жизненный цикл главного потока(Main thread), который исчисляется в кол-ве кадров, в первом кадре мы создаем еще один параллельный поток(New thread) который существует определенное кол-во кадров и потом благополучно закрывается.
В этот же New thread и поступает задача с контейнером.
Теперь взглянем на верхнюю часть картинки.
Белая полоса Allocation показывает время существования контейнера. В первом кадре происходит аллокация контейнера — открытие моста, до этого момента контейнер не существовал, после выполнения всех расчетов в задаче, контейнер высвобождается из памяти и в 9’ом кадре мост закрывается.
Также на этой полоске (Allocation) есть временные отрезки(Temp, TempJob и Presistent), каждый этот отрезок показывает предположительное время существования контейнера.
Для чего эти отрезки нужны!? Дело в том что выполнение задачи по продолжительности могут быть разные, мы можем выполнять их прямо в том же методе где и создали, или же можем растянуть время выполнения задачи если она достаточно сложная, а эти отрезки показывают как срочно и как долго задача сможет использовать данные в контейнере.
Если все еще не понятно, я разберу каждый тип аллокации на примере.
Теперь можно перейти к практической части создания контейнеров, для этого вернемся в метод Start скрипта TestJob и создадим новый экземпляр контейнера NativeArray и не забывайте подключать нужные библиотеки.
Для создания нового экземпляра контейнера необходимо указать в его конструкторе размер, и тип аллокации. В этом примере используется тип Temp, так как задача будет выполняться только в методе Start.
Теперь инициализируем точно такую же переменную array в самой структуре задачи SimpleJob.
Готово. Теперь можно создать саму задачу и передать в нее экземпляр массива.
Для запуска задачи в этот раз будем использовать ее хэндл JobHandle, чтобы получить его вызовем тот же метод Schedule.
Теперь можно вызывать метод Complete у ее хэндла и проверить выполнена ли задача, чтобы вывести текст в консоль.
Если запустить задачу в таком виде то после запуска игры вы получите жирную красную ошибку о том что не высвободили контейнер array из ресурсов после выполнения задачи.
Чтобы этого избежать вызовем метод Dispose у контейнера после выполнения задачи.
После чего можно спокойно запускать заново.
Но задача же ничего не выполняет! — тогда добавим в нее пару действий.
В методе Execute я умножаю индекс каждого элемента массива на самого себя и записываю обратно в массив array, чтобы вывести результат в консоль в методе Start.
Какой будет результат в консоли если выведем последний элемент массива возведенный в квадрат?
Вот так можно создавать контейнеры, помещать их в задачи и выполнять действия над ними.
Это был пример c использованием типа аллокации Temp, который подразумевает выполнение задачи в течении одного кадра. Этот тип лучше использовать когда вам необходимо быстро выполнить вычисления не нагружая главный поток, но нужно быть осторожным если задача будет слишком сложная или если их будет очень много, то могут возникнуть провисания, в этом случае лучше использовать тип TempJob который я разберу далее.
TempJob
В этом примере я немного изменю структуру задачи SimpleJob и унаследую ее от другого интерфейса IJobParallelFor.
Также раз задача будет выполняться дольше чем один кадр то будем выполнять и собирать результаты задачи в разных методах Awake и Start представленный в виде корутины. Для этого изменим немного внешний вид класса TestJob.
В методе Awake будем создавать задачу и контейнер векторов, а в методе Start выводить полученные данные и высвобождать ресурсы.
Здесь опять создается контейнер array с типом аллокации TempJob, после чего создаем задачу и получаем ее хэндл вызвав метод Schedule с небольшими изменениями.
Первый параметр в методе Schedule указывает сколько раз выполнится задача, здесь то же число что и размер массива array.
Второй параметр указывает на сколько блоков поделить выполнение задачи.
Какие еще блоки?
Раньше для того чтобы выполнить задачу поток просто один раз вызывал метод Execute, теперь же вызвать этот метод нужно 100 раз, поэтому планировщик, чтобы не нагружать какой то отдельный поток, разбивает эти 100 раз повторений на блоки которые распределяет между потоками. В примере сотня повторений будет поделена на 5 блоков по 20 повторений в каждом, то есть предположительно планировщик распределит эти 5 блоков в 5 потоков, где каждый поток вызовит метод Execute 20 раз. На практике конечно не факт что планировщик именно так и поступит, все зависит от загруженности системы, так что может и все 100 повторений произойдут в одном потоке.
Теперь можно вызвать метод Complete у хэндла задачи.
В корутине Start будем проверять выполнение задачи после чего произведем очистку контейнера.
Теперь перейдем к действиям в самой задаче.
После выполнения задачи в методе Start выведем все элементы массива в консоль.
Готово, можно запустить и посмотреть на результат.
Чтобы понять в чем заключается разница между IJob и IJobParallelFor взглянем на изображения ниже.
Для примера можно и в IJob использовать простой цикл for для выполнения вычислений несколько раз, но в любом случае поток сможет только один раз вызвать метод Execute за все время работы задачи — это как заставить одного человека выполнить сотню одних и тех же действий подряд.
IJobParallelFor предлагает не просто выполнить задачу в одном потоке несколько раз, а еще и распределить эти повторения между другими потоками.
В целом тип аллокации TempJob отлично подходит для большинства задач которые выполняются в течении нескольких кадров.
Но что если вам нужно хранить данные даже после выполнения задачи, что если после получения результата их не нужно сразу же уничтожать. Для этого необходимо использовать тип аллокации Persistent, который подразумевает высвобождение ресурсов тогда “когда нужно будет!”.
Persistent
Опять вернемся в класс TestJob и изменим его. Теперь будем создавать задачи в методе OnEnable, проверять их выполнение в методе Update и зачищать ресурсы в методе OnDisable.
В примере будем двигать объект в методе Update, для расчета траектории будем использовать два векторных контейнера — inputArray в который будем помещать текущее положение и outputArray откуда будем принимать полученные результаты.
Структуру задачи SimpleJob также немного изменим унаследовав ее от интерфейса IJob чтобы выполнить ее один раз.
В саму задачу мы также будем предавать два векторных контейнера, один вектор позиции и числовую дельту, которая будет смещать объект к цели.
Атрибут ReadOnly и WriteOnly показывают потоку ограничения в действиях связанных с данными внутри контейнеров. ReadOnly предлагает потоку только читать данные из контейнера, атрибут WriteOnly наоборот — дает возможность потоку только записывать данные в контейнер. Если вам нужно выполнять сразу два этих действия с одним контейнером тогда не нужно помечать его атрибутом вообще.
Перейдем в метод OnEnable класса TestJob где будут инициализироваться контейнеры.
Размеры контейнеров будут единичными так как нужно передавать и принимать параметры только один раз. Тип аллокации будет Persistent.
В методе OnDisable будем высвобождать ресурсы контейнеров.
Создадим отдельный метод CreateJob где будем создавать задачу с ее хэндлом и там же будем заполнять ее данными.
На самом деле inputArray здесь не особо то и нужен так как можно передавать в задачу и просто вектор направления, но так я думаю будет лучше понятно зачем вообще нужны эти атрибуты ReadOnly и WriteOnly.
В методе Update будем проверять выполнена ли задача, после чего применим полученный результат транформу объекта и снова ее запустим.
Перед запуском немного подправим метод OnEnable так чтобы задача создавалась сразу после инициализации контейнеров.
Готово, теперь можно перейти к самой задаче и выполнить нужные вычисления в методе Execute.
Чтобы увидеть результат работы можно кинуть скрипт TestJob на какой то объект и запустить игру.
Например у меня спрайт просто понемногу смещается вправо.
В общем тип аллокации Persistent отлично подходит для повторно используемых контейнеров, которые нет необходимости каждый раз уничтожать и создавать заново.
Так какой же тип использовать!?
Тип Temp лучше использовать для быстрого выполнения вычислений, но если задача будет слишком сложной и большой могут возникнуть провисания.
Тип TempJob отлично подходит для работы с объектами unity, так можно изменять параметры объектов и применять их, к примеру, в следующем кадре.
Тип Persistent можно использовать тогда когда вам не важна скорость, а просто необходимо постоянно вычислять какие-то данные на стороне, к примеру обрабатывать данные по сети, или работу ИИ.
JobHandle
Отдельно стоит все же разобрать возможности хэндла задачи, ведь кроме как проверять процесс выполнения задачи, этот маленький хэндл еще может создавать целые сети задач через зависимости(хотя я больше предпочитаю называть их очередями).
К примеру если вам нужно выполнить две задачи в определенной последовательности то для этого нужно просто вложить хэндл одной задачи в хэндл другой.
Выглядит это примерно так.
Каждый отдельный хэндл изначально содержит свою задачу, а вот уже при комбинировании получаем новый хэндл с двумя задачами.
Последовательность выполнения сохраняется и планировщик не начнет выполнять следующую задачу пока не убедится в выполнении предыдущей, но важно помнить, что свойство хэндла IsCompleted будет ожидать выполнения всех задач находящихся в нем.
Заключение
Контейнеры
Безопасность!
Не пытайтесь использовать статические данные в задаче(Random и другие), любое обращение к статическим данным нарушит безопасность системы. На самом деле в данный момент можно обращаться к статическим данным, но только если вы уверены что они не изменяются в процессе работы — то есть полностью статичны и доступны только для чтения.
Когда использовать систему задач?
Все эти примеры что приведены здесь в статье лишь условные, и показывают как нужно работать с этой системой, а не когда нужно ее использовать. Систему задач можно использовать и без ECS, нужно понимать что система также потребляет ресурсы при работе и, что по любому поводу сразу писать задачи, создавать кучи контейнеров просто бессмысленно — все станет еще хуже. К примеру пересчитать массив размером 10 тысяч элементов будет не правильно — у вас больше времени уйдет на работу планировщика, а вот пересчитать все полигоны огромного террейна или вообще сгенерировать его — правильное решение, можно разбить террейн на задачи и обрабатывать каждую в отдельном потоке.
В общем если вы постоянно занимаетесь сложными вычислениями в проектах и постоянно ищите новые возможности как сделать этот процесс менее ресурсоемким, то Job System это именно то что вам нужно. Если вы постоянно работает со сложными вычислениями неотделимо от объектов и хотите чтобы ваш код работал быстрее и поддерживался на большинстве платформ, то ECS вам точно в этом поможет. Если вы создаете проекты только под WebGL тогда это не для вас, на данный момент Job System не поддерживает работу в браузерах, хотя это уже проблема не юнитеков, а самих разработчиков браузеров.
View a Job
В Управляемом экземпляре Azure SQL в настоящее время поддерживается большинство функций агента SQL Server (но не все). Подробные сведения см. в статье Различия в T-SQL между Управляемым экземпляром SQL Azure и SQL Server.
В этой статье описывается, как просматривать задания агента Microsoft SQL Server в SQL Server с помощью среды SQL Server Management Studio или Transact-SQL.
Перед началом
безопасность
Использование среды SQL Server Management Studio
Просмотр задания
В обозревателе объектов подключитесь к экземпляру компонента Компонент SQL Server Database Engineи разверните его.
Раскройте узел Агент SQL Server, а затем узел Задания.
Правой кнопкой мыши щелкните задание и выберите Свойства.
Использование Transact-SQL
Просмотр задания
В обозревателе объектов подключитесь к экземпляру компонента Компонент Database Engine.
На стандартной панели выберите пункт Создать запрос.
Скопируйте следующий пример в окно запроса и нажмите кнопку Выполнить.
Использование управляющих объектов SQL Server
Просмотр задания
Воспользуйтесь классом Job на любом языке программирования, таком как Visual Basic, Visual C# или PowerShell. Дополнительные сведения см. в статье Управляющие объекты SQL Server (SMO).
Продвинутые абстракции Kubernetes: Job, CronJob
Что такое Job и CronJob в Kubernetes, для чего они нужны, а для чего их использовать не стоит.
Эта статья — выжимка из лекции вечерней школы «Слёрм Kubernetes».
Job: сущность для разовых задач
Job (работа, задание) — это yaml-манифест, который создаёт под для выполнения разовой задачи. Если запуск задачи завершается с ошибкой, Job перезапускает поды до успешного выполнения или до истечения таймаутов. Когда задача выполнена, Job считается завершённым и больше никогда в кластере не запускается. Job — это сущность для разовых задач.
Когда используют Job
При установке и настройке окружения. Например, мы построили CI/CD, который при создании новой ветки автоматически создаёт для неё окружение для тестирования. Появилась ветка — в неё пошли коммиты — CI/CD создал в кластере отдельный namespace и запустил Job — тот, в свою очередь, создал базу данных, налил туда данные, все конфиги сохранил в Secret и ConfigMap. То есть Job подготовил цельное окружение, на котором можно тестировать и отлаживать новую функциональность.
При выкатке helm chart. После развёртывания helm chart с помощью хуков (hook) запускается Job, чтобы проверить, как раскатилось приложение и работает ли оно.
Таймауты, ограничивающие время выполнения Job
Job будет создавать поды до тех пор, пока под не завершится с успешным результатом. Это значит, что если в поде есть ошибка, которая приводит к неуспешному результату (exit code не равен 0), то Job будет пересоздавать этот под до бесконечности. Чтобы ограничить перезапуски, в описании Job есть два таймаута: activeDeadlineSeconds и backoffLimit.
activeDeadlineSeconds — это количество секунд, которое отводится всему Job на выполнение. Обратите внимание, это ограничение не для одного пода или одной попытки запуска, а для всего Job.
Например, если указать в Job, что activeDeadlineSeconds равен 200 сек., а наше приложение падает с ошибкой через 5 сек., то Job сделает 40 попыток и только после этого остановится.
backoffLimit — это количество попыток. Если указать 2, то Job дважды попробует запустить под и остановится.
Параметр backoffLimit очень важен, потому что, если его не задать, контроллер будет создавать поды бесконечно. А ведь чем больше объектов в кластере, тем больше ресурсов API нужно серверам, и что самое главное: каждый такой под — это как минимум два контейнера в остановленном состоянии на узлах кластера. При этом поды в состоянии Completed или Failed не учитываются в ограничении 110 подов на узел, и в итоге, когда на узле будет несколько тысяч контейнеров, докер-демону скорее всего будет очень плохо.
Учитывая, что контроллер постоянно увеличивает время между попытками запуска подов, проблемы могут начаться в ночь с пятницы на понедельник. Особенно, если вы не мониторите количество подов в кластере, которые не находятся в статусе Running.
Удаление Job
После успешного завершения задания манифесты Job и подов, созданных им, остаются в кластере навсегда. Все поля Job имеют статус Immutable, то есть «неизменяемый», и поэтому обычно при создании Job из различных автоматических сценариев сначала удаляют Job, который остался от предыдущего запуска. Практика генерации уникальных имен для запуска таких Job может привести к накоплению большого количества ненужных манифестов.
В Kubernetes есть специальный TTL Controller, который умеет удалять завершенные Job вместе с подами. Вот только он появился в версии 1.12 и до сих пор находится в статусе alpha, поэтому его необходимо включать с помощью соответствующего feature gate TTLAfterFinished.
ttlSecondsAfterFinished — указывает, через сколько секунд специальный TimeToLive контроллер должен удалить завершившийся Job вместе с подами и их логами.
Манифест
Посмотрим на пример Job-манифеста.
В начале указаны название api-группы, тип сущности, имя и дальше — спецификация.
В спецификации указаны таймауты и темплейт пода, который будет запускаться. Опции backoffLimit: 2 и activeDeadlineSeconds: 60 значат, что Job будет пытаться выполнить задачу не более двух раз и в общей сложности не дольше 60 секунд.
template — это описание пода, который будет выполнять задачу; в нашем случае запускается простой контейнер busybox, который выводит текущую дату и передаёт привет из Kubernetes.
Практические примеры
И посмотрим, что получилось.
Видим, что контейнер поднялся и завершился в статусе Completed. В отличие от приложений, которые всегда работают и имеют статус Running.
Статистику по Job можно посмотреть следующей командой.
Видим, что завершились все задания, время выполнения — 5 секунд.
Ненужный Job обязательно надо удалять. Потому что, если мы не удалим его руками, Job и под будут висеть в кластере всегда — никакой garbage collector не придёт и не удалит их.
Если у вас запускаются по 10-20 заданий в час, и никто их не удаляет, они копятся и в кластере появляется много абстракций, которые никому не нужны, но место занимают. А как я уже говорил выше, каждый под в состоянии Completed — это, как минимум, два остановленных контейнера на узле. А докер демон начинает притормаживать, если на узле оказывается несколько сотен контейнеров, и не важно, работают они или остановлены.
Команда для удаления:
Что будет, если сломать Job
Job, который выполняется без проблем, не очень интересен. Давайте мы над ним немного поиздеваемся.
Поправим yaml: добавим в темплейт контейнера exit 1. То есть скажем Job’у, чтобы он завершался с кодом завершения 1. Для Kubernetes это будет сигналом о том, что Job завершился неуспешно.
Применяем и смотрим, что происходит: один контейнер создался и упал с ошибкой, затем ещё и ещё один. Больше ничего не создаётся.
В статистике подов видим, что создано три пода, у каждого статус Error. Из статистики Job следует, что у нас создан один Job и он не завершился.
Если посмотреть описание, то увидим, что было создано три пода, и Job завершился, потому что был достигнут backoffLimit.
Обратите внимание! В yaml лимит равен 2. То есть, если следовать документации, Job должен был остановиться после двух раз, но мы видим три пода. В данном случае «после выполнения двух раз» значит 3 попытки. Когда мы проделываем то же самое на интенсиве с сотней студентов, то примерно у половины создаётся два пода, а у оставшихся три. Это надо понять и простить.
Проверка ограничения по времени
Сделаем бесконечный цикл и посмотрим, как работает ограничение по времени — activeDeadlineSeconds.
Ограничения оставим теми же (60 секунд), изменим описание контейнера: сделаем бесконечный цикл.
Если посмотреть в логи, то увидим, что каждую секунду у нас появляется новый «Hello» — всё как надо.
Через 60 секунд под оказывается в статусе Terminating (иногда это происходит через ± 10 сек).
Вспомним, как в Kubernetes реализована концепция остановки подов. Когда приходит время остановить под, то есть все контейнеры в поде, контейнерам посылается sigterm-сигнал и Kubernetes ждёт определённое время, чтобы приложение внутри контейнера отреагировало на этот сигнал.
В нашем случае приложение — это простой bash-скрипт с бесконечным циклом, реагировать на сигнал некому. Kubernetes ждёт время, которое задано в параметре graceful shutdown. По дефолту — 30 секунд. То есть если за 30 секунд приложение на sigterm не среагировало, дальше посылается sigkill и процесс с pid 1 внутри контейнера убивается, контейнер останавливается.
Спустя чуть более 100 секунд под удалился. Причем ничего в кластере не осталось, потому что единственный способ остановить что-то в контейнере — это послать sigterm и sigkill. После этого приходит garbage collector, который удаляет все поды в статусе Terminating, чтобы они не засоряли кластер.
В описании Job мы увидим, что он был остановлен, так как активность превысила допустимую.
Поле restartPolicy
Этот параметр говорит kubelet, что делать с контейнером после того, как он был завершён с ошибкой. По умолчанию стоит политика Always, то есть если у нас контейнер в поде завершился, kubelet этот контейнер перезапускает. Причем, все остальные контейнеры в поде продолжают работать, а перезапускается только упавший контейнер.
Это политика по умолчанию, и если её применить в Job, то Job-контроллер не сможет получить информацию о том, что под был завершён с ошибкой. С его точки зрения под будет очень долго выполняться, а то, что kubelet перезапускает упавший контейнер, Job-контроллер не увидит.
CronJob: создание объектов Job по расписанию
Job позволяет выполнить разовые задачи, но на практике постоянно возникает потребность выполнять что-то по расписанию. И вот здесь Kubernetes предлагает CronJob.
CronJob — это yaml-манифест, на основании которого по расписанию создаются Job’ы, которые в свою очередь создают поды, а те делают полезную работу.
На первый взгляд, всё вроде бы просто, но, как и в предыдущем случае, тут есть куча мест, где можно испытать боль.
В манифесте CronJob указывают расписание и ещё несколько важных параметров.
И два параметра, которые влияют на историю выполнения.
Посмотрим на манифест CronJob и поговорим о каждом параметре подробнее.
schedule — это расписание в виде строчки, которая имеет обычный cron-формат. Строчка в примере говорит о том, что наш Job должен выполняться раз в минуту.
concurrencyPolicy — этот параметр отвечает за одновременное выполнение заданий. Бывает трёх видов: Allow, Forbid, Replace.
Allow позволяет подам запускаться. Если за минуту Job не отработал, все равно будет создан ещё один. Одновременно могут выполняться несколько Job’ов.
Например, если один Job выполняется 100 сек., а Cron выполняется раз в минуту, то запускается Job, выполняется 61 сек., в это время запускается ещё один Job. В итоге в кластере одновременно работают два Job’a, которые выполняют одну и ту же работу. Возникает положительная обратная связь: чем больше Job’ов запущено, тем больше нагрузка на кластер, тем медленнее они работают, тем дольше они работают и тем больше одновременных подов запускается — в итоге всё застывает под бешеной нагрузкой.
Replace заменяет запущенную нагрузку: старый Job убивается, запускается новый. На самом деле это не самый лучший вариант, когда прерываем то, что уже выполнялось, и начинаем ту же самую задачу выполнять заново. В каких-то случаях это возможно, в каких-то неприемлемо.
Forbid запрещает запуск новых Job’ов, пока не отработает предыдущий. С этой политикой можно быть уверенным, что всегда запускается только один экземпляр задачи. Поэтому Forbid используют наиболее часто.
jobTemplate — это шаблон, из которого создаётся объект Job. Ну а всё остальное мы уже видели в манифесте Job.
Посмотрим, что получилось:
Увидим название CronJob, расписание, параметр запуска, количество активных Job’ов и сколько времени они работают.
Раздел Suspend — временная приостановка CronJob. В данном случае указано значение False. Это значит, что CronJob выполняется. Можно отредактировать манифест и поставить опцию True, и тогда он не будет выполняться, пока мы его снова не запустим.
Active — сколько Job’ов создано, Last Schedule — когда последний раз исполнялся.
Теперь можно посмотреть статистику по Job’ам и подам.
Видно, что создан один Job.
Под создан, он выполнил полезную работу.
Что получается: CronJob создал Job, Job создал под, под отработал, завершился — всё здорово.
Ещё раз посмотрим на CronJob:
Last Schedule был 19 секунд назад. Если посмотреть на Job, то увидим, что у нас появился следующий Job и следующий под.
Возникает вопрос: а что будет, если CronJob отработает хотя бы пару недель? Неужели у нас в кластере будет столько же Job’ов и подов в статусе Completed, сколько в этой паре недель минут?
Когда CronJob’ы только появились и были на стадии альфа-тестирования, примерно это и происходило: делали CronJob раз в минуту, смотрели — работает, всё здорово, а через неделю кластер становился неработоспособным, потому что количество остановленных контейнеров на узлах было ошеломляющим. Теперь же ситуация изменилась.
Снова откроем манифест и посмотрим, что было добавлено:
Появились опции failedJobHistorLimit со значением 1 и successfulJobHistoryLimit со значением 3. Они отвечают за количество Job’ов, которые остаются одновременно в кластере. То есть CronJob не только создаёт новые Job’ы, но и удаляет старые.
Когда только контроллер CronJob создавался, эти опции не были установлены по умолчанию и CronJob за собой ничего не удалял. Было много возмущений от пользователей, и тогда поставили дефолтные лимиты.
И на сладкое — про startingDeadlineSeconds
В параметре startingDeadlineSeconds указывают количество секунд, на которое можно просрочить запуск Job. Если по каким-то причинам Job не создался и с момента, когда его надо было создать, прошло больше секунд, чем указано в этом параметре, то он и не будет создан. А если меньше, то хоть и с опозданием, Job будет создан.
Тут есть небольшая ловушка, если concurrencyPolicy разрешают одновременное создание Job, то при большом значении параметра startingDeadlineSeconds возможен одновременный запуск десятков пропущенных Job одновременно. Для уменьшения всплеска нагрузки в код Kubernetes захардкожены лимиты и запрещающие процедуры:
Если параметр startingDeadlineSeconds не указан в манифесте:
CronJob контроллер при создании Job смотрит на время последнего запуска — значение LastscheduleTime в status: и считает, сколько времени прошло с последнего запуска. И если это время достаточно велико, а точнее за этот промежуток времени CronJob должен был отработать 100 раз или больше, но у нее этого не получилось:
В этом случае происходит нечто странное: CronJob перестает работать, новые Job’ы больше не создаются. Сообщение об этом приходит в Events, но хранится там недолго. Для восстановления работы приходится удалять CronJob и создавать его заново.
И еще более странное:
Если установлен параметр startingDeadlineSeconds, то поведение немного меняется. 100 пропущенных запусков должны уложиться в количество секунд, указанных в этом параметре. т. е. если в расписании стоит выполняться раз в минуту, а startingDeadlineSeconds меньше 6000 секунд, тогда CronJob будет работать всегда, но в этом случае при политике Allow возможен одновременный запуск множества Job.
И наконец, любопытный side-эффект:
Если установить опцию startingDeadlineSeconds равной нулю, Job’ы вообще перестают создаваться.
Если вы используете опцию startingDeadlineSeconds, указывайте её значение меньше, чем интервал выполнения в расписании, но не ноль.
Применяйте политику Forbid. Например, если было пропущено 5 вызовов и наступило очередное время исполнения, то контроллер не будет 5 раз запускать пропущенные задачи, а создаст только один Job. Это логичное поведение.
Особенность работы CronJob
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.
Вольный перевод на русский:
CronJob создаёт объект Job примерно один раз на каждое время исполнения по расписанию. Мы говорим «примерно», потому что иногда бывают случаи, когда создаются два Job’а одновременно или ни одного. Мы делаем всё, чтобы сделать подобные случаи как можно более редкими, но полностью избежать этого не получается. Следовательно, Job’ы должны быть идемпотентны.
Идемпотентны — должны выполняться на одной и той же среде несколько раз и всегда возвращать одинаковый результат.
В общем, используйте CronJob на свой страх и риск.
В качестве альтернативы CronJob можно использовать под, в котором запущен самый обычный crond. Без выкрутасов. Старый добрый cron работает без проблем, и мы всегда знаем, что задачи будут выполнены один раз. Надо только побеспокоиться, чтобы внутри пода с кроном не выполнялись одновременно несколько задач.
Изучить продвинутые абстракции и попрактиковаться в работе с Kubernetes можно с помощью видеокурса Kubernetes База. В октябре 2020 мы обновили курс, подробности здесь.
Автор статьи: Сергей Бондарев — практикующий архитектор Southbridge, Certified Kubernetes Administrator, один из разработчиков kubespray с правами на принятие pull request.