Оптимизация API: как мы сократили трафик на 60% для агрегатора

Клиент и проблема

«НовостиПлюс» — крупный российский новостной агрегатор с 2 миллионами активных пользователей в день. Платформа агрегирует контент из 450 источников, доставляя до 15 миллионов API-запросов в час в пиковые часы.

К нам обратились с конкретной проблемой: стоимость трафика CDN выросла до 2.8 млн рублей в месяц, а пользователи в регионах с нестабильным мобильным интернетом жаловались на медленную загрузку ленты новостей (3-5 секунд на 4G).

После первичного аудита мы выявили четыре точки неэффективности:

  • Изображения — JPEG без оптимизации, средний вес превью 120 КБ
  • Формат API — JSON с избыточными полями, средний ответ 9 КБ
  • Сжатие — gzip для всего трафика, без специализации
  • Протокол — TCP/HTTP2, потери пакетов на мобильном интернете вызывают head-of-line blocking

Мы предложили комплексную оптимизацию по четырём направлениям с целевым показателем: сокращение трафика на 50%+ и двукратное ускорение доставки контента.

Этап 1: замена JPEG на WebP

Первый и самый значимый шаг — миграция изображений с JPEG на формат WebP. У «НовостиПлюс» через CDN проходило около 160 тысяч изображений в секунду в прайм-тайм.

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

# Конфигурация конвертации для разных форматов
# Превью в ленте (320x180)
cwebp -q 80 -m 6 -af -sharp_yuv input.jpg -o output.webp

# Полноразмерные фото в статьях (1200x675)
cwebp -q 85 -m 4 -sharp_yuv input.jpg -o output.webp

# Аватарки и иконки (64x64)
cwebp -q 75 -m 6 -lossless input.png -o output.webp

Nginx был настроен на автоматическую отдачу WebP браузерам с поддержкой:

map $http_accept $webp_suffix {
    default "";
    "~*webp" ".webp";
}

server {
    location /images/ {
        try_files $uri$webp_suffix $uri =404;
        add_header Vary Accept;
        add_header Cache-Control "public, max-age=31536000";
    }
}

Для 1% пользователей со старыми браузерами без WebP-поддержки мы сохранили fallback на оптимизированный JPEG (mozjpeg, quality 80).

Результаты перехода на WebP

A/B тест на 20% трафика в течение недели показал:

МетрикаJPEGWebPИзменение
Средний размер превью120 КБ72 КБ−40%
Время загрузки ленты (P50)1.8 сек1.5 сек−17%
Время загрузки ленты (P90)4.2 сек3.5 сек−17%
Просмотры контентаbaseline+1.1%рост
DAUbaseline+1.3%рост

Рост DAU на 1.3% при 2 млн пользователей — это 26 000 дополнительных активных пользователей в день, что напрямую повлияло на рекламную выручку.

Этап 2: замена JSON на MessagePack + zstd

API «НовостиПлюс» возвращал JSON-ответы. Типичный ответ ленты новостей (20 статей) весил 9 КБ в сжатом gzip виде и 38 КБ в несжатом. Мы заменили JSON на бинарный формат MessagePack и gzip-сжатие на zstd со словарём.

MessagePack — бинарный формат, совместимый с JSON по структуре, но компактнее за счёт бинарной кодировки типов:

# Python: переход с JSON на MessagePack
import msgpack
import json

# Было
@app.get("/api/v2/feed")
async def get_feed():
    data = await fetch_articles(limit=20)
    return JSONResponse(content=data)  # 38 KB raw, 9 KB gzip

# Стало
@app.get("/api/v3/feed")
async def get_feed(request: Request):
    data = await fetch_articles(limit=20)
    accept = request.headers.get("Accept", "")
    if "application/msgpack" in accept:
        packed = msgpack.packb(data, use_bin_type=True)
        return Response(
            content=packed,
            media_type="application/msgpack"
        )  # 28 KB raw, 8 KB zstd
    return JSONResponse(content=data)

Для сжатия мы обучили zstd-словарь на 10 000 типичных API-ответах:

