Периодическое выполнение заданий

Прямо сейчас мы пишем новые главы самоучителя!
Эта страница находится в разработке.

Показать, что уже готово

Приложение в этой главе не предназначено для использования в production без доработки. Готовое к работе в production приложение мы получим в конце руководства.

Подготовка окружения

Подготовьте рабочее окружение согласно инструкциями главы “Подготовка окружения”, если это ещё не сделано.

Рабочее окружение работало, но перестало? Инструкции из этой главы не работают? Может помочь:

Работает ли Docker?

Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Запустим Docker:

sudo systemctl restart docker

Убедимся, что Docker запустился:

sudo systemctl status docker

Результат выполнения команды, если Docker успешно запущен:

● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2021-06-24 13:05:17 MSK; 13s ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
   Main PID: 2013888 (dockerd)
      Tasks: 36
     Memory: 100.3M
     CGroup: /system.slice/docker.service
             └─2013888 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

dockerd[2013888]: time="2021-06-24T13:05:16.936197880+03:00" level=warning msg="Your kernel does not support CPU realtime scheduler"
dockerd[2013888]: time="2021-06-24T13:05:16.936219851+03:00" level=warning msg="Your kernel does not support cgroup blkio weight"
dockerd[2013888]: time="2021-06-24T13:05:16.936224976+03:00" level=warning msg="Your kernel does not support cgroup blkio weight_device"
dockerd[2013888]: time="2021-06-24T13:05:16.936311001+03:00" level=info msg="Loading containers: start."
dockerd[2013888]: time="2021-06-24T13:05:17.119938367+03:00" level=info msg="Loading containers: done."
dockerd[2013888]: time="2021-06-24T13:05:17.134054120+03:00" level=info msg="Daemon has completed initialization"
systemd[1]: Started Docker Application Container Engine.
dockerd[2013888]: time="2021-06-24T13:05:17.148493957+03:00" level=info msg="API listen on /run/docker.sock"

Теперь проверим, что Docker доступен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Перезагружали компьютер после подготовки окружения?

Запустим кластер minikube, уже настроенный в начале главы “Подготовка окружения”:

minikube start

Выставим Namespace по умолчанию, чтобы не указывать его при каждом вызове kubectl:

kubectl config set-context minikube --namespace=werf-guide-app

Результат успешного выполнения команды:

😄  minikube v1.20.0 on Ubuntu 20.04
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🎉  minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
    ▪ Using image registry:2.7.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying registry addon...
🔎  Verifying ingress addon...
🌟  Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄  Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default

Убедитесь, что вывод команды содержит строку:

Restarting existing docker container for "minikube"

Если её нет, значит был создан новый кластер minikube вместо использования старого. В таком случае повторите установку рабочего окружения с minikube с самого начала.

Теперь запустите команду в фоновом PowerShell-терминале и не закрывайте его:

minikube tunnel --cleanup=true

Запустим кластер minikube, уже настроенный в начале главы “Подготовка окружения”:

minikube start --namespace werf-guide-app

Выставим Namespace по умолчанию, чтобы не указывать его при каждом вызове kubectl:

kubectl config set-context minikube --namespace=werf-guide-app

Результат успешного выполнения команды:

😄  minikube v1.20.0 on Ubuntu 20.04
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🎉  minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
    ▪ Using image registry:2.7.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying registry addon...
🔎  Verifying ingress addon...
🌟  Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄  Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default

Убедитесь, что вывод команды содержит строку:

Restarting existing docker container for "minikube"

Если её нет, значит был создан новый кластер minikube вместо использования старого. В таком случае повторите установку рабочего окружения с minikube с самого начала.

Случайно удаляли Namespace приложения?

Если вы непреднамеренно удалили Namespace приложения, то необходимо выполнить следующие команды, чтобы продолжить прохождение руководства:

kubectl create namespace werf-guide-app
kubectl create secret docker-registry registrysecret \
  --docker-server='https://index.docker.io/v1/' \
  --docker-username='<имя пользователя Docker Hub>' \
  --docker-password='<пароль пользователя Docker Hub>'

Результат успешного выполнения команды:

namespace/werf-guide-app created
secret/registrysecret created
Ничего не помогло, окружение или инструкции по-прежнему не работают?

Если ничего не помогло, то пройдите главу “Подготовка окружения” с начала, подготовив новое окружение с нуля. Если и это не помогло, тогда, пожалуйста, расскажите о своей проблеме в нашем Telegram или оставьте Issue на GitHub, и мы обязательно вам поможем.

Подготовка репозитория

Обновим существующий репозиторий с приложением:

Выполним следующий набор команд в PowerShell:

