Skip to content

Практическая работа №18: Профилирование и устранение блокировки TTFB

Цель работы: научиться находить, на каком узле цепочки обработки запроса теряется время до первого байта ответа (TTFB), и устранять задержку за счёт распараллеливания независимых операций и таймаутов на медленные внешние вызовы.

Основные теоретические положения

Section titled “Основные теоретические положения”

1. Архитектура backend-приложения и путь запроса

Section titled “1. Архитектура backend-приложения и путь запроса”

Типичный запрос проходит цепочку: nginx (реверс-прокси) → приложение (воркер PHP-FPM/Python WSGI или event loop Node.js/asyncio) → база данных / внешние сервисы → обратно клиенту. Задержка может возникнуть на любом из этих узлов, и без измерения каждого участка отдельно невозможно понять, где именно теряется время.

2. TTFB: из каких фаз складывается метрика

Section titled “2. TTFB: из каких фаз складывается метрика”

Time To First Byte (TTFB) — время от отправки запроса до получения первого байта ответа. Складывается из сетевой задержки, времени ожидания в очереди перед свободным воркером и времени непосредственной обработки запроса приложением (включая все синхронные и асинхронные операции внутри обработчика). Диагностика начинается с логирования времени выполнения каждого шага внутри обработчика — без этого невозможно понять, какая именно операция “съедает” TTFB.

3. Последовательное и параллельное выполнение независимых операций

Section titled “3. Последовательное и параллельное выполнение независимых операций”

Если обработчик запроса выполняет несколько операций, не зависящих друг от друга по данным (например, запрос профиля пользователя и запрос списка его последних заказов), их последовательное выполнение (await a(); await b();) складывает время обеих операций. Если же они выполняются параллельно (asyncio.gather(a(), b()) в Python, Promise.all([...]) в JavaScript), общее время равно времени самой долгой из них, а не их сумме. Операции, зависящие друг от друга по данным (результат одной нужен для вызова другой), распараллелить нельзя.

4. Таймауты и деградация функциональности вместо отказа

Section titled “4. Таймауты и деградация функциональности вместо отказа”

Если один из внешних вызовов (например, сервис рекомендаций) может работать медленно или недоступен, вся страница не должна зависеть от его ответа. Решение — обернуть вызов в таймаут (asyncio.wait_for в Python) и, если он не уложился в отведённое время, вернуть страницу без соответствующего блока, а не заставлять пользователя ждать или показывать ошибку целиком.

Алгоритм выполнения работы

Section titled “Алгоритм выполнения работы”

Получение исходного кода проекта

Section titled “Получение исходного кода проекта”

Для выполнения работы потребуется стартовый проект с эмуляцией сервиса главной страницы.

  1. Клонирование с GitHub:
    Terminal window
    git clone https://github.com/ghostITwe/optimization-web-apps.git
    cd optimize-web-apps/lab18-ttfb

Установите зависимости:

Terminal window
pip install -r requirements.txt

1. Изучение и анализ текущей реализации

Section titled “1. Изучение и анализ текущей реализации”
  1. Откройте starter/main.py — обработчик handle_homepage выполняет три операции: get_user, fetch_recommendations, get_recent_orders.
  2. Запустите стартовый сервер:
    Terminal window
    cd starter
    uvicorn main:app --reload --port 8000
  3. В другом терминале замерьте текущее время ответа:
    Terminal window
    python ../benchmark.py http://127.0.0.1:8000/
  4. Зафиксируйте среднее время ответа (должно быть около 1.9–2.0 секунды) и определите по коду, какая из трёх операций вносит основной вклад в задержку.

2. Распараллеливание независимых операций

Section titled “2. Распараллеливание независимых операций”
  1. Определите, какие из трёх операций не зависят друг от друга по данным (fetch_recommendations и get_recent_orders не используют результат друг друга).
  2. Перепишите обработчик так, чтобы эти две операции выполнялись параллельно через asyncio.gather, а не последовательно через два await подряд.
  3. Сохраните изменения и повторно замерьте время ответа тем же скриптом benchmark.py. Зафиксируйте новый результат.

3. Таймаут на медленный внешний вызов

Section titled “3. Таймаут на медленный внешний вызов”
  1. Оберните вызов fetch_recommendations в asyncio.wait_for с таймаутом 500 миллисекунд.
  2. Реализуйте деградацию: если таймаут сработал, страница должна вернуться без блока рекомендаций (пустой список), а не с ошибкой.
  3. Убедитесь, что при этом get_user и get_recent_orders по-прежнему отрабатывают полностью.

4. Финальная проверка и сравнение

Section titled “4. Финальная проверка и сравнение”
  1. Запустите автоматическую проверку:
    Terminal window
    pip install pytest pytest-asyncio httpx
    pytest tests/test_ttfb.py -v
    Тест должен пройти — это означает, что ответ укладывается менее чем в 1 секунду.
  2. Сравните итоговое время ответа с результатом из пункта 1 (должно быть в 10+ раз быстрее — около 0.13 секунды у эталонного решения).
  3. Подготовьте краткий отчёт: время “до”/“после”, какая операция была причиной задержки, почему get_user нельзя было распараллелить с остальными.
  1. Из каких составляющих складывается TTFB? Какая из них оказалась узким местом в этой работе?
  2. Почему asyncio.gather уменьшает именно суммарное время ответа, а не нагрузку на CPU?
  3. По какому признаку определяется, можно ли распараллелить две операции внутри одного обработчика, а какие — нельзя?
  4. Зачем нужен таймаут на внешний вызов рекомендаций, если можно просто дождаться его ответа?
  5. Что произойдёт с TTFB, если сервис рекомендаций будет отвечать не за 1.8 секунды, а зависать бесконечно, и почему в решении с таймаутом это не страшно?