SymfonySeniorExperience

Представьте, 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
  • Двигается от симптома к гипотезам и проверкам
  • Отличает баг инструмента от ошибки использования или окружения

Related topics