open atlas
↑ К треку
Go с нуля до senior GO · 12 · 03

Health-пробы и жизненный цикл: liveness, readiness, startup и последовательность остановки без потерянных запросов

Liveness перезапускает, readiness маршрутизирует, startup даёт фору на старте — проверка зависимостей в liveness превращает блип базы в шторм рестартов. Настоящий readiness пингует нужное сервису, гаснет первым на SIGTERM, а весь дрейн умещается в terminationGracePeriodSeconds.

GO Senior ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior

Файловер базы занял 28 секунд. Вызванный им инцидент — 40 минут. Каждый под флота гонял liveness-пробу по /health — хендлеру, который кто-то однажды улучшил пингом базы, потому что больше проверок звучало надёжнее. Когда начался файловер, двести совершенно здоровых подов стали проваливать liveness одновременно; три промаха спустя кубелеты убили их все. У инцидента появился второй акт: сотни перезапускающихся процессов с холодными кэшами одновременно переподключаются к только что ожившей базе — лавина коннектов, которая снова её роняет, что снова проваливает liveness, что снова перезапускает флот. 28-секундный блип, закольцованный в 40 минут теми самыми пробами, что должны были добавить надёжности. Постмортем уложился в одно предложение: liveness отвечает «перезапускать ли меня», readiness — «слать ли мне трафик», а проверку базы привязали к тому вопросу, где ответом служит рестарт. У другой команды в том же квартале был зеркальный баг — без startup-пробы, с 90-секундной миграцией схемы на старте и liveness, взводящимся на тридцатой секунде: CrashLoopBackOff (петля перезапусков с нарастающей паузой, которую Kubernetes применяет к падающему контейнеру), навсегда, на каждом деплое. Kubernetes добросовестно исполняет любую ошибку, которую вы закодируете.

Три пробы — три разных вопроса

К концу урока вы будете точно знать, какой из трёх проб принадлежит какой вопрос — и почему путаница между ними превращает 28-секундный блип в 40-минутный инцидент при каждом повторении.

Liveness спрашивает: сломан ли процесс без шанса на самопочинку — дедлок, клин, порча состояния? Провал означает рестарт контейнера кубелетом, поэтому хендлер обязан проверять только то, что рестарт чинит: цикл обработки отвечает — и всё. Зависимости он не проверяет никогда: рестарт не чинит базу — он лишь конвертирует любой сбой зависимости в шторм рестартов на весь флот, ровно как в крючке. Readiness спрашивает: слать ли этому поду трафик прямо сейчас? Провал убирает под из эндпоинтов сервиса — обратимо, дёшево и ровно то, что нужно при сбое зависимости, перегрузке или дрейне. Вот где живут проверки зависимостей. Startup спрашивает: закончилась ли загрузка? Пока она не пройдена, liveness и readiness приостановлены — бюджет на миграции и прогрев кэшей равен failureThreshold, умноженному на periodSeconds, а без неё liveness убивает медленный старт на своём пороге и зацикливается навсегда.

livenessProbe:
  httpGet: { path: /healthz, port: 8080 }
  periodSeconds: 10
  failureThreshold: 3        # 30 с настоящего клина до рестарта
readinessProbe:
  httpGet: { path: /readyz, port: 8080 }
  periodSeconds: 5
  failureThreshold: 2        # вывод из ротации за ~10 с
startupProbe:
  httpGet: { path: /healthz, port: 8080 }
  periodSeconds: 5
  failureThreshold: 36       # до 180 с на загрузку, прежде чем взведётся liveness
Викторина

Ваша liveness-проба зовёт хендлер, который пингует базу. У базы 30-секундный файловер. Что произойдёт с флотом?

Настоящий readiness на Go

Readiness-хендлер, безусловно возвращающий 200, — это театр: он сообщает Kubernetes, что под готов обслуживать, когда сам под не знает ничего. Настоящий readiness проверяет то, без чего сервис не обслужит ни один запрос, с таймаутом, и несёт флаг дрейна:

var ready atomic.Bool // станет true после завершения загрузки

mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK) // liveness: процесс жив — и ничего больше
})

mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
	if !ready.Load() {
		http.Error(w, "draining", http.StatusServiceUnavailable)
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
	defer cancel()
	if err := db.PingContext(ctx); err != nil { // зависимость, нужная каждому запросу
		http.Error(w, "db unreachable", http.StatusServiceUnavailable)
		return
	}
	w.WriteHeader(http.StatusOK)
})

