← Блог
1 апреля 2026 · 10 мин чтения

Как мы снизили P99 latency с 200ms до 40ms

Три месяца назад P99 наших edge-ответов болтался около 200 мс. Для CDN это неприемлемо. Вот история о том, как мы нашли и устранили узкие места.

Отправная точка

Мы измеряли latency от момента получения TCP-соединения до отправки последнего байта ответа. Метрики на старте:

Перцентиль Latency
P50 45 мс
P95 120 мс
P99 200 мс
P99.9 450 мс

P50 был приличным, но хвост распределения — катастрофа. Каждый сотый запрос ждал 200 мс, каждый тысячный — почти полсекунды.

Шаг 1: Профилирование

Первым делом мы разбили latency на компоненты:

  1. TLS handshake — 20-80 мс
  2. Request parsing — <1 мс
  3. Cache lookup — 1-5 мс
  4. Origin fetch (cache miss) — 50-300 мс
  5. Response write — зависит от размера

Два очевидных виновника: TLS handshake и origin fetch при промахах кэша.

Шаг 2: TLS оптимизация

TLS 1.3

Миграция с TLS 1.2 на TLS 1.3 сократила handshake с 2-RTT до 1-RTT. Для клиентов в Азии (RTT ~100 мс) это минус 100 мс на каждое новое соединение.

0-RTT Resumption

TLS 1.3 поддерживает 0-RTT resumption — клиент может отправить данные вместе с первым пакетом. Мы включили 0-RTT для GET-запросов, добавив защиту от replay-атак на уровне приложения.

OCSP Stapling

Без OCSP stapling браузер делает отдельный запрос к CA для проверки сертификата. Мы включили stapling и кэшируем OCSP-ответы агрессивно.

Результат: P99 TLS handshake снизился с 80 мс до 25 мс.

Шаг 3: TCP тюнинг

Дефолтные настройки Linux не оптимальны для высоконагруженных серверов:

# Увеличенные буферы
net.core.rmem_max = 33554432
net.core.wmem_max = 33554432
net.ipv4.tcp_rmem = 4096 87380 33554432
net.ipv4.tcp_wmem = 4096 65536 33554432

# BBR congestion control
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

# Быстрое переиспользование TIME_WAIT сокетов
net.ipv4.tcp_tw_reuse = 1

BBR особенно важен — он лучше справляется с lossy сетями и показывает стабильно низкую latency по сравнению с CUBIC.

Шаг 4: Origin fetch

При промахе кэша мы идём к origin-серверу клиента. Это inherently медленно, но можно оптимизировать:

Connection pooling

Вместо нового TCP-соединения на каждый запрос — пул keep-alive соединений к каждому origin. Экономия 1-2 RTT на запрос.

HTTP/2 к origin

Мультиплексирование запросов через одно соединение. Особенно помогает при thundering herd — когда много клиентов одновременно запрашивают один и тот же некэшированный ресурс.

Stale-while-revalidate

Для контента с SWR мы отдаём устаревший кэш немедленно, а обновление делаем асинхронно. Пользователь не ждёт origin.

Шаг 5: Географическая оптимизация

Мы добавили PoP в регионах с высоким трафиком, но плохой связностью:

Для каждого региона P50 упал на 30-50 мс просто за счёт физической близости.

Шаг 6: Edge Functions latency

Отдельная история — V8 isolates. Основные оптимизации:

Результаты

После трёх месяцев оптимизаций:

Перцентиль Было Стало Улучшение
P50 45 мс 12 мс -73%
P95 120 мс 28 мс -77%
P99 200 мс 40 мс -80%
P99.9 450 мс 85 мс -81%

Уроки

  1. Измеряй перцентили, не средние. Среднее может быть отличным, пока P99 убивает UX.
  2. Latency складывается. 10 компонентов по 5 мс = 50 мс. Оптимизируй каждый.
  3. Сеть — это физика. Скорость света не обманешь, ставь серверы ближе к пользователям.
  4. Defaults are not optimal. Дефолты ОС настроены на универсальность, не на производительность.

Работа над latency никогда не заканчивается. Мы продолжаем мониторить и оптимизировать, цель — P99 <30 мс для всех регионов.