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

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

N+1: чтение кода и логов

Суть Читай реальные ORM-циклы, batch-функцию DataLoader, query log и fan-out-хендлер — предскажи число round-trip и выбери фикс с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

N+1 диагностируется чтением кода и логов, а не догадками. Прочитай сниппет или трейс, посчитай round-trip’ы, которые он выпустит, затем выбери фикс, к которому сениор тянется первым.

Цель

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

Сниппет 1 — невинный цикл

const orders = await Order.findAll({ where: { userId } }); // 50 строк
for (const order of orders) {
  const customer = await order.getCustomer(); // один запрос на order
  render(order, customer.name);
}
Викторина

На 50 заказах сколько запросов это выпустит, и какой единственный фикс с наибольшим рычагом?

Сниппет 2 — batch-функция DataLoader

const userLoader = new DataLoader(async (ids) => {
  const users = await db.user.findMany({ where: { id: { in: ids } } });
  return users; // возвращено в порядке БД
});
// резолвер: return userLoader.load(post.authorId)
Викторина

Этот DataLoader батчит корректно, но имеет баг корректности, который вскроет load test. В чём он?

Сниппет 3 — query log

Started GET "/dashboard"
User Load (0.4ms)     SELECT * FROM users WHERE id = 42
Project Load (0.6ms)  SELECT * FROM projects WHERE user_id = 42 LIMIT 50
Task Load (0.3ms)     SELECT * FROM tasks WHERE project_id = 1
Task Load (0.3ms)     SELECT * FROM tasks WHERE project_id = 2
... (ещё 48 строк Task Load)
Comment Load (0.2ms)  SELECT * FROM comments WHERE task_id = 1
... (ещё 243 строки Comment Load)
Completed 200 OK in 980ms (Views: 250ms | ActiveRecord: 689ms)
Викторина

Читая этот лог, что происходит и какой точный фикс?

Сниппет 4 — fan-out-хендлер

const user   = await userService.get(id);    // 30 мс
const posts  = await postsService.get(id);    // 30 мс
const notifs = await notifService.get(id);    // 30 мс
const billing = await billingService.get(id); // 30 мс
return assemble(user, posts, notifs, billing);
Викторина

Эти четыре вызова сервисов независимы (ни один не потребляет результат другого). Какой wall-clock сейчас и какой фикс?

Итог

Каждый N+1 читается в коде или логах: lazy relation внутри цикла — это 1+N запросов, фиксится eager loading’ом; batch-функция DataLoader обязана вернуть результаты, позиционно выровненные по входным ids; query log с повторяющимися похожими SELECT’ами вскрывает вложенные уровни N+1, фиксящиеся вложенным preload; а независимые последовательные вызовы сервисов схлопываются с sum в max через параллельный dispatch. Сначала посчитай round-trip’ы, подбери фикс под форму данных, затем перечитай лог, чтобы подтвердить падение счётчика.

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

Trademarks belong to their respective owners. Editorial reference only.