API
GraphQL N+1: сбатчь и закали API
Читать про N+1 — не то же самое, что смотреть, как лог БД выстреливает 51 раз на один HTTP-запрос, а потом загнать его обратно к 2. Построй небольшой GraphQL API на намеренно вложенных данных, измерь шторм, сбатчь его DataLoader, затем закали против форм запроса, которых DataLoader не касается — с доказательством на каждом шаге.
Преврати ментальную модель юнита в воспроизводимый цикл: воспроизведи N+1 и докажи его счётчиками SQL/резолверов, почини корректным пер-запросным DataLoader, защити API от depth, complexity и alias-атак и подтверди всё числами до/после.
Построй (или возьми) GraphQL API над реляционной схемой с вложенными списками, воспроизведи его шторм N+1 под нагрузкой, схлопни его DataLoader и добавь защиты по форме запроса — доказывая каждый шаг измеренными счётчиками SQL, счётчиками резолверов и задержкой.
- Таблица до/после: суммарно SQL-запросов, счётчики вызовов резолверов и p99 задержка для одного и того же запроса на 50 постов под одинаковой нагрузкой — измеренные со стенда, а не оценённые.
- DataLoader'ы инстанцируются на каждый запрос в context factory (продемонстрировано), а тест доказывает, что инстанс в module scope протёк бы между двумя симулированными tenant'ами или отдал бы устаревшую строку.
- Тесты контракта batch проходят: dedup возвращает одно обращение к БД для дважды упомянутого автора, а тест строк не в порядке всё равно мапит каждого автора корректно.
- Три собранных запроса-атаки (глубокий рекурсивный запрос, 5-уровневый запрос с first:100 и документ на 1000 алиасов) каждый отклонены на валидации с названной защитой, которая его поймала.
- Раздели схему на два subgraph'а Apollo Federation (posts и users), подтверди, что router батчит кросс-субграфовые ссылки в один вызов _entities, и покажи, что __resolveReference всё равно нуждается в своём DataLoader, чтобы избежать внутрисубграфового N+1.
- Реализуй resolver lookahead для одного глубокого пути (posts { author { profile } }), читая info AST и выдавая один JOIN; измерь его против трёх стопкой DataLoader-обращений и отметь компромисс по изоляции.
- Добавь multi-tenant колонку и докажи, что изоляция tenant'ов принадлежит SQL-фильтру внутри batch-функции, а не только пер-запросному scope — напиши тест, падающий при изоляции только через scope и проходящий с фильтром tenant_id.
- Подключи CI-гейт, который гоняет запрос на 50 постов против canary и валит сборку, если число вызовов любого type.field-резолвера выросло выше базы (ловя регресс N+1 до релиза).
Это цикл, который ты запускаешь в каждом реальном инциденте производительности GraphQL: сначала построй стенд доказательств (счётчики SQL и резолверов), воспроизведи и измерь N+1, почини пер-запросным DataLoader, написанным по контракту порядка-и-формы, докажи корректность тестами dedup и строк не в порядке, затем наслои защиты по форме запроса, которые DataLoader не заменяет — depth, мультипликативный complexity, алиас-капы. Подтверди числами до/после под идентичной нагрузкой. Сделав это раз на игрушечном API, ты доводишь production-диагностику до уровня мышечной памяти.