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

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

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

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

Здоровье сервисов healthcheck

Сервис из урока 19 умеет умирать красиво — но между рождением и смертью процесс должен ещё доказать, что жизнеспособен, а оркестратор и балансировщик должны уметь это проверить. Тема урока — healthcheck go kit: самый маленький пакет библиотеки и самый дисциплинирующий. Вопросов у инфраструктуры два, и это разные вопросы: «процесс жив — или его перезапустить?» и «инстансу можно слать трафик — или увести его к соседам?». Первый отвечает liveness, второй — readiness, и readiness go-сервиса — это врата, через которые проходит трафик: из урока 12 ты помнишь регистрацию в Consul, из урока 11 — балансировку, из урока 19 — Deregister перед смертью; сегодня замыкается начало цикла — registrar.Register() сработает ровно в момент готовности, а не раньше. Ты подключишь Handler из пакета kit/healthcheck, соберёшь /readyz с проверкой зависимостей, свяжешь пробы Kubernetes с HTTP-чеком Consul и настроишь честное вычеркивание из ротации. Healthcheck go kit не решает за тебя, что считать здоровьем, — он даёт интерфейс из одного метода; содержание здоровья — твоя работа, и этот урок про неё.

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

Процесс жив и сервис здоров — не одно и то же#

За двумя словами «здоров» прячутся два разных вопроса, и путать их дороже, чем любую ошибку компиляции. Вопрос первый — жизни: процесс способен ответить на сетевой запрос, его внутренние циклы не зависли, горутины не утекли бесконечно. Отвечает liveness, и цена ложного «нет» велика: оркестратор убивает контейнер и поднимает заново — это рестарт с потерей кэшей и соединений из урока 17, а не сбой. Вопрос второй — готовности: инстанс способен обработать полезный вызов, его зависимости доступны, прогрев окончен, а сам он не в дренаже. Отвечает readiness, и цена ложного «нет» мала: балансировщик временно увёл трафик соседям, процесс продолжает жить. Цены объясняют правила. Если readiness «упала» — никто не пострадал, поэтому readiness проверяет всё, от чего зависит ответ: базу из урока 17, флаг прогрева, глубину очередей. Если liveness «упала» — все пять подов сервиса, чья база мигрирует, перезапустятся одновременно, и кратковременный сбой превращается в полный отказ: так readiness, поставленная на место liveness, роняет кластер. Go-процесс может отвечать на HTTP, будучи бесполезным: дедлок не убивает процесс — его должен заметить кто-то снаружи, и этим кем-то будут пробы. Пакет healthcheck go kit — пара десятков строк, но за ними стоит вся эта дисциплина.

Два эндпоинта: /healthz и /readyz#

Канон простой: два HTTP-пути, два разных ответа, один и тот же процесс. /healthz — путь liveness: отвечать должно почти всегда, пока мультиплексор вообще способен принять соединение; проверки зависимостей здесь запрещены по причинам блока выше. /readyz — путь readiness: агрегатор готовности, 200 только когда инстанс действительно готов взять работу; 503 — вежливое «не сейчас», а не ошибка. Имена не священны — Kubernetes-пробам всё равно, какой путь им дали, — но единообразие экономит время всему миру: /healthz и /readyz узнают на ревью без объяснений. Третий путь, /metrics из урока 07, не проба и не должен ею прикидываться: скрейп тяжёлый, и его успешность ничего не говорит о здоровье. Где жить эндпоинтам? В том же mux, что и бизнес-маршруты из урока 04, — отдельный админ-порт оправдан только когда health-эндпоинты надо спрятать от внешнего балансировщика. Аутентификации на пробах не нужно: они отвечают кодом и не раскрывают секретов, а проверка кода ответа — самый дешёвый чек в индустрии.

Пакет kit/healthcheck: один интерфейс и Handler

GO
import (
        "net/http"
        "sync/atomic"

        "github.com/go-kit/kit/healthcheck"
)

// Весь контракт пакета:
// type Service interface { Health() bool }

// Готовность — не домен строк: трогаем platform, не StringService
type readiness struct{ flag *atomic.Bool }

func (r readiness) Health() bool {
        return r.flag.Load() // прогрев завершён, дренаж не идёт
}

// Handler вернёт 200, когда Health() == true, и 503, когда false
mux.Handle("/readyz", healthcheck.NewHandler(readiness{readyFlag}))

