awesome-everything EN
↑ Обратно к восхождению

Производительность

Кросс-протокольный N+1: HTTP fan-out и Redis MGET

Суть Форма N+1 появляется в HTTP microservice fan-out, Redis key lookups и gRPC streaming — семейство фиксов то же самое: собрать, батчить, отправить один раз.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 11 min

REST endpoint собирает профиль пользователя, вызывая 8 downstream микросервисов — один для preferences, один для posts, один для notifications, один для billing и ещё четыре. Каждый вызов занимает 30 мс. Итоговая latency: 240 мс. Ни один вызов не медленный. Проблема в том, что все они серийные.

Форма появляется в каждом протоколе

Паттерн N+1 — не проблема только для баз данных. Везде, где программа делает множество небольших round-trip’ов вместо одной более крупной операции, применяется тот же множитель стоимости.

Overhead на round-trip различается по протоколу и расстоянию: Postgres на localhost ~0.5 мс, Redis на том же хосте ~0.1 мс, HTTP intra-DC ~2 мс, HTTP cross-region ~50 мс. Но математика та же: N вызовов × overhead на вызов = серийное wall-clock доминирует.

HTTP fan-out: вызывать сервисы параллельно

Profile aggregator, вызывающий 8 сервисов серийно:

// Серийно — платит 8 × RTT последовательно:
const user = await userService.get(userId);
const posts = await postsService.get(userId);
const notifs = await notifService.get(userId);
// ... ещё 5 вызовов
// Итого: ~240 мс при 30 мс на вызов
// Параллельно — wall-clock = max(latencies):
const [user, posts, notifs, ...rest] = await Promise.all([
  userService.get(userId),
  postsService.get(userId),
  notifService.get(userId),
  // ... ещё 5
]);
// Итого: ~35 мс (самый медленный вызов + небольшой coordination overhead)

Promise.all (Node/JS), errgroup.Wait (Go), CompletableFuture.allOf (Java), asyncio.gather (Python) — паттерн идентичен во всех runtime.

Wall-clock меняется с sum(latencies) до max(latencies).

Для 8 вызовов по 30 мс: 240 мс серийно против 35 мс параллельно — улучшение в 7 раз с одним структурным изменением.

ПротоколПаттерн N+1Батч-фикс
SQL / ORMLazy load на строкуJOIN / IN / preload / DataLoader
HTTP микросервисыСерийные вызовы сервисовPromise.all / errgroup / gather
RedisGET в циклеMGET / pipeline
gRPCUnary вызов на строкуBatch RPC / server streaming
File I/Oopen/read/close на файлio_uring batched submission

Redis: MGET вместо GET в цикле

Кэш-слой, получающий 100 элементов по одному:

# 100 round-trip'ов — GET в цикле:
items = keys.map { |k| redis.get(k) }

# Один round-trip — MGET:
items = redis.mget(*keys)

# Или pipeline для условной логики на элемент:
results = redis.pipelined { keys.each { |k| redis.get(k) } }

Redis RTT на том же хосте обычно 0.1–0.5 мс. Цикл из 100 GET’ов стоит 10–50 мс. Один MGET стоит 0.5–2 мс. Фикс — одна команда.

Для условной логики (когда нужно действовать по каждому результату перед следующим получением), используйте pipelining вместо MGET: отправить все команды сразу, получить все ответы сразу.

Зависимости вызовов сервисов — DAG-dispatch

Некоторые вызовы сервисов зависят от других. Цепочку зависимостей нельзя полностью распараллелить:

// Последовательная зависимость: посты нужны userId сначала
const userId = await authService.resolveToken(token);
// Затем эти можно запустить параллельно:
const [posts, notifs, billing] = await Promise.all([
  postsService.get(userId),
  notifService.get(userId),
  billingService.get(userId),
]);

Структура — DAG (directed acyclic graph). Сервисы без зависимостей стартуют сразу; сервисы, зависящие от более ранних результатов, ждут только своих прямых родителей. Большинство fan-out API имеют мелкие DAG’и (1–2 уровня зависимостей). Полностью серийные цепочки вызовов обычно случайны и могут быть разобраны трассировкой реальных зависимостей.

Почему это работает

LinkedIn feed-aggregator incident 2023: сервис вызывал 8 downstream микросервисов серийно, ~60 мс каждый. p99 был 480 мс. После распараллеливания с errgroup, p99 упал до ~80 мс — самый медленный одиночный вызов плюс coordination overhead. Это тот же класс фикса, что добавление .includes к ORM-запросу, применённый на один уровень протокола выше.

Governance: предотвращение возврата серийного fan-out

Статический анализ может поймать паттерн до выкатки:

  • JavaScript/Node: правило линтера, отклоняющее await внутри for-цикла над вызовами сервисов.
  • Пункт code review checklist: “вызывает ли эта функция удалённый сервис N раз в цикле? Если да — сигнализировать.”
  • Observability: панель дашборда по сервису, показывающая “fan-out factor” (downstream вызовов на входящий запрос). Алерт при росте.
  • Load-test assertions: trace assertions в нагрузочных тестах могут провалить PR’ы, увеличивающие fan-out сверх порога.
Викторина

REST endpoint вызывает 8 downstream микросервисов серийно, каждый занимает 30 мс. Итоговый p99 — 240 мс. Какой наиболее прямой структурный фикс?

Викторина

В цикле вызывается redis.get(key) для каждого из 100 элементов. Какой нативный Redis фикс на один trip?

Вспомните перед уходом
  1. 01
    Объясните, почему Promise.all снижает latency HTTP fan-out, и опишите изменение wall-clock.
  2. 02
    Что является Redis-эквивалентом SQL eager loading, и когда вместо него использовать pipelining?
Итог

N+1 — протокол-агностичный паттерн: серийные HTTP вызовы микросервисов, Redis GET в цикле и gRPC unary вызов на строку — все платят overhead round-trip N раз. Для HTTP fan-out, Promise.all и его эквиваленты меняют серийный sum(latencies) на параллельный max(latencies). Для Redis, MGET получает несколько ключей одной командой. Для gRPC, server streaming или batch RPC заменяет per-row unary вызовы. Семейство фиксов всегда одно: определить серийные round-trip’ы, собрать ID или вызовы, отправить один раз, распределить результаты. Протокол меняется; форма и семейство фиксов — нет.

Связанные уроки
встречается в159
Продолжить восхождение ↑N+1 в масштабе: исчерпание пула, изменения планов и денормализация
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.