Один из самых частых вопросов, которые мне задают как консультанту это: “В чем разница между liveness и readiness пробами?”. Следующий самый частый вопрос: “Какие из них нужны моему приложению?”.

Любой, кто пробовал за Duck Duck Go-ить этот вопрос знает, что на него непросто найти ответ в интернете. В этой статье, надеюсь, я смогу помочь вам ответить на эти вопросы самостоятельно. Я поделюсь своим мнением о том, каким образом лучше использовать liveness и readiness пробы в приложениях развернутых в Red Hat OpenShift. И я предлагаю не строгий алгоритм, а, скорее, общую схему, которую вы можете использовать для принятия своих собственных архитектурных решений.

Чтобы сделать абстракции более конкретными я предлагаю рассмотреть четыре общих примера приложений. Для каждого из них мы выясним нужно ли нам настраивать liveness и readiness probes, и если нужно, то как. Прежде чем перейти к примерам, давайте ближе посмотрим на эти два разных типа проб.

Примечание: Kubernetes недавно внедрили новую “startup” probe, доступную в OpenShift 4.5 clusters. Вы быстро разберетесь со startup probe, когда поймете liveness и readiness probes. Здесь я не буду рассказывать о startup probes.

Liveness и readiness probes

Liveness (работоспособности) и readiness (готовности) пробы это два основных типа проверок, доступных в OpenShift. Они имеют схожий интерфейс настройки, но разные значения для платформы.

Когда liveness проверка не проходит, то это сигнализирует OpenShift’у, что контейнер мертв и должен быть перезагружен. Когда readiness проверка не проходит, то это опознается OpenShift’ом как то, что проверяемый контейнер не готов к принятию входящего сетевого трафика. В будущем это приложение может прийти в готовность, но сейчас оно не должно принимать трафик.

Если liveness проверка успешна, и в то же время readiness проверка не прошла, OpenShift знает, что контейнер не готов принимать сетевой трафик, но он работает над тем, чтобы прийти в готовность. Например, часто приложениям нужно длительное время для инициализации или для синхронной обработки длительных запросов. (Синхронная обработка длительных запросов - это анти-паттерн, но, к несчастью, мы сталкиваемся с этим в некоторых legacy-приложениях.)

Далее мы подробнее рассмотрим специфику использования каждой из этих проверок. Как только мы поймем как работает каждая из них, я покажу примеры того, как они работают вместе в OpenShift.

Для чего нужны liveness проверки?

Liveness проверка отправляет сигнал OpenShift’у, что контейнер либо жив (прошел проверку), либо мертв (не прошел). Если контейнер жив, тогда OpenShift не делает ничего, потому что текущее состояние контейнера в порядке. Если контейнер мертв, то OpenShift пытается починить приложение через его перезапуск.

Название liveness проверки (проверка живучести) имеет семантическое значение. По сути проверка отвечает “Да” или “Нет” на вопрос: “Жив ли этот контейнер?”.

Что если я не установил liveness проверку?

Если вы не задали liveness проверку, тогда OpenShift будет принимать решение о перезапуске вашего контейнера на основе статуса процесса в контейнере с PID 1. Процесс с PID 1 это родительский процесс для всех других процессов, которые запущены внутри контейнера. Так как каждый контейнер начинает жить внутри его собственного пространства имен процессов, первый процесс в контейнере берет на себя особенные обязательства процесса с PID 1

Если процесс с PID 1 завершается и liveness пробы не заданы, OpenShift предполагает (обычно безопасно), что контейнер умер. Перезапуск процесса - это единственное не зависящее от приложения универсально эффективное корректирующее действие. Пока PID 1 жив, не зависимо от того, живы ли дочерние процессы, OpenShift оставит контейнер работать дальше.

Если ваше приложение это один процесс, и он имеет PID 1, то это поведение по умолчанию может быть именно тем, что вам нужно, и тогда нет необходимости в liveness пробах. Если вы используйте init инструменты, такие как tini или dumb-init, тогда это может быть не то, что вы хотите. Решение о том, задавать ли свои liveness вместо того, чтобы использовать поведение по умолчанию, зависит от специфики каждого приложения.

Для чего нужны readiness пробы?

