Содержание урока · 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
GOimport (
"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.Собери стартовую секвенцию
Восстанови порядок запуска: конструктор слушателя, включение готовности, регистрация в реестре. Три пропуска — по порядку.
Полный цикл здоровья инстанса
Расставь этапы от рождения до следующего рождения.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карта проб и чеков#
| Сигнал | Путь/механика | 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 LB | active-чек на /readyz | по расписанию чека | upstream исключён из балансировки |
| kit/healthcheck | любой mux-путь | Health() == false | код 503 — решение принимает проба |
| /metrics (урок 07) | скрейп Prometheus | не проба вообще | тяжёлый скрейп как чек = ложные сбои |
| Watchdog-пульс | атомарный таймштамп | внутренние горутины зависли | liveness честно краснеет — рестарт оправдан |
Два правила, которые спасают ночные дежурства. Liveness отвечает только за процесс: мультиплексор жив и внутренний пульс бьётся — 200; база, кэш и брокер в этом ответе не участвуют, иначе кратковременный сбой зависимости превращается в рестарт всех инстансов одновременно. Readiness не используется для выключения процесса: 503 выводит из ротации, но ничего не останавливает — для корректной остановки есть SIGTERM и секвенция из урока 19. Смешаешь направления — получишь либо вечный рестарт, либо зомби в ротации.
readyFlag.Store(true), Register, первый 200 на /readyz, каждое 503 — всё это по строке в лог из урока 06 с парой during/readiness или during/startup. По этим строкам видно, сколько секунд инстанс грелся, как быстро попал в ротацию и сколько раз выпадал из неё за неделю. Без таких строк «клиенты жалуются на таймауты по ночам» остаётся гаданием: readiness-дрожание — первая причина флапающего трафика, и оно невидимо без лога переходов.
Пробы живут в сетевом мире, а в песочнице курса сервисы не исполняются, поэтому интерактивы построены на чтении кода. Локально всё проверяется за минуту: подними stringsvc из урока 17, попроси curl'ом /healthz и /readyz, останови хранилище и посмотри, как /readyz краснеет, а процесс остаётся живым и /healthz зелёным.
Термины урока
Что дальше#
Цикл здоровья замкнулся: сервис из уроков 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 перестанет слать вызовы, и верни обратно — засеки оба перехода по логам.
Личные заметкиПусто
Вопросы и ответы по уроку
Что именно проверять на /readyz, если зависимостей много?
Каждую, но дёшево: ping'и и existence-чеки с таймаутом в единицы секунд, а не бизнес-запросы. Агрегатор отвечает 200 только когда всё зелёное; частичная готовность балансировщику бесполезна. Если какая-то зависимость не критична для ответов (например, аналитика), её чек выведи из агрегации и следи по метрикам из урока 07 — не стоит вычёркивать инстанс из ротации из-за необязательного компонента.
Нужна ли аутентификация на health-эндпоинтах и что им можно раскрывать?
Аутентификация не нужна: проба отвечает кодом и обязана быть доступна оркестратору и балансировщику. Раскрывать версию, имена зависимостей и адреса не стоит — это карта для атаки; детальную диагностику отдай /debug/pprof на внутреннем порту или метрикам из урока 07, закрытым периметром сети. Здоровье — бинарный факт, и чем меньше проба говорит, тем лучше.
Consul-чек и readinessProbe дублируют друг друга — что выбрать?
Это один вопрос на разных этажах трафика: readinessProbe кормит Kubernetes Service, Consul-чек — sd-клиентов и внешние балансировщики. Если сервис живёт внутри кластера и наружу смотрит только через Service — достаточно проб; если клиенты ходят через Consul (как в курсе с уроков 11-12) — нужен и HTTP-чек на тот же /readyz. Один и тот же флаг готовности кормит оба механизма — и они не разъедутся.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
gRPC-Go: высокопроизводительный RPC
Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.
- Писать .proto-схемы и генерировать код
- Реализовывать unary и streaming сервисы
- Управлять метаданными, дедлайнами и отменами
- Строить interceptors для логов и аутентификации