16. Healthcheck и readiness probe
Когда врач скорой помощи осматривает пациента, он в первую очередь проверяет базовые рефлексы: есть ли дыхание и бьется ли сердце. Лишь убедившись, что пациент жив, врач выясняет: готов ли человек сейчас встать и бежать марафон.
В мире микросервисов и облачных оркестраторов (Kubernetes, Nomad, Docker Swarm) действует точно такая же медицинская логика. Оркестратор — это автоматический дежурный врач. С помощью специальных HTTP-запросов (Health Probes) он непрерывно опрашивает каждый контейнер. Если процесс впал в кому из-за взаимной блокировки горутин (deadlock), его нужно немедленно реанимировать (перезапустить). Но если процесс просто ожидает прогрева кэша или восстановления соединения с базой данных, убивать его нельзя — нужно лишь временно отвести от него поток клиентского трафика.
Понимание фундаментальной разницы между Liveness и Readiness пробами — граница, отделяющая стабильные кластеры от систем, страдающих от каскадных падений и бесконечных перезапусков.
flowchart TD
A["Kubelet инициирует запрос"] --> B{"Тип HTTP-зонда"}
B -->|"liveness"| C["Проверка внутреннего состояния<br>горутин, блокировок, памяти"]
B -->|"readiness"| D["Проверка внешних зависимостей<br>БД, Redis, очереди"]
C -->|"200 OK"| E["Процесс работает<br>Kubelet не вмешивается"]
C -->|"5xx или Timeout"| F["Kubelet перезапускает под<br>Graceful shutdown + restart"]
D -->|"200 OK"| G["Сервис в пуле Endpoints<br>Принимает трафик"]
D -->|"5xx или Timeout"| H["Исключение из пула Endpoints<br>Трафик идёт на реплики"]
classDef kube fill:#0f172a,stroke:#6366f1,stroke-width:2px,color:#f8fafc;
classDef check fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
classDef ok fill:#064e3b,stroke:#10b981,stroke-width:2px,color:#f8fafc;
classDef err fill:#450a0a,stroke:#f43f5e,stroke-width:2px,color:#f8fafc;
class A,B kube;
class C,D check;
class E,G ok;
class F,H err;
Разделение ответственности. Liveness и Readiness
В контейнерных оркестраторах (Kubernetes, Docker Swarm, Nomad) жизненный цикл сервиса управляется через HTTP-зонды. Они сигнализируют платформе о состоянии процесса: «завис ли он» и «готов ли принимать трафик». Неправильная реализация зондов — одна из самых частых причин каскадных падений, 502-х ошибок при деплое и бесконечных рестартов подов.
В Go реализация зондов тривиальна синтаксически, но требует глубокого понимания механики оркестратора, работы сетевого стека и стоимости операций на уровне CPU и памяти.
1. Архитектура HTTP-зондов в Kubernetes
Существует три типа проб, каждый из которых выполняет строго определённую функцию:
- Liveness (Жизнеспособность): Отвечает на вопрос «Процесс мёртв?». Если зонд падает, оркестратор принудительно перезапускает контейнер. Не должен проверять внешние зависимости (БД, кеш, очереди). Временный сетевой разрыв не должен убивать под.
- Readiness (Готовность): Отвечает на вопрос «Могу ли я обрабатывать запросы?». Если зонд падает, оркестратор исключает под из пула
Endpoints. Трафик перенаправляется на реплики. Должен проверять критические зависимости. - Startup (Запуск): Даёт сервису время на инициализацию (миграции, загрузка кешей, прогрев пулов соединений). Пока не ответит
200, liveness-проба не активна.
2. Идиоматичная реализация на Go
Зонды находятся на hot-path оркестратора. Они вызываются каждые 1-5 секунд на каждый под. Любая аллокация или блокировка в обработчике создаёт кумулятивный overhead на кластере.
package health
import (
"net/http"
"sync/atomic"
)
// Probes управляет состояниями жизнеспособности и готовности.
// Использует атомарные операции для lock-free чтения на горячем пути.
type Probes struct {
isReady atomic.Int32 // 0 = not ready, 1 = ready
}
func NewProbes() *Probes {
p := &Probes{}
p.isReady.Store(0) // Изначально сервис не готов
return p
}
func (p *Probes) SetReady(ready bool) {
var val int32
if ready {
val = 1
}
p.isReady.Store(val)
}
// LivenessHandler проверяет только базовую работоспособность процесса.
func (p *Probes) LivenessHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// Использование константного среза избегает аллокаций в куче на каждый вызов
w.Write([]byte(`{"status":"ok"}`))
}
}
// ReadinessHandler возвращает готовность на основе атомарного флага.
func (p *Probes) ReadinessHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if p.isReady.Load() == 1 {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ready"}`))
} else {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte(`{"status":"not ready"}`))
}
}
}
3. Механика работы и Mechanical Sympathy
Понимание того, как зонды взаимодействуют с рантаймом Go и ядром Linux, критично для стабильности.
Сетевой стек и syscalls
Каждый зонд kubelet — это новый TCP-соединение (в старых версиях K8s) или мультиплексированный поток (HTTP/2 в новых). Для Go net/http сервера это означает:
accept()syscall → получение файлового дескриптора- Регистрация в
netpoll(epoll/kqueue) - Запуск горутины-обработчика
read()→ парсинг HTTP-заголовковwrite()→ отправка ответа- Закрытие соединения (если нет keep-alive)
Частые зонды (каждую секунду) на 100-подовом кластере генерируют сотни accept/close syscall в секунду. Это нагружает планировщик, увеличивает churn горутин и создает contention на уровне netpoll.
Атомарные операции vs Мьютексы
Для флага готовности atomic.Int32 предпочтительнее sync.RWMutex. atomic.Store/Load компилируется в инструкции mov с префиксами памяти (x86) или ldxr/stxr (ARM). Это lock-free операция, выполняемая за 1-2 такта CPU в User Space. sync.RWMutex при высокой конкуренции вызывает futex syscall, переводит горутины в состояние _Gwaiting и требует переключения контекста ядра. На hot-path разница достигает 10-50x.
4. Асинхронная проверка зависимостей
Readiness должен проверять БД, кеш или очереди, но никогда не должен делать это синхронно внутри HTTP-обработчика. Блокировка обработчика приведёт к таймауту пробы, исключению пода из сервиса и увеличению нагрузки на оставшиеся реплики.
Правильный паттерн — фоновый воркер, который обновляет атомарный флаг:
func StartDependencyChecker(p *Probes, db *sql.DB) {
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
err := db.PingContext(ctx)
cancel()
// Атомарное обновление состояния готовности
p.SetReady(err == nil)
}
}()
}
5. Ловушки и антипаттерны
6. Собеседование и типичные вопросы
Итог
- Разделяйте liveness (процесс жив) и readiness (зависимости доступны) на уровне кода и конфигурации оркестратора.
- Используйте
atomicпримитивы для управления флагами готовности, избегая блокировок на горячем пути. - Никогда не проверяйте внешние зависимости внутри liveness-обработчика.
- Асинхронные воркеры обновляют состояние readiness, обработчики только читают атомарный флаг.
- Оптимизируйте аллокации: константные JSON-ответы, отсутствие рефлексии, минимальный набор заголовков.
- Настройте таймауты HTTP-сервера специально для probe-портов, чтобы избежать утечки дескрипторов.
Следующая статья: 17. Метрики и базовый monitoring