Deep Engineering
Средний·Опубликовано·20 МИН

Health-checks: три разные проверки, которые зовут одним словом

Liveness спрашивает «жив ли ты», readiness — «готов ли ты принимать», startup — «успел ли ты подняться». Их путают, и цена путаницы измерима: проба, завязанная на общую базу, превращает одну аварию базы в перезапуск всех реплик сразу.

Полное техническое изложение

TL;DR

Проверка здоровья — не один вопрос, а три. Startup спрашивает, успело ли приложение подняться; liveness — поможет ли перезапуск этого процесса; readiness — можно ли слать сюда трафик. Различаются они не строгостью, а последствием: перезапуск, ожидание или вывод из ротации. Поэтому и содержимое каждой пробы выводится из её последствия: проверять надо то, что этим последствием чинится.

Отсюда главная ошибка: проба, завязанная на общую зависимость, превращает одну аварию в две. Перезапуск не делает доступнее то, что находится снаружи процесса, — база, сеть и соседний сервис от него не оживают, зато процесс теряет прогретые кеши и пулы соединений. Модель: недоступность базы стоит 1000 неуспешных запросов сама по себе и 1200, если liveness проверяет базу. Лишние 200 — это перезапуск всех реплик разом.

Дальше — числа и границы. У деградировавшей реплики один liveness даёт 520 медленных ответов, liveness плюс readiness — 80, то есть в 6,5 раза меньше; полное отсутствие проб — 1600, потому что балансировщик продолжает слать нагрузку на больную реплику. Ровно так же он слепнет от readiness, который всегда отвечает «готов». Но и обратная крайность существует: readiness — не сумма всех зависимостей сервиса, а ответ на вопрос, способна ли реплика обслужить хоть какой-то трафик. Числа здесь модельные: они выводятся из объявленных допущений, и содержательны отношения между строками, а не абсолютные значения.

Порог входа
Перед уроком достаточно понимать
  • сервис работает не в одном экземпляре: несколько одинаковых реплик обслуживают общий поток запросов;
  • перед ними стоит балансировщик, который решает, кому отдать очередной запрос, и может перестать отдавать их кому-то одному;
  • за самими репликами кто-то следит и умеет перезапустить ту, с которой что-то не так.
Заранее знать не нужно
  • чем liveness отличается от readiness и зачем нужен startup — это и есть содержание урока;
  • как оркестратор настраивает пробы: периоды, пороги, имена полей;
  • каскад перезапусков, вывод из ротации, ложно-отрицательная проба.

Что здесь на самом деле спрашивают

Лестница обычно такая:

  1. «Зачем нужны проверки здоровья?» — разминка.
  2. «Чем liveness отличается от readiness?» — начало содержания.
  3. «Что проверять внутри пробы?» — главный вопрос темы.
  4. «Почему liveness не должен ходить в базу?» — вопрос про каскад.
  5. «Зачем нужна отдельная startup-проба?» — вопрос про медленный старт.
  6. «Что будет, если readiness врёт?» — вопрос про обратную ошибку.
модель с допущениямиЧисла этого урока — из симуляции bench/probes/model.py, а не из замера кластера. Допущения: три реплики, постоянный поток, деградация одной реплики с 10-й секунды, недоступность общей базы с 20-й по 30-ю, проба раз в секунду, порог три неудачи, перезапуск пять секунд.

Этот урок стоит не на замере, а на модели: у поведения проб нет общего стандарта — есть документация отдельных продуктов. Модель отвечает не на вопрос «сколько именно», а на вопрос «что с чем связано», и допущения у неё названы.

База: три вопроса, а не одна проверка

У сервиса несколько одинаковых реплик, и снаружи не видно, что происходит внутри любой из них. Спросить можно ровно одним способом: постучаться и посмотреть на ответ. Отсюда и берётся проверка здоровья — короткий вопрос, который кто-то задаёт реплике регулярно, и заранее назначенное действие на случай отрицательного ответа.

Вся тема стоит на том, что «здоров ли ты» — это не один вопрос. Действий, на которые способен наблюдатель, несколько, и разные действия чинят разное. Поэтому проверок три, и у каждой свой вопрос:

ПробаНа какой вопрос отвечаетЧто делают при ответе «нет»
startupприложение уже запустилось?ничего: продолжают ждать и не судят реплику строго
livenessпоможет ли перезапуск этого процесса?перезапускают процесс
readinessможно ли слать сюда трафик?перестают слать трафик, процесс не трогают

Средняя колонка у liveness сформулирована непривычно, и это главная мысль урока. Обычно говорят «жив ли процесс» — но на такой вопрос проба не нужна: мёртвый процесс не ответит вовсе, и это видно без всяких проверок. Проба существует ради другого случая: процесс жив, отвечает на запросы, а работать перестал. И решает она не «жив или нет», а стоит ли применить к нему единственное имеющееся лекарство — перезапуск.

