Опирается на правила: R-RES-HC-1R-RES-HC-4 и R-RES-HC-X1R-RES-HC-X2 из Resilience Style Guide → раздел 10. Health checks.

Важно знать

  • На каждую внешнюю системуHealthIndicator бин: SberHealthIndicator implements HealthIndicator.
  • Probe cached с TTL 30s. Не каждый /actuator/health запрос ходит в Sber.
  • Probe-метод — light: GET /health или OPTIONS /, не реальный бизнес-вызов (register, confirmPayment).
  • Health отражается в /actuator/health/<system>. K8s livenessProbe смотрит на /actuator/health/liveness (overall), readinessProbe — на /actuator/health/readiness (включая внешние).
  • Sync-probe без кеша = DDoS внешней системы силами k8s (probe каждые 5s × N pod-ов = десятки RPS только от health-check'ов).
  • Business-операция в probe изменяет состояние, плодит test-данные, нагружает систему.

Health check внешней системы — это способ k8s узнать, что мы не в состоянии нормально работать, не «глобальный pinger Sber». Поэтому он должен быть дешёвым (для нашей системы и для внешней) и точным (отражать реальную готовность принимать трафик). Раскрытие раздела 10 гайда.

HealthIndicator на каждую внешнюю систему

R-RES-HC-1: реализуем HealthIndicator бин на каждую внешнюю систему.

@Component
@RequiredArgsConstructor
public class SberHealthIndicator implements HealthIndicator {

    private final SberHealthApi sberHealthApi;
    private final Clock clock;

    private volatile Health lastResult = Health.unknown().build();
    private volatile Instant lastProbe = Instant.EPOCH;
    private static final Duration TTL = Duration.ofSeconds(30);

    @Override
    public Health health() {
        if (Duration.between(lastProbe, clock.instant()).compareTo(TTL) < 0) {
            return lastResult;
        }
        lastProbe = clock.instant();
        try {
            SberHealthResponse resp = sberHealthApi.health();
            lastResult = "UP".equals(resp.getStatus())
                ? Health.up().withDetail("version", resp.getVersion()).build()
                : Health.down().withDetail("reason", resp.getReason()).build();
        } catch (Exception e) {
            lastResult = Health.down().withDetail("error", e.getMessage()).build();
        }
        return lastResult;
    }
}

Spring Boot Actuator автоматически подхватит этот бин под /actuator/health/sber.

Cached probe с TTL 30s

R-RES-HC-2: probe cached, не дёргается на каждый actuator-запрос.

Зачем кэш:

  • K8s по умолчанию probes каждые 10s. С 5 pod'ами это 30 запросов в минуту к Sber только за health.
  • При TTL 30s — реальный поход к Sber раз в 30 секунд независимо от частоты actuator-запросов.
  • 30s — компромисс: достаточно свежо чтобы заметить деградацию за минуту, не сильно нагружает внешнюю систему.

Реализации:

  1. @Cacheable("health.sber") с Cache бином, у которого expireAfterWrite: 30s. Просто, но требует cache-инфраструктуры.
  2. Ручной volatile Instant + Health (как в примере выше). Простота, нет внешних зависимостей.
  3. Reactive Mono.cache(Duration) — если кода базовый стек реактивный.

Для большинства случаев — вариант 2.

Light probe — GET /health, не business

R-RES-HC-3: probe-метод — лёгкий технический endpoint, не бизнес-операция.

// ХОРОШО — light probe
SberHealthResponse resp = sberHealthApi.health();           // GET /health

// ХОРОШО — если у системы нет /health, OPTIONS на root
sberApi.options();                                          // OPTIONS /

Если внешняя система не предоставляет /health:

  • Использовать OPTIONS на корневом endpoint — обычно отвечает быстро без реальной обработки.
  • Использовать самый дешёвый GET (например, list metadata) с ?limit=1 — минимизировать нагрузку.
  • В крайнем случае — просто TCP-connect проверить (без HTTP).

Никаких бизнес-операций:

  • Не getOrderStatus(testOrderId) — нагружает систему, портит метрики.
  • Не register(testAmount) — изменяет состояние, плодит test-данные.

Структура /actuator/health

R-RES-HC-4: правильная организация exposure'а в Actuator.

management:
  endpoint.health:
    probes.enabled: true
    show-details: always              # only-when-authorized в проде
    show-components: always
    group:
      readiness:
        include: readinessState, db, sber, odnakassa
      liveness:
        include: livenessState
  endpoints.web.exposure:
    include: health, info, metrics, prometheus
  server.port: 9090                   # actuator на отдельном порту

Что важно:

  • liveness — только в livenessState (приложение запущено и не зависло). Не включаем внешние системы — k8s не должен убивать pod, если Sber лежит.
  • readiness — включает БД и критичные внешние системы. Pod выходит из Service backend pool, когда не готов принимать трафик (нет БД — точно не готов; нет Sber — может, готов, может, нет).
  • Per-system endpoints доступны как /actuator/health/sber, /actuator/health/odnakassa — SRE видит детали.

Какие external включать в readiness — решение бизнес-домена:

  • Если без Sber сервис физически не может ничего делать (платёжный шлюз) → включаем.
  • Если без Sber только некоторые операции откажут → не включаем (handler сам вернёт 503 на конкретный endpoint).

Что запрещено

Sync-probe без кеша

R-RES-HC-X1: probe ходит к Sber на каждый actuator-запрос.

// ПЛОХО — без кеша
@Override
public Health health() {
    try {
        SberHealthResponse resp = sberHealthApi.health();   // ← каждый раз
        return Health.up().build();
    } catch (Exception e) {
        return Health.down().build();
    }
}

Что произойдёт:

  • K8s livenessProbe + readinessProbe — каждый 10s. 2 probes × 5 pod'ов = 60 запросов в минуту к Sber.
  • Если Prometheus тоже скрэйпит metrics endpoint каждые 15s, добавляется ещё load.
  • При деградации Sber probes начинают висеть до timeout, что парализует actuator — другие health-check'и тоже не отвечают.

Корректно: TTL 30s, как показано выше.

Probe с business-операцией

R-RES-HC-X2: health-probe вызывает реальный business-метод.

// ПЛОХО — probe делает реальный платёжный запрос
@Override
public Health health() {
    try {
        sberApi.register(testRegisterRequest);              // ← реальный платёж с тестовыми данными
        return Health.up().build();
    } catch (Exception e) {
        return Health.down().build();
    }
}

Что не так:

  • Изменяет состояние. Каждые 30 секунд в Sber приходит test-register. После года работы — 1 миллион test-записей в их БД.
  • Нагружает. Реальные операции дороже OPTIONS.
  • Портит метрики. Бизнес-аналитика видит «register volume» с учётом наших probes — искажает реальные показатели.
  • Может выполниться нежелательно. Если probe идёт через retry — три тест-вызова за раз.

Корректно: light technical endpoint (GET /health, OPTIONS /). Если нет — самый дешёвый read.

Что запрещено — таблица

АнтипаттернПравилоЧто взамен
Sync-probe без TTL-кешаR-RES-HC-X1TTL 30s
Probe вызывает business-операциюR-RES-HC-X2Light technical endpoint
Внешние системы в livenessProbeR-RES-HC-4Только в readinessProbe
HealthIndicator без try-catch (выкидывает exception наружу)R-RES-HC-1catch + Health.down()
Один общий HealthIndicator на несколько системR-RES-HC-1Per-system bean
Actuator endpoints на основном HTTP-портуR-RES-HC-4management.server.port: 9090

Куда дальше