Пакет kit/healthcheck — эталон go-kit миниатюры: интерфейс Service из одного метода и Handler, который маппит bool на код ответа: true — 200, false — http.StatusServiceUnavailable (503). Никакого JSON по умолчанию: пробы читают код, а не тело. Канонный нюанс: Health() мы реализуем не на StringService — готовность не бизнес-логика, и плодить методы на доменном типе ради инфраструктуры не стоит; маленькая структура в internal/platform хранит флаг, который композиционный корень из урока 17 переворачивает. Механика та же, что у endpoint'ов: чем меньше пакет знает о мире, тем дольше он живёт.

Liveness: дешёвый пульс и сторожевой пёс#

Базовый liveness бесплатен: если HTTP-мультиплексор ответил 200 на /healthz, значит accept-цикл жив, планировщик Go не умер, сетевой стек жив — уже немало. Но процесс может жить и болеть тихо: горутина-батчер из урока 19 зависла на мьютексе, канал остановки никто не слушает, пул из следующего урока занял все воркеры. Для таких болезней нужен сторожевой пёс: фоновая горутина-пульс каждые N секунд обновляет атомарный таймштамп, а /healthz сравнивает его с порогом — пульс отстал, значит внутренние циклы встали, и проба честно отвечает 503, отправляя процесс на рестарт. Два анти-паттерна, которые роняют сервисы каждый день. Первый: liveness, стучащаяся в базу, — миграция на пять минут станет рестартом всех инстансов. Второй: тяжёлая логика в пробе — сборка отчётов на /healthz периодически превышает timeoutSeconds, kubelet насчитывает failureThreshold и уходит в цикл рестартов. Проба обязана быть локальной, дешёвой и предсказуемой по времени — это не HTTP-запрос, это пульс.

Readiness: агрегатор зависимостей

GO
// /readyz: все зависимости опрошены — с таймаутом, а не навечно
func readyz(repo *Repo) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
                ctx, cancel := context.WithTimeout(r.Context(), time.Second)
                defer cancel()

                if repo.Ping(ctx) != nil { // хранилище из урока 17
                        w.WriteHeader(http.StatusServiceUnavailable)
                        return
                }
                w.WriteHeader(http.StatusOK)
        }
}
mux.HandleFunc("/readyz", readyz(repo))

Три правила готовности. Таймаут у каждого чека: зависшая зависимость не должна висеть пробу дольше секунды — kubelet и Consul тоже измеряют timeoutSeconds и Timeout, и проба, что отвечает дольше, чем им позволено, выглядит как упавшая. Чеки — дешёвые локальные операции вроде ping: полноценные бизнес-запросы на /readyz превращают пробу в нагрузочный тест. Агрегация честная: одна упавшая зависимость — 503 для всего эндпоинта, частичная готовность балансировщику не нужна. Ровно этот путь пропишешь в Check.HTTP при регистрации из урока 12 — и вычеркивание из ротации станет автоматическим.

Пробы Kubernetes: три вида, один процесс#

Kubernetes спрашивает те же два вопроса, но тремя пробами. livenessProbe на /healthz: три неудачи подряд (failureThreshold) с периодом periodSeconds — kubelet убивает контейнер; сначала SIGTERM, так что graceful shutdown из урока 19 успевает отработать, а при зависшем дренаже — SIGKILL. readinessProbe на /readyz: неудача не убивает, а исключает под из Endpoints Service — его перестают видеть клиенты, обращающиеся через сервисный DNS; первый же успех возвращает в ротацию. startupProbe — третий вид, для медленного старта: пока она не ответила успех, liveness и readiness вообще не запускаются; это решение для прогрева кэшей и lazy-соединений, элегантнее, чем initialDelaySeconds с магическим числом секунд. Все пробы берут httpGet с path и port — тот самый mux, что ты собрал в блоке выше. Для gRPC-сервера из урока 13 существует зеркальная механика: стандартный сервис grpc.health.v1.Health и тип пробы grpc — тот же вопрос «жив?» в протоколе вместо HTTP.

Интеграция с балансировщиком: три этажа трафика#

Трафик распределяется на трёх этажах, и health-сигналы у каждого свои. Этаж первый — клиентская балансировка из урока 11: sd.Endpointer держит соединение с Consul через blocking queries, а Consul хранит мнение о здоровье инстанса — HTTP-чек, зарегистрированный в уроке 12 с Interval и DeregisterCriticalServiceAfter. Красный чек — инстанс critical, sd-клиенты вычёркивают его из списка, lb.Balancer перестаёт слать вызовы. Этаж второй — серверные балансировщики вроде nginx и HAProxy: active-чеки на /readyz по расписанию, неудачи исключают upstream из ротации; идея та же, синтаксис другой. Этаж третий — Kubernetes Service: readinessProbe управляет Endpoints, и никакого Consul не нужно, если ты внутри кластера. Общая логика одна: 503 на /readyz — инстанс врежется из списков, процесс живи. Отсюда золотая связка с уроком 19: ручной вывод инстанса из ротации без смерти — это просто флаг готовности. Выключил, дождался, что lb увёл трафик, сделал обслуживание, включил обратно — дренаж без Deregister и без рестарта.

