Express.jsSeniorExperience

Как понять, что проблема в сервисе связана с Express.js, а не с базой, сетью, инфраструктурой или бизнес-логикой?

Локализация начинается с timing middleware и измерения event loop lag. Синтетический эндпоинт без I/O изолирует Express от внешних зависимостей; дальше — EXPLAIN ANALYZE в БД, CPU profiler и curl с фазовыми таймингами.

Методология локализации проблемы

Диагностика начинается с разделения временных затрат на составляющие. Проблема может находиться в одном из четырёх слоёв: Express/Node.js (CPU, event loop), база данных (медленный запрос, locks), сеть/инфраструктура (latency, DNS, TLS) или бизнес-логика (алгоритм, внешний API).

Шаг 1: Измерить time-to-first-byte на каждом слое

Добавить timing middleware, который записывает длительность каждой фазы:

import { performance } from "perf_hooks";

app.use((req, res, next) => {
  const start = performance.now();
  res.on("finish", () => {
    const duration = performance.now() - start;
    // Лог с методом, путём, статусом и временем
    console.log(
      JSON.stringify({ method: req.method, path: req.path, status: res.statusCode, ms: duration.toFixed(2) })
    );
  });
  next();
});

Если логи показывают, что 95-й перцентиль latency — сотни миллисекунд, а P50 — нормальный, это указывает на периодическую проблему (GC-пауза, lock contention, медленный запрос к внешнему API).

Шаг 2: Проверить event loop lag

Event loop lag >10ms — признак CPU-heavy кода или синхронных операций в обработчике:

import { monitorEventLoopDelay } from "perf_hooks";

const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();

setInterval(() => {
  console.log("EL lag p99 ms:", (h.percentile(99) / 1e6).toFixed(2));
  h.reset();
}, 5000);

Если lag стабильно низкий — проблема не в Node.js процессе.

Шаг 3: Изолировать I/O от приложения

Сделать запрос к БД напрямую (psql, Redis CLI, mongosh) и замерить время:

# PostgreSQL: план выполнения с реальными данными
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 42;

# Redis: измерить latency
redis-cli --latency-history -i 1

# curl с таймингами по фазам
curl -w "@curl-format.txt" -o /dev/null -s https://example.com/api/items

curl-format.txt:

time_namelookup: %{time_namelookup}\n
time_connect: %{time_connect}\n
time_appconnect: %{time_appconnect}\n
time_pretransfer: %{time_pretransfer}\n
time_starttransfer: %{time_starttransfer}\n
time_total: %{time_total}\n

Шаг 4: Профилировать Node.js процесс

Если event loop lag высокий — собрать CPU profile:

# Запустить с inspector
node --inspect server.js
# Подключиться в Chrome DevTools -> Performance -> Record
# Или использовать 0x для flame graph
npx 0x -- node server.js

Шаг 5: Проверить инфраструктуру

  • DNS-резолюцию внешних зависимостей: медленный DNS приводит к задержкам при первом запросе после TTL.
  • Connection pool: исчерпание пула соединений к БД заставляет запросы ждать свободного слота.
  • TLS handshake: если upstream HTTPS без keep-alive, каждый запрос платит 100–200ms за handshake.

Как отличить Express от остального

Если добавить эндпоинт без логики и I/O (res.json({ok:true})) и он отвечает быстро — Express и Node.js в порядке, проблема в обработчике. Если и этот эндпоинт медленный — проблема в уровне Node.js/системы (нехватка памяти, swap, перегруженный CPU).

Подводные камни

  • Синхронные методы fs (readFileSync, existsSync) в middleware блокируют event loop для всех параллельных запросов.
  • Медленный middleware (например, медленная десериализация JWT с RS256 при каждом запросе) сдвигает latency всего сервиса, хотя виновата не бизнес-логика.
  • Connection pool PostgreSQL/MySQL по умолчанию мал (pg: 10 соединений) — при высоком параллелизме запросы стоят в очереди внутри приложения, а не на уровне БД.
  • DNS-кэш в Node.js отсутствует по умолчанию — каждый запрос к внешнему сервису выполняет резолюцию; dns.lookup с кэшем или использование --dns-result-order=ipv4first решает проблему.
  • console.log синхронен в Node.js — при интенсивном логировании каждой строки запроса он может стать узким местом.
  • Memory leak в замыканиях middleware (накопление обработчиков событий) вызывает деградацию через GC-паузы, которые выглядят как периодические всплески latency.
  • Если сервис работает за nginx, проверить upstream keepalive — без него nginx создаёт новое TCP-соединение на каждый proxied запрос.

What hurts your answer

  • Сразу обвинять Express.js, не проверив соседние слои системы
  • Чинить симптом без минимального воспроизведения и evidence
  • Не учитывать версии, конфигурацию, окружение и recent changes

What they're listening for

  • Умеет локализовать проблему вокруг Express.js
  • Двигается от симптома к гипотезам и проверкам
  • Отличает баг инструмента от ошибки использования или окружения

Related topics