cd ~/werf-guide/app

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides//* .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Выполним следующий набор команд в Bash:

cd ~/werf-guide/app

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rf ~/werf-guide/guides//. .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Не работает? Попробуйте инструкции на вкладке “Начинаю проходить руководство с этой главы” выше.

Подготовим новый репозиторий с приложением:

Выполним следующий набор команд в PowerShell:

# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован
if (-not (Test-Path ~/werf-guide/guides)) {
  git clone https://github.com/werf/werf-guides $env:HOMEPATH/werf-guide/guides
}

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -Recurse -Force ~/werf-guide/app
cp -Recurse -Force ~/werf-guide/guides/ ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий
cd ~/werf-guide/app
git init
git add .
git commit -m initial

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides//* .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Выполним следующий набор команд в Bash:

# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован
test -e ~/werf-guide/guides || git clone https://github.com/werf/werf-guides ~/werf-guide/guides

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -rf ~/werf-guide/app
cp -rf ~/werf-guide/guides/ ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий
cd ~/werf-guide/app
git init
git add .
git commit -m initial

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rf ~/werf-guide/guides//. .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

В этой главе мы покажем как запускать периодические фоновые задания (job) на примере rails rake tasks, а также расскажем что такое CronJob, чем он отличается от Job и для чего ещё можно использовать CronJob, а также ответим на часто возникающие вопросы связанные с CronJob.

## Подготовка Возьмём за основу наше приложение. Состояние директории `rails-app` должно соответствовать шагу `examples/rails/019_fixup_consistency`: ```shell git clone https://github.com/werf/werf-guides cp -r werf-guides/examples/rails/019_fixup_consistency rails-app cd rails-app git init git add . git commit -m "initial" ```

Необходимо добавить dns-запись в файл hosts:

echo "$(minikube ip) debug-mails.example.com" | sudo tee -a /etc/hosts

Запускаем фоновые job

Рассмотрим подробнее задания, которые будут реализованы для регулярного выполнения в фоне.

Подготовка приложения

Чтобы добавить job в наше приложение нам необходимо скопировать файлы с кодом в наш репозиторий:

cp ../werf-guides/examples/rails/800_cron/app/controllers/api/labels_controller.rb app/controllers/api/labels_controller.rb
cp ../werf-guides/examples/rails/800_cron/app/mailers/notifications_mailer.rb app/mailers/notifications_mailer.rb
cp ../werf-guides/examples/rails/800_cron/app/views/notifications_mailer/labels_count_report_email.html.erb app/views/notifications_mailer/labels_count_report_email.html.erb
cp ../werf-guides/examples/rails/800_cron/config/application.rb config/application.rb
cp ../werf-guides/examples/rails/800_cron/config/routes.rb config/routes.rb
cp ../werf-guides/examples/rails/800_cron/.helm/templates/cleanup-labels.yaml .helm/templates/cleanup-labels.yaml
cp ../werf-guides/examples/rails/800_cron/.helm/templates/debug-mails.yaml .helm/templates/debug-mails.yaml
cp ../werf-guides/examples/rails/800_cron/.helm/templates/ingress.yaml .helm/templates/ingress.yaml
cp ../werf-guides/examples/rails/800_cron/.helm/templates/send-report.yaml .helm/templates/send-report.yaml
cp ../werf-guides/examples/rails/800_cron/lib/tasks/crons.rake lib/tasks/crons.rake

Задание по очистке устаревших записей в таблице labels

Job очищает записи в таблице labels с истекшим time to live имеющих срок жизни в 3 минуты. Задача может работать полностью автономно и не требует доступа к запущенному http-серверу. Особенность задачи в том, что она может долго выполняться в фоне, поэтому её выполнение внутри запущенного http-сервера не желательно. За реализацию задачи по очистке отвечает rake task crons:cleanup_labels. Применение job производится из директории проекта командой rake crons:cleanup_labels.

Регулярный запуск job реализован через CronJob (о том, что это такое напишем далее) с именем cleanup-labels с периодичностью в одну минуту. CronJob будет создавать Job по описанному jobTemplate. Затем при помощи этих Job будут создавать Pod-ы, затем каждый запускаемый Pod выполнит в отдельном контейнере команду rake crons:cleanup_labels.

Необходимые для реализации job helm-файл и rake-файлы:

Rake-файл, содержащий инструкции для cron

Helm-файл, необходимый для деплоя этого job в наше приложение.

Задание по периодической отсылке отчётов по почте

Job отправляет e-mail с текущим количеством labels администратору системы, реализовано внутри http-сервера и инициируется специальным запросом по api: /api/send-report, суть job состоит в том чтобы сделать post-запрос на бекенд basicapp. Во избежание избыточного выполнения job одновременно на нескольких репликах инициировать его необходимо вне http-сервера с нашим приложением.

