Когда приложение падает в продакшене, первое, на что смотрит Kubernetes — это health-эндпоинты. Если они настроены неправильно, одна лагающая база данных может положить весь сервис целиком, даже если с процессом всё в порядке. Разберём, как работают health checks в Spring Boot и как их настроить правильно.
Что такое health checks и зачем их два
Kubernetes задаёт каждому поду два вопроса:
- Жив ли процесс? Если нет — перезапустить.
- Готов ли он принимать трафик? Если нет — убрать из балансировки.
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 и алерты — как ставить цели по доступности и настраивать алерты.