Правая колонка отвечает и на главный вопрос урока: что положить внутрь каждой пробы? Ответ выводится из последствия — проверять надо то, что этим последствием чинится, и ничего больше.

Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Всё дальнейшее — про цену ошибок в этом выборе: сколько стоит отсутствие readiness, почему проба, заглядывающая в общую базу, добавляет отказов к чужой аварии, и почему слишком старательный readiness вреден так же, как слишком доверчивый.

Механизм 1: последствие и есть определение

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

Liveness — «поможет ли перезапуск этого процесса». Отрицательный ответ означает: процесс испорчен настолько, что вернуть его в рабочее состояние может только перезапуск. Взаимоблокировка, исчерпание пула потоков, бесконечный цикл — поломки внутренние и накопительные, и все они лечатся возвратом процесса в известное состояние.

Readiness — «можно ли слать сюда трафик сейчас». Отрицательный ответ означает: не присылайте мне запросы, но перезапускать меня не надо. Прогрев кеша, потеря соединения с зависимостью, временная перегрузка — состояния, из которых реплика выходит сама, если её не нагружать.

Startup — «успел ли подняться». Отдельная проба нужна, чтобы медленный старт не выглядел как смерть. Урок считает, что до успеха startup-пробы liveness не применяется, — в этом и есть смысл отдельной пробы.

Разница в последствии: liveness перезапускает, readiness убирает из ротации. Отсюда правило, из которого выводится всё остальное: проверять надо то, что можно починить последствием. Для liveness это читается буквально: внутрь неё попадает только то, что чинится перезапуском этого процесса.

Механизм 2: что даёт каждая проба

Модель: три реплики, постоянный поток, одна реплика с десятой секунды начинает отвечать медленно — но отвечать.

REQUESTS SERVED SLOWLY OR NOT AT ALL
------------------------------------
                  mode       slow     failed      total
                  none       1600       1000       6000
              liveness        520       1000       6000
    liveness+readiness         80       1000       6000
        liveness-on-db        640       1200       6000
модель с допущениямиbench/probes/model.py, модель. Абсолютные числа заданы допущениями (10 запросов на шаг, горизонт 60 с); содержательны отношения между строками.

Без проб — 1600 медленных ответов. Балансировщик не знает, что реплике плохо, и продолжает слать ей треть трафика всё время.

Только liveness — 520. Проба замечает беду и перезапускает реплику; перезапуск на пять секунд убирает её из строя, потом она возвращается — и снова деградирует. Помогает, но криво: реплика курсирует между «плохо» и «перезапускается».

Liveness плюс readiness — 80. Реплику выводят из ротации без перезапуска, и нагрузка на неё прекращается. В 6,5 раза меньше медленных ответов, чем при одном liveness.

Отсюда ответ на второй вопрос лестницы: readiness — не «то же самое, но мягче». Это другое последствие, и в случае деградации именно оно уместно: реплика, которую не нагружают, может восстановиться сама, а перезапущенная начинает с холодного старта.

Механизм 3: почему liveness не ходит в базу

Четвёртая строка прогона выше — про ошибку, которая добавляет отказов к чужой аварии. Проба здесь проверяет не только себя, но и общую зависимость: если база недоступна, liveness считается неудачным.

Её медленные ответы (640) близки к режиму с одним liveness — 520; вся разница ушла в колонку неуспешных, и ею занят этот раздел.

WHAT THE THREE MODES ACTUALLY CHANGE
------------------------------------
  no probes: slow answers                      1600
  liveness only: slow answers                  520
  liveness + readiness: slow answers           80
  liveness tied to the database: failures      1200
  the same failures without that tie           1000

  failures added by tying liveness to the database 200
модель с допущениямиbench/probes/model.py. Недоступность базы длится одинаково в обоих прогонах; разница в 200 неуспешных запросов — это цена перезапусков.

База лежит десять секунд в обоих прогонах, и эти десять секунд стоят 1000 неуспешных запросов независимо от проб — починить чужую аварию проба не может.

А вот лишние 200 — целиком заслуга пробы. Механизм такой: база недоступна → liveness не проходит у всех реплик одновременно → все реплики перезапускаются → пока они поднимаются, сервис не отвечает вообще, и это продолжается после того, как база уже вернулась.

Отсюда правило, которым стоит отвечать: liveness проверяет только то, что чинится перезапуском этого процесса. База, сеть и соседний сервис перезапуском не чинятся; их место — в readiness, где последствие мягче: реплику перестают нагружать, но она остаётся живой и вернётся сама.