А что если я хочу запускать CronJob в том же поде и контейнере, где работает наш http-сервер?

Так делать не принято и через CronJob в чистом виде не получится. Таким способом реализована очистка устаревших записей в таблице labels описанная выше. Для реализации такой схемы фоновое задание встраивается в само приложение (http-сервер в нашем случае). Инициировать выполнение этого задания можно либо средствами самого приложения, либо через CronJob (предпочтительный вариант, описанный ниже).

Внутри приложения за реализацию отправки отчетов отвечает метод контроллера labels send_report. Отправка происходит из самого http-сервера и реализована через специальный дебаг-smtp-сервер (mailhog), который деплоится вместе с нашим приложением по адресу debug-mails.example.com и позволяет просматривать перехваченные письма. Регулярный запуск job реализован через CronJob с именем send-report который будет создавать job по описанному jobTemplate и периодически запускаться каждую минуту. Каждая из job будет создавать pod который будет выполнять в отдельном контейнере запрос на http-сервер /api/send-report, в ответ на который сервер выполнит отправку -mail на почту администратору.

Необходимые для реализации job helm-файлы и исходный код: labels_controller.rb; notifications_mailer.rb; labels_count_report_email.html.erb; debug-mails.yaml; ingress.yaml; send-report.yaml.

Деплой и проверка работоспособности job

Закоммитим изменения:

git add .
git commit -m "Add cronjobs"

Запустим выкат:

werf converge --repo <имя пользователя Docker Hub>/werf-guided-rails

Проверим как выполняется очистка устаревших labels

Наш job удаляет labels, время жизни которых больше 3х минут. Перед выполнением задания создадим тестовые данные:

curl -XPOST "http://example.com/api/labels?label=aaa"
curl -XPOST "http://example.com/api/labels?label=bbb"
curl -XPOST "http://example.com/api/labels?label=ccc"
curl -XPOST "http://example.com/api/labels?label=ddd"
curl -XPOST "http://example.com/api/labels?label=eee"
curl -XPOST "http://example.com/api/labels?label=fff"
curl -XPOST "http://example.com/api/labels?label=ggg"

Проверим список вновь созданных labels:

curl "http://example.com/api/labels"

Для проверки работы Cronjob, после его деплоя необходимо периодически запускать следующую команду до тех пор пока не увидим новые поды с префиксом в имени cleanup-labels-:

kubectl -n werf-guided-rails get cj,job,pod

Просмотрим логи одного из вновь созданных подов:

kubectl -n werf-guided-rails logs -f pod/cleanup-labels-<podId>

Спустя 3 минуты снова проверим список существующих labels, который должен быть пустым:

shell curl "http://example.com/api/labels" `

Проверим как выполняется отправка отчетов

Проверять отправленные e-mail мы будем через smtp-сервер, который задеплоен с нашим приложением на предыдущем шаге. Для тестирования наше приложение настроено на отправку писем через этот smtp-сервер.

Перейдём по адресу http://debug-mails.example.com. Каждую минуту должно приходить новое письмо с отчетом о текущем количестве записей в таблице Labels.

Что такое CronJob

Job Каждый экземпляр job представляет собой одно фоновое выполнение какой-то задачи. Запускаемая задача будет работать в отдельном pod’е согласно описанию шаблона spec.template. После того как job завершилась, она переходит в final состояние, при этом однажды созданная в кластере, она не может быть обновлёна или изменёна после создания. Всё это означает что ресурс job — одноразовый, и чтобы инициировать повторное выполнение задачи необходимо создать новый экземпляр job но уже с другим именем.

CronJob Нужен чтобы организовать периодическое выполнение одной фоновой задачи. В нашем примере мы как раз используем CronJob. CronJob ­— такая же фоновая задача, но, в отличие от job - многоразовая. CronJob представляет собой надстройку над job, которая задаёт шаблон для генерации одноразовых job. Инструкции в секции spec.schedule позволяют задать периодичность в привычном формате crontab.

Если остались вопросы можете также ознакомиться с материалами про CronJob на официальном сайте kubernetes по ссылкам ниже. https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/; https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/.

Для чего ещё может быть использован CronJob

Вот некоторые из вариантов использования:

  • Периодическая индексация БД.
  • Обработчик очереди заданий.
    • Задания поступают в некоторую очередь.
    • CronJob инициирует работу обработчика следующего задания из этой очереди.
  • Инвалидация истекших пользовательских доступов в БД.
  • Очистка временных данных и устаревших пользовательских сессий в БД.
  • И другие.