Опирается на правила:
R-RES-HC-1…R-RES-HC-4иR-RES-HC-X1…R-RES-HC-X2из Resilience Rules → раздел 10. Health checks.
Важно знать
- На каждую внешнюю систему — отдельный
HealthIndicator-класс:SberHealthIndicator,OrderServiceHealthIndicator.- Probe cached с TTL ~30s через
{ result, at }+Date.now(). Не каждый/healthзапрос ходит в Sber.- Probe-метод — light:
GET /healthилиOPTIONS /на внешней системе, не бизнес-вызов (registerOrder,confirmPayment).- Health отражается в
/health/ready(readiness включает внешние системы) и/health/live(liveness — только приложение).- Внешние системы — только в readiness, не в liveness. k8s не должен убивать pod из-за того, что Sber лежит.
- Sync-probe без кеша = DDoS внешней системы: k8s probes каждые 10s × N pod'ов = десятки RPS только от health-check'ов.
- Business-операция в probe изменяет состояние, плодит тест-данные, искажает метрики.
Health check внешней системы — это способ k8s узнать, что pod не готов принимать трафик, не «глобальный pinger Sber». Поэтому он должен быть дешёвым (и для нашего приложения, и для внешней системы) и точным (отражать реальную готовность). Раскрытие раздела 10 гайда.
HealthIndicator на каждую внешнюю систему
R-RES-HC-1: на каждую внешнюю систему — отдельный класс, реализующий HealthIndicator из @nestjs/terminus.
import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult } from '@nestjs/terminus';
import { Agent, request } from 'undici';
@Injectable()
export class SberHealthIndicator extends HealthIndicator {
private cached?: { up: boolean; at: number };
constructor(private readonly agent: Agent) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
if (!this.cached || Date.now() - this.cached.at > 30_000) {
this.cached = { up: await this.probe(), at: Date.now() };
}
return this.getStatus(key, this.cached.up);
}
private async probe(): Promise<boolean> {
try {
const { statusCode } = await request('https://sber.example.com/health', {
dispatcher: this.agent,
method: 'GET',
headersTimeout: 3_000,
bodyTimeout: 3_000,
});
return statusCode >= 200 && statusCode < 300;
} catch {
return false;
}
}
}
@nestjs/terminus автоматически подберёт этот индикатор через HealthCheckService. Per-system подход — отдельный класс на sber, receipt, insurance. Единое имя системы для провайдера клиента, policy-набора и индикатора (R-RES-ISO-3).
Регистрация в HealthModule:
@Module({
imports: [TerminusModule],
controllers: [HealthController],
providers: [SberHealthIndicator, ReceiptHealthIndicator],
})
export class HealthModule {}
Cached probe с TTL 30s
R-RES-HC-2: probe cached, не ходит во внешнюю систему на каждый запрос.
Зачем кеш:
- k8s по умолчанию probes каждые 10s. С 5 pod'ами — 30 запросов в минуту к Sber только от health-check'ов.
- При TTL 30s — реальный probe к Sber раз в 30s независимо от частоты
/health-запросов. - 30s — компромисс: достаточно свежо чтобы заметить деградацию за разумное время, не нагружает внешнюю систему.
Реализация через { result, at } + Date.now() — самый простой вариант без внешних зависимостей. Подходит для большинства случаев.
private cached?: { up: boolean; at: number };
private readonly TTL_MS = 30_000;
private async cachedProbe(): Promise<boolean> {
if (!this.cached || Date.now() - this.cached.at > this.TTL_MS) {
this.cached = { up: await this.probe(), at: Date.now() };
}
return this.cached.up;
}
Альтернатива — NestJS CacheModule с TTL-конфигом, если кеш-инфраструктура уже есть в проекте. Для probe-кеша это избыточно.
Light probe — GET /health, не business
R-RES-HC-3: probe-метод — лёгкий технический endpoint, не бизнес-операция.
// ХОРОШО — light GET /health
const { statusCode } = await request('https://sber.example.com/health', {
method: 'GET',
dispatcher: this.agent,
});
// ХОРОШО — если у системы нет /health: OPTIONS на корень
const { statusCode } = await request('https://receipt.example.com/', {
method: 'OPTIONS',
dispatcher: this.agent,
});
Если внешняя система не предоставляет /health:
- OPTIONS на корневой endpoint — обычно отвечает немедленно без реальной обработки.
- GET самого дешёвого read-endpoint с минимальной нагрузкой (
?limit=1). - В крайнем случае — TCP-connect:
net.createConnection({ host, port })— убеждаемся, что порт слушает.
Никаких бизнес-операций:
- Не
POST /ordersс тестовыми данными — изменяет состояние, плодит мусор в БД Sber. - Не
GET /orders?customerId=health-check-test— нагружает систему, портит бизнес-метрики.
Структура /health endpoints в NestJS
R-RES-HC-4: liveness — только приложение, readiness — включая критичные внешние системы.
@Controller('health')
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly db: TypeOrmHealthIndicator,
private readonly sber: SberHealthIndicator,
private readonly receipt: ReceiptHealthIndicator,
) {}
@Get('live')
@HealthCheck()
liveness() {
return this.health.check([]); // только app alive — без внешних систем
}
@Get('ready')
@HealthCheck()
readiness() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.sber.isHealthy('sber'),
() => this.receipt.isHealthy('receipt'),
]);
}
}
k8s readinessProbe смотрит на /health/ready (включая внешние системы — pod выходит из Service backend pool, когда не готов принимать трафик). livenessProbe — на /health/live (только «приложение запущено» — k8s не убивает pod из-за того, что Sber лежит).
Манифест k8s:
livenessProbe:
httpGet:
path: /health/live
port: 3000
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 3000
periodSeconds: 10
failureThreshold: 2
Какие внешние системы включать в readiness — решение бизнес-домена:
- Без Sber OrderService физически не может принимать заказы → включаем в readiness.
- CustomerService использует ReceiptService только при определённых операциях → не включаем; handler сам вернёт 503 на конкретный endpoint.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Sync-probe без TTL-кеша (isHealthy ходит в Sber на каждый вызов) | R-RES-HC-X1 | { result, at } + TTL 30s |
Probe вызывает бизнес-операцию (registerOrder, getTransactions) | R-RES-HC-X2 | GET /health или OPTIONS / |
Внешние системы в /health/live (liveness) | R-RES-HC-4 | Только в /health/ready (readiness) |
Один общий HealthIndicator на несколько систем | R-RES-HC-1 | Per-system индикатор: SberHealthIndicator, ReceiptHealthIndicator |
HealthIndicator кидает исключение наружу (не обёрнут в try/catch) | R-RES-HC-1 | catch → return this.getStatus(key, false) |
probe() без timeout (зависание = весь health-check зависает) | R-RES-HC-2 | headersTimeout + bodyTimeout на undici-агенте |
Куда дальше
- Observability —
prom-clientметрики для CB и bulkhead; OTel-spans на adapter-методах. - Per-system isolation — структура per-system провайдеров, именование DI-токенов.
- Async и polling — task-queue вместо
setTimeout-цикла в handler. - Circuit breaker —
BrokenCircuitError→ port-исключение → 503/409. - Timeouts — иерархия
connect < headers < body < total. - Bulkhead — cockatiel
bulkhead(maxConcurrent, queueLimit)per-system. - Retry —
ExponentialBackoff, только при идемпотентности, не на 4xx. - Fallback — деградация без money-операций и тихого «успеха».
- Configuration — декларативный конфиг, zod/class-validator, per-system override.
- OpenAPI generator binding —
openapi-typescript, mapper DTO → domain. - Where protection goes — где именно в слоистой архитектуре NestJS стоит защита.