Порядок запуска: готовность раньше регистрации

GO
// фрагмент run() из урока 17, в порядке исполнения
ln, err := net.Listen("tcp", cfg.HTTPAddr) // 1. порт открыт
if err != nil {
        return err
}
go func() { // 2. транспорт отвечает: /healthz уже 200, /readyz пока 503
        if err := srv.Serve(ln); err != http.ErrServerClosed {
                errs <- err
        }
}()

warmup()              // 3. кэши, соединения, миграции
readyFlag.Store(true) // 4. /readyz начинает отвечать 200

if err := registrar.Register(); err != nil { // 5. урок 12: только теперь в каталог
        return err
}

Обещание, данное во вступлении: регистрация срабатывает в момент готовности. Слушатель открывается первым, иначе Consul-чек стучится в закрытую дверь и репортит critical ещё до Register. Прогрев идёт под 503 на /readyz: кто-то опросит — честно услышит «не готов», но в каталоге тебя ещё нет, и трафик не придёт. И только readyFlag.Store(true) открывает врата: Register объявляет инстанс миру, первый же чек Consul видит 200, blocking queries из урока 11 разносят новость балансировщикам. Обратный порядок — Register до прогрева — окно, в которое lb.Retry из урока 11 отрабатывает выстрелы в пустоту: ошибки видны клиентам, метрики обгорели, репутация подмочена.

База данных ушла на короткую миграцию, все пять инстансов сервиса это увидели. Какая проба должна ответить 503 и что произойдёт?

Что вернёт Handler из kit/healthcheck, если Health() вернула false?

Прогрев инстанса занимает две минуты: кэши, соединения, lazy-инициализация. Как настроить пробы, чтобы под не убивали и не вычёркивали зря?

Один сбой, десять секунд, возврат

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

// readinessProbe: periodSeconds 5, failureThreshold 1, successThreshold 1
// В 12:00:00 /readyz ответил 503 (Ping превысил таймаут)
// В 12:00:03 Ping снова отвечает за 5 мс
// В 12:00:05 /readyz снова 200
// Под — один из пяти за балансировщиком Kubernetes Service.

Собери стартовую секвенцию

Восстанови порядок запуска: конструктор слушателя, включение готовности, регистрация в реестре. Три пропуска — по порядку.

// порядок: порт, транспорт, прогрев, готовность, каталог ln, err := net.("tcp", cfg.HTTPAddr) // 1. соединения принимаются if err != nil { return err } go func() { errs <- srv.Serve(ln) }() // /healthz уже 200, /readyz пока 503 warmup() // кэши и соединения готовы ready.(true) // 2. /readyz начал отвечать 200 if err := registrar.(); err != nil { // 3. только теперь в Consul (урок 12) return err }

Полный цикл здоровья инстанса

Расставь этапы от рождения до следующего рождения.

Строки

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

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

Карта проб и чеков#

СигналПуть/механика503/критично, когдаПоследствие отказа
livenessProbe/healthzпроцесс не отвечает вовсе (или пульс отстал)kubelet перезапускает контейнер
readinessProbe/readyzзависимости недоступны, прогрев, ручное обслуживаниепод убран из Endpoints Service
startupProbe/readyz до стартапрогрев ещё идётliveness и readiness приостановлены
Consul HTTP-чек (урок 12)/readyzто же, что readinessинстанс critical: sd-клиенты вычеркнут
Consul TTL-чекagent PassTTL/FailTTLсервис не подтвердил TTL вовремяаналогично, но инициатива у сервиса
Server-side LBactive-чек на /readyzпо расписанию чекаupstream исключён из балансировки
kit/healthcheckлюбой mux-путьHealth() == falseкод 503 — решение принимает проба
/metrics (урок 07)скрейп Prometheusне проба вообщетяжёлый скрейп как чек = ложные сбои
Watchdog-пульсатомарный таймштампвнутренние горутины завислиliveness честно краснеет — рестарт оправдан

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

Термин

Liveness

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

Проба вопроса «жив ли процесс»: 200, пока HTTP-мультиплексор и внутренний пульс живы; зависимостей не проверяет — kubelet при отказе перезапускает контейнер.

1 / 6

Что дальше#

