Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 19 из 25 ~16 мин +20 XP

Health-check: liveness и readiness

Health check на Go — это два маленьких endpoint, которые оркестратор опрашивает чаще любых метрик: liveness отвечает на вопрос «жив ли процесс», readiness probe — на вопрос «может ли он принимать запросы». Один неверный ответ расставляет судьбу пода: рестарт или вывод из балансировщика, и перепутать их — значит превратить затычку базы в каскад рестартов.

Содержание урока · 8 разделов

Метрики из прошлых уроков рассказывают, что происходит с сервисом, но кто-то должен решать, жив ли он вообще, — и этот кто-то не человек. Оркестратор опрашивает сервер по расписанию: 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

GO
var 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 делают узкой — пинг с коротким дедлайном, а не реальный бизнес-запрос, и следят за долей времени, когда реплики не готовы: график с частыми провалами — повод пересмотреть список проверок, а не хвалить систему за честность.

Здоровье, которое видно на дашборде

GO
var (
        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 честно разделены, перенос сервиса между способами запуска не меняет ни строчки кода: меняется только тот, кто спрашивает.

Liveness против readiness#

Критерийlivenessreadiness
Вопросжив ли процессможет ли обслуживать трафик
Проверяетсясам процесс, ничего внешнегожёсткие зависимости с таймаутами
При провалерестарт подавывод из балансировщика
Частота проваларедкое событие, аномалиярабочее состояние при деплое и затычках
Ошибка дизайнапинг базы внутриагрегированный отчёт обо всём
Таймаутминимальныйменьше таймаута пробы оркестратора

Проверка: последствия провала liveness

Что делает оркестратор, когда liveness-проба перестала отвечать?

Проверка: что должно быть внутри healthz

Что правильный healthz-хендлер проверяет внутри?

База замолчала: два ответа

Прочитайте код и выберите, что он выведет:

// db перестал отвечать на ping

// GET /healthz  -> ?
// GET /readyz   -> ?  (пинг с таймаутом 500ms)

Соберите readyz

Допишите освобождение контекста, пинг и ответ об ошибке.

func readyz(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond) defer () if err := db.(ctx); err != nil { http.(w, "not ready", http.StatusServiceUnavailable) return } w.Write([]byte("ready")) }

Путь readiness-пробы

Расставьте события от запроса оркестратора до действия.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: health-check

Термин

Liveness

нажмите, чтобы перевернуть
Определение

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

1 / 5

Практика — на локальном 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-лог и бизнес-метрики запросов.

Личные заметкиПусто
Хранятся только в вашем браузере (Prometheus · Health-check: liveness и readiness)

Вопросы и ответы по уроку

Чем readiness отличается от liveness на практике?

Последствием провала и списком проверок. Liveness проверяет только собственный процесс и при провале рестартует под; readiness проверяет жёсткие зависимости с таймаутами и при провале выводит под из балансировки. Перепутать их — значит получить шторм рестартов при затычке базы или работу сервиса с мёртвым процессом.

Нужны ли пробы, если Prometheus и так скрейпит сервис?

Да: скрейп говорит «метрики живы», но не управляет трафиком. Оркестратор по пробам решает, куда слать запросы и когда рестартовать, и делает это в реальном времени независимо от скрейпа. Health-check и метрики дополняют друг друга: первый — управление, вторые — наблюдение.

Как часто оркестратор должен опрашивать пробы?

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

Термины урока

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

Следующая ступень учебного пути

WebSocket25 уроков~13 чпрактика25 уроков уже доступны

WebSocket в Go: real-time чаты и уведомления

Курс WebSocket на Go: gorilla/websocket, эхо-сервер, hub-паттерн, комнаты, авторизация соединений, ping/pong, reconnect и масштабирование. 25 уроков.

  • Поднимать WS-сервер и апгрейдить HTTP-соединения
  • Писать readPump/writePump без гонок на каждое соединение
  • Строить hub: регистрацию, рассылку и комнаты
  • Авторизовывать соединения и ограничивать сообщения