6. Health checks
Пульс системы: Зачем сервису рассказывать о своих болезнях
Мы последовательно спроектировали настоящую инженерную крепость: наш Go-микросервис защищен от сетевых аномалий внешнего мира с помощью 1. Circuit breaker и 3. Timeout, а внутренние ресурсы надежно изолированы переборками 4. Bulkhead и сбросом лишней нагрузки 5. Load shedding.
Но любая неприступная крепость может пасть из-за скрытой внутренней катастрофы.
Что произойдет, если горутина внутри сервиса захватила мьютекс и зависла в бесконечной взаимной блокировке (Deadlock)? Или если пул соединений с базой данных исчерпался из-за незакрытых транзакций, и каждый входящий запрос обречен на вечное ожидание свободного сокета? С точки зрения операционной системы процесс контейнера абсолютно жив, потребление CPU близко к нулю, системные вызовы не падают. Но функционально сервис превратился в зомби — он не способен обработать ни одного полезного байта.
Если инфраструктурный оркестратор (Kubernetes, Nomad или внешний L7-балансировщик) не узнает об этом внутреннем параличе, он продолжит направлять тысячи пользовательских запросов в этот мертвый узел.
Паттерн Health Checks (Проверки жизнедеятельности / Самодиагностика) — это стандартизированный протокол внутренней телеметрии, с помощью которого микросервис непрерывно рапортует инфраструктуре о своем физическом и функциональном состоянии. В этой статье мы разберем трехуровневую модель проб, объясним, почему смешивание диагностических эндпоинтов с бизнес-трафиком убивает кластер, и выясним, как правильно организовать проверки в рантайме Go.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph Orchestrator["Оркестратор (Kubernetes Kubelet / Балансировщик)"]
direction TB
Probe["Регулярный опрос эндпоинтов"]:::process
end
subgraph ServicePod["Pod микросервиса на Go"]
direction TB
Startup["1. Startup Probe (:8081/healthz)<br/>Прогрев кэшей и схемы"]
Liveness["2. Liveness Probe (:8081/healthz)<br/>Проверка отсутствия дедлоков"]
Readiness["3. Readiness Probe (:8081/readyz)<br/>atomic.Bool: Готовность к трафику"]
end
Probe --> Startup
Startup -->|"Успех"| Liveness
Startup -->|"Успех"| Readiness
Liveness -->|"Провал"| Kill["SIGKILL: Перезапуск контейнера"]
Readiness -->|"Провал"| Remove["Исключение из EndpointSlice<br/>(Трафик не идет)"]
Readiness -->|"Успех"| Serve["Включение в ротацию балансировки"]
Три кита Health Checks в мире Kubernetes
В современных облачных архитектурах де-факто стандартом стала трехуровневая модель проб, реализованная в Kubernetes (см. 2. Kubernetes. Основы). Оркестратор регулярно задает сервису три принципиально разных вопроса, и на каждый из них должен следовать свой строго изолированный ответ:
1. Liveness Probe (Проверка живости)
- Вопрос: «Ты вообще жив, способен выполнять инструкции процессора или наглухо завис в дедлоке?»
- Действие при сбое: Kubelet безжалостно убивает контейнер (посылая сигналы
SIGTERM, а затемSIGKILL) и перезапускает новый экземпляр пода. - Имплементация: Предельно легковесный эндпоинт. Он не должен проверять базы данных или внешние сетевые сервисы! Его единственная задача — вернуть
200 OK, подтверждая, что рантайм Go активен, а планировщик горутин жив.
2. Readiness Probe (Проверка готовности)
- Вопрос: «Ты готов прямо в эту секунду принимать и качественно обслуживать клиентский трафик?»
- Действие при сбое: Оркестратор не убивает процесс, но моментально исключает под из балансировки (удаляет его IP из сетевых таблиц Service и EndpointSlice). Трафик перестает поступать в узел до тех пор, пока проверка снова не вернет успех.
- Имплементация: Проверяет локальное состояние готовности. Если сервис временно потерял связь с базой данных, прогревает локальные кэши или сработал Load Shedder — проверка возвращает статус
503 Service Unavailable. Сервис получает время на восстановление, не подвергаясь разрушительным перезапускам.
3. Startup Probe (Проверка запуска)
- Вопрос: «Ты уже завершил начальную инициализацию или все еще читаешь конфигурации?»
- Зачем нужна: Крупные сервисы на Go при старте могут вычитывать гигабайты справочников в память, компилировать регулярные выражения или прогревать пулы соединений в течение 30–60 секунд. Если бы оркестратор сразу опрашивал Liveness Probe с коротким таймаутом, он бы циклически убивал сервис еще до завершения его старта (CrashLoopBackOff). Startup Probe блокирует Liveness и Readiness до своего первого успешного срабатывания.
Архитектура на Go: Изоляция портов
Анатомия катастрофы (Mechanical Sympathy):
Представьте, что ваш сервис захлестнула непредвиденная волна входящих запросов. Лимиты горутин исчерпаны, семафоры переборок заполнены, а очередь входящих TCP-соединений в ядре Linux (somaxconn и backlog сокета) переполнена под завязку.
Kubelet каждые 10 секунд отправляет стандартный проверочный HTTP-запрос на http://pod-ip:8080/health. Но сетевой стек ядра Linux вынужден отбрасывать (drop) пакет TCP SYN, потому что очередь somaxconn сокета 8080 переполнена!
Kubelet ловит таймаут соединения, решает, что сервис умер, и... принудительно убивает живой, активно работающий под! Нагрузка с убитого пода мгновенно перераспределяется на оставшиеся поды кластера, вызывая цепную реакцию массовых перезапусков (Restart Storm). Кластер складывается за считанные минуты.
Архитектурный стандарт Senior-инженера:
Инфраструктурные маршруты (Health Checks, метрики Prometheus, отладочный профилировщик pprof) обязаны обслуживаться на выделенном изолированном сетевом порту (например, :8081), защищенном от внешнего интернет-трафика.
package main
import (
"context"
"log"
"net/http"
"sync/atomic"
"time"
)
// Состояние готовности сервиса: lock-free флаг в памяти
var isReady atomic.Bool
func main() {
// 1. ИНФРАСТРУКТУРНЫЙ СЕРВЕР (Порт :8081)
// Доступен только Kubelet и сборщику метрик внутри кластера
infraMux := http.NewServeMux()
// Liveness: проверяет только живучесть самого процесса Go
infraMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("200 OK: Process alive"))
})
// Readiness: зависит от внутренней готовности компонентов
infraMux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
if isReady.Load() {
w.WriteHeader(http.StatusOK)
w.Write([]byte("200 OK: Ready to serve traffic"))
return
}
// 503 сообщает Kubelet: процесс жив, но трафик слать нельзя
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("503: Initializing or degraded"))
})
infraServer := &http.Server{
Addr: ":8081", // ВАЖНО: изолированный сокет!
Handler: infraMux,
ReadTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
}
go func() {
log.Println("Инфраструктурный сервер запущен на :8081")
if err := infraServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Ошибка инфраструктурного сервера: %v", err)
}
}()
// 2. БИЗНЕС-СЕРВЕР (Порт :8080)
// Принимает пользовательские запросы
businessMux := http.NewServeMux()
businessMux.HandleFunc("/api/v1/orders", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Order processed"))
})
businessServer := &http.Server{
Addr: ":8080",
Handler: businessMux,
}
// Фоновый процесс инициализации и проверки зависимостей
go func() {
log.Println("Инициализация зависимостей: подключение к БД и прогрев кэша...")
time.Sleep(2 * time.Second) // Имитация прогрева
isReady.Store(true) // Включаем прием трафика
log.Println("Сервис полностью готов к приему трафика")
}()
log.Println("Бизнес-сервер слушает на :8080")
if err := businessServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Ошибка бизнес-сервера: %v", err)
}
}
Архитектурные ловушки (Deep Health Checks)
Представьте сценарий: между кластером микросервисов и базой данных произошел минутный сбой сетевого оборудования (Network Partition).
Что произойдет, если база проверяется внутри Liveness Probe:
- Все 100 подов вашего микросервиса одновременно не могут достучаться до БД.
- Liveness Probe на всех 100 подах синхронно возвращает ошибку.
- Kubelet на всех серверах начинает одновременно убивать и перезапускать все поды сервиса (массовый CrashLoopBackOff).
- Когда сеть восстанавливается, базе данных приходится выдерживать одновременный шквал от 100 поднимающихся подов, пытающихся открыть соединения через
database/sqlпул, провести TLS-хендшейки и аутентификацию (Thundering Herd). СУБД падает уже под тяжестью самих поднимающихся сервисов!
Что произойдет, если проверка зависимостей вынесена в Readiness Probe:
- Все 100 подов проваливают Readiness.
- Оркестратор плавно исключает их из балансировки: пользователи временно видят ошибку
503. - Но процессы приложений остаются в живых! Горутины не перезапускаются, открытые TCP-сокеты к БД в пуле
database/sqlсохраняются или спокойно восстанавливаются в фоновом режиме. - Как только сетевой сбой устранен, Readiness мгновенно возвращает
200 OK, Kubernetes возвращает поды в ротацию, и система оживает за доли секунды без единого рестарта.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph BadLiveness["Антипаттерн: Проверка БД в Liveness"]
NetFail1["Сбой сети к БД"]:::data --> LFail["Liveness провален"]
LFail --> KillAll["Одновременный рестарт всех 100 подов"]
KillAll --> CrashLoop["CrashLoopBackOff + Перегрузка БД"]
end
subgraph GoodReadiness["Правильный подход: Проверка БД в Readiness"]
NetFail2["Сбой сети к БД"]:::data --> RFail["Readiness провален"]
RFail --> Delist["Временное исключение из балансировки"]
Delist --> KeepAlive["Процессы живы, пулы коннектов целы"]
KeepAlive --> Restore["Мгновенный возврат в ротацию при починке сети"]
end
Золотые правила проектирования проверок:
- Liveness: Строго локальная, внутренняя проверка жизнеспособности процесса. Никаких внешних сетевых вызовов.
- Readiness: Проверяет внешние зависимости, НО исключительно через фоновые воркеры с кэшированием статуса в атомарных переменных
atomic.Bool. Если при каждом входящем хелсчеке от Kubelet (10 раз в секунду на 100 подах) выполнять реальный SQL-запросSELECT 1, вы создадите бесполезную нагрузку в 1000 RPS на базу данных исключительно ради работы проверок!
Итог
- Разделение зон ответственности: Liveness отвечает за живучесть процесса (защита от дедлоков), Readiness — за готовность принимать трафик (проверка зависимостей), Startup — за безопасный прогрев сервиса при старте.
- Строгая изоляция портов: Диагностические и бизнес-эндпоинты обязаны жить на разных сетевых портах и сокетах, чтобы сетевой затор на бизнес-маршрутах не привел к ложному убийству контейнера оркестратором.
- Табу на Deep Liveness Checks: Проверка удаленных баз данных и очередей в Liveness Probe превращает локальный сетевой сбой в полномасштабный шторм перезапусков всего кластера.
- Легковесность рантайма Go: Проверки должны отрабатывать за наносекунды на базе неблокирующих атомарных переменных (
atomic.Bool), обновляемых фоновыми диагностическими горутинами.
Мы научились безошибочно рапортовать инфраструктуре о своем физическом здоровье. Но что делать, если зависимость (например, сервис рекомендаций или аналитики) все же вышла из строя, однако мы не хотим полностью блокировать пользователя и возвращать ошибку 503? На помощь приходит искусство частичного функционирования: 7. Graceful degradation.