Цикл здоровья замкнулся: сервис из уроков 17-19 умеет рождаться в момент готовности (этот урок), жить под трафиком с оглядкой на пробы и умирать без жертв (урок 19) — а readiness-флаг стал ручным тормозом для обслуживания без рестартов. Но до сих пор каждый вызов отвечал за себя: вопрос пришёл — ответ ушёл, и p99 укладывался в единицы секунд. Реальные сервисы делают и длинную работу: импорты, экспорты, пересчёты. Урок 21-async введёт асинхронность: 202 Accepted, задачу с ID, очередь и worker pool рядом с сервисом — и readiness из этого урока получит нового подопечного: глубину очереди. Карта курса — на странице курса; регистрация и консул-чеки — в уроке 12-discovery; структура файлов, где живёт platform, — в уроке 17-struktura; секвенция остановки, с которой стыкуется старт, — в уроке 19-graceful.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Liveness и readiness — два разных вопроса: «перезапустить ли процесс?» и «пускать ли трафик?»; проверка зависимостей — всегда readiness.
  • kit/healthcheck — интерфейс Service{Health() bool} и Handler: true — 200, false — http.StatusServiceUnavailable.
  • /healthz — дешёвый пульс без зависимостей; /readyz — агрегатор чеков с таймаутами; /metrics — не проба.
  • startupProbe закрывает медленный старт: пока не ответит успех, остальные пробы не работают.
  • Пробы Kubernetes: liveness перезапускает контейнер, readiness двигает Endpoints, Consul-чек кормит sd-клиентов из уроков 11-12.
  • Порядок запуска: слушатель → транспорт → прогрев → ready.Store(true) → registrar.Register() — регистрация в момент готовности.
  • 503 на /readyz — обратимый выключатель: инстанс выходит из ротации и возвращается сам; процесс остаётся жив.
  • Watchdog-пульс превращает внутренние дедлоки в честный 503 на liveness — рестарт становится оправданным.

Домашнее задание

Научи stringsvc отвечать за здоровье. Первое: добавь в internal/platform флаг готовности на atomic.Bool, подключи /healthz через healthcheck.NewHandler и /readyz с Ping'ом хранилища и таймаутом в секунду. Второе: собери стартовую секвенцию из урока — слушатель, Serve, прогрев с логом, ready.Store(true), registrar.Register() — и убедись по логам, что Register срабатывает после первого 200 на /readyz. Третье: добавь watchdog-горутину: тикер раз в две секунды обновляет таймштамп, /healthz отвечает 503, если пульс отстал больше чем на пять; проверь, что умышленно зависший батчер выводит пробу в красное. Четвёртое: пропиши в манифесте три пробы — liveness на /healthz, readiness на /readyz, startupProbe с периодом в секунду; проверь на локальном кластере, что под с неразогретым кэшем не попадает в Endpoints. Пятое: выведи инстанс из ротации флагом через env из урока 18, дожди, пока lb из урока 11 перестанет слать вызовы, и верни обратно — засеки оба перехода по логам.

Личные заметкиПусто
Хранятся только в вашем браузере (go-kit · Здоровье сервисов healthcheck)

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

Что именно проверять на /readyz, если зависимостей много?

Каждую, но дёшево: ping'и и existence-чеки с таймаутом в единицы секунд, а не бизнес-запросы. Агрегатор отвечает 200 только когда всё зелёное; частичная готовность балансировщику бесполезна. Если какая-то зависимость не критична для ответов (например, аналитика), её чек выведи из агрегации и следи по метрикам из урока 07 — не стоит вычёркивать инстанс из ротации из-за необязательного компонента.

Нужна ли аутентификация на health-эндпоинтах и что им можно раскрывать?

Аутентификация не нужна: проба отвечает кодом и обязана быть доступна оркестратору и балансировщику. Раскрывать версию, имена зависимостей и адреса не стоит — это карта для атаки; детальную диагностику отдай /debug/pprof на внутреннем порту или метрикам из урока 07, закрытым периметром сети. Здоровье — бинарный факт, и чем меньше проба говорит, тем лучше.

Consul-чек и readinessProbe дублируют друг друга — что выбрать?

Это один вопрос на разных этажах трафика: readinessProbe кормит Kubernetes Service, Consul-чек — sd-клиентов и внешние балансировщики. Если сервис живёт внутри кластера и наружу смотрит только через Service — достаточно проб; если клиенты ходят через Consul (как в курсе с уроков 11-12) — нужен и HTTP-чек на тот же /readyz. Один и тот же флаг готовности кормит оба механизма — и они не разъедутся.

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

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

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

gRPC25 уроков~14 чпродвинутый25 уроков уже доступны

gRPC-Go: высокопроизводительный RPC

Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.

  • Писать .proto-схемы и генерировать код
  • Реализовывать unary и streaming сервисы
  • Управлять метаданными, дедлайнами и отменами
  • Строить interceptors для логов и аутентификации