Опирается на правила:
R-OBS-HC-1,R-OBS-HC-2,R-OBS-HC-3,R-OBS-HC-X1,R-OBS-HC-X2,R-OBS-HC-X3из Observability Style Guide → раздел 4. Health checks.
Важно знать
- Liveness и readiness — две разные probe с разной семантикой и разными зависимостями.
/health/live— UP пока процесс жив. Зависит только от самого процесса — внешних систем нет./health/ready— UP когда сервис готов принимать трафик: БД подключена, зависимости прогреты.- Custom-checker с TTL-кешем для внешних систем — без кеша Kubernetes probe дудосит зависимости.
/infoотдаётversion,commit,build_time— чтобы знать, что реально крутится в проде.- Health — техническое состояние процесса, не бизнес.
pendingOrders > N→ DOWN — антипаттерн.- Liveness не зависит от DB: при лаге Postgres K8s пересоздаст pod и попадёт в restart-loop.
- Management-сервер — отдельный порт от бизнес-сервера (
R-OBS-CFG-1).
Health-probe — сигнал для Kubernetes и load balancer: пускать трафик в pod или нет. Неверная реализация даёт cascading failure: временный лаг БД → liveness DOWN → K8s убивает pod → новый pod стартует, та же БД лагает → loop, сервис за минуту полностью недоступен.
Liveness vs Readiness — разделение семантик
R-OBS-HC-1: два отдельных endpoint на management-сервере. В Go Actuator-а нет — реализуем руками, что даёт полный контроль.
// internal/platform/metrics/server.go
func StartManagement(addr string, ready Checker) *http.Server {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
mux.HandleFunc("/health/live", liveHandler)
mux.Handle("/health/ready", &readyHandler{check: ready})
mux.HandleFunc("/info", infoHandler(buildVersion, buildCommit, buildTime))
return &http.Server{Addr: addr, Handler: mux}
}
func liveHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"UP"}`))
}
type readyHandler struct {
check Checker
}
func (h *readyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if err := h.check.Check(r.Context()); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
_ = json.NewEncoder(w).Encode(map[string]string{
"status": "DOWN",
"reason": "dependency unavailable",
})
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"UP"}`))
}
| Endpoint | Что проверяет | Реакция Kubernetes |
|---|---|---|
/health/live | Процесс жив, не deadlocked | DOWN → рестарт pod |
/health/ready | БД доступна, зависимости прогреты | DOWN → снять из Service endpoints |
Сценарий: Postgres лагает на 30 секунд из-за VACUUM FULL. Readiness → DOWN, трафик уходит на другие реплики. Liveness остаётся UP — рестарт ничего не изменит, БД та же. Без этого разделения K8s убивает все pods, сервис недоступен полностью.
Kubernetes-манифест:
spec:
containers:
- name: order-service
livenessProbe:
httpGet:
path: /health/live
port: 9090
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 9090
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 2
Порт 9090 — management-порт (R-OBS-CFG-1), не бизнес 8080. Ограничивается сетевой политикой — внешние клиенты не видят /metrics и /health.
Custom-checker с TTL-кешем
R-OBS-HC-2: для каждой критичной внешней системы — отдельный Checker. Без TTL-кеша: K8s проверяет readiness каждые 5 секунд, 10 реплик → 20 ping/s на Postgres только от health-probe.
Интерфейс:
// internal/platform/health/checker.go
type Checker interface {
Check(ctx context.Context) error
}
Реализация для pgxpool.Pool:
// internal/platform/health/postgres.go
type pgChecker struct {
db *pgxpool.Pool
mu sync.Mutex
lastOK time.Time
ttl time.Duration
}
func NewPostgresChecker(db *pgxpool.Pool, ttl time.Duration) Checker {
return &pgChecker{db: db, ttl: ttl}
}
func (c *pgChecker) Check(ctx context.Context) error {
c.mu.Lock()
defer c.mu.Unlock()
if time.Since(c.lastOK) < c.ttl {
return nil
}
if err := c.db.Ping(ctx); err != nil {
return fmt.Errorf("postgres ping: %w", err)
}
c.lastOK = time.Now()
return nil
}
Несколько зависимостей — composite-checker:
// internal/platform/health/composite.go
type compositeChecker struct {
checkers []Checker
}
func NewComposite(checkers ...Checker) Checker {
return &compositeChecker{checkers: checkers}
}
func (c *compositeChecker) Check(ctx context.Context) error {
for _, ch := range c.checkers {
if err := ch.Check(ctx); err != nil {
return err
}
}
return nil
}
Сборка в main.go:
pgChecker := health.NewPostgresChecker(pgPool, 10*time.Second)
redisChecker := health.NewRedisChecker(redisClient, 10*time.Second)
ready := health.NewComposite(pgChecker, redisChecker)
managementSrv := platform.StartManagement(cfg.ManagementAddr, ready)
Для сервиса customer-service с зависимостью на внешний SberPay API — checker делает легкий HEAD /ping с timeout 2s, не платёжную операцию.
/info с версией и коммитом
R-OBS-HC-3: при инциденте первый вопрос — «какая версия сейчас в проде». /info отвечает сразу.
// internal/platform/metrics/server.go
func infoHandler(version, commit, buildTime string) http.HandlerFunc {
body, _ := json.Marshal(map[string]string{
"version": version,
"commit": commit,
"build_time": buildTime,
})
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(body)
}
}
Значения передаются через -ldflags при сборке:
# Makefile
BUILD_VERSION := $(shell git describe --tags --always)
BUILD_COMMIT := $(shell git rev-parse --short HEAD)
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
build:
go build \
-ldflags="-X main.buildVersion=$(BUILD_VERSION) \
-X main.buildCommit=$(BUILD_COMMIT) \
-X main.buildTime=$(BUILD_TIME)" \
./cmd/order-service
В main.go:
var (
buildVersion = "dev"
buildCommit = "unknown"
buildTime = "unknown"
)
Результат — /info отдаёт:
{
"version": "v1.4.2",
"commit": "5380f21",
"build_time": "2026-05-25T22:25:30Z"
}
Сборка management-сервера в main.go
Полный пример с errgroup — бизнес и management работают параллельно, shutdown скоординирован:
// cmd/order-service/main.go
func run(ctx context.Context, cfg Config) error {
pgPool, err := pgxpool.New(ctx, cfg.DatabaseURL)
if err != nil {
return fmt.Errorf("pgxpool: %w", err)
}
defer pgPool.Close()
ready := health.NewComposite(
health.NewPostgresChecker(pgPool, 10*time.Second),
)
router := buildRouter(cfg, pgPool)
businessSrv := &http.Server{
Addr: cfg.Addr,
Handler: otelhttp.NewHandler(router, "order-service"),
}
managementSrv := platform.StartManagement(cfg.ManagementAddr, ready)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
if err := businessSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("business server: %w", err)
}
return nil
})
g.Go(func() error {
if err := managementSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("management server: %w", err)
}
return nil
})
g.Go(func() error {
<-gCtx.Done()
_ = businessSrv.Shutdown(context.Background())
_ = managementSrv.Shutdown(context.Background())
return nil
})
return g.Wait()
}
При SIGTERM: readiness переходит в DOWN (новые запросы не поступают), in-flight запросы дообрабатываются за ShutdownTimeout, процесс завершается.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Business-state в readiness (pendingOrders > N) | R-OBS-HC-X1 | техническое состояние; бизнес → SLO и Prometheus |
| liveness проверяет DB / Redis / внешний API | R-OBS-HC-X2 | только readiness зависит от внешних систем |
Probe вызывает бизнес-операцию (INSERT INTO product_checks) | R-OBS-HC-X3 | lightweight: db.Ping() или TTL-кешированный результат |
| Checker без TTL-кеша при periodSeconds 5 | R-OBS-HC-2 | TTL 5–30 секунд, реализация через sync.Mutex + time.Time |
/health/* на бизнес-порту без сетевой защиты | R-OBS-CFG-1 | отдельный management-порт, сетевая политика |
Нет /info — версия неизвестна в проде | R-OBS-HC-3 | -ldflags с version/commit/build_time |
Куда дальше
- Конфигурация — management-порт, APP_ENV, slog.LevelVar, pprof без auth.
- Context propagation — RequestID и Auth middleware; ctx в горутинах.
- Logging — slog.NewJSONHandler, структурные поля, ошибки атрибутом.
- Metrics — promauto, RED-middleware на chi, бизнес-метрики.
- SLO и алерты — SLO через RED-гистограммы, multi-window burn-rate.
- Tracing — otelhttp, otelpgx, defer span.End(), sampling.