Сервисы OpenShift используют readiness проверки (проверки готовности) для того, чтобы узнать когда проверяемый контейнер будет готов принимать сетевой трафик. Если ваш контейнер вошел в состояние, когда он все ещё жив, но не может обрабатывать входящий трафик (частый сценарий во время запуска), вам нужно, чтобы readiness проверка не проходила. В этом случае OpenShift не будет отправлять сетевой трафик в контейнер, который к этому не готов. Если OpenShift преждевременно отправит трафик в контейнер, это может привести к тому, что балансировщик (или роутер) вернет 502 ошибку клиенту и завершит запрос, либо клиент получит сообщение с ошибкой “connection refused”.

Как и liveness проверки, название readiness проба (проба готовности) передает семантическое значение. Фактически это проверка отвечает на вопрос: “Готов ли этот контейнер принимать сетевой трафик?”.

Что если я не задам readiness пробу?

Если вы не зададите readiness проверку, OpenShift решит, что контейнер готов к принятию трафика как только процесс с PID 1 запустился. Это никогда не является желаемым поведением.

Принятие готовности без проверки приведет к ошибкам (таким, как 502 от роутера OpenShift) каждый раз при запуске нового контейнера, при масштабировании или развертывании. Без readiness проб вы будете получать пакет ошибок при каждом развертывании, когда старые контейнеры выключаются и запускаются новые. Ели вы используете автоматическое масштабирование, тогда в зависимости от установленного порога метрик новые инстансы могут запускаться и останавливаться в любое время, особенно во время колебаний нагрузки. Когда приложение будет масштабироваться вверх или вниз, вы будете получать пачки ошибок, так как контейнеры, которые не совсем готовы получать сетевой трафик включаются в распределение балансировщиком.

Вы можете легко исправить эти проблемы через задание readiness проверки. Проверка дает OpenShift'у возможность спросить ваш контейнер, готов ли он принимать трафик.

Теперь давайте взглянем на конкретные примеры, которые помогут нам понять разницу между двумя типами проб и то, как важно задать их правильно.

Примечание: Из-за того, что у этих двух разных типов проверок одинаковый интерфейс часто возникает путаница. Но наличие двух или более типов проб - это хорошее решение: это делает OpenShift гибче для разных типов приложений. Доступность обеих liveness и readiness проб критична для репутации OpenShift как Container-as-a-Service, который подходит для широкого спектра приложений.

Пример 1: Сервер для отдачи статического контента (Nginx)

Рис. 1: Пример реализации сервера для отдачи статики Nginx
Рис. 1: Пример реализации сервера для отдачи статики Nginx

В примере приложение, изображенное на рисунке 1 - это простой сервер для отдачи статики, который использует Nginx, чтобы отдавать файлы. Сервер запускается быстро и проверить, обрабатывает ли сервер трафик легко: вы можете запросить определенную страницу и проверить, что вернулся 200 код HTTP ответа.

Нужны ли liveness пробы?

Приложение запускается быстро и завершается, если обнаружит ошибку, которая не позволит ему отдавать страницы. Поэтому, в данном случае, нам не нужны liveness пробы. Завершенный процесс Nginx означает, что приложение умерло и его нужно перезапустить (отметим, что неполадки, такие как проблемы с SELinux или неправильно настроенные права в файловой системе могут быть причиной выключений Nginx, но перезапуск не исправит их в любом случае).

Нужны ли readiness пробы?

Nginx обрабатывает входящий трафик поэтому нам нужны readiness пробы. Всегда, когда вы обрабатываете сетевой трафик вам нужны readiness пробы для предотвращения возникновения ошибок во время запуска контейнеров, при развертывании или масштабировании. Nginx запускается быстро, поэтому может вам повезет и вы не успеете увидеть ошибку, но мы все ещё хотим предотвратить передачу трафика до тех пор, пока контейнер не придет в готовность, в соответствии с best practice.

Добавление readiness проб для сервера

Нам нужно будет сделать определенные изменения для каждого примера, но сначала, приведем здесь первую часть Deployment. Мы изменим этот файл по ходу статьи, но верхняя часть останется той же. В следующих примерах, нам нужно будет изменять только секцию template.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: application-nginx
  name: application-nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: application-nginx
  template:
    metadata:
      labels:
        app: application-nginx
    spec:
      #  Will appear below as it changes

Здесь настройка проверки для первого примера:

    spec:
      containers:
      - image: quay.io/<username>/nginx:latest
        name: application-nginx
        imagePullPolicy: Always
        ports:
        - containerPort: 8443
          protocol: TCP
        readinessProbe:
          httpGet:
            scheme: HTTPS
            path: /index.html
            port: 8443
          initialDelaySeconds: 10
          periodSeconds: 5
