Полный аудит производительности и ресурсоёмкости репозитория
Для проекта the-platform: микросервисная CRM-платформа — FastAPI + asyncpg +
PostgreSQL + Redis + RabbitMQ бэкенд-сервисы, React/Vite/TS фронтенд
(the-frontend), Docker/Kubernetes/Helm инфраструктура, нагрузочное
тестирование на k6 в load-testing/.
РОЛЬ
Компания, для которой делается продукт, крайне чувствительна к расходу вычислительных ресурсов (CPU, RAM, сетевой трафик, стоимость инфраструктуры) — производительность и экономичность рассматриваются как приоритет первого класса, а не как техдолг "когда-нибудь потом". Аудит должен находить реальные, измеримые проблемы с привязкой к file:line и количественной оценкой воздействия (латентность, CPU, RAM, число запросов к БД, размер трафика), а не составлять общий чек-лист "best practices" без проверки на конкретной кодовой базе.
ВХОДНЫЕ ДАННЫЕ
$ARGUMENTS — необязательно путь к отчёту предыдущего аудита или к
load-testing/reports для сравнения "было → стало". Если не передан — ищи
сам: load-testing/ANALYSIS_GUIDE.md, load-testing/reports, и любые
предыдущие отчёты аудита производительности, упомянутые в диалоге или
репозитории.
Это аудит ВСЕГО репозитория. Если задача на самом деле касается только
одной фичи/ветки/PR — это не тот скилл, используй performance-audit-feature
вместо полного разбора всей кодовой базы (иначе периметр окажется избыточным
и находки не будут привязаны к тому, что реально важно проверить).
КЛЮЧЕВОЙ ПРИНЦИП: ИЗМЕРЯЙ, А НЕ ДОГАДЫВАЙСЯ
Главная причина, по которой аудиты производительности проваливаются или, наоборот, наносят вред — это два симметричных провала: (а) находки без доказательства реального воздействия ("это может быть медленно") и (б) слепое накручивание "оптимизаций" (кэширование, мемоизация, denormalization) там, где нет измеренной проблемы, что усложняет код и создаёт новый класс багов (протухший кэш, race condition) ради несуществующего выигрыша. Обоих провалов быть не должно:
- Для каждой находки, где это технически возможно, подтверждай воздействие
измерением: EXPLAIN ANALYZE для SQL-запроса, профилирование
(py-spy/cProfile) для CPU-хотспота, реальный размер бандла для
фронтенда, вывод существующих k6-сценариев из
load-testing/для нагрузочных характеристик API. Находка без числа — гипотеза, а не находка; помечай её явно как "не подтверждено измерением" и указывай, что нужно для подтверждения. - Не предлагай оптимизацию там, где нет доказанной проблемы. Если код "неидиоматичен", но не находится на горячем пути и не потребляет заметных ресурсов — фиксируй как cosmetic/low, не как performance- находку. Три одинаковых строки лучше преждевременной абстракции; тот же принцип применим к кэшам и мемоизации.
- Различай "теоретически неоптимально" и "реально дорого при текущем/ожидаемом объёме данных". N+1 запрос на таблице с 10 строками в dev-окружении — не то же самое, что N+1 на таблице контактов/сделок с сотнями тысяч строк в проде. Явно указывай, при каком объёме данных находка становится критичной.
- Если оптимизация уже была сделана раньше (проверь git log/комментарии) — проверь, не регрессировала ли она позже (например, кэш добавили, но инвалидацию забыли при рефакторинге соседнего модуля).
- Формулируй статус явно: "подтверждено измерением" / "правдоподобно, но не измерено" / "не является проблемой при текущем объёме данных" / "уже оптимизировано корректно".
МЕТОДОЛОГИЯ: ТРИ НЕЗАВИСИМЫХ СРЕЗА
Проведи аудит тремя независимыми методами и не позволяй одному срезу подменять другой — у них разная слепая зона.
СРЕЗ 1 — Инструментальный анализ и профилирование
- Backend (Python/FastAPI): включи логирование SQL (echo=True/aiosqlalchemy debug) на ключевых сценариях и найди N+1 запросы и запросы без LIMIT/пагинации; там, где доступно подключение к БД — прогони EXPLAIN ANALYZE на самых частых/тяжёлых запросах и зафиксируй отсутствие индексов (seq scan там, где ожидается index scan); используй py-spy/cProfile для профилирования CPU на подозрительных горячих путях (webhook-обработчики, sync-джобы, сериализация больших ответов).
- База данных: pg_stat_statements (если включён) для топ запросов по суммарному времени и по числу вызовов; список таблиц без индексов на колонках, используемых в WHERE/JOIN/ORDER BY (сверь со схемой миграций каждого сервиса); размер таблиц и рост со временем, если есть доступ к метрикам.
- Redis: паттерны ключей без TTL (риск неограниченного роста памяти), команды KEYS/SCAN по всему пространству ключей в хот-пасе, размер значений (сериализация целых объектов вместо нужных полей).
- RabbitMQ: конфигурация prefetch/QoS, глубина очередей, retry-политики без backoff/DLQ.
- Docker-образы: dive или
docker historyпо каждому собираемому образу — размер слоёв, неиспользуемые build-инструменты и dev-зависимости, попавшие в финальный образ; сравни с multi-stage build там, где он есть/отсутствует. - Frontend (the-frontend): построй production-сборку и проанализируй
бандл (
vite build --mode production+ rollup-plugin-visualizer/ source-map-explorer, если подключены, либо разбор dist/ по размеру чанков вручную); Lighthouse (или аналог) по ключевым страницам для метрик LCP/TBT/bundle size, если есть возможность поднять фронтенд локально. - Нагрузочное тестирование: в репозитории уже есть k6-сценарии и
отчёты в
load-testing/(см.load-testing/ANALYSIS_GUIDE.md,load-testing/k6,load-testing/reports). Прочитай существующие отчёты — какие узкие места уже были найдены ранее и не устранены; если возможно поднять окружение — прогони актуальные сценарии заново и сравни с сохранёнными baseline-отчётами. - Kubernetes/Helm: checkov/kube-linter или ручной разбор чартов на предмет resources requests/limits (пустые/отсутствующие — риск noisy neighbor и throttling; завышенные — прямой перерасход бюджета на инфраструктуру), интервалы liveness/readiness проб (слишком частые пробы = постоянная фоновая нагрузка на N реплик), конфигурация HPA и её пороги.
СРЕЗ 2 — Ручной построчный разбор кода
Раздели кодовую базу на независимые зоны и разбери КАЖДУЮ (не по диагонали, не полагаясь только на grep-паттерны):
- Каждый backend-сервис отдельно (gateway, все
*-serviceдиректории вservices/). - Frontend / SPA (
the-frontend). - Фоновые обработчики, боты и интеграции с внешними API (telegram-bot-service, telegram-connector-service, whatsapp-personal-service, kommo-integration-service, verification-bot-service, ai-bot-service) — это код с наибольшим риском постоянной фоновой нагрузки (поллинг, синки, long-polling), а не только запрос-ответ по требованию.
- Общие библиотеки (
libs/shared_auth,libs/shared_metrics) — код, который выполняется на КАЖДОМ запросе КАЖДОГО сервиса; даже небольшая неэффективность здесь умножается на количество сервисов и реплик. - Инфраструктурные конфиги (
helm/,docker-compose*.yml,deploy/,infrastructure/).
По каждой зоне ищи (детальный чек-лист ниже) и для каждой находки указывай: file:line, тип проблемы, количественную оценку воздействия (или пометку "не измерено"), severity, статус (см. выше).
СРЕЗ 3 — Архитектурный обзор
Независимо от построчного разбора оцени, способна ли текущая архитектура УДЕРЖИВАТЬ экономичность при росте нагрузки, а не только не иметь явных проблем сегодня:
- "Болтливая" межсервисная архитектура: сколько последовательных синхронных HTTP-вызовов делает один пользовательский запрос через gateway → сервисы → сервисы (построй цепочку для 2-3 самых частых сценариев: логин, список сделок/лидов, отправка сообщения в чат). Каждый лишний хоп — это латентность и CPU/сеть, умноженные на трафик.
- Оправданность фиксированного числа реплик (в
docker-compose.microservices.ymlgateway поднят в 3 экземплярах и т.п.) — есть ли данные о реальной нагрузке, оправдывающие это число, или это "на всякий случай"? То же для аналогичных решений в helm-values (replicaCount). - Дублирование общих библиотек (shared_auth, shared_metrics) по сервисам вместо единого пакета — если производительный фикс/оптимизация сделаны в одной копии, распространяется ли это автоматически на остальные, или патч придётся катить N раз (тот же риск, что и для security-патчей).
- Общая стратегия кэширования: есть ли она вообще как осознанное решение (что кэшируется, на каком уровне, с какой инвалидацией), или кэш добавляется точечно и бессистемно там, где кто-то один раз заметил тормоза.
- Совмещение OLTP-нагрузки (транзакционные сервисы) и тяжёлой аналитики (Superset, analytics-service) на одной БД/инстансе — конкуренция за ресурсы.
- Наличие процесса: прогоняется ли нагрузочное тестирование (k6 в
load-testing/) регулярно или разово; есть ли бюджет производительности (performance budget) для фронтенд-бандла и API-латентности, закреплённый в CI, а не только "по ощущениям".
ДЕТАЛЬНЫЙ ЧЕК-ЛИСТ ПО КАТЕГОРИЯМ
- Асинхронный backend: блокирующие вызовы в event loop — синхронные
HTTP-клиенты (requests и т.п.) вместо httpx.AsyncClient/aiohttp внутри
async-обработчиков; синхронный I/O (открытие файлов, time.sleep,
синхронные драйверы БД) внутри async def; CPU-тяжёлые операции (парсинг
больших JSON/XML, шифрование, обработка изображений) на event loop без
ThreadPoolExecutor/ProcessPoolExecutor; последовательные
awaitтам, где вызовы независимы и могли бы идти параллельно черезasyncio.gather(типичный паттерн в gateway при обращении к нескольким сервисам для сборки одного ответа). - База данных (PostgreSQL) — N+1 запросы (цикл с запросом к БД
внутри, где можно один JOIN/
selectinload/joinedload); отсутствующие индексы на колонках в WHERE/ORDER BY/JOIN, особенно на внешних ключах (user_id, tenant_id, integration_id) и мультитенантной фильтрации;SELECT */полная сериализация там, где нужны 2-3 поля; списковые эндпоинты без пагинации/LIMIT; размер пула соединений относительно числа реплик и лимита PostgreSQL; долгие транзакции с внешними HTTP-вызовами внутри (категорически недопустимый паттерн); повторяющиеся идентичные запросы в рамках одного request-response цикла. - Кэширование (Redis) — ключи без TTL; отсутствие кэша для дорогих часто повторяющихся и редко меняющихся вычислений (агрегаты дашбордов, справочники, роли); cache stampede (нет lock/single-flight при протухании); кэш без инвалидации при изменении исходных данных (фиксируй отдельно как корректностную, не только perf-проблему); KEYS/SCAN по всей базе в хот-пасе, хранение больших блобов вместо специализированного хранилища.
- Межсервисное взаимодействие (HTTP) — отсутствие таймаутов на исходящих запросах; отсутствие retry с exponential backoff (либо retry без backoff — thundering herd при частичном инциденте); дублирующие вызовы одного сервиса вместо batched-вызова; полная пересылка тяжёлых payload там, где нужна часть данных.
- Очереди (RabbitMQ) и фоновые обработчики — prefetch/QoS (слишком большой перегружает консьюмера, слишком маленький недоиспользует параллелизм); poison message без DLQ; частота поллинга внешних API (kommo-integration-service, telegram-connector-service, whatsapp-personal-service) относительно реальной потребности, размер батча, обработка rate-limit (429/ретраи).
- Сериализация и размер payload — Pydantic-модели, возвращающие значительно больше полей, чем реально используется; логирование целиком крупных объектов/payload в hot path; отсутствие сжатия (gzip/brotli) на gateway для крупных JSON-ответов.
- Frontend (React/Vite/TS) — размер production-бандла и отсутствие code-splitting/lazy-loading для редко используемых маршрутов; неоптимизированные изображения/статические ассеты; избыточные ре-рендеры на больших списках/таблицах (лидов/сделок) — отсутствие виртуализации, новые объекты/колбэки в render без memo там, где профайлер явно показывает горячую точку (не добавляй memo/useCallback повсеместно "на всякий случай" — это тоже стоит ресурсов без измеренной пользы); waterfall-загрузка данных вместо параллельной; слишком частый polling там, где уместнее WebSocket/SSE, либо WebSocket с избыточными reconnect/heartbeat.
- Docker-образы — размер финального образа по каждому Dockerfile (services/*, the-frontend); отсутствие multi-stage build там, где build-зависимости попадают в финальный слой; избыточный базовый образ (full OS вместо slim/alpine/distroless), если это не создаёт проблем совместимости.
- Kubernetes/Helm — resources requests/limits: отсутствующие (noisy neighbor, OOM-kill) и одновременно завышенные "с запасом" без основания на реальном потреблении; интервалы/таймауты liveness/readiness/startup проб на большое число реплик; replicaCount и HPA-пороги — обоснованы ли числами; init-контейнеры и стартовая логика подов — не блокируют ли готовность дольше необходимого.
- Наблюдаемость как источник накладных расходов — кардинальность метрик (shared_metrics): лейблы с высокой кардинальностью (user_id, request_id как label вместо значения); частота/объём сэмплирования трейсов; объём и уровень логирования в проде (DEBUG-логи "для удобства"); синхронная отправка метрик/трейсов в hot path вместо батчинга/асинхронной отправки.
- Алгоритмическая эффективность и структуры данных — квадратичные и худшие по сложности операции над коллекциями, которые на реальных объёмах данных станут узким местом (списки лидов/контактов/сообщений чата растут со временем); повторный парсинг/пересчёт одних и тех же данных в рамках одного запроса; ненужное глубокое копирование крупных структур; отсутствие батчинга для массовых операций (импорт лидов, синки с внешними CRM) там, где объём операций регулярно велик.
- Нагрузочное тестирование и бюджеты производительности — актуальность k6-сценариев (load-testing/k6) относительно текущих API-контрактов; расхождение между узкими местами, задокументированными в load-testing/reports и load-testing/ANALYSIS_GUIDE.md, и текущим состоянием кода (исправлены ли они на самом деле, или отчёт остался неактуализированным — "исправлено на бумаге"); наличие или отсутствие зафиксированных бюджетов производительности в CI (максимальный размер бандла, максимальная латентность p95) — без такого гейта регрессии производительности проникают в прод незамеченными.
EDGE CASES, КОТОРЫЕ ЧАСТО ПРОПУСКАЮТ
- "На dev-данных работает быстро" — проблема проявляется только на реальных объёмах данных клиента (тысячи лидов, десятки тысяч сообщений в чате); всегда прикидывай ожидаемый рост.
- Отладочные удобства, забытые в проде: verbose-логирование, отключённое сжатие ответов, отключённый кэш "чтобы было проще дебажить" — и не включённые обратно.
- Health/readiness-пробы, дёргающие БД или внешние сервисы на каждой N-секундной проверке, умноженные на число реплик — суммарно заметная фоновая нагрузка на БД без бизнес-пользы.
- Несколько независимых сервисов, опрашивающих один и тот же внешний API дублирующим образом вместо общего кэша/единой точки синхронизации.
- Retry-логика без backoff, усиливающая нагрузку именно в момент частичного инцидента (когда ресурсы и так в дефиците).
- Тяжёлые аналитические запросы (Superset/analytics-service), выполняющиеся на той же БД/инстансе, что и транзакционная нагрузка, в часы пиковой нагрузки на CRM.
- "Оптимизация ради оптимизации": кэш/мемоизация/денормализация, добавленные без измеренной проблемы — увеличивают сложность и объём кода, который надо содержать и который сам потребляет ресурсы (память кэша, синхронизация), без доказанной выгоды. Фиксируй и такие находки — предлагай упрощение.
- Различия конфигурации между окружениями (dev/staging/prod) в docker-compose/helm-values — оптимизация, сделанная для одного окружения, может отсутствовать в другом.
- Забытые после экспериментов feature flags, удваивающие работу (пишем в старую и новую систему одновременно "на время миграции", которая давно завершилась).
ФОРМАТ ОТЧЁТА
- Executive summary (для руководства, без технического жаргона): что расходует ресурсы заметнее всего, что можно оптимизировать без риска для функциональности в первую очередь, ориентировочный эффект (в терминах латентности/нагрузки/инфраструктурных ресурсов).
- Таблица KPI: число находок по severity (critical/high/medium/low), число находок, подтверждённых измерением, vs "правдоподобно, но не измерено"; ориентировочная суммарная экономия ресурсов при устранении критичных находок, если её можно оценить.
- Если это повторный аудит — таблица сопоставления "пункт предыдущего отчёта/load-testing отчёта → текущее состояние (file:line) → статус (не исправлено / формально / выборочно / исправлено)".
- Раздел "исправлено, но не работает" отдельно (например: индекс добавлен в миграции, но запрос всё равно делает seq scan из-за несовпадения типов/функции в WHERE).
- Полный список находок с привязкой file:line, количественной оценкой воздействия (или явной пометкой "не измерено" и что нужно для измерения), severity и конкретной рекомендацией по исправлению (не "оптимизировать запрос", а "добавить индекс на (tenant_id, created_at)", "заменить N+1 на selectinload", "вынести вызов внешнего API из тела транзакции").
- Раздел "что сделано хорошо" — эффективные паттерны в кодовой базе, которые стоит тиражировать на другие сервисы, а не переделывать.
- План действий по срокам: быстрые точечные фиксы без риска регрессии (индексы, таймауты, исправление N+1) — в первую очередь; изменения, требующие тестирования под нагрузкой — во вторую; архитектурные решения (пересмотр стратегии кэширования, сокращение числа межсервисных хопов, план масштабирования) — как решения уровня руководства/тимлидов.
- Раздел "методология и ограничения покрытия" — какие инструменты и измерения были использованы, какие сервисы/зоны НЕ были профилированы или нагружены и почему (нет доступа к прод-метрикам, нет возможности поднять окружение и т.п.), чтобы отсутствие находок в непокрытой зоне не читалось как "там всё оптимально".
ПРАВИЛА ОФОРМЛЕНИЯ НАХОДОК
Для каждой находки обязательны:
- Путь к файлу и номер строки (или диапазон).
- Название проблемы и категория (см. чек-лист выше).
- Количественная оценка воздействия: измеренная (число запросов к БД, EXPLAIN ANALYZE вывод, время профилирования, размер бандла/образа в МБ, латентность p50/p95) либо явно помеченная как неизмеренная оценка с обоснованием, почему она вероятна.
- Условие, при котором проблема становится критичной (объём данных, число одновременных пользователей, частота вызова) — если оно не выполняется сегодня, но выполнится при росте, укажи это явно, не занижай и не завышай срочность.
- Severity с обоснованием (какая доля трафика/данных затронута, какой ресурс расходуется: CPU, RAM, сетевой трафик, число соединений с БД, стоимость инфраструктуры).
- Статус относительно предыдущего аудита/load-testing отчёта, если применимо.
- Рекомендация по исправлению — конкретная и с сохранением текущей функциональности (аудит не должен предлагать менять поведение системы, только эффективность его реализации).
ЗАПУСК АУДИТА (практическая инструкция)
- Определи периметр: перечисли все backend-сервисы (services/), фронтенд (the-frontend), общие библиотеки (libs/), фоновые боты/коннекторы, инфраструктурные конфиги (helm/, docker-compose*.yml, deploy/, infrastructure/), существующие нагрузочные тесты (load-testing/).
- Прочитай существующие артефакты нагрузочного тестирования (load-testing/ANALYSIS_GUIDE.md, load-testing/reports) ПЕРЕД началом ручного разбора — это готовый источник уже известных узких мест, не дублируй работу, а проверь, устранены ли они.
- Запусти доступные инструменты СРЕЗА 1 по каждому сервису/образу/чарту, где это возможно в текущем окружении; сохрани сырой вывод для приложений к отчёту.
- Раздели ручной разбор СРЕЗА 2 на независимые куски (по сервису/зоне) — если доступен Agent tool, запусти несколько независимых субагентов на разные зоны параллельно (в foreground, если результат нужен сразу в этом диалоге), чтобы не пропустить объём и не дать одному агенту "срезать угол" по всей кодовой базе разом.
- Проведи архитектурный обзор СРЕЗА 3 отдельно, независимо от результатов среза 2.
- Сведи все три среза в единый отчёт по формату выше, убери дубликаты, но не объединяй находки разной природы (инструмент нашёл подозрительный паттерн ≠ ручной разбор подтвердил реальное воздействие — фиксируй оба факта, если они есть, с соответствующим статусом подтверждения).
- Если это повторный аудит — обязательно перепроверь КАЖДЫЙ пункт предыдущего отчёта и каждую находку из load-testing/reports по текущему состоянию кода, а не полагайся на статус, заявленный командой.
- Явно укажи, какие проверки НЕ были выполнены (нет доступа к прод-метрикам/pg_stat_statements, нет возможности поднять окружение для профилирования или прогона k6, нет данных о реальных объёмах клиентских данных) — это часть честного отчёта, а не его слабость.
- Ни одна рекомендация не должна менять наблюдаемое поведение/ функциональность системы — только эффективность реализации. Если для оптимизации неизбежно требуется изменение поведения (например, ужесточение пагинации по умолчанию) — отметь это отдельно и явно.
Это аудит, не имплементация: правки вносит разработчик по итогам отчёта, не ты в рамках этого скилла.