ADG Оставить заявку
Блог Инфраструктура 4 мин чтения

Nginx 1.13.9 и HTTP/2 server push: TTFB падает, worker_processes пересматриваем

Nginx 1.13.9 mainline получил HTTP/2 server push и улучшенную SSL-терминацию. Включаем в production и разбираемся с настройкой воркеров под мультиплексирование.

Контекст момента

Nginx 1.13.9 (mainline) добавил поддержку HTTP/2 server push и улучшил производительность SSL-терминации

В феврале nginx mainline прокатил 1.13.9 с тем, чего ждали давно: нормальный HTTP/2 server push. Не экспериментальный флаг, не патч из ветки ngx_http_v2_module - а директива http2_push прямо в конфиге. Мы сразу поставили на пару проектов managed-сопровождения и прогнали неделю в продакшне. Впечатления - ниже.

Откуда взялся повод

Мы с HTTP/2 в nginx работаем с версии 1.9.5, когда модуль вошёл в mainline. До сих пор это было в основном про мультиплексирование запросов и отказ от domain sharding - стандартные выигрыши. Server push как концепция понятен: вместо того чтобы ждать пока браузер разберёт HTML и запросит CSS/JS, nginx сам инициирует отправку этих ресурсов по той же TCP-сессии. TTFB у самой страницы не меняется, но критические ресурсы приходят раньше - браузер не тратит время на round trip за ними.

На практике push работает только если знаешь, что именно нужно отправить. Для статических сайтов это тривиально: index.html всегда тянет за собой одни и те же CSS и JS. Для динамики - сложнее, но тоже решаемо через заголовок Link от бэкенда.

Что включали

Конфиг получился компактным:

server {
    listen 443 ssl http2;

    http2_push /static/css/main.css;
    http2_push /static/js/bundle.js;

    location = / {
        http2_push_preload on;
        proxy_pass http://backend;
    }
}

http2_push - явный список ресурсов для push на уровне сервера. http2_push_preload on - nginx читает заголовок Link: </res>; rel=preload из ответа бэкенда и сам инициирует push для перечисленных там ресурсов. Второй вариант гибче: бэкенд знает контекст запроса и может менять набор ресурсов в зависимости от страницы.

Из нюансов: push работает только по HTTPS. Это очевидно для HTTP/2, но важно проверить - если где-то остался listen 80 без редиректа или ssl не включён явно, директива http2_push просто игнорируется без ошибок в лог.

Про TTFB и замеры

Выигрыш заметен на статических ресурсах с длинным TTL, которые при первом визите браузер ещё не кэшировал. На повторных визитах - нуль, и это нормально: push не должен работать для закэшированного контента. Nginx 1.13.9 не умеет проверять кэш браузера самостоятельно - это проблема спецификации, не реализации. Обойти можно cookie-based эвристикой: бэкенд ставит куку на первый визит и при наличии куки не шлёт заголовки Link. Мы так и сделали на одном проекте.

Измерять лучше через WebPageTest с принудительным clear cache, а не через Chrome DevTools в обычном режиме - DevTools на повторных загрузках показывает картину без push-эффекта. Числа называть не будем - на каждом проекте свои, зависят от размера ресурсов и задержки между клиентом и сервером.

worker_processes и мультиплексирование - где неожиданно

Вот здесь пришлось думать больше, чем планировали. Стандартная рекомендация для worker_processes - auto или равно числу ядер. При HTTP/1.1 это логично: соединений много, каждый воркер тянет свой набор.

HTTP/2 меняет картину. Один клиент - одно TCP-соединение, но много параллельных потоков. При высоком числе одновременных пользователей worker_processes auto на 4-ядерном хосте даёт 4 воркера. Каждый воркер при HTTP/2 держит меньше соединений, но каждое соединение дороже - SSL handshake, ведение таблицы потоков HPACK-компрессии заголовков. На нашем железе мы видели ситуацию когда один воркер уходил в 100% CPU на SSL-терминации при пиковой нагрузке, пока остальные три сидели недогруженными. Распределение соединений по воркерам в nginx не идеально равномерное - accept_mutex и reuseport дают разный результат.

Что сделали:

  • worker_processes auto оставили, но добавили worker_cpu_affinity auto - воркеры привязываются к ядрам, меньше cache bouncing.
  • worker_rlimit_nofile подняли с дефолтного до 65535 - при мультиплексировании число открытых файловых дескрипторов на воркер растёт быстрее чем при HTTP/1.1.
  • http2_max_concurrent_streams явно ограничили: дефолт 128 потоков на соединение в нашем случае оказался избыточным, 64 достаточно и меньше давит на буферы.

Это не рецепт - это наш конкретный случай на конкретном железе. Мораль в том, что переход на HTTP/2 с push - не «обновил пакет и забыл», а повод пересмотреть профиль нагрузки.

SSL-терминация в 1.13.9

Отдельно отметим улучшения в SSL: nginx 1.13.9 лучше работает с TLS session tickets и стал чуть аккуратнее с буферизацией SSL-записей. На практике это означает что ssl_session_cache shared:SSL:10m теперь реально помогает при нескольких воркерах - shared кэш сессий работает между воркерами без лишних handshake при перепрыгивании клиента между ними.

Если ещё не мигрировали с 1.12 stable - повода спешить нет, это mainline. Но для тех кто уже держит mainline-ветку: 1.13.9 без сюрпризов, апгрейд чистый.

Где сейчас

HTTP/2 server push на двух проектах в продакшне, ещё несколько в очереди. Заметного регресса не поймали, TTFB на первых загрузках приятнее. Дальше - смотрим как поведёт себя под реальной нагрузкой осеннего сезона, пока данных немного.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.