Рис. 2: Реализация сервера Nginx для отдачи статики с настроенными readiness проверками
Рис. 2: Реализация сервера Nginx для отдачи статики с настроенными readiness проверками

Пример 2: Сервер заданий (без REST API)

Многие приложения имеют HTTP веб-компонент, а так же компонент выполняющий асинхронные “задания”. Серверу с “заданиями” не нужны readiness проверки, так как он не обрабатывает входящий трафик. Однако, ему в любом случае нужны liveness проверки. Если процесс, выполняющий задания умирает, то контейнер становится бесполезным, и задания будут накапливаться в очереди. Обычно, перезапуск контейнера является правильным решением, поэтому liveness пробы идеальны для такого случая. 

Пример с приложением на рис. 3 это простой сервер заданий который забирает и запускает задания из очереди . Он не обслуживает напрямую входящий трафик.

Рис. 3: Реализация сервера заданий без liveness проверок.
Рис. 3: Реализация сервера заданий без liveness проверок.

Я уже упоминал, что этот тип приложений выигрывает от наличия liveness проб, но более детальное пояснение не повредит в любом случае.

Нужны ли нам liveness пробы? 

Когда сервер заданий запущен корректно, это будет живой процесс. Если контейнер сервера заданий перестает работать, это скорее всего означает сбой, необработанное исключение или что-то в этом духе. В этом случае настройка проверки будет зависеть от того, запущен ли процесс нашего задания с PID 1.

Если наш процесс задания запущен с PID 1, то он завершится, когда поймает исключение. Без настроенных liveness проверок OpenShift распознает завершение процесса с PID 1 как смерть и перезапустит контейнер. Для простого сервера заданий перезапуск может быть нежелательным поведением.

Однако, в реальной жизни ситуации могут быть сложнее. Например, если наш процесс с заданием получил deadlock, он может все ещё считаться живым, так как процесс запущен, но он определенно в нерабочем состоянии и должен быть перезапущен.

Чтобы помочь вычислить deadlock, наше приложение будет писать текущее системное время в миллисекундах в файл/tmp/jobs.updateкогда выполняет задание. Это время потом будет проверяться через shell команду (через exec liveness пробы) для того, чтобы убедиться, что текущее задание не запущено дольше определенного значения таймаута. Тогда приложение само сможет проверять себя на liveness выполняя/usr/bin/my-application-jobs --alive.

Мы можем настроить liveness так (опять же, я пропускаю первую часть YAML-файла Deployment’a, который я показывал ранее):

    spec:
      containers:
      - image: quay.io/<username>/my-application-jobs:latest
        name: my-application-jobs
        imagePullPolicy: Always
        livenessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - "/usr/bin/my-application-jobs --alive"
          initialDelaySeconds: 10
          periodSeconds: 5

Нужны ли readiness проверки?

В это случае readiness не нужны. Помним, что readiness пробы отправляют OpenShift сигнал о том, что контейнер готов принимать трафик и поэтому может быть добавлен в конфигурацию балансировщика. Так как это приложение не обрабатывает входящий трафик, оно не нуждается в проверке на готовность. Мы можем отключить readiness пробу. Рис. 4 показывает реализацию сервиса заданий с настроенными liveness проверками.

Рис. 4: Реализация сервера заданий с liveness проверками
Рис. 4: Реализация сервера заданий с liveness проверками

Пример 3: Приложение с рендерингом на стороне сервера с API

Рис. 5: SSR приложение без каких-либо проверок
Рис. 5: SSR приложение без каких-либо проверок

Это пример стандартного приложение рендеринга на стороне сервера SSR: оно отрисовывает HTML-страницы на сервере по востребованию, и отправляет его клиенту. Мы можем собрать такое приложение используя Spring Boot, PHP, Ruby on Rails, Django, Node.js, или любой другой похожий фреймворк.

Нужны ли нам liveness проверки?

Если приложение запускается за несколько секунд или меньше, то liveness проверки, скорее всего, не обязательны. Если запуск занимает больше нескольких секунд, мы должны настроить liveness проверки, чтобы понять, что контейнер инициализировался без ошибок и не произошел сбой.

В этом случае мы могли бы использовать liveness пробы типа exec, которые запускает команду в shell, чтобы убедиться, что приложение по-прежнему работает. Эта команда будет отличаться в зависимости от приложения. Например, если приложение создает PID файл, мы можем проверить, что он все еще жив:

        livenessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - "[ -f /run/my-application-web.pid ] && ps -A | grep my-application-web"
          initialDelaySeconds: 10
          periodSeconds: 5

