lenec ru

← все посты

Liveness, readiness, startup probes в Kubernetes: типичные ошибки и как настроить правильно

13K

Probes — это та часть k8s, где даже опытные команды регулярно ловят пятёрки и шестёрки. Liveness убивает живой pod в момент пика нагрузки, readiness не отдаёт трафик после рестарта секунд тридцать, startup вообще не настроен и приложение с долгим стартом крутится в CrashLoopBackOff. Разберу, что чем отличается, как они на самом деле работают и какие конфиги я ставлю в продакшене.

Версия — k8s 1.30. Примеры на YAML, рабочие, можно копировать.

Три probes, три разных задачи

В Kubernetes есть три типа probes, и их часто путают.

  • livenessProbe — «приложение живое или зависло?». Если падает несколько раз подряд — kubelet перезапускает контейнер.
  • readinessProbe — «можно ли слать трафик?». Если падает — pod выкидывается из endpoints сервиса, трафик не идёт. Контейнер не перезапускается.
  • startupProbe — «приложение уже запустилось?». Пока эта probe не прошла, liveness и readiness не выполняются. После успеха startup отключается навсегда.

Главная путаница: liveness != «приложение работает», а «приложение зависло так, что только рестарт поможет». Если liveness падает, потому что приложение долго стартует — ты получишь бесконечный рестарт. Если оно зависло из-за внешней БД, и ты убиваешь pod — БД от этого не оживёт, ты только усугубишь шторм.

Типичные ошибки

1. Liveness и readiness ходят на один и тот же эндпоинт

Самый частый антипаттерн:

livenessProbe:
  httpGet:
    path: /health
    port: 8080
readinessProbe:
  httpGet:
    path: /health
    port: 8080

А внутри /health приложение ходит в БД и Redis. Падает Redis на 5 секунд — все pods вылетают с liveness fail, kubelet их рестартует, новые pods долго стартуют, БД получает шторм коннектов от перезапускающихся клиентов, и вот ты уже в ночной инциденте.

Правильно: разнести healthchecks по смыслу.

livenessProbe:
  httpGet:
    path: /healthz   # просто "процесс жив"
    port: 8080
readinessProbe:
  httpGet:
    path: /readyz    # "готов принимать трафик"
    port: 8080

В /healthz — только проверка, что процесс живой и event loop крутится. Без походов в БД, без проверок зависимостей. Возвращаешь 200 — и всё.

В /readyz — можно проверить, что connection pool к БД жив, что нужные кеши прогреты. Если БД недоступна, pod уходит из endpoints и не получает трафик, но не убивается.

2. Нет startupProbe для медленных приложений

Java-приложения, которые стартуют 60 секунд, или Python с миграциями БД на старте — классика.

Без startupProbe ты вынужден ставить initialDelaySeconds: 90 на liveness, чтобы он не убил приложение раньше времени. Но потом, в работе, тебе хочется, чтобы liveness реагировал быстро — за 5-10 секунд. С одним initialDelaySeconds так не сделать.

startupProbe решает это:

startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30
  periodSeconds: 10
  # суммарно даёт 300 секунд на старт
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 10
  failureThreshold: 3
  # после старта — 30 секунд на отвал

Пока startup не прошёл успешно — liveness/readiness не запускаются. Прошёл — и дальше живёт обычная liveness с быстрой реакцией.

3. Слишком жёсткий failureThreshold

По умолчанию failureThreshold: 3 и periodSeconds: 10. То есть после 30 секунд непрерывных факапов pod рестартует. На сетевых glitch-ах это бывает мало, особенно если сервис ходит в БД.

Я обычно ставлю failureThreshold: 5 для liveness — это 50 секунд запаса, достаточно, чтобы не падать на коротких сетевых пиках, но мало, чтобы реально зависший pod не висел вечно.

4. exec-probe, который форкает тяжёлый процесс

Видел такое:

livenessProbe:
  exec:
    command: ["/bin/sh", "-c", "curl -f http://localhost:8080/health"]

Каждые 10 секунд kubelet поднимает shell, потом curl. На 200 pods на ноде это уже заметная нагрузка на CPU. Используй httpGet или tcpSocket, exec — только если реально нет HTTP-эндпоинта.

