Пока что все примеры, что я делал для Docker Swarm и Kubernetes постов, строились вокруг какого-нибудь сервиса: веб-сервера, очереди сообщений или шины сообщений. В принципе, оно и не удивительно, весь тот же Docker Swarm построен вокруг концепции сервисов. Да что там Swarm, сами «микро-сервисы», из-за которых мы контейнеры в облака забрасываем, ни о чём не говорят?
Но не всё есть «сервис» в облаках. Есть и эпизодические задачи: рутинное обслуживание, запланированные задачи, конечные вычислительные таски — всё, у чего есть начало и вполне определённый конец.
Например, возьмём юнит тесты. Если у меня есть набор тестов, который выполняется час, то его можно раскидать по шестидесяти контейнерам, забросить кластер, и надеяться, что теперь они выполнятся за минуту. Вполне ведь валидная задача, и тоже не сервис.
Провернуть что-нибудь подобное в Docker Swarm можно, но нужно было бы немного заморочиться. Как минимум, отключить перезапуск завершившихся контейнеров сервиса, и перестать передёргиваться используя команду docker service create
.
А в Kubernetes такая штука делается запросто. Там можно как отправлять одиночные поды в кластер, так и создавать специальные контроллеры для них, вроде Job или Cron Job. На всё это богатство выбора мы сегодня и посмотрим.
Подготовка
Чтобы сделать всё это самостоятельно понадобятся только VirtualBox
, minikube
и kubectl
. Ну или доступ к Google Container Engine. Подробности установки я уже описывал раньше, так что лучше мы перейдём сразу к делу.
Поды-работяги (pods)
Как я уже говорил, поды с задачами можно забрасывать прямо в кластер. Допустим у нас появилась странная одержимость математикой и мы решили найти все простые числа между единицей и семидесятью при помощи bash скрипта и почему-то в Kubernetes. Такое случается. И если следующая стрёмная строка скрипта сделает математическую часть:
1 |
current=0; max=70; echo 1; echo 2; for((i=3;i<=max;)); do for((j=i-1;j>=2;)); do if [ `expr $i % $j` -ne 0 ] ; then current=1; else current=0; break; fi; j=`expr $j - 1`; done; if [ $current -eq 1 ] ; then echo $i; fi; i=`expr $i + 1`; done |
…то эта конфигурация пода сможет отправить её в кластер:
1 2 3 4 5 6 7 8 9 10 11 |
apiVersion: v1 kind: Pod metadata: name: primes spec: containers: - name: primes image: ubuntu command: ["bash"] args: ["-c", "current=0; max=70; echo 1; echo 2; for((i=3;i<=max;)); do for((j=i-1;j>=2;)); do if [ `expr $i % $j` -ne 0 ] ; then current=1; else current=0; break; fi; j=`expr $j - 1`; done; if [ $current -eq 1 ] ; then echo $i; fi; i=`expr $i + 1`; done"] restartPolicy: Never |
Это ничем не примечательный под, единственной особенностью которого является отключённая реинкарнация (последняя строка). Теперь его можно забросить в кластер через kubectl create -f pod.yml
, подождать, пока он запустится, а затем ловить результаты, которые под заботливо сбрасывает в STDOUT:
1 2 3 4 5 6 7 8 9 10 11 |
kubectl get pod #NAME READY STATUS RESTARTS AGE #primes 1/1 Running 0 3s kubectl logs -f primes #1 #2 #3 #... #61 #67 |
Как только задача завершится, kubectl get pod
больше не будет возвращать нашего работягу — он же завершился. Но при этом он никуда и не делся. Завершённые контейнеры можно найти при помощи --show-all
параметра.
1 2 3 |
kubectl get pod --show-all #NAME READY STATUS RESTARTS AGE #primes 0/1 Completed 0 1m |
У такого подхода к запуску задач в кластере есть куча недостатков. Во-первых, если во время работы случится что-то нехорошее с хостом, на котором под запущен, то что-то нехорошее случится и с самим подом. А ведь было бы хорошо, если бы кто-то перезапустил его на новой машине.
Во-вторых, bash находит простые числа отвратительно медленно. Как можно ускорить процесс? Нет, не переписать всё на C. Можно разбить диапазон 1..70 на три меньших диапазона, и начать искать простые числа в них параллельно. Например, запустить очередь сообщений, положить диапазоны туда, и натравить на неё стайку подов. Но ведь все эти поды придётся запускать руками. А что если их сотня?
С другой стороны, есть Job контроллеры.
Jobs
Job — это такой специальный контроллер, который создаёт и отслеживает поды, выполняющие какую-то конечную задачу. Если под или хост под ним упадут с ошибкой, то Job перезапустит pod где-нибудь ещё. Кроме этого у него есть свойство parallelism
, которым можно контролировать, сколько именно подов будут работать над задачей, и ещё Job можно сразу сказать, сколько подов должны успешно завершиться (completions
), прежде чем вся задача будет считаться сделанной.
Превратить под-работягу в job-работягу — проще-простого:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
apiVersion: batch/v1 kind: Job metadata: name: primes spec: template: metadata: name: primes spec: containers: - name: primes image: ubuntu command: ["bash"] args: ["-c", "current=0; max=70; echo 1; echo 2; for((i=3;i<=max;)); do for((j=i-1;j>=2;)); do if [ `expr $i % $j` -ne 0 ] ; then current=1; else current=0; break; fi; j=`expr $j - 1`; done; if [ $current -eq 1 ] ; then echo $i; fi; i=`expr $i + 1`; done"] restartPolicy: Never |
По сути я просто сделал заголовок задачи и скопи-пастил pod в её template
секцию. Но чтобы сделать пример немного интереснее, можно разрешить запуск аж четырёх параллельных подов, и запускать их до тех пор, пока мы не получим 8 успешных результатов.
1 2 3 4 5 6 |
#.. spec: completions: 8 parallelism: 4 template: #... |
Конечно, все поды будут делать одну и ту же задачу. Как я уже говорил, в реальном мире мы бы положили описания задач в очередь сообщений или базу данных, куда бы поды обращались за новой порцией работы.
Теперь создаём Job, даём ей немного времени на запуск, и смотрим, что происходит с подами:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
kubectl create -f job.yml #job "primes" created # several seconds later kubectl get pods --show-all #NAME READY STATUS RESTARTS AGE #primes-5g2xp 0/1 Completed 0 31s #primes-9l9tf 0/1 ContainerCreating 0 4s #primes-d2jwk 0/1 Completed 0 14s #primes-pxhqx 0/1 ContainerCreating 0 4s #primes-rvq5x 0/1 Completed 0 31s #primes-rxdrw 1/1 Running 0 4s #primes-sw2lq 0/1 Completed 0 31s #primes-v5bv8 0/1 Completed 0 31s |
Поды действительно запускаются параллельно: 5 уже завершилось, один работает и ещё два только запускаются.
С параметрами parallelism
и completions
можно было бы и поэксперементировать. Например, если убрать parallelism
вовсе, то получилось бы 8 подов, запускаемых один за другим. А если убрать completions
, то Job запустил бы четыре параллельных пода, а после их завершения завершился бы и сам.
Cron Jobs
Подобно обычному Job, Cron Job тоже запускает поды с работой. В отличие от Job, он делает это по расписанию. Самая интересная его настройка — свойство schedule
, которое использует тот же формат записи, что и обычный линуксовый cron.
Предположим, что высчитывание простых чисел стало такой важной частью бизнеса, что нам теперь надо эти делать хотя бы раз в минуту. Вдруг числа поменялись.
Сделать это — вообще элементарно: я просто скопирую описание предыдущей задачи в jobTemplate
секцию Cron Job и задам «раз в минуту»:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
apiVersion: batch/v1beta1 kind: CronJob metadata: name: primes spec: schedule: "*/1 * * * *" jobTemplate: spec: completions: 8 parallelism: 4 template: metadata: name: primes spec: containers: - name: primes image: ubuntu command: ["bash"] args: ["-c", "current=0; max=70; echo 1; echo 2; for((i=3;i<=max;)); do for((j=i-1;j>=2;)); do if [ `expr $i % $j` -ne 0 ] ; then current=1; else current=0; break; fi; j=`expr $j - 1`; done; if [ $current -eq 1 ] ; then echo $i; fi; i=`expr $i + 1`; done;"] restartPolicy: Never |
И всё. Теперь задачу можно создавать и любоваться на поды, как и в прошлый раз.
1 2 3 4 5 6 7 8 9 10 |
kubectl create -f cron.yml #cronjob "primes" created kubectl get cronjobs #NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE #primes */1 * * * * False 0 <none> kubectl get cronjobs #NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE #primes */1 * * * * False 1 Tue, 28 Nov 2017 00:24:00 -0500 |
Визуально всё практически один в один. Только кладбище завершённых подов в системе будет расти со скоростью +8 штук в минуту.
1 2 3 4 5 6 7 8 9 10 |
kubectl get pods --show-all #NAME READY STATUS RESTARTS AGE #primes-1511846640-5m9tz 1/1 Running 0 5s #primes-1511846640-5xvqj 0/1 Completed 0 32s #primes-1511846640-dn8mq 1/1 Running 0 5s #primes-1511846640-g98qb 0/1 Completed 0 32s #primes-1511846640-hk7rl 0/1 Completed 0 32s #primes-1511846640-kkcks 1/1 Running 0 5s #primes-1511846640-vv5zm 0/1 Completed 0 32s #primes-1511846640-xlf4r 1/1 Running 0 5s |
Итого
Как видно, Kubernetes не просто умеет обращаться с однократными задачами, у него есть аж три способа, как это сделать. Для мелких и редких задач можно просто положить их в pod, закинуть прямо в кластер, и забирать результат при помощи kubectl logs %pod-name%
. Для задач, которые имеет смысл параллелить или хотя бы быть уверенным, что их перезапустят в случае ЧП, можно воспользоваться Job контроллером. Наконец, если в задаче маячит слово «расписание», то это определённо Cron Job.
Павел, огромное спасибо за все статьи про k8s, очень понятно написано, и главное сильно помогло!
Пожалуйста!
+
Паша, натыкаюсь уже на второй пост от тебя просто гуля про всякие тонкости в kubernetes. Мы тут GanttPRO в него перекатывем. И как раз все в тему. Просто написал, что-бы тебе было приятно 🙂
Сработало, мне приятно 🙂
Смотри, а вот такая ситуация:
Есть node.js микросервис. В нем, допустим, есть функция, которую надо вызывать раз в час.
Что-бы было нагляднее, например он забирает почту с imap и складывает во внешнюю базу.
И у нас этого микросервиса replicas: 3. Т.е. получается 3 пода одновременно
Раньше на виртуальной машине было просто, сам node.js шедулил себе эти задачи.
А тут полуается каждый под начал шедулить сам себе и 3 раза забирет одно и тоже 3 раза. А надо только раз в час, не 3 раза в час. При этом этот микросервис не такой простой и совсем не микро и сложо из него выдернуть эту задачу в отдельный под. Как поступить.
Хочется как-бы что-бы один под сказал, мол «Я сейчас главный, я сделаю таску, а вы, двас придурка по бокам, пока подождте. Но если я умру, один из вас должен меня заменить»
Это вообще законно 🙂 или что делать?
Смотри, реальная имплементация может оказаться проще, но согласно абстрактному учебнику микросервисов полагается разделять шедулинг и саму работу. А так как рабочих много, то должен появиться кто-то, кто эту работу распределит.
Можно сделать так: кубернетовский Cronjob раз в час отправляет сообщение в очередь, на которую подписаны твои рабочие. Так как очередь — это не обычный Pub/Sub, а именно очередь, то сообщения не дублируются по подписчикам, и только один из рабочих сможет это сообщение забрать. То есть архитектура будет такая: Cronjob -> MQ -> Worker(s).
Можно даже сделать ещё проще: Cronjob pod раз в час будет делать API вызов на сервис, который стоит перед твоими воркерами (
curl http://my-worker-service.local/do-stuff
). Так как сервис работает как load balancer, то только один под получит этот запрос.То есть есть как минимум два варианта:
1. Cronjob -> [publish] -> Message Queue -> [receive] -> Worker pod (subscriber)
2. Cronjob -> [http call] -> Workers service -> [load balanced] -> Worker pod
Про очереди сообщений тут — https://dotsandbrackets.com/asynchronous-communication-with-message-queue-ru/ — в части «Очереди сообщений» можно посмотреть, какие софтины бывают, и с ходу я бы смотрел в сторону RabbitMQ. Но вводить MQ в твою задачу похоже на перебор, так что попробуй второй вариант. Здесь — https://dotsandbrackets.com/kubernetes-example-ru/ — я когда-то писал про сервисы и их load balancer. Вряд ли узнаешь что-то новое, но на всякий случай.
Огромный респект! Я пока взахлеб погрузился в мир кубернетеса, сижу днями и ночами последние 2 недели, тыкаю туда сюда, деплою и смотрю на реакции подопытного )
Кстати, когда ты отвачаешь на комммент по идее мнедолжно приходить письмо, но его не было
Когда завершится аппокалипсис, приезжай и я тебе проставлю самое вкусное пиво за кубернетес и за Беларусь. Она тут жыве как никогда, людей как подменили 🙂
Круть, буду ждать, пока зомбиэпидемия закончится 🙂
Я посмотрю по почтовым настройкам, может какой чекбокс не проставил в вордпрессе. Не сильно обращал внимание на комменто-мыльные дела до сегодняшнего дня.
Кстати, если секретный способ фокусно прошариться в кубернетес. Есть же профильные сертификации — Certified Kubernetes Administrator (CKA) и Certified Kubernetes Application Developer (CKAD). Возьми где-нибудь подготовительный курс по одному или обоим и вперёд. Я так к гугловым сертификациям на LinuxAcademy готовился. Всякие обучающие платформу по-началу как правило дают неделю бесплатно, и этого времени вполне хватает на подготовку. А на выходе получается чёткая картина мира.