---
title: "Здоровье сервисов healthcheck"
description: "Liveness и readiness go-сервиса: пакет kit/healthcheck с Health() bool, пути /healthz и /readyz, пробы Kubernetes, HTTP-чек Consul и ротация балансировщика."
course: "go-kit: микросервисный тулкит"
url: "https://golangmaster.ru/courses/go-kit/lessons/20-healthcheck/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

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

*Курс «go-kit: микросервисный тулкит», урок 20 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-kit/lessons/20-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 не решает за тебя, что считать здоровьем, — он даёт интерфейс из одного метода; содержание здоровья — твоя работа, и этот урок про неё.

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

За двумя словами «здоров» прячутся два разных вопроса, и путать их дороже, чем любую ошибку компиляции. Вопрос первый — жизни: процесс способен ответить на сетевой запрос, его внутренние циклы не зависли, горутины не утекли бесконечно. Отвечает 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 и что произойдёт?

- liveness на /healthz: kubelet перезапустит все пять подов одновременно
- ✅ readiness на /readyz: поды выйдут из ротации, перезапуска не будет, при возврате базы вернутся сами
- никакая: проба не должна знать о зависимостях
- обе пробы: liveness для базы, readiness для процесса

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

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

- 200 с телом {"status": "unhealthy"}
- ✅ 503 — http.StatusServiceUnavailable, без тела
- 500 Internal Server Error
- Handler запишет ошибку в лог и не ответит

Весь пакет — интерфейс и маппинг bool на код: true — 200, false — 503. Тела нет: пробы Kubernetes и Consul-чеки читают статус-код, и это самый дешёвый контракт. Если хочешь JSON с деталями — оберни Handler своим http.Handler, но пробы должны по-прежнему принимать решение по коду, а не по телу.

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

- initialDelaySeconds: 120 на обеих пробах — магическое число, но работает
- ✅ startupProbe: пока не ответит успех, liveness и readiness не стартуют
- livenessProbe с failureThreshold: 1000 — пусть считает медленно
- отключить пробы и полагаться на DeregisterCriticalServiceAfter

initialDelaySeconds — гистерезис по часам: чуть медленнее прогрев — под убит, чуть быстрее — зря ждали. startupProbe решает по факту: прогрев закончился на 90-й секунде — пробы включаются на 90-й секунде, и никто ничего не заметил. failureThreshold в тысячи — маскировка: с порогом в тысячи попыток настоящий завис никогда не отловишь.

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

```go
// 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.
```

**Что выведет программа?**

- Под удалят из Endpoints навсегда: проба была красной — надо рестартовать
- ✅ На пять секунд под выпал из Endpoints, после успеха вернулся: клиенты почти ничего не заметили, ретраи из урока 11 закрыли дыру
- Трафик продолжил идти: Kubernetes прощает единичные сбои
- kubelet перезапустил контейнер по readinessProbe

Readiness — обратимый выключатель: единица провала — из Endpoints, первый успех — назад. Окно в один период — цена периодичности, и это нормально: клиенты и lb.Retry ретраят, а соседи принимают нагрузку. Вот почему сбой готовности — не инцидент, а деградация; инцидент — это когда 503 сыпятся минутами, и это видно на p99 и глубине очередей из урока 07.

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

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

```go
// порядок: порт, транспорт, прогрев, готовность, каталог
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
}
```

Подсказка: Первый — из пакета net, открывает порт; второй — метод atomic.Bool, переворачивающий флаг, который читает kit healthcheck; третий — парный к Deregister метод из урока 12.

Правильные ответы в пропусках: `Listen`, `Store`, `Register`

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

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

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

1. `net.Listen: порт открыт, соединения принимаются`
2. `go srv.Serve(mux): /healthz отвечает 200, /readyz честно отвечает 503`
3. `startupProbe ждёт успеха readiness: пробы liveness ещё не активны`
4. `прогрев окончен: ready.Store(true), /readyz стал отвечать 200`
5. `registrar.Register() из урока 12: инстанс объявлен в каталоге Consul`
6. `lb.Balancer из урока 11 узнал через blocking queries: трафик пошёл`
7. `зависимость недоступна: /readyz 503 — инстанс вычеркнут из ротации, процесс жив`
8. `зависимость вернулась: /readyz 200 — инстанс вернулся в ротацию сам`
9. `SIGTERM: Deregister и дренаж из урока 19 — цикл замкнулся до следующего рождения`

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

| Сигнал | Путь/механика | 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 без зависимостей, readiness без рестартов.** Два правила, которые спасают ночные дежурства. 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 зелёным.

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

- **Liveness** — Проба вопроса «жив ли процесс»: 200, пока HTTP-мультиплексор и внутренний пульс живы; зависимостей не проверяет — kubelet при отказе перезапускает контейнер.
- **Readiness** — Проба вопроса «готов ли инстанс брать работу»: зависимости, прогрев, дренаж; 503 выводит из ротации без смерти процесса, возврат 200 — автоматический.
- **kit/healthcheck** — Пакет из интерфейса Service{Health() bool} и NewHandler: true — 200, false — 503; канон — Health() реализуется в platform, не на StringService.
- **startupProbe** — Проба медленного старта: пока не ответит успех, liveness и readiness не запускаются; элегантная замена магическому initialDelaySeconds.
- **DeregisterCriticalServiceAfter** — Страховка из урока 12: инстанс, чей чек красный дольше срока, вычёркивается из каталога; работает и против kill -9 из урока 19.
- **Порядок запуска** — net.Listen → srv.Serve → прогрев → ready.Store(true) → registrar.Register(): регистрация только в момент готовности, иначе трафик приходит в неготовый инстанс.

### Что дальше

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

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

- 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. Один и тот же флаг готовности кормит оба механизма — и они не разъедутся.
