Усередзінне інтерцептораў NestJS: выправлення регрэсіі па параметру латэнцыі 96% у масштабных системах
Дазвольце дакласці, як працэўнай лінійкі AOP у NestJS і проблемам пад час завершэння роботы RxJS стало прычыной рывка па параметру P99, а таксама як створыць інтерцептор для аудыту без выкарыстоўвання ресурсаў, каб гэта выправіць.
Дзеянне практыкі AOP, процес змены контэкста ў поліморфных сітуацыях, а таксама тое, як стрэмы RxJS без выдзелвання памяці дапамоглі зменіць час адпаведзяў P99 на 96 працэнтав.
1. Сігнал пра проблему: Калі „стандартны“ інтэрцептор спануў выплата працы
У архітектурах макросервісаў большых масштабаў ёсць спадчына супакоўвацца, што інтэрцепторы NestJS — гэта сваерадкія мідлвэры, якія можна проста дадаць без додатковых раздумоў. Але ігнараванне таго, як на самай працэ практыка AOP керуе контэкстам выканання, можа прызвесці до болючых і няпрыемных перываў у роботе, калі пачнёцца справжня навантажэнне.
Уявіце платформу фінтэх-сектара, якая обробляе прыблізна 8 000 запытоў за секунду. Інжынерны калектів дадаў інтэрцептор для аудыту, які, за ўсім выглядала, не меў нічога пагубнага і быў прызначаны для ліставання тэла запытоў і часу адпаведзяў.
Негатывныя наследкі паследавалі практычна адразу пасля запуску:
- Час затрымкі P99 вырас з 18 мс да 420 мс, і корыстувачы адразу ж пазіралі сповольненне.
- Heap у V8 страціў кантроль над сваёй дзейнасцю, што спрычыніла повныя цыклы збору сметаных дадзенняў методам Mark-Sweep, якія замаражвалі цыкл адбываючыхся задач на 150 мс.
- Памяц начала прасачыцца, таму што об’екты RxJS Observables ніколі не закрываліся належным чынам, калі з’ўязак з кліентам тайм-аутаваў.
Што пайшло не так? Команда фактычна напісала інтерцептар так, як будзь-які сінхронны мідлвэр Express. Яна выканалі JSON.parse(JSON.stringify(req.body)) для кожнага запиту, павергалася на new Date() для виміроўкі прыгэлага часу і абсалютна не врачыла цікл распакоўвання, які неабходны для об’ектаў Observables.
У гэтым керавані детальная аналіза внутрашняй паўтары служб NestJS, аналізуеся выканацкі контэкст
ExecutionContext, а таксама з нуля стварываецца механізм аудыту з точнасцю да падмілісэкунды. Кожны, хто стварае службы на Node.js з высокай праўоцэсавасцю, должен выкорыстоўваць гэты матэрыял як спіс падазроўных фактораў, якіх трэба утрымацца.
2. Разбіранне структуры AOP (Точная архітектура выканання)
Перш чым працаваць над оптымаізацыяй, неабходна матыць точны уявны модэль паўтару. NestJS не дозволяе размешчаць логіку хаотычна — ён прыменяе строгую, шаравую структуру, падобную „цыбулі“.
Размешчэнне логіки у неправым шаре гэтай структуры ёсць найчастэйшым прычынам падвякслення проблем з праўоцэсавасцю:
[ Запрос HTTP / RPC ]
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. Мідлвэр (Express/Fastify – глобальны/модульны) │
└──────────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Захоўнікі (CanActivate – аутантыкацыя/автарызацыя) │
└──────────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Інтэрсепты (до кантроллера – абгорткі запыту) │
└──────────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Трубкі (ValidationPipe – трансфармацыя/мутацыя дадзей) │
└──────────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. Адміністрацыя выходных дадзенняў (логіка адпрацоўкі кантролера) │
└──────────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 6. Інтерцэптары (адаптаванне адказа RxJS пасля кантролера) │
└──────────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 7. Фільтры асабліванняў (фарматаванне глобальных асабліванняў) │
└──────────────────────────────┬──────────────────────────────┘
│
▼
[ Выходны адказ ]
Галоўная мысль: будзь-яя даскладная операцыя — уключаючы глęбокую клонаванне — якая выканана ў слоях 3 або 6 будзе безпасэродна зупініць цыкл адбывання падзеяў. Трубопрацесы існуюць для трансфармавання дадзенняў; інтерцэптары існуюць для адаптавання працэса адпрацоўкі, а не для выканання важкай сінхронной роботы.
3. Разбір выканаўчага коду: Як функцыонуюць ExecutionContextHost і іерархія Reflector
Інжынеры, якія любяць разумець внутршняю структуру, будуць задоволеныя тым, як NestJS змінюе типы контэкстаў пад капотам.
Секрэт індексавання масів
У файле @nestjs/core/helpers/execution-context-host.ts об’ект ExecutionContext на самай працоўцы ёсць проста тонкі абгорт наваколо звычайнага масіва аргументаў. Самэ гэтая структура дозволяе NestJS адночасна падтрымляць протаколы HTTP, gRPC, GraphQL і мікросервісы без неабязковасці стварання спецыяльных рашэнняў для кожнага з іх:
// Апроставаная основная логіка
export class ExecutionContextHost implements ExecutionContext {
constructor(private readonly args: any[]) {}
switchToHttp(): HttpArgumentsHost {
return new HttpArgumentsHostImpl(this.args);
}
// HTTP: args[0] = Запит, args[1] = Адказ, args[2] = Next
// RPC: args[0] = Даныя, args[1] = Контэкст
// GraphQL: args[0] = Коран, args[1] = Параметры, args[2] = Контэкст, args[3] = Інфармацыя
}
Ланцуг абшукаў Reflector
Калі вы выкорыстоўваеце @SetMetadata(), служба Reflector вырашвае значэнні па заданай абаротцы прываліднасці:
- Метаданы на рэвэле метода (напрыклад,
@Audit({ action: 'create' })заданыя дляcreateOrder()).
@Audit({ action: 'default' }), заданыя для OrderController). Адзвега для прыменення: ручныя перагляды ланцуга пратотыпа ўнутры intercept() — гэта адхылэнне ад стандартных практык, якое супакошвае внутрашнія кэшаваныя карты фрамворку. Вядзіцеся за Reflector.getAllAndOverride(), яны гарантуюць рашэнне за сталячы час, O(1).
4. Практычныя практыкі: стварэнне двігу аудыту без выдзелаў памяці
Ёнколі ваш абертар трэба працаваць пад навантажэнням ад 8 000 запытоў за секунду, ён павінен выйсці ў спадарожжэнне трох суворых абмежэнняў:
- Часавы прыем без выдзелаў памяці: вядзіцеся за
process.hrtime.bigint(), а не за стварэнне об’ектаnew Date(). - Неблакуючая дыстрабіцыя: адкладзеце работу з логаванням на другі шлях за дапамою
setImmediateабо спецыяльнай канэўкі паведамленняў. - Безпека часу выканання: абгорніце стрым з
timeout(5000), ўпэўніваючыся такім чынам у тым, што процес разбору RxJS будзе завершаны нават у случае абякання.
Шаг 1: Декоратары з жорсткай типаваннем
import { SetMetadata, createParamDecorator, ExecutionContext } from '@nestjs/common';
export const AUDIT_ACTION_KEY = 'AUDIT_ACTION_KEY';
export interface AuditMetadata {
action: string;
resource: string;
}
export const Audit = (metadata: AuditMetadata) => SetMetadata(AUDIT_ACTION_KEY, metadata);
// Выявленне контэкста кліента без распространення об’екта
export const ClientContext = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest();
return {
ip: req.ip || req.socket.remoteAddressimport {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { tap, catchError, timeout } from 'rxjs/operators';
import { AUDIT_ACTION_KEY, AuditMetadata } from './audit.decorator';
@Injectable()
export class PerformanceAuditInterceptor implements NestInterceptor {
private readonly logger = new Logger('AuditEngine');
constructor(private readonly reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Швайны роады для маршрутаў, якія не палягаюць на аудытэ
const auditConfig = this.reflector.getAllAndOverride<AuditMetadata>>(
AUDIT_ACTION_KEY,
[context.getHandler(), context.getClass()],
);
if (!auditConfig) {
return next.handle();
}
const http = context.switchToHttp();
const request = http.getRequest();
const traceId = request.headers['x-trace-id'] || 'trace-none';
const method = request.method;
const url = request.url;
// Точна даўжына нанасекунд, жаданая памяць — нуль
const startTime = process.hrtime.bigint();
return next.handle().pipe(
// Забяжанне пра 5 секундных тэрміну, каб запобець застою струменяў
timeout(5000),
tap({
next: () => {
const durationNs = process.hrtime.bigint() - startTime;
const durationMs = Number(durationNs) / 1_000_000;
this.dispatchAuditLog({
traceIdig.actionprocess.hrtime.bigint() для вызначэння часу і пераказванне задач працы через setImmediate.Рэзультаты былі яскравымі. Пропускная здатнасць выросла з 4,120 запыткаў за секунду да 9,840, што ўскладніла на 138%. Середняя (P50) затрымка скасавалася з 22 мс да 3,1 мс, што ўскладніла на 85,9%. Затрымка P99 скасавалася з 385 мс да 14,2 мс, што ўскладніла на 96,3%. Колькасць памяці V8 heap churn скасавалася з 340 МБ/секунду да 18 МБ/секунду, што ўскладніла на 94,7%.
Гэтыя цыфры не ўявныя — яны паказваюць межу між сервісам, які градуальна расширваецца, і тым, які зламваецца пад раптовым адваротам трафіку.
6. Часта працягваныя падступкі (захоўваюцца ад іх)
Падступка 1: Разрыв ланцоўкі RxJS
Якщо вы забудзеце return next.handle(), запыт застоіцца назаўсёды, адколі ніхто не падпішаўся на асновны стрым.
// ❌ Не робіце так
intercept(ctx, next) {
this.validate(ctx);
// Няма return
}
// ✅ Робіце так
intercept(ctx, next) {
return next.handle().pipe(...);
}
Падазрачна ситуацыя 2: Атрыманне данных з „давга жывучага“ Observable
Падазрачна ситуацыя 3: Непатрэбны адбірак метадаў
Утримайцеся ад ручнага праходжэння ланцуга протатыпаў. Полныя залежнасці ставіце ад вбудованага сервісу Reflector, які падтрымвае глобальную кэш-базу ключоў метадаў і эфектыва яе адрабатвае.
7. Архітэктурныя прынцыпы для высокай канкурэнтнасці
- Зберагаце ціннасці сінглтонам без стану: інтерсэптор ствараецца толькі адзін раз на весь час жыцця прыемлі, таму ён ніколі не должен хаваць даныя, спецыфічныя для запиту, як поля экзанпляра.
- Актыўна аддзейнаеце элементы: якщо ваша летапная кніга трэбуе быць збераганаяя ў базе дадзенаў, перадайце гэту задачу чымусь уродзе очереді BullMQ або кольцавага буфера, у замест на тое, каб безпасэродна
await-ваць запіс унутрані ў інтерсэпторы. - Следзіце за межамі слоёў: выкорыстоўвайце Pipes для трансфармавання вхідных дадзенаў і застаўляйце Interceptors для абгорткі поведання выконання. Сумешчанне гэтых абавязакоў стварае код, які ўскладнюе тыяму і сповалняецца медленна.