Нужны ли readiness пробы?

Так как данное приложение обрабатывает входящий трафик нам определенно нужны readiness пробы. Без readiness OpenShift немедленно отправит сетевой трафик в наш контейнер после его запуска, независимо от того, готово приложение или нет. Если контейнер начинает отбрасывать запросы, но не завершает работу аварийно, он продолжит получать трафик бесконечно, что, конечно же, не то, что нам нужно.

Мы хотим, чтобы OpenShift удалил контейнер из балансировщика, если приложение перестанет возвращать правильные ответы. Мы можем использовать readiness пробы, подобные этой, чтобы сообщить OpenShift, что контейнер готов к приему сетевого трафика:

        readinessProbe:
          httpGet:
            scheme: HTTPS
            path: /healthz
            port: 8443
          initialDelaySeconds: 10
          periodSeconds: 5

Для удобства, вот полный YAML для этого приложения из этого примера:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: backend-service
  name: backend-service
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend-service
  template:
    metadata:
      labels:
        app: backend-service
    spec:
      containers:
      - image: quay.io/<username>/backend-service:latest
        name: backend-service
        imagePullPolicy: Always
        ports:
        - containerPort: 8443
          protocol: TCP
        readinessProbe:
          httpGet:
            scheme: HTTPS
            path: /healthz
            port: 8443
          initialDelaySeconds: 10
          periodSeconds: 5

На рисунке 6 показана схема приложения SSR с настроенными liveness и readiness пробами.

Рис. 6. Приложение SSR с настроенными liveness и readiness пробами.
Рис. 6. Приложение SSR с настроенными liveness и readiness пробами.

Пример 4: Собираем все вместе

В конечном, сложном и реалистичном приложении у вас могут быть элементы из всех трех предыдущих примеров. Рассмотрение их по отдельности полезно при анализе работы проб, но также полезно увидеть, как они могут работать вместе для обслуживания большого приложения с миллионами запросов. Этот последний пример объединяет остальные три.

Рис. 7: Реалистичный пример приложения в OpenShift для изучения использования проб.
Рис. 7: Реалистичный пример приложения в OpenShift для изучения использования проб.

В примере приложение состоит из трех частей, это контейнеры:

  • Сервер приложений: Этот сервер предоставляет REST API и выполняет рендеринг на стороне сервера для некоторых страниц. Эта конфигурация широко распространена, поскольку приложения, которые создаются в качестве простых серверов для отрисовки, позже расширяются для обеспечения конечных точек REST API.

  • Сервер для отдачи статики Nginx: У этого контейнера есть две задачи: он отображает статические ресурсы для приложения (например, ресурсы JavaScript и CSS). И также он реализует завершение TLS-соединений (Transport Layer Security) для сервера приложений, действуя как обратный прокси для определенных URL. Это также широко используемая настройка.

  • Сервер заданий: этот контейнер не обрабатывает входящий сетевой трафик самостоятельно, но обрабатывает задания. Сервер приложений помещает каждое задание в очередь, где сервер заданий берет его и выполняет. Сервер заданий разгружает сервер приложений, чтобы он мог сосредоточиться на обработке сетевых запросов, а не на обработке длинных заданий.

Приложение также включает в себя несколько сервисов хранения данных:

  • Реляционная база данных: реляционная база данных будет хранить состояние для нашего приложения. Почти каждое приложение нуждается в какой-либо базе данных, и реляционные являются наиболее предпочтительным выбором.

  • Очередь. Очередь предоставляет серверу приложений путь «первым пришел - первым вышел» (FIFO) для передачи задач серверу заданий. Сервер приложений всегда будет пушить задания в очередь, а сервер заданий извлекать.

Наши контейнеры разделены на два пода:

  • Первый под состоит из нашего сервера приложений и Nginx TLS-прокси или сервера для статики. Это упрощает работу сервера приложений, позволяя ему взаимодействовать напрямую через HTTP. Благодаря расположению в одном поде эти контейнеры могут безопасно и напрямую связываться с минимальной задержкой. Они также могут получить доступ к общему volume space. Эти контейнеры также нужно масштабировать вместе и рассматривать как единое целое, поэтому под является идеальным решением.

  • Второй под состоит из сервера заданий. Этот сервер необходимо масштабировать независимо от других контейнеров, поэтому он должен находиться в собственном поде. Поскольку весь стейт хранится в базе данных и очереди, сервер заданий может легко получить доступ к необходимым ему ресурсам.

