← назад к разделу

Когда приложение падает в продакшене, первое, на что смотрит Kubernetes — это health-эндпоинты. Если они настроены неправильно, одна лагающая база данных может положить весь сервис целиком, даже если с процессом всё в порядке. Разберём, как работают health checks в Spring Boot и как их настроить правильно.

Что такое health checks и зачем их два

Kubernetes задаёт каждому поду два вопроса:

  1. Жив ли процесс? Если нет — перезапустить.
  2. Готов ли он принимать трафик? Если нет — убрать из балансировки.

Spring Boot Actuator отвечает на эти вопросы через два отдельных эндпоинта: /actuator/health/liveness и /actuator/health/readiness.

Включить их в application.yml:

management:
  endpoint:
    health:
      probes:
        enabled: true
      show-details: always
  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true

Теперь доступны два эндпоинта с разной семантикой:

ЭндпоинтЧто означает UPЧто делает K8s при DOWN
/actuator/health/livenessпроцесс жив, JVM отвечаетперезапускает pod
/actuator/health/readinessсервис готов: БД подключена, прогрев завершёнснимает pod из балансировки

Почему liveness и readiness нельзя смешивать

Представьте: база данных подлагивает 30 секунд из-за технического обслуживания. Что должно произойти?

  • Readiness должен стать DOWN — трафик уйдёт на другие реплики, которые работают нормально.
  • Liveness должен остаться UP — перезапуск пода не исправит ситуацию с базой, а только добавит хаоса.

Если liveness зависит от базы данных, происходит следующее: база лагает → liveness DOWN → Kubernetes убивает pod → новый pod стартует, та же база всё ещё лагает → DOWN снова → убивает снова. За минуту все реплики падут, сервис недоступен.

Liveness проверяет только сам процесс: JVM жива, потоки не заморожены, диск доступен. Внешние зависимости — только в readiness.

Пример опасного кода, который нельзя использовать для liveness:

// НЕЛЬЗЯ — liveness упадёт вместе с базой
@Component
public class CustomLivenessIndicator implements HealthIndicator {
    public Health health() {
        try {
            jdbcTemplate.execute("SELECT 1");
            return Health.up().build();
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}

Kubernetes manifests для probes

spec:
  containers:
    - name: order-service
      livenessProbe:
        httpGet:
          path: /actuator/health/liveness
          port: 8081
        initialDelaySeconds: 30
        periodSeconds: 10
        failureThreshold: 3
      readinessProbe:
        httpGet:
          path: /actuator/health/readiness
          port: 8081
        initialDelaySeconds: 5
        periodSeconds: 5
        failureThreshold: 2

Порт 8081 — отдельный management port (management.server.port). Это позволяет ограничить /actuator/* сетевой политикой так, чтобы он был доступен только внутри кластера.

Custom HealthIndicator для внешних систем

Spring Boot автоматически проверяет базу данных и Redis, если они подключены. Для остальных внешних систем нужно писать свой HealthIndicator.

Проблема в том, что Kubernetes опрашивает readiness каждые 5 секунд, и при 10 репликах это 2 запроса в секунду только от health checks. Если каждый из них реально обращается к внешнему провайдеру — получается непрерывная нагрузка, которая может исчерпать лимиты API.

Решение — TTL-кеш: проверяем провайдера не чаще раза в 10-30 секунд, остальным запросам отдаём сохранённый результат.

@Component
@RequiredArgsConstructor
public class PaymentProviderHealthIndicator implements HealthIndicator {

    private final PaymentProviderClient client;
    private final AtomicReference<CachedHealth> cache = new AtomicReference<>();
    private static final Duration TTL = Duration.ofSeconds(10);

    @Override
    public Health health() {
        var cached = cache.get();
        if (cached != null && cached.expiresAt().isAfter(Instant.now())) {
            return cached.health();
        }

        var health = checkProvider();
        cache.set(new CachedHealth(health, Instant.now().plus(TTL)));
        return health;
    }

    private Health checkProvider() {
        try {
            client.ping();
            return Health.up().withDetail("provider", "payment").build();
        } catch (Exception e) {
            return Health.down(e).withDetail("provider", "payment").build();
        }
    }

    private record CachedHealth(Health health, Instant expiresAt) {}
}

ping() должен быть лёгким запросом — GET /health или OPTIONS /. Не нужно создавать тестовые данные или выполнять реальные бизнес-операции: каждые 5 секунд на 10 репликах это уже 120 реальных бизнес-операций в минуту от одних лишь проверок здоровья.

/actuator/info: какая версия сейчас в продакшене

Классическая ситуация при инциденте: «какая версия сейчас задеплоена?» Без специальной настройки ответ приходится искать в логах CI/CD. Гораздо удобнее — спросить у самого сервиса.

Добавляем в Gradle git-commit-id-plugin, дописываем в application.yml:

management:
  info:
    git:
      mode: full
    build:
      enabled: true
info:
  service:
    name: ${spring.application.name}

После этого /actuator/info отвечает:

{
  "git": {
    "commit": {
      "id": "5380f21abc...",
      "time": "2026-05-25T22:24:00Z"
    },
    "branch": "main"
  },
  "build": {
    "version": "1.4.2",
    "time": "2026-05-25T22:25:30Z",
    "artifact": "order-service"
  },
  "service": {
    "name": "order-service"
  }
}

Частые ошибки

Бизнес-метрики вместо технического состояния. «Если накопилось больше 1000 необработанных заказов — выставить DOWN» — это не про здоровье процесса. Health DOWN приводит к тому, что K8s снимает реплику из балансировки, оставшиеся реплики получают ещё больше заказов, накопление растёт быстрее. Спираль, которая заканчивается полной недоступностью сервиса.

Бизнес-метрики отслеживают через Prometheus + алертинг, отдельно от health checks.

HealthIndicator без кеша. Если убрать TTL-кеш из примера выше — каждый вызов probe будет реально ходить к провайдеру. При многих репликах и частых проверках это создаёт нагрузку, которая мешает реальным запросам.

Liveness зависит от внешних систем. Это самая опасная ошибка: приводит к каскадному перезапуску всех pod при любой проблеме с зависимостями.

Коротко

  • Health checks бывают двух видов: liveness (жив ли процесс) и readiness (готов ли принимать трафик).
  • Liveness зависит только от самого процесса — JVM, потоки, диск. Никаких внешних систем.
  • Readiness проверяет доступность зависимостей: базу, кеши, внешние API.
  • Для каждой внешней системы пишем свой HealthIndicator с TTL-кешем, чтобы не нагружать провайдера частыми проверками.
  • Probe-метод должен быть лёгким: GET /health, OPTIONS /. Не бизнес-операция.
  • Health — техническое состояние процесса и его зависимостей. Бизнес-метрики — в Prometheus.
  • /actuator/info с git-commit-id-plugin позволяет мгновенно ответить на вопрос «что сейчас в продакшене».

Что почитать дальше

  • Метрики и Micrometer — как мониторить бизнес-метрики через Prometheus.
  • Трассировка запросов — как связать логи и запросы через trace ID.
  • SLO и алерты — как ставить цели по доступности и настраивать алерты.