5. tcpSocket вместо httpGet

Простой tcpSocket просто открывает соединение и закрывает. Если приложение приняло коннект, но потом висит — probe всё равно зелёная.

Я ставлю tcpSocket только для приложений без HTTP (например, gRPC до того, как появилась grpc-проба нативно). С k8s 1.24+ есть grpc: probe — пользуйся ей для gRPC-сервисов.

Графовый dependency check в readyz — отдельная тема

В /readyz мы решаем «готов ли pod к трафику?». Но что считать готовностью?

Жёсткое правило: проверяй только то, без чего pod вообще не может ответить ни на один запрос. БД — да, если все эндпоинты ходят в БД. Redis — нет, если есть fallback на in-memory cache.

Иначе ты ловишь cascade failure: упал Redis → все pods показывают readyz=false → endpoints пустые → клиенты получают 503 → клиенты ретраят → нагрузка на сервис, который мог бы хотя бы отдавать кеш, выросла в 10 раз.

Лучше отдавать 503 на конкретные эндпоинты, которым нужен Redis, и 200 на остальные. Pod остаётся в endpoints, частичная функциональность работает.

Готовый пример из прода

Так я конфигурирую API-сервис на Go в e-commerce. Стартует 2-3 секунды, миграций БД на старте нет (отдельный init-job).

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 4
  template:
    spec:
      containers:
      - name: api
        image: registry.example.com/api:v1.5.0
        ports:
        - containerPort: 8080
          name: http
        startupProbe:
          httpGet:
            path: /healthz
            port: http
          failureThreshold: 12
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /healthz
            port: http
          periodSeconds: 10
          timeoutSeconds: 2
          failureThreshold: 5
        readinessProbe:
          httpGet:
            path: /readyz
            port: http
          periodSeconds: 5
          timeoutSeconds: 2
          failureThreshold: 3
          successThreshold: 1
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            memory: 512Mi

В коде сервиса:

// /healthz — просто "процесс крутится"
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
})

// /readyz — проверяем зависимости
mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
    defer cancel()

    if err := db.PingContext(ctx); err != nil {
        http.Error(w, "db down", http.StatusServiceUnavailable)
        return
    }
    w.WriteHeader(http.StatusOK)
})

Обрати внимание на timeoutSeconds: 2 в pробах — без явного таймаута дефолт 1 секунда, что иногда мало.

Graceful shutdown и preStop

Probes не закроют тебе все вопросы рестарта. Когда pod останавливается, k8s одновременно посылает SIGTERM контейнеру и убирает его из endpoints. Но обновление endpoints происходит асинхронно через kube-proxy, ingress-controller, и до них доходит за 1-3 секунды. Всё это время pod может получать новые запросы, а его процесс уже завершается.

Лечится preStop hook, который добавляет искусственную задержку:

lifecycle:
  preStop:
    exec:
      command: ["sh", "-c", "sleep 10"]
terminationGracePeriodSeconds: 30

Sleep 10 секунд — pod ждёт, пока endpoints обновятся, потом нормально завершается. terminationGracePeriodSeconds должна быть больше preStop + времени на graceful shutdown приложения.

Что запомнить

liveness — про зависание, readiness — про готовность к трафику, startup — про долгий старт. Не ходи в БД из liveness. Используй startupProbe для медленных приложений вместо больших initialDelaySeconds. Разнеси /healthz и /readyz по эндпоинтам, ставь явные timeouts. Добавь preStop hook для graceful shutdown.

Куда копать: официальная документация по probes, и обязательно — посты Tim Hockin про probe semantics. У него хорошо написано, почему именно три типа, а не два.

Комментарии 1

  • Игорь Лебедев

    У нас на startupProbe несколько раз стреляло то же, что описано: failureThreshold по умолчанию 3, а сервис на холодном старте грел кэш 90 секунд. Перешли на startupProbe с failureThreshold=30 и periodSeconds=10, liveness вообще включается только после её прохождения. Без startupProbe liveness начинал ронять под до того, как тот успевал отдать первый 200.

Войдите, чтобы оставить комментарий.