# Обучение словаря zstd на типичных API ответах
zstd --train /tmp/api_samples/*.msgpack -o /etc/zstd/feed_dict.zst

# Размер словаря: 112 КБ
# Сжатие с словарём: в 7 раз быстрее gzip при сопоставимом размере

В nginx мы настроили выбор метода сжатия по заголовку клиента:

# Приоритет: zstd > brotli > gzip
map $http_accept_encoding $compression {
    default          "gzip";
    "~*zstd"         "zstd";
    "~*br"           "br";
}

Сравнение форматов сериализации

МетрикаJSON + gzipMessagePack + zstdРазница
Размер raw38 КБ28 КБ−26%
Размер сжатый9 КБ8 КБ−11%
Время сжатия7 мкс1 мкс7x быстрее
Время десериализации (сервер)12 мкс6.5 мкс46% быстрее
CPU нагрузка при 15M req/h18% ядра3% ядра6x меньше

Главный выигрыш — не размер (всего 11%), а скорость сжатия: zstd со словарём в 7 раз быстрее gzip, что освободило процессорные ресурсы серверов.

Этап 3: переход на QUIC/HTTP3

Третий этап — замена TCP на QUIC для доставки контента. Для новостного агрегатора с преобладанием мобильного трафика (72% пользователей) это был критический шаг.

Проблемы TCP на мобильном интернете:

  • Head-of-line blocking — потеря одного пакета блокирует все потоки в HTTP/2 мультиплексировании
  • Долгий handshake — TCP + TLS = 2-3 RTT до первого байта данных
  • Потеря соединения при смене сети — переход WiFi → 4G обрывает все TCP-соединения

QUIC решает все три проблемы: независимые потоки, 0-RTT handshake, connection migration по Connection ID.

Мы развернули nginx с модулем QUIC на фронтенд-серверах:

# nginx.conf — включение QUIC/HTTP3
http {
    server {
        listen 443 quic reuseport;
        listen 443 ssl;

        ssl_certificate /etc/ssl/newsplus.crt;
        ssl_certificate_key /etc/ssl/newsplus.key;

        # Включаем 0-RTT
        ssl_early_data on;

        # Анонсируем HTTP/3 через Alt-Svc
        add_header Alt-Svc 'h3=":443"; ma=86400';

        # QUIC-специфичные настройки
        quic_retry on;
        quic_gso on;

        location /api/ {
            proxy_pass http://backend;
            proxy_http_version 1.1;
        }

        location /images/ {
            root /var/www/cdn;
            add_header Cache-Control "public, max-age=604800";
        }
    }
}

Важный нюанс: у примерно 1% пользователей UDP заблокирован на уровне провайдера или корпоративного файрвола. Для них QUIC автоматически fallback-ится на TCP/HTTP2.

Замеры скорости QUIC vs TCP

Мы провели A/B тест на 50% трафика в течение двух недель:

МетрикаTCP/HTTP2QUIC/HTTP3Улучшение
Доставка изображений (P50)180 мс100 мс−45%
API ответы (P50)95 мс93 мс−2%
Загрузка ленты на 4G3.2 сек1.8 сек−44%
Просмотры контентаbaseline+1.6%рост
Connection migration events0 (обрыв)14K/деньбесшовно

Наибольший выигрыш — доставка изображений (−45%). API-ответы улучшились незначительно, так как они и без того маленькие по размеру.

Выбор алгоритма управления перегрузкой

QUIC позволяет выбирать congestion control algorithm на уровне приложения, а не ядра ОС (как в TCP). Мы протестировали три варианта для разных типов контента:

АлгоритмТипЛучше дляГде мы применили
CubicLoss-basedСтабильные сетиНе использовали
NewRenoLoss-basedAPI, малые файлыAPI-запросы, статика
BBRDelay-basedПотоковое видео, большие файлыВидеоплеер (анонсы)

Конфигурация в nginx для разных location:

# API — NewReno для быстрого старта на малых объёмах
location /api/ {
    quic_cc newreno;
    proxy_pass http://api_backend;
}

# Видео — BBR для стабильной пропускной способности
location /video/ {
    quic_cc bbr;
    proxy_pass http://video_backend;
}

NewReno агрессивнее занимает канал при малых объёмах передачи, а BBR лучше поддерживает стабильный битрейт для видеоконтента. Подробнее о протоколах передачи данных — на itfresh.ru.

Сетевые ограничения и реальность мобильного интернета

При оптимизации мы столкнулись с реалиями российского мобильного интернета, которые важно учитывать:

  • RTT 90-й перцентиль — 300 мс для мобильных пользователей (физическое ограничение скорости света + маршрутизация)
  • Packet loss на 4G/WiFi — 0.1-0.5% в нормальных условиях, до 3% в метро и электричках
  • UDP заблокирован у ~1% пользователей (корпоративные сети, некоторые провайдеры)

Мы добавили Forward Error Correction (FEC) для критичных API-ответов — добавочные пакеты с избыточными данными позволяют восстановить потерянный пакет без ретрансмиссии:

# FEC настройка — 10% избыточности для API
# Каждый 10-й пакет содержит XOR предыдущих 9
quic_fec_scheme xor;
quic_fec_ratio 10;

Это добавляет 10% к трафику, но исключает задержки на ретрансмиссию, что критично для интерактивных API-запросов в ленте.

Итоговые результаты проекта

Проект занял 6 недель: 2 недели на аудит и прототипирование, 2 недели на реализацию, 2 недели на A/B тестирование и раскатку. Команда: 2 backend-инженера, 1 DevOps, 1 мобильный разработчик (для поддержки MessagePack и QUIC в приложениях).

Суммарные результаты всех четырёх оптимизаций:

МетрикаДоПослеИзменение
Трафик CDN в месяц82 ТБ33 ТБ−60%
Стоимость CDN2.8 млн ₽/мес1.15 млн ₽/мес−59%
Загрузка ленты (мобильные, P50)2.8 сек1.2 сек−57%
Загрузка ленты (мобильные, P90)5.1 сек2.3 сек−55%
Просмотры контентаbaseline+2.7%+250М/мес
CPU нагрузка на API-серверах45%32%−29%

Экономический эффект за первый год: экономия на CDN составила 19.8 млн рублей, а рост просмотров на 2.7% принёс дополнительную рекламную выручку около 8.5 млн рублей.

Часто задаваемые вопросы

Не всегда. MessagePack даёт выигрыш в скорости сериализации (46%) и размере (10-25%), но усложняет отладку — бинарный формат нельзя прочитать глазами. Рекомендуем для высоконагруженных API (более 10K req/s) и мобильных приложений, где каждый килобайт важен. Для внутренних сервисов и публичных API с малой нагрузкой JSON удобнее.
Три основных: блокировка UDP у части пользователей (1-3%), необходимость открыть UDP-порт 443 на всех файрволах, повышенная нагрузка на CPU из-за шифрования в userspace. Всегда нужен fallback на TCP/HTTP2. Также не все CDN полноценно поддерживают HTTP/3.
В 2026 году WebP поддерживается 97%+ браузеров, включая Chrome, Firefox, Safari (с версии 14), Edge. Проблемы могут быть только у устаревших версий Safari на iOS 13 и старее. Рекомендуем всегда реализовать fallback через тег picture или server-side определение по заголовку Accept.
zstd с обученным словарём сжимает в 7 раз быстрее gzip при сопоставимом или лучшем коэффициенте сжатия. Основной выигрыш — снижение CPU нагрузки на серверах. Для статического контента лучше использовать brotli (максимальное сжатие), а zstd — для динамических API-ответов, где важна скорость.
Только через A/B тестирование с контрольной группой. Мы выделяли 20-50% трафика на оптимизированную версию и отслеживали: технические метрики (размер, скорость), продуктовые метрики (просмотры, DAU, время сессии) и бизнес-метрики (CPM рекламы, выручка). Минимальная длительность теста — 2 недели для статистической значимости.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#оптимизация API#WebP замена JPEG#MessagePack JSON#QUIC HTTP3#zstd сжатие#производительность API#оптимизация трафика#снижение латентности
Комментарии 0

Оставить комментарий

загрузка...