Дисциплина режет в обе стороны: сюда входят только жёсткие зависимости — те, без которых падает каждый запрос. Вписать в readiness необязательный кэш или некритичный даунстрим — значит позволить одному мигающему сайдкару вывести весь флот из ротации; необязательные зависимости деградируют ответы, а не «разготавливают» поды. И держите таксономию эндпоинтов в порядке: /healthz тривиален и существует для кубелета, /readyz проверяет зависимости и ворота трафика, /debug/pprof — для инженеров и никогда не публичен: импорт net/http/pprof побочным эффектом регистрирует хендлеры на DefaultServeMux — так профайлеры и оказываются торчащими в интернет; отдавайте его на отдельном localhost- или внутрикластерном листенере.

Почему это работает

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

Последовательность остановки и бюджет грейса

Завершение — это распределённое рукопожатие, и пробы — его половина. На SIGTERM удаление из эндпоинтов распространяется асинхронно — балансировщики шлют новые запросы ещё несколько секунд. Сервер, мгновенно перестающий принимать соединения, отдаёт connection refused живому трафику на каждом деплое. Правильный порядок: сначала провалить readiness, переждать распространение, затем дрейнить — прямая спираль в урок graceful shutdown из go/03, теперь с кластерной хореографией:

<-ctx.Done()                 // SIGTERM от кубелета
ready.Store(false)           // 1. /readyz теперь 503 — началось удаление из эндпоинтов
time.Sleep(5 * time.Second)  // 2. поглощаем лаг распространения
shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx) // 3. дрейним запросы в полёте

Арифметика не обсуждается: terminationGracePeriodSeconds (по умолчанию 30) должен покрыть сон на распространение плюс дедлайн дрейна плюс запас — по его истечении SIGKILL приходит безусловно и забирает недоделанный дрейн с собой. Пять плюс двадцать оставляют пять секунд запаса внутри дефолта — а если ваш самый медленный легитимный запрос идёт 25 секунд, дефолтный бюджет уже сломан: поднимайте грейс до 40, а не сбривайте сон. Тайминг проб входит в ту же арифметику с другой стороны: readiness переключается за failureThreshold, умноженный на periodSeconds, то есть при 2 x 5 с балансировщик в худшем случае начнёт убирать вас секунд через десять после флипа — сон покрывает лаг от контроллера до дата-плейна, каденс пробы покрывает обнаружение.

Викторина

Почему на SIGTERM флип readiness обязан случиться до srv.Shutdown, и зачем между ними сон?

Вспомните перед уходом
  1. 01
    Назови точную семантику трёх проб и продовую историю отказа за каждым правилом подключения.
  2. 02
    Пройди последовательность от SIGTERM до выхода с арифметикой грейс-бюджета для сервиса, чей самый медленный легитимный запрос идёт 20 секунд.
Итог

Три пробы — три разных вопроса, и каждая продовая история этого урока выросла из ответа не на тот. Liveness спрашивает, поможет ли рестарт: держите его хендлер тривиальным — 200, доказывающий, что процесс обрабатывает запросы, — потому что единственный ответ кубелета — убийство, а убийство не чинит базу; привязанный к зависимости, liveness конвертирует 28-секундный блип в самоподдерживающийся шторм рестартов флота. Readiness спрашивает, слать ли трафик сейчас: вот где живёт пинг пула базы с секундным таймаутом, потому что ответ — вывод из эндпоинтов — обратим, сохраняет процесс тёплым и ровно уместен при сбоях, перегрузке и дрейне; ограничьте его жёсткими зависимостями, иначе один мигающий необязательный сайдкар разготовит весь флот. Startup спрашивает, закончилась ли загрузка, приостанавливая остальные пробы в бюджете failureThreshold на period — недостающая деталь каждого CrashLoopBackOff на медленной миграции. Таксономия эндпоинтов следует вопросам: /healthz тривиален, /readyz честен, /debug/pprof никогда не публичен — pprof сам регистрируется на DefaultServeMux. Завершение — это флаг readiness по совместительству: SIGTERM, флип /readyz в 503, сон около пяти секунд, пока удаление из эндпоинтов проходит каденс обнаружения и лаг дата-плейна, затем srv.Shutdown с границей по самому медленному легитимному запросу — хореография graceful shutdown из go/03, теперь знающая о кластере. Сложите последовательность против terminationGracePeriodSeconds с запасом и поднимайте грейс, а не сбривайте шаги: SIGKILL приходит по расписанию и не договаривается. Теперь, когда вы ревьюите конфиг проб в PR, первый взгляд — на liveness-хендлер: если он обращается к внешней зависимости, вы знаете, что именно и куда перенести.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.