Представьте, endpoint на Symfony иногда отвечает за секунды вместо миллисекунд. Как вы будете искать причину?
Диагностику начинают с Symfony Profiler (Timeline + Doctrine вкладки), затем ищут N+1-запросы, медленный SQL через EXPLAIN ANALYZE, блокировки сессий и таймауты внешних HTTP-вызовов; в production используют Blackfire или OpenTelemetry.
Диагностика медленного endpoint в Symfony
Когда endpoint иногда отвечает за секунды вместо миллисекунд, проблема почти всегда кроется в одном из трёх мест: база данных, внешние HTTP-вызовы или блокировки в коде. Задача — последовательно исключать слои.
Шаг 1: Профилировщик Symfony
Включите Symfony Profiler в dev-окружении и изучите вкладку Timeline. Она покажет, сколько времени заняло каждое событие: роутинг, контроллер, рендер, SQL-запросы.
# Включить профилировщик (уже включён в dev по умолчанию)
composer require --dev symfony/profiler-pack
# Посмотреть профили через веб-интерфейс
http://localhost/_profiler
Шаг 2: Анализ SQL через Doctrine
На вкладке Doctrine профилировщика видны все запросы с временем выполнения. Ищите N+1 — когда цикл в PHP порождает десятки одинаковых SELECT.
<?php
// Плохо: N+1 — для каждого поста отдельный SELECT автора
$posts = $postRepository->findAll();
foreach ($posts as $post) {
echo $post->getAuthor()->getName(); // LazyLoading!
}
// Хорошо: JOIN FETCH в одном запросе
$posts = $postRepository->createQueryBuilder('p')
->leftJoin('p.author', 'a')
->addSelect('a')
->getQuery()
->getResult();
Шаг 3: Медленные запросы в PostgreSQL/MySQL
Включите slow query log на уровне СУБД и ищите запросы дольше 100 мс. Затем запустите EXPLAIN ANALYZE.
-- PostgreSQL: найти медленные запросы
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- Объяснить план запроса
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 42;
Шаг 4: Внешние HTTP-вызовы
Symfony HttpClient по умолчанию синхронный. Если контроллер вызывает сторонний API, тайм-аут этого API напрямую влияет на ваш ответ. Проверьте через Stopwatch:
<?php
use Symfony\Component\Stopwatch\Stopwatch;
$stopwatch = new Stopwatch();
$stopwatch->start('external_api');
$response = $this->httpClient->request('GET', 'https://slow-api.example.com/data');
$event = $stopwatch->stop('external_api');
echo $event->getDuration(); // мс
Шаг 5: Блокировки и сессии
PHP-сессии с file-блокировками вызывают последовательное выполнение параллельных запросов одного пользователя. Убедитесь, что сессия закрывается сразу после чтения, или переключитесь на Redis-хранилище.
# config/packages/framework.yaml
framework:
session:
handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler
cookie_secure: auto
cookie_samesite: lax
Шаг 6: Blackfire или Tideways для production
В production Symfony Profiler отключён. Используйте Blackfire Agent — он позволяет профилировать без overhead для конечных пользователей.
# Профилировать один запрос через Blackfire CLI
blackfire curl https://example.com/api/slow-endpoint
# Или сравнить два варианта (Reference vs Candidate)
blackfire run --references=5 php bin/console app:process
Шаг 7: Метрики и APM
Подключите OpenTelemetry или Datadog APM. Symfony имеет готовую интеграцию через пакет open-telemetry/opentelemetry-auto-symfony. Это позволяет видеть распределение времени ответа по перцентилям (p95, p99) и коррелировать аномалии с деплоями или всплесками нагрузки.
Подводные камни
- Профилировщик в dev-окружении сам создаёт overhead; не доверяйте абсолютным цифрам, только относительным.
- N+1 не виден без принудительной гидрации — lazy-loading коллекций выглядит нормально до первого foreach.
- Иногда медленность периодическая: Doctrine второй уровень кэша инвалидируется и холодный запрос проваливается в базу.
- Redis-блокировки при параллельных записях в одну ключ-сессию тоже могут стать узким местом.
- EXPLAIN ANALYZE в production может быть опасен на больших таблицах без LIMIT — он реально выполняет запрос.
- Symfony EventListener с приоритетом 0 выполняется для каждого запроса — проверьте, нет ли тяжёлой логики в kernel.request или kernel.response.
- GC (Garbage Collector) PHP может срабатывать непредсказуемо при большом количестве объектов Doctrine в UnitOfWork.
- Проблема может быть не в коде, а в DNS-резолвинге при обращении к внешним сервисам из контейнера — проверьте /etc/resolv.conf.
What hurts your answer
- Сразу обвинять Symfony, не проверив соседние слои системы
- Чинить симптом без минимального воспроизведения и evidence
- Не учитывать версии, конфигурацию, окружение и recent changes
What they're listening for
- Умеет локализовать проблему вокруг Symfony
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения