Метрики из прошлых уроков рассказывают, что происходит с сервисом, но кто-то должен решать, жив ли он вообще, — и этот кто-то не человек. Оркестратор опрашивает сервер по расписанию: docker healthcheck, systemd, Kubernetes-пробы — названия разные, механика одна: два endpoint, два разных вопроса и два разных последствия неудачи. Первый вопрос — liveness: процесс отвечает и не завис? Неудача означает рестарт. Второй — readiness: готов ли этот экземпляр брать трафик? Неудача означает вывод из балансировщика при живом процессе. Health check go-сервиса занимает десяток строк net/http, но именно здесь чаще всего ломают доступность сервиса руками: вешают проверку базы на liveness и получают шторм рестартов при затычке базы, ведь рестарт не лечит чужой сервис. В этом уроке соберём оба endpoint, разложим, что в них проверять, а что категорически нет, и связем здоровье с метриками так, чтобы график показывал то же, что видит оркестратор.
Health check на Go: liveness и readiness probe#
Два endpoint живут в одном сервисе и никогда не дублируют друг друга. Liveness — про сам процесс: если он не отвечает, значит deadlock, зависшая горутина или паника в цикле событий, и лечится это только перезапуском. Readiness probe — про способность обслуживать: пул соединений к базе установлен, кэш прогрет, миграции накатаны, потребитель очереди подключён. Ответственность распределяется так: при провале liveness оркестратор убивает и создаёт под заново, при провале readiness — просто перестаёт слать трафик и ждёт восстановления. Из этой логики вытекает первое правило: провал liveness должен быть редким событием, а провал readiness — рабочим состоянием, которое случается при каждом деплое и каждой затычке зависимости. Второе правило: оба endpoint обязаны отвечать быстро — оркестратор не различает «думает» и «умер», и таймаут пробы на его стороне короче, чем вам кажется. Третье: пути стабильны — healthz и readyz — это интерфейс для всей инфраструктуры, и менять его по прихоти одного сервиса нельзя.
Два endpoint на net/http
GOvar ready atomic.Bool
func healthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
func readyz(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
defer cancel()
if err := db.PingContext(ctx); err != nil {
http.Error(w, "db not ready", http.StatusServiceUnavailable)
return
}
ready.Store(true)
w.Write([]byte("ready"))
}healthz не проверяет ничего: процесс дотянулся до хендлера — значит, жив, и это весь контракт liveness. readyz проверяет одну жёсткую зависимость с контекстом на пятьсот миллисекунд: таймаут пробы обязан быть меньше таймаута оркестратора, иначе ответы придут после решения. Ошибка зависимости превращается в 503, и оркестратор выводит под из балансировки, оставляя процесс жить.
Readiness probe и зависимости: что проверять#
Список проверок readiness составляется по одному критерию: без этой зависимости сервис обязан отказывать клиентам, а не деградировать. Пинг базы с таймаутом — классика; подключение к очереди, если без неё сообщения теряются; прогретый кэш, если холодный старт даёт лавину промахов в зависимости; завершённая миграция, если старая версия кода не понимает новую схему. Каждая проверка получает собственный дедлайн, суммарный ответ укладывается в таймаут пробы, и никакая проверка не должна падать на минуту — пятьсот миллисекунд с отказом лучше пяти секунд с успехом: оркестратор не ждёт. Противоположный список — то, что не проверяется никогда: тяжёлые внешние API, медленные партнёрские шлюзы, всё, что поддаётся деградации через таймауты и ретраи в бизнес-коде. Readiness probe — бинарный ответ на жёсткую зависимость, а не агрегированный отчёт о здоровье инфраструктуры. Практическая проверка списка: если сервис может частично работать при провале проверки — проверка лишняя и её место в метриках, а не в readiness.
Ловушка каскада: когда readiness делает хуже#
Спорное место уроков про пробы: readiness может снизить доступность вместо её повышения. Сценарий: база замедлилась на две минуты, все реплики сервиса честно вернули 503 по пингу, балансировщик вывел весь трафик, и пользователи увидели outage даже на запросах, не трогающих базу. Между тем сервис мог бы отвечать из кэша, отдавать читаемые данные с деградацией и обрабатывать быстрый трафик. Правило границы: readiness false — это честное «я не могу обслуживать ни один запрос», а не «одна из моих зависимостей грустит». Хронически медленная база лечится таймаутами, пулом соединений и ретраями в коде, а не выкатыванием всего сервиса из ротации. Отсюда рабочая практика: проверку зависимости в readiness делают узкой — пинг с коротким дедлайном, а не реальный бизнес-запрос, и следят за долей времени, когда реплики не готовы: график с частыми провалами — повод пересмотреть список проверок, а не хвалить систему за честность.
Здоровье, которое видно на дашборде
GOvar (
readinessGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "app_ready",
Help: "Готовность сервиса: 1 готов, 0 нет",
})
readinessChecks = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "app_readiness_checks_total", Help: "Проверки readiness"},
[]string{"result"},
)
)
func readyz(w http.ResponseWriter, r *http.Request) {
if err := db.PingContext(r.Context()); err != nil {
readinessGauge.Set(0)
readinessChecks.WithLabelValues("fail").Inc()
http.Error(w, "not ready", http.StatusServiceUnavailable)
return
}
readinessGauge.Set(1)
readinessChecks.WithLabelValues("ok").Inc()
w.Write([]byte("ready"))
}Тот же готовый хендлер получает метрики: датчик app_ready отражает текущее состояние для панели, счётчик с лейблом result добавляет оборот проверок. Связка делает здоровье наблюдаемым: оркестратор действует по HTTP-ответу, а человек видит на графике в Grafana из тринадцатого урока, когда и как часто сервис выпадал из ротации. Счётчик проверок заодно измеряет частоту опроса без чтения конфигов оркестратора.
Отдельная фаза жизни — старт. Пока сервис прогревается — устанавливает пул соединений, читает конфиг, прогревает кэш, — readiness честно отвечает отказом, и это правильно: трафик на холодный сервис не нужен. Проблема появляется, когда прогрев долгий: оркестраторы с коротким окном начальных проб успевают объявить под мёртвым, и в Kubernetes для этого придумали startupProbe — отдельную пробу, которая даёт процессу стартовать без давления readiness. Своими руками то же самое решает атомарный флаг: ready стоит false при старте и поднимается после прогрева, а хендлер readyz сначала проверяет флаг, потом зависимости. Второй момент фазы — выключение: при получении сигнала завершения сервис перестаёт быть готовым до дренажа соединений, и пробы оркестратора перестают слать трафик, пока graceful shutdown дообслуживает живые запросы. Так health-check оборачивается вокруг обеих границ жизни процесса — и первой, и последней.
Пробы без Kubernetes: docker и systemd#
Механика health check не принадлежит Kubernetes, и держать оба endpoint стоит даже там, где оркестратора нет. В docker-compose сервис healthcheck стучится curl-ом в свой healthz или readyz по расписанию и помечает контейнер unhealthy — статусы видны в docker ps и docker inspect, а для зависимостей есть условие depends_on с условием healthy, которое задержит старт сервиса до готовности базы. В systemd за живость отвечает Restart=always вместе с WatchdogSec и вызовом sd_notify: процесс сам сообщает о живости, а супервизор перезапускает при молчании. За балансировку без Kubernetes отвечает nginx: upstream с check-запросом на readyz выводит мёртвую реплику из ротации руками конфига или плагином. Смысл тот же на всех уровнях: два вопроса — жив и готов — и два разных действия. Пока endpoint честно разделены, перенос сервиса между способами запуска не меняет ни строчки кода: меняется только тот, кто спрашивает.
Разработчик ставит пинг базы в healthz, потому что «health — это когда всё работает». Дальше сценарий заученный: база затыкается, liveness падает у всех реплик разом, оркестратор начинает рестартовать поды, каждый рестарт создаёт лавину новых соединений на больную базу, и всё это в момент, когда сервису нужен покой. Рестарт не лечит чужой сервис — liveness отвечает только за собственный процесс. Пинг базы живёт в readiness или вообще в метрике.
Пробы не должны проходить аутентификацию, писать в access-лог и попадать в бизнес-метрики запросов: иначе график RPS раз в тридцать секунд подпрыгивает, а лог пестрит двухсотыми без тела. В chi пробы монтируются до цепочки middleware отдельной группой, аналогично в gin — до глобального логгера; как раскладывать middleware по порядку, разобрано в уроке про порядок middleware chi. Health check go-сервиса — служебный интерфейс: чем тише он в телеметрии, тем чище читаются графики.
Liveness против readiness#
| Критерий | liveness | readiness |
|---|---|---|
| Вопрос | жив ли процесс | может ли обслуживать трафик |
| Проверяется | сам процесс, ничего внешнего | жёсткие зависимости с таймаутами |
| При провале | рестарт пода | вывод из балансировщика |
| Частота провала | редкое событие, аномалия | рабочее состояние при деплое и затычках |
| Ошибка дизайна | пинг базы внутри | агрегированный отчёт обо всём |
| Таймаут | минимальный | меньше таймаута пробы оркестратора |
Проверка: последствия провала liveness
Что делает оркестратор, когда liveness-проба перестала отвечать?
Проверка: что должно быть внутри healthz
Что правильный healthz-хендлер проверяет внутри?
База замолчала: два ответа
Прочитайте код и выберите, что он выведет:
// db перестал отвечать на ping
// GET /healthz -> ?
// GET /readyz -> ? (пинг с таймаутом 500ms)Соберите readyz
Допишите освобождение контекста, пинг и ответ об ошибке.
Путь readiness-пробы
Расставьте события от запроса оркестратора до действия.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: health-check
Практика — на локальном Go: песочница сайта не исполняет net/http с настоящей базой, поэтому поднимите сервис из урока про первый /metrics endpoint, добавьте оба endpoint и уроните зависимость специально: посмотрите, что healthz продолжает отвечать, а readyz отдаёт 503, и что панель app_ready в Grafana из тринадцатого урока рисует окно неготовности. Алерт на app_ready == 0 в течение минуты собирается по шаблону урока про алерты. Следующий урок про стресс-тест нагрузит сервис, у которого уже есть и метрики, и пробы: под нагрузкой станет видно, как readiness и латентность ведут себя вместе. В итоговом проекте курса оба endpoint встанут в полный набор мониторинга, а весь маршрут — на странице Prometheus. Для деплойной стороны — сборка в Docker и health-чеки — есть урок про деплой Gin.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Liveness спрашивает о живости процесса и при провале рестартует под; проверять чужие сервисы в нём нельзя.
- Readiness probe спрашивает о готовности обслуживать и выводит под из балансировщика без рестарта.
- Каждая проверка зависимости идёт с дедлайном, а суммарный ответ укладывается в таймаут пробы.
- Медленная зависимость — таймауты и ретраи в коде, а не выкатывание реплик из ротации.
- app_ready и счётчик проверок делают здоровье видимым на дашборде и завязывают на него алерт.
- Пробы монтируются вне аутентификации и access-лога: служебный интерфейс должен быть тихим.
- Readiness закрывает обе границы жизни: false при старте до прогрева и false при graceful shutdown.
Домашнее задание
Добавьте в учебный сервис healthz и readyz на net/http: первый без проверок, второй с пингом зависимости под контекстом на пятьсот миллисекунд. Поставьте атомарный флаг готовности, который поднимается после прогрева, и продемонстрируйте оба ответа: живой сервис с больной базой. Заведите датчик app_ready и счётчик readinessChecks{result}, соберите панель с окнами неготовности и напишите алерт по app_ready. Проверьте, что пробы не попадают в access-лог и бизнес-метрики запросов.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем readiness отличается от liveness на практике?
Последствием провала и списком проверок. Liveness проверяет только собственный процесс и при провале рестартует под; readiness проверяет жёсткие зависимости с таймаутами и при провале выводит под из балансировки. Перепутать их — значит получить шторм рестартов при затычке базы или работу сервиса с мёртвым процессом.
Нужны ли пробы, если Prometheus и так скрейпит сервис?
Да: скрейп говорит «метрики живы», но не управляет трафиком. Оркестратор по пробам решает, куда слать запросы и когда рестартовать, и делает это в реальном времени независимо от скрейпа. Health-check и метрики дополняют друг друга: первый — управление, вторые — наблюдение.
Как часто оркестратор должен опрашивать пробы?
Типичный интервал — раз в несколько секунд до минуты, и его выбирает конфигурация оркестратора, а не ваш код. Ваша обязанность — отвечать быстрее: таймаут проверки внутри хендлера должен быть заметно меньше таймаута пробы, иначе ответы опаздывают и выглядят как провал.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
WebSocket в Go: real-time чаты и уведомления
Курс WebSocket на Go: gorilla/websocket, эхо-сервер, hub-паттерн, комнаты, авторизация соединений, ping/pong, reconnect и масштабирование. 25 уроков.
- Поднимать WS-сервер и апгрейдить HTTP-соединения
- Писать readPump/writePump без гонок на каждое соединение
- Строить hub: регистрацию, рассылку и комнаты
- Авторизовывать соединения и ограничивать сообщения