Node.js Internals
Async Hooks: Трекинг асинхронности
Представь: у тебя 1000 одновременных HTTP запросов. Каждый запрос делает 5-10 async операций (БД, Redis, HTTP calls). Как отследить, какой лог принадлежит какому запросу? Как измерить, где тормозит конкретный запрос? Обычные подходы (передавать requestId через все функции) превращают код в ад. Async Hooks решает эту проблему **элегантно**.
- **APM системы** (DataDog, New Relic, Sentry) используют async_hooks для автоматического трейсинга без изменения прикладного кода
- **NestJS** использует AsyncLocalStorage для Dependency Injection и REQUEST scope providers
- **Prisma** использует async_hooks для автоматического управления database transactions в async коде
- **Distributed tracing** (Jaeger, Zipkin) в микросервисах - без async_hooks невозможно связать запросы между сервисами
Зачем трекить асинхронность
В Node.js каждая асинхронная операция создаёт **невидимую цепочку вызовов**. Когда запрос проходит через `async` функции, промисы, таймеры и коллбэки - контекст теряется. Как понять, какой логгер принадлежит какому запросу? Как измерить время выполнения асинхронной цепи? Как отследить утечки памяти в промисах?
**Async Hooks** - это низкоуровневый API, который даёт вам рентген асинхронности. Он показывает рождение и смерть каждого промиса, каждого таймера, каждого TCP соединения. Это инструмент для профайлеров, APM систем и request tracing.
**Ключевая идея:** Каждая асинхронная операция в Node.js - это **ресурс** с уникальным ID. Async Hooks позволяет подписаться на события жизненного цикла этих ресурсов.
Каждый `asyncId` уникален, `triggerAsyncId` показывает родителя. Так Node.js строит **асинхронное дерево вызовов**.
Что происходит, когда промис создаётся внутри setTimeout?
Жизненный цикл асинхронного ресурса
Каждый асинхронный ресурс проходит четыре стадии: **init → before → after → destroy**. Это похоже на pipeline процессора, но для асинхронных операций.
**Важно:** `before` и `after` могут вызываться **несколько раз** для одного ресурса (например, у стрима или сокета), но `init` и `destroy` - **ровно один раз**.
**Performance warning:** Async Hooks имеют накладные расходы ~10-30%. Используйте только для debugging и profiling, не в продакшене без необходимости.
Почему before/after могут вызываться несколько раз для одного asyncId?
createHook API и executionAsyncId
API `async_hooks.createHook()` принимает объект с коллбэками. Кроме `init/before/after/destroy`, есть два важных helper-метода: `executionAsyncId()` и `triggerAsyncId()`.
**Паттерн:** Используйте `executionAsyncId()` как ключ для хранения контекста запроса. Это позволяет связать логи, метрики и трейсы с конкретным HTTP запросом.
Пример: request tracing для HTTP сервера. Каждый запрос получает уникальный ID, который пробрасывается через всю async цепочку.
**Проблема этого подхода:** Нужно вручную чистить Map, иначе утечка памяти. Для продакшена лучше использовать `AsyncLocalStorage` (следующий концепт).
Что вернёт executionAsyncId() внутри промиса, созданного в setTimeout?
AsyncLocalStorage: Context без боли
`AsyncLocalStorage` - это **высокоуровневая обёртка** над async_hooks, которая решает проблему прокидывания контекста через async цепочки. Это как thread-local storage, но для асинхронного кода.
**Ключевое отличие:** С AsyncLocalStorage не нужно вручную управлять Map и чистить память. Node.js делает это автоматически.
**Магия:** Контекст автоматически пробрасывается через `await`, `.then()`, `setTimeout()`, `setImmediate()`, EventEmitter и даже через worker threads (с ограничениями).
**Production паттерн:** Используйте AsyncLocalStorage для request tracing, логирования, метрик и feature flags. Это стандарт де-факто в современном Node.js.
В чём главное преимущество AsyncLocalStorage перед ручным Map + async_hooks?
Debugging асинхронных потоков
Async Hooks - мощный инструмент для **debugging сложных async багов**: утечки памяти, race conditions, потерянные промисы. Можно построить карту всех активных ресурсов и найти, что не чистится.
**Паттерн:** Если видите утечку типа `PROMISE`, ищите промисы без `.catch()` или забытые listeners на EventEmitter.
Другой use case - **визуализация async flow**. Можно построить граф зависимостей между ресурсами и понять, почему запрос висит.
**Overhead:** Трекинг всех ресурсов может создать сотни тысяч записей в графе. Для продакшена используйте sampling (отслеживайте только 1% запросов).
Как найти утечку памяти с помощью async_hooks?
Production паттерны
В продакшене async_hooks используется для **APM систем** (Application Performance Monitoring), **distributed tracing** и **context propagation**. Вот проверенные паттерны.
Паттерн 1: Request Context Middleware
Паттерн 2: Distributed Tracing (OpenTelemetry style)
Паттерн 3: Feature Flags с контекстом
**Real-world examples:** AsyncLocalStorage используется в **NestJS** (для DI контекста), **Prisma** (для transaction management), **Sentry** (для error context), **Pino** (для structured logging).
**Performance tip:** Используйте `als.enterWith()` вместо `als.run()` если нужно установить контекст без создания нового scope. Это немного быстрее.
AsyncLocalStorage работает как thread-local storage в Java - каждый поток имеет свою копию переменной
AsyncLocalStorage привязан к **async execution context**, не к физическим потокам. Один Node.js процесс (один поток) может иметь сотни изолированных async контекстов
Node.js однопоточный (event loop), но async_hooks создаёт **виртуальные контексты** для каждой async цепочки. Это ближе к 'continuation-local storage', чем к thread-local.
Для чего используется AsyncLocalStorage в production приложениях?
Ключевые идеи
- **Async Hooks** даёт доступ к жизненному циклу асинхронных ресурсов (промисы, таймеры, сокеты) через коллбэки `init/before/after/destroy`
- **executionAsyncId()** возвращает ID текущего async контекста, **triggerAsyncId()** - ID родителя. Так строится async дерево вызовов
- **AsyncLocalStorage** - высокоуровневая обёртка над async_hooks для пробрасывания контекста без явной передачи параметров
- **Use cases:** request tracing, structured logging, distributed tracing, APM, debugging утечек памяти, feature flags с контекстом
- **Trade-off:** Async Hooks добавляет 10-30% overhead. Используйте sampling в продакшене или только для критичных операций
Связанные темы
Async Hooks тесно связаны с другими аспектами Node.js:
- Event Loop — Async Hooks отслеживают фазы event loop: timers, I/O, setImmediate. Понимание event loop необходимо для понимания, когда вызываются before/after
- Promises & Async/Await — Каждый промис создаёт async ресурс. Понимание промисов помогает понять, почему before/after вызываются для .then()
- Streams — Стримы - это long-lived ресурсы, которые вызывают before/after многократно для каждого chunk
- Cluster & Worker Threads — AsyncLocalStorage НЕ работает между worker threads по умолчанию - нужно явно передавать контекст
Вопросы для размышления
- Как реализовать request tracing в микросервисной архитектуре с помощью AsyncLocalStorage?
- В каких случаях async_hooks может привести к утечкам памяти? Как этого избежать?
- Почему AsyncLocalStorage не работает между worker threads? Как можно решить эту проблему?
- Задача: измерить время выполнения каждой функции в async цепочке. Как использовать async_hooks для этого?