Опирается на правила:
R-OBS-HC-1…R-OBS-HC-3иR-OBS-HC-X1…R-OBS-HC-X3из Observability Style Guide → раздел 4. Health checks.
Важно знать
- Liveness и readiness — разные probe с разной семантикой и разными зависимостями.
/health/live— UP пока процесс жив и event loop не застрял. Не должен зависеть от внешних систем./health/ready— UP когда сервис готов принимать трафик: БД подключена, критичные зависимости отвечают.- Custom
HealthIndicatorдля критичных внешних систем реализуется с TTL-кешем: без него probe дудосит provider каждые 5 секунд × N реплик./infoотдаёт git-sha, версию и build-time — обязательно для отладки в проде.- Health — техническое состояние процесса, не бизнес.
orderCount > N → DOWN— антипаттерн.- Liveness не зависит от DB: иначе K8s уходит в restart-loop при кратковременном лаге на стороне PostgreSQL.
- В NestJS health-роуты лучше выносить на отдельный management-порт (:9090), закрытый network policy.
Health checks — то, на что смотрит Kubernetes, ALB и load balancer, решая «дать ли трафик в этот pod». Неправильно настроенные probes — главный источник каскадного отказа: одна реплика DB лагает → все pods unhealthy → весь сервис недоступен.
Установка @nestjs/terminus
npm install @nestjs/terminus
Подключение в AppModule (или в выделенный HealthModule):
import { TerminusModule } from '@nestjs/terminus';
@Module({
imports: [TerminusModule],
})
export class HealthModule {}
Liveness vs Readiness
R-OBS-HC-1: два endpoint-а с явно разграниченными зависимостями.
import { Controller, Get } from '@nestjs/common';
import {
HealthCheck,
HealthCheckService,
TypeOrmHealthIndicator,
} from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly db: TypeOrmHealthIndicator,
) {}
@Get('live')
@HealthCheck()
liveness() {
return this.health.check([]);
}
@Get('ready')
@HealthCheck()
readiness() {
return this.health.check([
() => this.db.pingCheck('postgres'),
]);
}
}
| Endpoint | Что проверяет | Что делает K8s |
|---|---|---|
/health/live | Процесс отвечает, event loop жив | UP → продолжать; DOWN → рестартует pod |
/health/ready | БД, критичные зависимости | UP → шлёт трафик; DOWN → снимает из Service endpoints |
Семантическое различие критично:
- Если БД недоступна 10 секунд — readiness DOWN (трафик уйдёт на другие реплики), liveness UP (рестарт pod-а не поможет — та же БД).
- Если process завис — liveness DOWN, K8s убивает pod, новый стартует с чистым event loop.
K8s манифест:
spec:
containers:
- name: order-service
livenessProbe:
httpGet:
path: /health/live
port: 9090
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 9090
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 2
Порт 9090 — отдельный management-server, не business 3000. Это позволяет ограничить /health/* и /metrics сетевой политикой.
Custom HealthIndicator с TTL-кешем
R-OBS-HC-2: для каждой критичной внешней системы — отдельный HealthIndicator. Чтобы probe не делала десятки запросов к provider'у каждые 5 секунд — TTL-кеш.
Пример: SberPaymentHealthIndicator (проверяет доступность платёжного шлюза).
import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
import { SberPaymentClient } from '../payment/sber-payment.client';
interface CachedResult {
result: HealthIndicatorResult;
expiresAt: number;
}
@Injectable()
export class SberPaymentHealthIndicator extends HealthIndicator {
private cache: CachedResult | null = null;
private readonly ttlMs = 10_000;
constructor(private readonly client: SberPaymentClient) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
const now = Date.now();
if (this.cache && this.cache.expiresAt > now) {
return this.cache.result;
}
const result = await this.check(key);
this.cache = { result, expiresAt: now + this.ttlMs };
return result;
}
private async check(key: string): Promise<HealthIndicatorResult> {
try {
await this.client.ping();
return this.getStatus(key, true, { provider: 'sber' });
} catch (err) {
const result = this.getStatus(key, false, { provider: 'sber' });
throw new HealthCheckError('SberPayment ping failed', result);
}
}
}
Подключение в контроллере:
@Get('ready')
@HealthCheck()
readiness() {
return this.health.check([
() => this.db.pingCheck('postgres'),
() => this.sberPayment.isHealthy('sber-payment'),
]);
}
Без TTL-кеша: K8s проверяет readiness каждые 5 секунд × 10 реплик × isHealthy дёргает Sber → 120 ping/min на платёжный шлюз только от health-check. Rate-limit срабатывает, реальные транзакции фейлятся, readiness DOWN → cascading.
TypeOrmHealthIndicator (pingCheck) дёшевый — выполняет SELECT 1 во внутреннем pool. Его TTL-кешировать не нужно.
/info с git-sha и версией сборки
R-OBS-HC-3: /info обязателен для отладки версии в проде. Без него каждый инцидент начинается с вопроса «какая сборка задеплоена».
В NestJS нет встроенного /actuator/info, поэтому делается простой контроллер, который читает переменные окружения, выставленные CI/CD:
import { Controller, Get } from '@nestjs/common';
@Controller('info')
export class InfoController {
@Get()
info() {
return {
service: {
name: process.env.SERVICE_NAME ?? 'order-service',
version: process.env.APP_VERSION ?? 'unknown',
},
git: {
commitId: process.env.GIT_COMMIT_SHA ?? 'unknown',
branch: process.env.GIT_BRANCH ?? 'unknown',
},
build: {
time: process.env.BUILD_TIME ?? 'unknown',
},
};
}
}
В Dockerfile (или Helm chart) переменные выставляются из build args:
ARG GIT_COMMIT_SHA
ARG GIT_BRANCH
ARG BUILD_TIME
ARG APP_VERSION
ENV GIT_COMMIT_SHA=$GIT_COMMIT_SHA \
GIT_BRANCH=$GIT_BRANCH \
BUILD_TIME=$BUILD_TIME \
APP_VERSION=$APP_VERSION
Результат /info:
{
"service": { "name": "order-service", "version": "2.4.1" },
"git": { "commitId": "5380f21abc3d", "branch": "main" },
"build": { "time": "2026-06-18T14:22:00Z" }
}
Отдельный management-port
R-OBS-CFG-1: health-routes и /metrics — на отдельном порту, не на business 3000.
Паттерн: второй NestFactory.create в bootstrap:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
const mgmt = await NestFactory.create(ManagementModule);
mgmt.setGlobalPrefix('');
await mgmt.listen(9090);
}
ManagementModule экспортирует только HealthController, InfoController и /metrics. Business-эндпоинты на :9090 недоступны.
Что запрещено
Business-state в health check
R-OBS-HC-X1: «если pending заказов у Customer > 1000 → DOWN» — бизнес-метрика, не техническое состояние.
Health DOWN → K8s снимает реплику из Service endpoints → меньше реплик обрабатывают очередь → ещё хуже. Бизнес-метрики мониторятся через Prometheus + alerting (см. SLO и алерты).
Liveness зависит от внешних систем
R-OBS-HC-X2: классическая ловушка на PostgreSQL.
// КАТАСТРОФА — liveness DOWN при лаге DB
@Get('live')
@HealthCheck()
liveness() {
return this.health.check([
() => this.db.pingCheck('postgres'), // НЕЛЬЗЯ в liveness
]);
}
Сценарий: PG лагает 30 секунд из-за VACUUM FULL. Liveness DOWN → K8s убивает pod → новый стартует, та же DB лагает → DOWN → убивает → loop. Все реплики падают за минуту.
Liveness зависит только от самого процесса. Внешние — только в readiness.
Health-probe делает бизнес-операцию
R-OBS-HC-X3: «давайте в probe создадим тестовый Product и удалим». Каждые 5 секунд × 10 реплик = ddos самих себя + грязные данные в аналитике.
Probe — light: SELECT 1, ping, cached value. Не бизнес-логика.
Что запрещено — таблица
| Антипаттерн | Правило | Что взамен |
|---|---|---|
| Business-state в health check | R-OBS-HC-X1 | техническое состояние; бизнес → SLO/alerts |
| Liveness зависит от DB/Redis | R-OBS-HC-X2 | только readiness зависит от внешних |
| Probe делает бизнес-операцию | R-OBS-HC-X3 | light probe (ping, cached value) |
| HealthIndicator без TTL-кеша | R-OBS-HC-2 | TTL 5–30 секунд на custom indicator |
/health без management-port изоляции | R-OBS-CFG-1 | отдельный :9090, закрытый network policy |
Нет /info с git-sha | R-OBS-HC-3 | InfoController с env-переменными из CI/CD |
Куда дальше
- Observability → раздел 4. Health checks — нормативные формулировки правил.
- Конфигурация — management-port, exposed endpoints, изоляция.
- Metrics — бизнес-метрики мониторятся через prom-client, не через health.
- Logging — nestjs-pino, structured JSON, DI-логгер.
- Tracing — OTel автоинструментация, manual spans, sampling.
- Context propagation — AsyncLocalStorage, requestId, userId в guard.
- SLO и алерты — бизнес-цели и error budget отдельно от probes.