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 для этого?

Связанные уроки

  • os-05-sync
Async Hooks: Трекинг асинхронности

0

1

Войти