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 на первых загрузках приятнее. Дальше - смотрим как поведёт себя под реальной нагрузкой осеннего сезона, пока данных немного.