И вторая часть того же правила: общая зависимость делает пробы синхронными. Если проверка одинакова у всех реплик и зависит от одного внешнего объекта, они отказывают все разом — и запас надёжности, ради которого их и держат три, исчезает.

Глубже: границы правила и обе ошибки readiness

Обратная ошибка устроена наоборот и стоит в модели больше всего медленных ответов. Readiness, который отвечает «готов» всегда — например, потому что это статический обработчик, возвращающий 200, — превращает балансировщик в слепого: он продолжает слать запросы туда, где их некому обслужить.

Отдельного режима для такого readiness в модели нет; ближайший к нему — режим вообще без проб: 1600 медленных ответов. Балансировщик в обоих случаях одинаково слеп, поэтому число то же. Реплика жива и формально готова, а запросы на неё продолжают идти.

Отсюда практическая проверка, которую стоит называть на собеседовании: readiness обязан отражать способность обслужить запрос, а не факт запуска процесса. Минимальная честная реализация проверяет то, без чего запрос не выполнится: соединение с базой, наличие места в пуле, состояние очереди задач.

И обратная сторона: чем больше проверок внутри readiness, тем выше шанс, что она станет ложно-отрицательной — реплика уйдёт из ротации из-за пустяка. Поэтому проверяют не «всё, что есть», а то, без чего этот сервис не обслуживает свои запросы.

Здесь и стоит назвать границу правила, симметричную границе для liveness. Из фразы «зависимости живут в readiness» напрашивается вывод «значит, надо перечислить в ней все зависимости» — и это тоже ошибка, просто другая. Readiness отвечает не на вопрос «всё ли вокруг меня в порядке», а на вопрос способна ли эта реплика обслужить хоть какой-то трафик. Сервис, который без второй базы всё ещё отвечает на часть своих запросов, обязан остаться в ротации, когда вторая база недоступна: выведя его, вы превратите частичную деградацию в полный отказ — и сделаете это своими руками, а не аварией.

А ещё readiness, перечисляющий все зависимости, возвращает ровно ту болезнь, которую лечил предыдущий раздел. Общая зависимость делает пробы синхронными независимо от того, в какой из них она записана: все реплики уходят из ротации одновременно, потому что смотрят на один и тот же внешний объект. Последствие мягче, чем при liveness, — реплики хотя бы не перезапускаются, — но резерв исчезает так же.

Как отвечать на собеседовании

Короткий ответ: проб три, и различаются они последствием — liveness перезапускает, readiness выводит из ротации, startup откладывает liveness на время запуска. Поэтому вопрос liveness звучит не «жив ли процесс», а «поможет ли перезапуск этого процесса»: внутрь неё попадает только то, что чинится перезапуском, а зависимости — в readiness.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

Хороший ответ отличают три вещи. Первая — вы называете цену ошибки числом: в модели liveness, завязанный на базу, добавил 200 неуспешных запросов к тем 1000, что стоила сама авария, потому что перезапустились все реплики сразу. Вторая — вы объясняете, зачем readiness при наличии liveness: перезапуск возвращает реплику в строй снова и снова, а вывод из ротации прекращает нагрузку; в модели 80 медленных ответов против 520. Третья — вы упоминаете обратную ошибку: readiness, который всегда отвечает «готов», делает балансировщик слепым, и это худший случай в модели — 1600.

И одна вещь, на которой легко переусердствовать. Из правила «зависимости — в readiness» не следует, что readiness должен быть суммой всех зависимостей. Точная формулировка звучит так: readiness отвечает на вопрос, способна ли реплика обслужить хоть какой-то трафик — и реплика, которая без одной из зависимостей обслуживает часть запросов, из ротации выходить не должна. Иначе проба сама превращает частичную деградацию в полный отказ, а общая зависимость снова делает все реплики синхронными.

Дальше спросят

Спросят дальше

Может ли readiness и liveness быть одним обработчиком?

Короткий ответ

Технически может. Но последствия у проб разные: одна перезапускает, другая перестаёт нагружать. Последствия у проб разные: одна перезапускает, другая перестаёт нагружать. Общий обработчик означает, что любая причина «не готов» превращается в перезапуск.

В модели это ровно четвёртая строка: проба, проверяющая базу, добавила 200 неуспешных запросов сверх тех, что стоила сама недоступность базы. Разделение обработчиков — самая дешёвая мера в этой теме.

Спросят дальше

Зачем нужна отдельная startup-проба?

Короткий ответ

Чтобы медленный запуск не выглядел как смерть. Если приложение поднимается минуту, а liveness настроен на секунды, оно будет перезапущено, не успев стартовать, — и так по кругу.

Альтернатива без отдельной пробы — растянуть пороги liveness настолько, чтобы их хватило на запуск. Но тогда та же растянутая проверка действует и в установившемся режиме, и настоящая взаимоблокировка будет замечена через минуту вместо секунды. Отдельная проба разделяет эти два режима.