Если вы читали предыдущие примеры, решению здесь вы не удивитесь. Для интеграции мы переключаем сервер приложений на использование HTTP и порта 8080 вместо HTTPS и 8443 для readiness проб. Мы также добавляем liveness пробы на сервер приложений, чтобы прикрыть нас, если сервер приложений не завершает работу в случае ошибки. Таким образом, наш контейнер будет перезапущен Kubelet'ом, когда он «мертв»:

# Pod One - Application Server and Nginx
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: my-application-web
  name: my-application-web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-application-web
  template:
    metadata:
      labels:
        app: my-application-web
    spec:
      containers:
      - image: quay.io/<username>/my-application-nginx:latest
        name: my-application-nginx
        imagePullPolicy: Always
        ports:
        - containerPort: 8443
          protocol: TCP
        livenessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - "[ -f /run/nginx.pid ] && ps -A | grep nginx"
          initialDelaySeconds: 10
          periodSeconds: 5
        readinessProbe:
          httpGet:
            scheme: HTTPS
            path: /index.html
            port: 8443
          initialDelaySeconds: 10
          periodSeconds: 5
      - image: quay.io/<username>/my-application-app-server:latest
        name: my-application-app-server
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
          protocol: TCP
        livenessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - "/usr/bin/my-application-web --alive"
          initialDelaySeconds: 10
          periodSeconds: 5
        readinessProbe:
          httpGet:
            scheme: HTTP
            path: /healthz
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5

# Pod Two - Jobs Server
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: my-application-jobs
  name: my-application-jobs
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-application-jobs
  template:
    metadata:
      labels:
        app: my-application-jobs
    spec:
      containers:
      - image: quay.io/<username>/my-application-jobs:latest
        name: my-application-jobs
        imagePullPolicy: Always
        livenessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - "/usr/bin/my-application-jobs --alive"
          initialDelaySeconds: 10
          periodSeconds: 5

На рисунке 8 показаны полные примеры приложений с обеими настроенными пробами.

Рис. 8: Полный пример приложений с обеими настроенными пробами.
Рис. 8: Полный пример приложений с обеими настроенными пробами.

Что насчет одинаковых liveness и readiness проб?

На мой взгляд, хотя этот шаблон используется чересчур часто, в некоторых случаях он имеет смысл. Если приложение начинает некорректно отвечать на HTTP-запросы и, вероятно, никогда не восстановит свою работу без перезапуска, то вы, вероятно, захотите, чтобы OpenShift перезапустил под. Было бы лучше, если бы ваше приложение восстанавливалось само по себе, но это бывает нецелесообразно в реальном мире.

Если у вас есть HTTP endpoint, который может быть исчерпывающим индикатором, вы можете настроить и liveness и readiness пробы на работу с этим endpoint. Используя один и тот же endpoint убедитесь, что ваш под будет перезапущен, если этот endpoint не сможет вернуть корректный ответ.

Финальные мысли

Liveness и readiness пробы отправляют разные сигналы в OpenShift. Каждый имеет свое определенное значение и они не взаимозаменяемы. Не пройденная liveness проверка говорит OpenShift, что контейнер должен быть перезапущен. Не пройденная readiness проба говорит OpenShift придержать трафик от отправки в этот контейнер.

Нет универсального рецепта для проб, потому что “правильный” выбор будет зависеть от того, как написано приложение. Приложению, которое умеет самовосстанавливаться нужна иная настройка от того, которое просто дает сбой и умирает.

Выбирая корректные проверки для приложения я обращаюсь к семантическому смыслу в сочетании с поведением приложения. Зная, что проваленная liveness проба перезапустит контейнер, а проваленная readiness проба удалит его из балансировщика. Обычно это не сложно, определить какие пробы нужны приложению.

Мы рассмотрели реалистичные примеры, но вы можете увидеть значительно более сложные случаи в реальных системах. Для инстанса в сервис-ориентированной архитектуре (SOA), сервис может зависеть от другого сервиса при обработке соединений. Если нисходящий сервис не готов, должен ли проходить readiness проверку восходящий сервис или нет? Ответ зависит от приложения. Вам нужно провести анализ стоимости/преимуществ решения, чтобы определить, что добавочная сложность того стоит.

Хотя концептуально пробы очень просты, они могут быть сложны на практике. Лучший подход для нахождения оптимального решения - это итеративный! Проведите тестирование производительности и хаотичное тестирование для наблюдения за поведением вашей конфигурации и улучшайте её в процессе. Мы редко делаем все правильно с первого раза.