Производительность
N+1: диагностика, батчинг и гейт
Читать про N+1 — не то же, что вытащить 300 запросов из одного page load. Построй небольшой сервис, демонстрирующий N+1 в трёх формах — ORM-страница-список, дерево GraphQL-резолверов и service fan-out — затем сбей число запросов и загейти так, чтобы оно не вернулось.
Превратить модель юнита в воспроизводимый цикл: посчитать round-trip’ы на request, выбрать семейство фикса по cardinality и происхождению lookup’а, доказать падение счётчика по query log, затем добавить CI-гейт и DoS-guard, чтобы регрессия не доехала до релиза.
Возьми небольшой сервис (свой или стартовую форму ниже) с тремя намеренными точками N+1 — ORM-эндпоинт-список, вложенный GraphQL-запрос и последовательный service/cache fan-out — и доведи число round-trip'ов каждой до структурного минимума, доказывая каждый шаг query log'ом и числами до/после.
- Таблица до/после на каждую точку: запросов на request, p99 latency и (для fan-out) wall-clock — измеренные из query log и load test, а не оценочные.
- Query log после каждого фикса показывает число round-trip'ов на структурном минимуме (2–4 для ORM-страницы, один на тип для DataLoader, один батч/параллельный dispatch для fan-out).
- Batch-функция DataLoader проверена на возврат результатов в порядке входных ids с null'ами для отсутствующих ids, а её кэш request-scoped (без утечки между request'ами).
- CI-гейт продемонстрирован: роняет PR, повторно вводящий N+1, затем проходит после восстановления фикса.
- Абзац-резюме, называющий, какое семейство фикса применено в каждой точке и почему оно побило альтернативы для этой cardinality и происхождения lookup'а.
- Добавь DoS-amplification guard на GraphQL-эндпоинт — depth limit, потолок query-complexity и per-request query budget — и покажи, что сконструированный глубоко вложенный запрос отклоняется до удара по БД.
- Воспроизведи инцидент исчерпания пула соединений: задай малый пул, нагрузи N+1-версию до 503, затем покажи, как фиксированная версия держит ~10× пропускной способности на том же пуле, с метриками насыщения пула до/после.
- Прогони EXPLAIN ANALYZE на новом IN-list запросе при 10, 500 и 5000 родительских ids и отчитайся, переключается ли plan (index → bitmap → seq scan); задокументируй любой размер, где фикс деградирует.
- Добавь APM-трейс (Tempo/Datadog/Honeycomb или OpenTelemetry) и сними waterfall до и после — высокую колонну коротких DB-спанов, схлопывающуюся в несколько — как визуальное доказательство рядом с числами.
Это цикл, который ты гоняешь в каждом реальном инциденте N+1: сначала инструментируй query count, подбери семейство фикса под cardinality и происхождение lookup’а (батчи ORM-страницу, DataLoader’ь дерево резолверов, параллель fan-out), докажи падение числа round-trip’ов по логу, затем загейти в CI, чтобы регрессия не переехала повторно. Сделать это однажды через все три формы протокола превращает продакшн-версию в мышечную память — а CI-гейт это то, что не даёт выигрышу размыться в следующем квартале.