Спросят дальше

Что проверять внутри readiness?

Короткий ответ

То, без чего этот сервис не может обслужить запрос: соединение с базой, если каждый запрос идёт в базу; место в пуле; заполненность кеша, если без него ответы бессмысленны. Не «всё, что есть»: каждая лишняя проверка увеличивает шанс ложного вывода из ротации.

Полезная проверка при написании: если проба вернёт «не готов», сможет ли реплика вернуться сама? Если да — это readiness. Если помочь может только перезапуск — это liveness.

Спросят дальше

Почему перезапуск иногда всё-таки помогает?

Короткий ответ

Потому что часть поломок — внутренние и накопительные: утечка дескрипторов (урок про лимиты), взаимоблокировка, пул потоков, из которого никто не возвращается. Перезапуск возвращает процесс в известное состояние, и это единственное лекарство, которое не требует понимать причину.

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

Частые заблуждения

Утверждение

liveness и readiness — это одно и то же с разными именами

На самом деле

У них разные последствия: liveness перезапускает процесс, readiness перестаёт слать ему запросы. В модели для деградировавшей реплики это 520 медленных ответов против 80 — в 6,5 раза, — потому что перезапущенная реплика возвращается в строй и снова деградирует, а выведенная из ротации просто не нагружается.

Утверждение

проба должна проверять всё, от чего зависит сервис

На самом деле

В liveness — только то, что чинится перезапуском этого процесса. Модель: проба, проверяющая общую базу, добавила 200 неуспешных запросов сверх тех 1000, что стоила сама недоступность базы, потому что все реплики перезапустились разом и поднимались уже после её возвращения. С readiness механически складывать все зависимости тоже нельзя: она отвечает на вопрос, способна ли реплика обслужить хоть какой-то трафик, и лишняя проверка внутри неё превращает частичную деградацию в полный отказ.

Утверждение

если реплика отвечает медленно, её надо перезапустить

На самом деле

Перезапуск возвращает процесс в известное состояние ценой потери прогретых кешей и соединений — и деградировавшая реплика вернётся и деградирует снова. В модели это видно прямо: только liveness даёт 520 медленных ответов против 80 при выводе из ротации.

Утверждение

readiness, который всегда отвечает «готов», безвреден

На самом деле

Это худший случай в модели: 1600 медленных ответов против 80. Балансировщик слепнет и продолжает слать нагрузку туда, где её некому обслужить. Readiness обязан отражать способность обслужить запрос, а не факт того, что процесс запущен.

Утверждение

startup-проба — необязательная тонкость

На самом деле

Без неё пороги liveness приходится растягивать под самый медленный запуск — и та же растянутая проверка действует потом в установившемся режиме, где настоящую взаимоблокировку заметят через минуту вместо секунды. Отдельная проба разделяет запуск и работу.

Утверждение

проверки здоровья повышают доступность сами по себе

На самом деле

Они меняют распределение отказов, а не их количество. Недоступность базы в модели стоила 1000 неуспешных запросов при любых пробах: чужую аварию проба не чинит. Что она может — не превратить одну аварию в две.

Утверждение

общая зависимость в пробе делает картину точнее

На самом деле

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

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона модели, а не назначен.

Практика · что напечатает

Три реплики, одна деградирует с десятой секунды, общая база недоступна с двадцатой по тридцатую. Модель прогоняется в четырёх режимах. Печатаются три числа: медленные ответы без проб; медленные ответы при liveness плюс readiness; и разница в числе неуспешных запросов между режимом, где liveness проверяет базу, и режимом, где не проверяет. Что напечатает этот код?
none = simulate("none")
liveness = simulate("liveness")
both = simulate("liveness+readiness")
on_db = simulate("liveness-on-db")
print(none["slow"])
print(both["slow"])
print(on_db["failed"] - liveness["failed"])

Практика · оцените

Одна реплика из трёх деградировала. Во сколько раз readiness сокращает число медленных ответов по сравнению с одним только liveness?
раза

Проверка знаний

Вопрос 1 из 6

Чем liveness отличается от readiness?

Источники и что читать дальше

1 ИСТОЧНИК

  1. Модель этого урока: пробы и каскад перезапусковИсточник. Внешнего первоисточника у этого урока нет: общего стандарта на поведение проб не существует, есть документация отдельных продуктов. Поэтому урок стоит на модели с объявленными допущениями: три реплики, постоянный поток, одна реплика деградирует с десятой секунды, общая база недоступна с двадцатой по тридцатую, проба раз в секунду, порог — три неудачи подряд, перезапуск пять секунд. Всё, что печатает прогон, выводится из этих правил и проверяется чтением скрипта./ru/bench/probes/model.py