Производительность
Обнаружение N+1: query logs, APM traces и CI gates
Страница занимает 1.2 секунды. CPU flame graph — плоский. Коллега настаивает, что профилировщик, наверное, ошибается. Профилировщик не ошибается — вы смотрите не в тот инструмент. N+1 живёт в off-CPU wait time между запросами, а не в какой-то функции.
Почему CPU-профили пропускают N+1
CPU-профилировщик сэмплирует стек-трейсы работающих потоков. Проблема N+1 не нагружает CPU в каком-то одном месте. Каждый запрос отправляется за микросекунды; программа затем ждёт базу данных (off-CPU); ответ приходит быстро; отправляется следующий запрос. Ожидание — это узкое место, но оно происходит off-CPU.
Профилировщик видит быстрые всплески без ничего широкого. Совокупная стоимость распределена по многим быстрым точкам вызова — не сконцентрирована в одной медленной функции. CPU flame graph будет выглядеть здоровым, пока страница занимает 1.2 секунды.
Правильные инструменты
1. Database query log
Наиболее прямой инструмент. Каждая крупная СУБД может логировать каждый запрос с его длительностью и меткой времени.
# Postgres — логировать всё в разработке
log_min_duration_statement = 0
# MySQL slow query log
slow_query_log = 1
long_query_time = 0
# ORM-level logging
# Rails: config.log_level = :debug (ActiveRecord logger выводит каждый запрос)
# Django: LOGGING = { 'loggers': { 'django.db.backends': { 'level': 'DEBUG' } } }
# SQLAlchemy: echo=TrueСигнатура N+1 безошибочна: от десятков до сотен похожих запросов для одного запроса, часто один и тот же SELECT, отличающийся только параметром id = ? в WHERE.
2. APM tracing — waterfall view
Инструменты мониторинга производительности приложений (Datadog APM, Grafana Tempo, New Relic, Honeycomb с автоинструментированием) захватывают database span’ы в одном ряду с span’ами выполнения кода.
N+1 waterfall выглядит как высокая колонка коротких параллельных полосок — 50 или 100 коротких database span’ов в одном trace запроса. Один взгляд на trace view делает паттерн очевидным так, как логи сами по себе не делают.
Предупреждайте, когда количество span’ов p95 на endpoint растёт более чем на 30% за неделю.
3. Счётчик запросов на запрос
Пользовательский middleware, считающий запросы на request и логирующий или предупреждающий при превышении:
# Rails — middleware считает запросы на request
ActiveSupport::Notifications.subscribe('sql.active_record') do
RequestStore[:query_count] ||= 0
RequestStore[:query_count] += 1
end
after_action do
if RequestStore[:query_count] > 20
Rails.logger.warn "QUERY_COUNT #{params[:controller]}##{params[:action]}: #{RequestStore[:query_count]}"
end
endПубликуйте как Prometheus histogram по endpoint. Предупреждайте, когда p99 endpoint превышает 20 или растёт более чем на 50% за неделю.
| Инструмент | Лучше для | Сигнатура |
|---|---|---|
| CPU профилировщик | Compute hotspots | Неправильный инструмент — пропускает N+1 |
| Query log | Подтвердить N+1, найти место вызова | Много похожих SELECT’ов на запрос |
| APM trace | Визуальный waterfall, продакшн | Высокая колонка коротких DB span’ов |
| Счётчик запросов | Алертинг, тренды по endpoint’у | queries_per_request p99 > 20 |
CI gate — поймать на этапе PR
Наиболее эффективный механизм против N+1 — поймать регрессию на этапе PR, а не в продакшне. Команды, использующие этот gate, сообщают о снижении инцидентов N+1 в продакшне на 70–90%. Первоначальная настройка: 1–2 недели. Текущее обслуживание: минимальное — gate запускается в CI.
Rails: гем bullet вызывает исключение при любом lazy-loaded relation, обращение к которому происходит при рендеринге запроса. Тест контроллера, вызывающий N+1, не проходит.
# spec/spec_helper.rb
Bullet.enable = true
Bullet.raise = true # превращает N+1 в провал тестаDjango: assertNumQueries(N) в тестовом фреймворке считает точные запросы на view:
def test_orders_page(self):
with self.assertNumQueries(2): # проваливается если N+1 проскочил
response = self.client.get('/orders/')SQLAlchemy / Hibernate: включить статистику количества запросов и проверять на тест:
# SQLAlchemy — проверить количество запросов через events
# Hibernate — проверить через Statistics.getPrepareStatementCount()Паттерн во всех них: PR запускает тесты, тесты выполняют запросы, счётчик запросов превышает порог по endpoint’у, тест проваливается, PR не может влиться.
Почему это работает
Off-CPU профилирование (eBPF off-CPU profiles, Java async-profiler в режиме wall-clock, JFR socket-wait events) — четвёртый вариант. Оно показывает, что приложение тратит большую часть wall-time в ожидании socket базы данных, а не выполняя код. Off-CPU профилирование — окончательный инструмент для класса проблем “программа медленная, но CPU profile здоровый” — что и является сигнатурой N+1.
Чтение реального query log
# tail -f log/development.log | grep -E "SELECT|Loading"
Started GET "/dashboard"
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = 42
Project Load (0.6ms) SELECT "projects".* FROM "projects" WHERE "projects"."user_id" = 42 LIMIT 50
Task Load (0.3ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = 1
Task Load (0.3ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = 2
Task Load (0.4ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = 3
... (ещё 47 строк Task Load)
Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."task_id" = 1
Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."task_id" = 2
... (ещё 243 строки Comment Load)
Completed 200 OK in 980ms (Views: 250.3ms | ActiveRecord: 689.4ms)Читаем: 980 мс итого, 689 мс из которых — ActiveRecord на 300+ запросов. Два уровня N+1:
- Уровень 2 (projects → tasks): 50 отдельных Task Load запросов, по одному на project.
- Уровень 3 (tasks → comments): 200–300 отдельных Comment Load запросов, по одному на task.
Фикс: projects.includes(tasks: :comments).limit(50) в Rails (или эквивалент в вашем ORM). С 300 запросов до 4 — один для user, один для projects, один для tasks (IN), один для comments (IN). Время ActiveRecord: 689 мс → ~15 мс.
APM trace показывает 142 database span'а на запрос на endpoint /dashboard, при p99 1.8 с. Span'ы короткие (1–3 мс каждый). Что является диагнозом?
PR добавляет новый сериализатор, обращающийся к order.customer.name для каждого заказа в списке. Тесты проходят. Какой CI-механизм поймал бы этот N+1 до выкатки?
Упорядочите уровни обнаружения N+1 от самого дешёвого до самого труднодоступного:
- 1 Development query log — включить ORM logger, считать строки на запрос
- 2 CI gate — bullet/assertNumQueries провалит PR до выкатки
- 3 APM trace на staging — waterfall показывает колонки DB span'ов
- 4 Алерт в продакшне — queries_per_request p99 скачет на живом endpoint'е
- 01Почему CPU-профиль пропускает N+1, и какие три инструмента правильно его обнаруживают?
- 02Опишите паттерн CI gate для предотвращения регрессий N+1 от попадания в продакшн.
N+1 — off-CPU: программа ждёт между запросами, а не внутри какой-то функции. Именно поэтому CPU-профили выглядят здоровыми, пока страница медленная. Правильные инструменты — query log (повторяющиеся похожие SELECT’ы), APM traces (высокая колонка коротких DB span’ов) и счётчики запросов на запрос (алерт при p99 > 20). Наиболее эффективное предотвращение — CI gate (Rails bullet или Django assertNumQueries), проваливающий PR’ы с lazy-load регрессиями до попадания в продакшн. Количество запросов — метрика, которая важна; p99 latency — её результат.
встречается в159
- Путь запроса: семь остановок от сокета до ответаjunior
- Accept и парсинг: от очереди ядра до типизированного запросаmiddle
- Маршрутизация и middleware: что выполняется и в каком порядкеmiddle
- Обработчик и ответ: от бизнес-логики до байтов на проводеmiddle
- Стриминг и backpressure: когда клиент читает медленнее, чем вы пишетеsenior
- Таймауты и хвостовая задержка: бюджеты, дедлайны и ловушка fan-outsenior
- Middleware и DI: два паттерна, формирующие любой backendjunior
- Пишем middleware: сигнатуры, next() и три модели фреймворковmiddle
- Инверсия управления: как зависимости добираются до классаmiddle
- Скоупы и время жизни DI: singleton, request, transientmiddle
- DI как шов для тестов: фейки, моки и граница, которая важнаsenior
- DI-контейнеры в продакшене: графы разрешения, циклы и когда не стоитsenior
- Блокирующий vs неблокирующий I/O: два способа ждатьjunior
- Event loop: один поток, упорядоченные фазыmiddle
- Что блокирует цикл: CPU-работа и синхронные вызовыmiddle
- Вынос CPU-работы: worker threads и пул libuvmiddle
- Backpressure и ограниченная конкурентностьsenior
- Пропускная способность под нагрузкой: хвостовая задержка и насыщениеsenior
- Зачем пул: цена создания соединенияjunior
- Размер пула: почему больше не значит быстрееmiddle
- Взятие и таймауты: очередь ожидания — настоящий дроссель задержкиmiddle
- Стратегии retry: backoff, jitter и thundering herdmiddle
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Задачи, микрозадачи и scheduler.yield()middle
- Точность таймеров, троттлинг и фоновая работаmiddle
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- LCP: четыре фазы, одна доминирующая стоимостьmiddle
- INP: input delay, processing, presentationmiddle
- Lab vs field: почему они расходятся и как использовать каждыйmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое индекс и как он ускоряет запросыjunior
- Leading-column rule: почему порядок столбцов в composite-индексе важенmiddle
- Partial, expression и covering-индексыmiddle
- Типы индексов: GIN, GiST, BRIN, Hash, Bloom и HOT-обновленияmiddle
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- Упражнение по проектированию индексов: стратегия полнотекстового поискаsenior
- EXPLAIN и планы выполнения: что решает планировщик и почемуjunior
- Типы сканирования: Seq, Index, Bitmap, Index-Onlymiddle
- Алгоритмы соединения и каскад ошибок оценки строкmiddle
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Расширенная статистика: исправление ошибок оценки для коррелированных колонокsenior
- Кеш планов, настройка константных стоимостей и внутренности планировщикаsenior
- Производственные режимы отказа и стабильность плановsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Биты в проводеjunior
- Математика задержкиmiddle
- Bufferbloat и перегрузкаsenior
- Граница физического уровняsenior
- Номера последовательности и состояние соединенияmiddle
- Управление потоком и перегрузкойmiddle
- BBR, производственная наблюдаемость и за пределами TCPsenior
- CDN: контент по соседствуjunior
- Anycast и GeoDNS: маршрутизация к ближайшему edgemiddle
- Многоуровневый кеш и Cache-Controlmiddle
- Заголовок Vary и cache keysmiddle
- Stale-while-revalidate и cache stampedesenior
- Edge workers и edge-side compositionsenior
- CDN: операции и observabilitysenior
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- WebSocket vs SSE vs long-polling: выбор правильного транспортаmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Алгоритмы балансировки: от round-robin до power-of-two-choicesmiddle
- L4 vs L7 балансировка и сохранение IP клиентаmiddle
- Health checks, connection draining и slow startmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- QUIC-потоки и head-of-line blockingjunior
- Объединённое рукопожатие и 1-RTTmiddle
- Connection ID и миграция сетиmiddle
- Обнаружение потерь и управление перегрузкойmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- Развёртывание и стоимость CPUsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- Двенадцать слоёв: один URL, семь действующих лицjunior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Критический путь рендеринга и Core Web Vitalsmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Метрики и cardinality: cost-модель time-series databasemiddle
- Логи и объём: cost-модель структурного логированияmiddle
- Трейсы и сэмплирование: cost-модель distributed tracingmiddle
- Join-ключи и exemplar''''ы: как три сигнала становятся компонуемымиmiddle
- Observability 2.0: широкие события и сдвиг стоимостиsenior
- Режимы сбоя и инженерная практика: cardinality budget''''ы, PII и сэмплированиеsenior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- Log levels и маршрутизация алертовmiddle
- Стратегии sampling и стоимость логовmiddle
- PII-редакция и log injectionsenior
- Propagation trace-контекста в логахsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- Сигналы OTel, Semantic Conventions и проводной формат OTLPmiddle
- Авто-инструментирование и ручные спаны: правило 80/20 в OTelmiddle
- Collector OTel: receivers, processors, exporters и паттерны развёртыванияmiddle
- Стратегии сэмплирования: head, tail и parent-basedmiddle
- Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverlesssenior
- Эксплуатация OTel Collector: надёжность, version skew, режимы отказа и управлениеsenior
- RED и USE: два чек-листа, одна дисциплина триажаjunior
- Инструментация RED в Prometheus: счётчики, гистограммы и дисциплина cardinalitymiddle
- USE на Linux: CPU, память, диск, сеть и PSImiddle
- Golden signals, структура дашборда и auto-RED в service meshmiddle
- Cardinality как драйвер затрат: label, PII, exemplars и семплированиеmiddle
- Native histograms, SLO и паттерны production-сбоевmiddle
- Выбор SLI и SLO-целей: отношения, не ощущенияmiddle
- Multi-window multi-burn-rate-алертинг: почему AND лучше ORmiddle
- Error budget policy, latency SLO и составные journeysmiddle
- Iceberg SLI, математика составного SLO и SLA vs SLOsenior
- Flame graph: читаем картинку, которая показывает, куда ушло времяjunior
- Sampling vs instrumentation profiling: почему 99 Гц побеждает в productionmiddle
- Типы профилей: CPU, память, off-CPU, mutex — какой когда братьmiddle
- Continuous profiling: always-on flame graphs с eBPF и корреляцией trace-idmiddle
- Как flame graph строится из сэмплов и как использовать его в productionmiddle
- Linux perf, внутренности eBPF, PGO и ограничения sampling''''аsenior
- Profiling в production: безопасность, war stories, OTel profiles и дизайн инфраструктурыsenior
- Debugging-воронка: SLO → RED → trace → profilejunior
- Архитектура OTel: один SDK, четыре сигнала, один wire-форматmiddle
- Экономия на observability: удерживаем затраты в пределах 5% inframiddle
- Масштаб, безопасность и ROI наблюдаемых системsenior