деплой приложения в kubernetes
Стратегии деплоя в Kubernetes: rolling, recreate, blue/green, canary, dark (A/B-тестирование)
Прим. перев.: Этот обзорный материал от Weaveworks знакомит с наиболее популярными стратегиями выката приложений и рассказывает о возможности реализации наиболее продвинутых из них с помощью Kubernetes-оператора Flagger. Он написан простым языком и содержит наглядные схемы, позволяющие разобраться в вопросе даже начинающим инженерам.
Схема взята из другого обзора стратегий выката, сделанного в Container Solutions
Одной из самых больших проблем при разработке cloud native-приложений сегодня является ускорение деплоя. При микросервисном подходе разработчики уже работают с полностью модульными приложениями и проектируют их, позволяя различным командам одновременно писать код и вносить изменения в приложение.
Более короткие и частые развертывания имеют следующие преимущества:
В этой публикации мы обсудим различные стратегии деплоя в Kubernetes, в том числе rolling-развертывания и более продвинутые методы, такие как канареечные (canary) выкаты и их разновидности.
Стратегии деплоя
Существует несколько различных типов стратегий развертывания, коими можно воспользоваться в зависимости от цели. Например, вам может потребоваться внести изменения в некое окружение для дальнейшего тестирования, или в подмножество пользователей/клиентов, или возникнет необходимость провести ограниченное тестирование на пользователях, прежде чем сделать некую функцию общедоступной.
Rolling (постепенный, «накатываемый» деплой)
Это стандартная стратегия развертывания в Kubernetes. Она постепенно, один за другим, заменяет pod’ы со старой версией приложения на pod’ы с новой версией — без простоя кластера.
Kubernetes дожидается готовности новых pod’ов к работе (проверяя их с помощью readiness-тестов), прежде чем приступить к сворачиванию старых. Если возникает проблема, подобное накатываемое обновление можно прервать, не останавливая всего кластера. В YAML-файле с описанием типа deployment’а новый образ заменяет собой старый образ:
Параметры накатываемого обновления можно уточнить в файле манифеста:
Recreate (повторное создание)
В этом простейшем типе развертывания старые pod’ы убиваются все разом и заменяются новыми:
Соответствующий манифест выглядит примерно так:
Blue/Green (сине-зеленые развертывания)
Стратегия сине-зеленого развертывания (иногда ее ещё называют red/black, т.е. красно-чёрной) предусматривает одновременное развертывание старой (зеленой) и новой (синей) версий приложения. После размещения обеих версий обычные пользователи получают доступ к зеленой, в то время как синяя доступна для QA-команды для автоматизации тестов через отдельный сервис или прямой проброс портов:
После того, как синяя (новая) версия была протестирована и был одобрен ее релиз, сервис переключается на неё, а зеленая (старая) сворачивается:
Canary (канареечные развертывания)
Канареечные выкаты похожи на сине-зеленые, но лучше управляются и используют прогрессивный поэтапный подход. К этому типу относятся несколько различных стратегий, включая «скрытые» запуски и А/В-тестирование.
Эта стратегия применяется, когда необходимо испытать некую новую функциональность, как правило, в бэкенде приложения. Суть подхода в том, чтобы создать два практически одинаковых сервера: один обслуживает почти всех пользователей, а другой, с новыми функциями, обслуживает лишь небольшую подгруппу пользователей, после чего результаты их работы сравниваются. Если все проходит без ошибок, новая версия постепенно выкатывается на всю инфраструктуру.
Хотя данную стратегию можно реализовать исключительно средствами Kubernetes, заменяя старые pod’ы на новые, гораздо удобнее и проще использовать service mesh вроде Istio.
Например, у вас может быть два различных манифеста в Git: обычный с тегом 0.1.0 и «канареечный» с тегом 0.2.0. Изменяя веса в манифесте виртуального шлюза Istio, можно управлять распределением трафика между этими двумя deployment’ами:
Пошаговое руководство по реализации канареечных развертываний с помощью Istio можно найти в материале GitOps Workflows with Istio. (Прим. перев.: Мы также переводили материал про канареечные выкаты в Istio здесь.)
Канареечные развертывания с Weaveworks Flagger
Weaveworks Flagger позволяет легко и эффективно управлять канареечными выкатами.
Flagger автоматизирует работу с ними. Он использует Istio или AWS App Mesh для маршрутизации и переключения трафика, а также метрики Prometheus для анализа результатов. Кроме того, анализ канареечных развертываний можно дополнить вебхуками для проведения приемочных (acceptance) тестов, нагрузочных и любых других типов проверок.
На основе deployment’а Kubernetes и, при необходимости, горизонтального масштабирования pod’ов (HPA), Flagger создает наборы из объектов (deployment’ы Kubernetes, сервисы ClusterIP и виртуальные сервисы Istio или App Mesh) для проведения анализа и реализации канареечных развертываний:
Реализуя контур управления (control loop), Flagger постепенно переключает трафик на канареечный сервер, параллельно измеряя ключевые показатели производительности, такие как доля успешных HTTP-запросов, средняя продолжительность запроса и здоровье pod’ов. Основываясь на анализе KPI (ключевых показателей эффективности), канареечная часть либо растет, либо сворачивается, и результаты анализа публикуются в Slack. Описание и демонстрацию этого процесса можно найти в материале Progressive Delivery for App Mesh.
Dark (скрытые) или А/В-развертывания
Скрытое развертывание — еще одна вариация канареечной стратегии (с ней, кстати, Flagger тоже может работать). Разница между скрытым и канареечным развертыванием состоит в том, что скрытые развертывания имеют дело с фронтендом, а не с бэкендом, как канареечные.
Другое название этих развертываний — А/В-тестирование. Вместо того, чтобы открыть доступ к новой функции всем пользователям, ее предлагают лишь ограниченной их части. Обычно эти пользователи не знают, что выступают тестерами-первопроходцами (отсюда и термин «скрытое развертывание»).
С помощью переключателей функциональности (feature toggles) и других инструментов можно следить за тем, как пользователи взаимодействуют с новой функцией, увлекает ли она их или они считают новый пользовательский интерфейс запутанным, и другими типами метрик.
Flagger и A/B-развертывания
Помимо маршрутизации с учётом весов, Flagger также может направлять на канареечный сервер трафик в зависимости от параметров HTTP. При А/В-тестировании можно использовать заголовки HTTP или файлы cookie для перенаправления определенного сегмента пользователей. Это особенно эффективно в случае frontend-приложений, требующих привязки сессии к серверу (session affinity). Дополнительную информацию можно найти в документации Flagger.
Автор выражает благодарность Stefan Prodan, инженеру Weaveworks (и создателю Flagger), за все эти потрясающие схемы деплоя.
Практика с dapp. Часть 2. Деплой Docker-образов в Kubernetes с помощью Helm
dapp — наша Open Source-утилита, помогающая DevOps-инженерам сопровождать процессы CI/CD (Обновлено 13 августа 2019 г.: в настоящее время проект dapp переименован в werf, его код полностью переписан на Go, а документация значительно улучшена) (подробнее о ней читайте в анонсе). В документации к ней приведён пример сборки простого приложения, а подробнее этот процесс (с демонстрацией основных возможностей dapp) был представлен в первой части статьи. Теперь, на основе того же простого приложения, покажу, как dapp работает с кластером Kubernetes.
Как и в первой статье, все дополнения для кода приложения symfony-demo есть в нашем репозитории. Но обойтись Vagrantfile в этот раз не получится: Docker и dapp придётся поставить локально.
Запуск кластера с помощью Minikube
Теперь нужно создать кластер Kubernetes, где dapp запустит приложение. Для этого будем использовать Minikube как рекомендуемый способ запуска кластера на локальной машине.
Установка проста и заключается в скачивании Minikube и утилиты kubectl. Инструкции доступны по ссылкам:
После успешного старта можно посмотреть, что есть в кластере:
Подготовка, шаг №1: реестр для образов
Итак, мы запустили кластер Kubernetes в виртуальной машине. Что ещё понадобится для запуска приложения?
Во-первых, для этого нужно загрузить образ туда, откуда кластер сможет его получить. Можно использовать общий Docker Registry или же установить свой Registry в кластере (мы так делаем для production-кластеров). Для локальной разработки тоже лучше подойдет второй вариант, а реализовать его с dapp совсем просто — для этого есть специальная команда:
После её выполнения в списке системных процессах появляется такое перенаправление:
… а в namespace под названием kube-system создаётся Registry и прокси к нему:
Видно, что тег образа в Registry составлен из имени dimg и имени ветки (через дефис).
Подготовка, шаг №2: конфигурация ресурсов (Helm)
Поэтому наш следующий шаг — это установка Helm. Официальную инструкцию можно найти в документации проекта.
(Здесь и далее знаком «. » вручную выделены строки, на которые стоит обратить внимание.)
То есть: появился Deployment под названием tiller-deploy с одним ReplicaSet и одним подом (Pod). Для Deployment сделан одноимённый Service ( tiller-deploy ), который открывает доступ через порт 44134.
Подготовка, шаг №3: IngressController
Третья часть — сама конфигурация для приложения. На данном этапе нужно понять, что требуется выложить в кластер, чтобы приложение заработало.
Предлагается следующая схема:
… и посмотрим, что появилось в кластере:
Описание конфигурации для Helm
Теперь можно описать конфигурацию приложения. Базовую конфигурацию поможет сгенерировать команда helm create имя_приложения :
Мы сейчас создали описание chart’а. Chart — это единица конфигурации для Helm, можно думать о нём как о неком пакете. Например, есть chart для nginx, для MySQL, для Redis. И с помощью таких chart’ов можно собрать нужную конфигурацию в кластере. Helm выкладывает в Kubernetes не отдельные образы, а именно Chart’ы (официальная документация).
Файл Chart.yaml — это описание chart’а нашего приложения. Здесь нужно указать как минимум имя приложения и версию:
Директория charts пока пуста, потому что наш chart приложения пока не использует внешние chart’ы.
Наконец, директория templates — здесь хранятся шаблоны YAML-файлов с описанием ресурсов для их размещения в кластере. Сгенерированные шаблоны не сильно нужны, поэтому с ними можно ознакомиться и удалить.
Для начала опишем простой вариант Deployment для нашего приложения:
В конфигурации описано, что нам нужна пока что одна реплика, а в template указано, какие поды нужно реплицировать. В этом описании указывается образ, который будет запускаться и порты, которые доступны другим контейнерам в поде.
Переменая KUBERNETES_DEPLOYED нужна, чтобы Helm обновлял поды, если мы обновим образ без изменения тега. Это удобно для отладки и локальной разработки.
Далее опишем Service:
Первый деплой
Теперь всё готово, чтобы запустить dapp kube deploy (Обновлено 13 августа 2019 г.: подробнее см. в документации werf):
Видим, что в кластере появляется под в состоянии ContainerCreating :
… и через некоторое время всё работает:
Создан ReplicaSet, Pod, Service, то есть приложение запущено. Это можно проверить «по старинке», зайдя в контейнер:
Открываем доступ
Чтобы увидеть нормальную страницу приложения, нужно развернуть приложение с другой настройкой либо для тестов закомментировать этот блок. Повторный деплой в Kubernetes (после правок в коде приложения) выглядит так:
Под пересоздался, можно зайти браузером и увидеть красивую картинку:
С помощью Minikube и Helm можно тестировать свои приложения в кластере Kubernetes, а dapp поможет в сборке, развёртывании своего Registry и самого приложения.
В статье не упомянуты секретные переменные, которые можно использовать в шаблонах для приватных ключей, паролей и прочей закрытой информации. Об этом напишем отдельно.
Сборка и дeплой приложений в Kubernetes с помощью dapp и GitLab CI
В предыдущих статьях о dapp было рассказано про сборку приложений и про запуск в Minikube. При этом dapp запускался локально на машине разработчика. Однако инструмент задумывался для поддержки процессов непрерывной интеграции (CI) и сами мы используем его в основном в связке с GitLab. Чем dapp помогает в процессах CI/CD?
Во-первых, конечно же, это сборка. Dapp позволяет ускорить инкрементальную сборку приложений, привязывая команды сборки к изменениям между коммитами в Git-репозитории (подробнее об этом и других оптимизациях на этапе сборки см. в докладе «Собираем Docker-образы для CI/CD быстро и удобно вместе с dapp»: статья, видео).
В-третьих, в dapp реализована логика очистки кэша как в локальном, так и в удалённом Registry с Docker-образами. Очистка была улучшена в последних версиях: теперь dapp удаляет образы, созданные по веткам Git, если эти ветки удалены из Git-репозитория. С образами по тегам немного сложнее: остаётся не более 10 образов не старше 30 дней. В следующих версиях планируется сделать настраиваемые политики очистки.
Всё перечисленное критично в большей степени для сервера сборки, поэтому далее с помощью несложного стенда GitLab + Minikube покажу пример внедрения dapp в процессы непрерывной интеграции и доставки (CI/CD).
Обновлено 13 августа 2019 г.: в настоящее время проект dapp переименован в werf, его код переписан на Go, а документация значительно улучшена.
Стенд GitLab + Minikube + dapp
Стенд состоит из установленного GitLab, GitLab Runner, Registry и кластера Kubernetes:
Схема приближена к варианту, который используется нами в реальных проектах. Вкратце всё работает так:
В качестве приложения опять используется symfony-demo. Сборка этого проекта в локальном варианте была описана в статье «Практика с dapp. Часть 1: Сборка простых приложений», а пример выката приложения в Minikube был описан в статье «Практика с dapp. Часть 2. Деплой Docker-образов в Kubernetes с помощью Helm». Отличие от второй статьи будет в том, что Registry для Minikube становится внешним (расположен в виртуальной машине с GitLab) и команда dapp kube minikube setup не требуется.
Подготовка хост-системы
Перед созданием виртуальных машин лучше заранее добавить имена хостов, например, в /etc/hosts :
192.168.33.20 gitlab.example.com # доступ к интерфейсу gitlab
192.168.33.20 registry.gitlab.example.com # доступ к registry
192.168.33.100 cluster.example.com # доступ к приложению и к api k8s
GitLab в виртуальной машине
После vagrant up нужно установить GitLab Runner по инструкции проекта. Также потребуется инсталляция Docker и dapp. И для деплоя — скачать бинарники kubectl и Helm.
Minikube
Время запустить и настроить Minikube. Инструкция довольно проста: скачать бинарник проекта и вызвать команду:
К сожалению, для работы в составе стенда придётся остановить кластер ( minikube stop ) и вручную отредактировать
Настройка GitLab Runner
Если Minikube уже был установлен ранее по второй части, то tiller уже есть и достаточно команды:
Если minikube не был ранее установлен, то нужно, чтобы установился tiller:
Для dapp необходимо добавить plugin template :
Импорт проекта и pipeline
Теперь можно импортировать репозиторий symfony-demo в GitLab. В списке проектов нажать New Project, выбрать Import, затем Repo by URL, ввести URL, группу и имя нового проекта.
Задание Build
Задание Deploy
Здесь тоже первым запуском выводится версия dapp. Второй запуск — выкат приложения в кластер.
Можно заметить, что в пространстве имён используется суффикс stage — это сделано, чтобы показать, что имя namespace можно задать любое. В полноценном варианте pipeline потребуется создать несколько заданий для нужных окружений.
Helm-шаблоны
А в backend.yaml — такой ресурс:
… и imagePullSecrets в spec шаблонов контейнеров:
Выкат по таким Helm-шаблонам уже должен быть успешен — результат можно наблюдать в браузере по адресу http://cluster.example.com/symfony-demo :
Резюмируя
В целом можно считать, что развёрнут стенд с процессом CI/CD, очень приближенным к тому, что работает у наших клиентов. Следующими шагами будет усложнение pipeline (см. «GitLab CI для непрерывной интеграции и доставки в production. Часть 1: наш пайплайн» и «Часть 2: преодолевая трудности»), введение динамических окружений (обзорная статья коллеги), добавление очистки Registry по расписанию, добавление запуска интеграционных тестов. Вопросы про описанный стенд и про dapp можно задать в комментариях и в нашем Telegram-чате.
Лучшие практики для деплоя высокодоступных приложений в Kubernetes. Часть 1
Развернуть в Kubernetes приложение в минимально рабочей конфигурации нетрудно. Но когда вы захотите обеспечить своему приложению максимальную доступность и надежность в работе, вы неизбежно столкнётесь с немалым количеством подводных камней. В этой статье мы попытались систематизировать и ёмко описать самые важные правила для развертывания высокодоступных приложений в Kubernetes.
Функциональность, которая не доступна в Kubernetes «из коробки», здесь почти не будет затрагиваться. Также мы не будем привязываться к конкретным CD-решениям и опустим вопросы шаблонизации/генерации Kubernetes-манифестов. Рассмотрены только общие правила, касающиеся того, как Kubernetes-манифесты могут выглядеть в конечном итоге при деплое в кластер.
1. Количество реплик
Вряд ли получится говорить о какой-либо доступности, если приложение не работает по меньшей мере в двух репликах. Почему при запуске приложения в одной реплике возникают проблемы? Многие сущности в Kubernetes (Node, Pod, ReplicaSet и др.) эфемерны, т. е. при определенных условиях они могут быть автоматически удалены/пересозданы. Соответственно, кластер Kubernetes и запущенные в нём приложения должны быть к этому готовы.
К примеру, при автомасштабировании узлов вниз, какие-то узлы вместе с запущенными на них Pod’ами будут удалены. Если в это время на удаляемом узле работает ваше приложение в одном экземпляре, то неизбежна полная — хотя обычно и непродолжительная — недоступность приложения. В целом, при работе в одной реплике любое нештатное завершение работы приложения будет означать простой. Таким образом, приложение должно быть запущено по меньшей мере в двух репликах.
При этом, если реплика экстренно завершает работу, то чем больше рабочих реплик было изначально, тем меньше просядет вычислительная способность всего приложения. К примеру, если у приложения всего две реплики и одна из них перестала работать из-за сетевых проблем на узле, то приложение теперь сможет выдержать только половину первоначальной нагрузки (одна реплика доступна, одна — недоступна). Конечно, через некоторое время новая реплика приложения будет поднята на новом узле, и работоспособность полностью восстановится. Но до тех пор увеличение нагрузки на единственную рабочую реплику может приводить к перебоям в работе приложения, поэтому количество реплик должно быть с запасом.
Рекомендации актуальны, если не используется HorizontalPodAutoscaler. Лучший вариант для приложений, у которых будет больше нескольких реплик, — настроить HorizontalPodAutoscaler и забыть про указание количества реплик вручную. О HorizontalPodAutoscaler мы поговорим в следующей статье.
2. Стратегия обновления
Альтернативные стратегии деплоя (blue-green, canary и др.) часто могут быть гораздо лучшей альтернативой RollingUpdate, но здесь мы не будем их рассматривать, так как их реализация зависит от того, какое ПО вы используете для деплоя. Это выходит за рамки текущей статьи. (См. также статью «Стратегии деплоя в Kubernetes: rolling, recreate, blue/green, canary, dark (A/B-тестирование)» в нашем блоге.)
3. Равномерное распределение реплик по узлам
Очень важно разносить Pod’ы приложения по разным узлам, если приложение работает в нескольких репликах. Для этого рекомендуйте планировщику не запускать несколько Pod’ов одного Deployment’а на одном и том же узле:
4. Приоритет
priorityClassName влияет на то, какие Pod’ы будут schedule’иться в первую очередь, а также на то, какие Pod’ы могут быть «вытеснены» (evicted) планировщиком, если места для новых Pod’ов на узлах не осталось.
Cluster. Priority > 10000. Критичные для функционирования кластера компоненты, такие как kube-apiserver.
Daemonsets. Priority: 10000. Обычно мы хотим, чтобы Pod’ы DaemonSet’ов не вытеснялись с узлов обычными приложениями.
Production-high. Priority: 9000. Stateful-приложения.
Production-medium. Priority: 8000. Stateless-приложения.
Production-low. Priority: 7000. Менее критичные приложения.
Default. Priority: 0. Приложения для окружений не категории production.
Это предохранит нас от внезапных evict’ов важных компонентов и позволит более важным приложениям вытеснять менее важные при недостатке узлов.
5. Остановка процессов в контейнерах
При остановке контейнера всем процессам в нём отправляется сигнал, указанный в STOPSIGNAL (обычно это TERM ). Но не все приложения умеют правильно реагировать на него и делать graceful shutdown, который бы корректно отработал и для приложения, запущенного в Kubernetes.
Например, чтобы сделать корректную остановку nginx, нам понадобится preStop-хук вроде этого:
sleep 3 здесь для страховки от race conditions, связанных с удалением endpoint.
(Более подробно про graceful shutdown для nginx в связке с PHP-FPM вы можете узнать из другой нашей статьи.)
Стоит заметить, что preStop-хук выполняется блокирующе, т. е. STOPSIGNAL будет послан только после того, как preStop-хук отработает. Тем не менее, отсчет terminationGracePeriodSeconds идёт и в течение работы preStop-хука. А процессы, запущенные в хуке, равно как и все процессы в контейнере, получат сигнал KILL после того, как terminationGracePeriodSeconds закончится.
6. Резервирование ресурсов
Планировщик на основании resources.requests Pod’а принимает решение о том, на каком узле этот Pod запустить. К примеру, Pod не будет schedule’иться на узел, на котором свободных (т. е. non-requested) ресурсов недостаточно, чтобы удовлетворить запросам (requests) нового Pod’а. А resources.limits позволяют ограничить потребление ресурсов Pod’ами, которые начинают расходовать ощутимо больше, чем ими было запрошено через requests. Лучше устанавливать лимиты равные запросам, так как если указать лимиты сильно выше, чем запросы, то это может лишить другие Pod’ы узла выделенных для них ресурсов. Это может приводить к выводу из строя других приложений на узле или даже самого узла. Также схема ресурсов Pod’а присваивает ему определенный QoS class: например, он влияет на порядок, в котором Pod’ы будут вытесняться (evicted) с узлов.
Поэтому необходимо выставлять и запросы, и лимиты и для CPU, и для памяти. Единственное, что можно/нужно опустить, так это CPU-лимит, если версия ядра Linux ниже 5.4 (для EL7/CentOS7 версия ядра должна быть ниже 3.10.0-1062.8.1.el7).
(Подробнее о том, что такое requests и limits, какие бывают QoS-классы в Kubernetes, мы рассказывали в этой статье.)
Также некоторые приложения имеют свойство бесконтрольно расти в потреблении оперативной памяти: к примеру, Redis, использующийся для кэширования, или же приложение, которое «течёт» просто само по себе. Чтобы ограничить их влияние на остальные приложения на том же узле, им можно и нужно устанавливать лимит на количество потребляемой памяти. Проблема только в том, что, при достижении этого лимита приложение будет получать сигнал KILL. Приложения не могут ловить/обрабатывать этот сигнал и, вероятно, не смогут корректно завершаться. Поэтому очень желательно использовать специфичные для приложения механизмы контроля за потреблением памяти в дополнение к лимитам Kubernetes, и не доводить эффективное потребление памяти приложением до limits.memory Pod’а.
Конфигурация для Redis, которая поможет с этим:
Понятное дело, что во всех этих случаях limits.memory должен быть выше, чем пороги срабатывания вышеуказанных механизмов.
В следующей статье мы также рассмотрим использование VerticalPodAutoscaler для автоматического выставления ресурсов.
7. Пробы
В Kubernetes пробы (healthcheck’и) используются для того, чтобы определить, можно ли переключить на приложение трафик (readiness) и не нужно ли приложение перезапустить (liveness). Они играют большую роль при обновлении Deployment’ов и при запуске новых Pod’ов в целом.
Сразу общая рекомендация для всех проб: выставляйте высокий timeoutSeconds. Значение по умолчанию в одну секунду — слишком низкое. Особенно критично для readinessProbe и livenessProbe. Слишком низкий timeoutSeconds будет приводить к тому, что при увеличении времени ответов у приложений в Pod’ах (что обычно происходит для всех Pod’ов сразу благодаря балансированию нагрузки с помощью Service) либо перестанет приходить трафик почти во все Pod’ы (readiness), либо, что ещё хуже, начнутся каскадные перезапуски контейнеров (liveness).
7.1 Liveness probe
На практике вам не так часто нужна liveness probe (дословно: «проверка на жизнеспособность»), насколько вы думаете. Её предназначение — перезапустить контейнер с приложением, когда livenessProbe перестаёт отрабатывать, например, если приложение намертво зависло. На практике подобные deadlock’и скорее исключение, чем правило. Если же приложение работает, но не полностью (например, приложение не может само восстановить соединение с БД, если оно оборвалось), то это нужно исправлять в самом приложении, а не накручивать «костыли» с livenessProbe.
И хотя как временное решение можно добавить в livenessProbe проверку на подобные состояния, по умолчанию livenessProbe лучше либо совсем не использовать, либо делать очень простую liveness-пробу, вроде проверки возможности TCP-соединения (обязательно выставьте большой таймаут). В таком случае это поможет приложению перезапуститься при возникновении очевидного deadlock’а, но при этом приложение не подвергнется риску войти в цикл перезапусков, когда перезапуск не может помочь.
И риски, которые плохая livenessProbe несёт, весьма серьезные. Самые частые случаи: когда livenessProbe перестаёт отрабатывать по таймауту из-за повышенной нагрузки на приложение, а также когда livenessProbe перестаёт работать, т. к. проверяет (прямо или косвенно) состояние внешних зависимостей, которые сейчас отказали. В последнем случае последует перезагрузка всех контейнеров, которая при лучшем раскладе ни к чему не приведет, а при худшем — приведет к полной (и, возможно, длительной) недоступности приложения. Полная длительная недоступность приложения может происходить, если при большом количестве реплик контейнеры большинства Pod’ов начнут перезагружаться в течение короткого промежутка времени. При этом какие-то контейнеры, скорее всего, поднимутся быстрее других, и на это ограниченное количество контейнеров теперь придется вся нагрузка, которая приведет к таймаутам у livenessProbe и заставит контейнеры снова перезапускаться.
Также, если все-таки используете livenessProbe, убедитесь, что она не перестает отвечать, если у вашего приложения есть лимит на количество установленных соединений и этот лимит достигнут. Чтобы этого избежать, обычно требуется зарезервировать под livenessProbe отдельный тред/процесс самого приложения. Например, запускайте приложение с 11 тредами, каждый из которых может обрабатывать одного клиента, но не пускайте извне в приложение более 10 клиентов, таким образом гарантируя для livenessProbe отдельный незанятый тред.
И, конечно, не стоит добавлять в livenessProbe проверки внешних зависимостей.
(Подробнее о проблемах с liveness probe и рекомендациях по предотвращению таких проблем рассказывалось в этой статье.)
7.2 Readiness probe
Дизайн readinessProbe (дословно: «проверка на готовность [к обслуживанию запросов]»), пожалуй, оказался не очень удачным. Она сочетает в себе две функции: проверять, что приложение в контейнере стало доступным при запуске контейнера, и проверять, что приложение остаётся доступным уже после его запуска. На практике первое нужно практически всегда, а второе примерно настолько же часто, насколько оказывается нужной livenessProbe. Проблемы с плохими readinessProbe примерно те же самые, что и с плохими livenessProbe, и в худшем случае также могут приводить к длительной недоступности приложения.
Когда readinessProbe перестаёт отрабатывать, то на Pod перестаёт приходить трафик. В большинстве случаев такое поведение мало помогает, т. к. трафик обычно балансируется между Pod’ами более-менее равномерно. Таким образом, чаще всего readinessProbe либо работает везде, либо не работает сразу на большом количестве Pod’ов. Есть ситуации, когда подобное поведение readinessProbe может понадобиться, но в моей практике это скорее исключение.
Тем не менее, у readinessProbe есть другая очень важная функция: определить, когда только что запущенное в контейнере приложение стало способно принимать трафик, чтобы не пускать трафик в ещё не доступное приложение. Эта же функция readinessProbe, напротив, нужна нам почти всегда.
Получается странная ситуация, что одна функция readinessProbe обычно очень нужна, а другая очень не нужна. Эта проблема была решена введением startupProbe, которая появилась в Kubernetes 1.16 и перешла в Beta в 1.18. Таким образом, рекомендую для проверки готовности приложения при его запуске в Kubernetes = 1.18 — использовать startupProbe. readinessProbe всё ещё можно использовать в Kubernetes >= 1.18, если у вас есть необходимость останавливать трафик на отдельные Pod’ы уже после старта приложения.
7.3 Startup probe
Обязательна к использованию, если ваше приложение принимает трафик и у вас Kubernetes >= 1.18.
8. Проверка внешних зависимостей
Часто можно встретить совет проверять внешние зависимости вроде баз данных в readinessProbe. И хотя такой подход имеет право на существование, предпочтительно разделять проверку внешних зависимостей и проверку на то, не стоит ли остановить идущий на Pod трафик, когда приложение в нём полностью утилизировано.
С помощью initContainers можно проверять внешние зависимости до того, как начнут запускаться startupProbe/readinessProbe основных контейнеров. В readinessProbe, соответственно, проверки внешних зависимостей уже не понадобится. Подобные initContainers не требуют изменений в коде приложения, не требуют собирать контейнеры приложения с дополнительными утилитами для проверок внешних зависимостей, а также в целом довольно просты в реализации:
Полный пример
Резюмируя, привёдем полный пример того, как уже с учётом всех вышеописанных рекомендаций может выглядеть Deployment stateless-приложения при его боевом развертывании.
Требования: Kubernetes >= 1.18, на узлах Ubuntu/Debian с версией ядра >= 5.4.
В следующий раз
ОБНОВЛЕНО: Вторая часть статьи вышла и доступна здесь.