Как я оптимизировал nginx и сайт стал летать
nginx из коробки — это не быстро
Когда я впервые поднял nginx на сервере, он работал нормально. Но страницы грузились за 2-3 секунды, и я знал, что можно быстрее. nginx по умолчанию настроен консервативно — чтобы работать на любом железе, а не выжимать максимум.
Я начал гуглить оптимизации и перепробовал их на своём сервере. Вот что реально дало прирост.
Что я сделал
Кеширование статики. Настроил expires для CSS, JS, картинок. Теперь браузер пользователя кеширует их на месяц. Повторные загрузки страницы стали быстрее на 40%. Добавил в конфиг:
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ { expires 30d; add_header Cache-Control "public"; }
Проверил через curl — до оптимизации сервер возвращал Cache-Control: no-cache. После — public, max-age=2592000. Разница видна сразу в инструментах разработчика: у незакешированных запросов столбец Size показывает реальный размер файла, у закешированных — (from disk cache) или (from memory cache). Это я и сам увидел, когда открыл вкладку Network и перезагрузил страницу: 18 запросов из 22 отдались из кеша.
Первую версию конфига я написал с ошибкой — забыл поставить обратный слеш перед точкой в регулярке. location у меня выглядел как location ~* .(css|js|png)$. И nginx интерпретировал точку как любой символ. В итоге кешировались вообще все запросы, включая HTML. Я заметил это случайно, когда обновил CSS, а он не менялся на сайте. Пришлось добавить галку в админке плагина кеширования и сбросить кеш браузера. После исправления регулярки всё встало на место. Мелочь, а полдня убил.
Gzip сжатие. Включил сжатие для HTML, CSS, JS, JSON. Размер страницы уменьшился с 300 KB до 80 KB. TTFB почти не изменился, но полная загрузка стала быстрее.
Настраивал так: gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml; gzip_min_length 1000; gzip_vary on;. Ключевой момент — gzip_min_length. Если поставить слишком маленькое значение, nginx будет сжимать даже крошечные файлы, а это лишняя нагрузка на CPU. Я поставил 1000 байт — всё, что меньше, сжимать бессмысленно, экономия копеечная. Проверял через curl с заголовком Accept-Encoding: gzip — если в ответе приходит Content-Encoding: gzip, значит, всё работает.
HTTP/2. Включил в nginx — multiplexing запросов. Теперь браузер может загружать несколько файлов одновременно через одно соединение. Раньше было 6 параллельных соединений, теперь — одно, но всё летит пачкой.
Тут был нюанс: для HTTP/2 обязательно нужен HTTPS. Без сертификата nginx даже не запустится с директивами http2. У меня как раз был настроен Let’s Encrypt, поэтому проблем не возникло. Если бы не было — пришлось бы сначала ставить certbot. В конфиге просто добавил listen 443 ssl http2; в server block. И убедился, что openssl версии 1.0.2 и выше — на моей Ubuntu 22.04 это было из коробки. Если у тебя древняя версия, http2 может не поддержаться.
Buffers. Увеличил proxy_buffers и client_body_buffer_size для Apache бэкенда. Чем больше буфер — тем реже nginx обращается к диску.
Выставил proxy_buffers 8 16k; proxy_buffer_size 32k;. До этого стояли значения по умолчанию — 8 4k. Я заметил проблему по логам: в error.log часто мелькало upstream sent too big header. Это значило, что ответ от Apache не влезает в буфер nginx, и тот пишет на диск. После увеличения буферов ошибка исчезла. Ещё я поднял client_max_body_size 100M;, потому что стандартный 1M не давал загружать большие картинки через админку. Тоже из личного опыта — пытался залить скриншот дашборда Grafana, а nginx возвращал 413. Пришлось лезть в конфиг.
Worker processes. Поставил число worker_processes = auto (по числу ядер). Раньше стояло 1, теперь 4. Разница в нагрузке — заметна.
Ещё добавил worker_connections 1024; multi_accept on;. Если оставить стандартные 512 соединений, на пике посещаемости nginx начинал отказывать в соединениях — я это увидел в netstat, когда количество установленных соединений упиралось в лимит. После правки перезагрузил конфиг через nginx -s reload, без перезапуска сервера. В логах перестали появляться ошибки 502, которые я принимал за проблемы Apache.
Ещё один грабли — open_file_cache
Про это редко пишут в гайдах, а зря. nginx по умолчанию каждый раз обращается к файловой системе, чтобы проверить, существует ли файл. На SSD это не страшно, но на обычном HDD — тормоза. Я включил open_file_cache max=2000 inactive=20s; open_file_cache_valid 60s; open_file_cache_min_uses 2;. Теперь nginx кеширует дескрипторы открытых файлов. Эффект заметил на статике: меньше обращений к диску, выше пропускная способность.
Цифры до и после
До оптимизации: страница грузилась 2.8 секунды, TTFB 0.4 секунды, запросов 45.
После: страница грузится 0.6 секунды, TTFB 0.03 секунды, запросов 22 (часть отдаётся из кеша браузера).
Разница в 4.5 раза — и это без смены хостинга или железа. Только настройки nginx.
Что не помогло
Pagespeed. Экспериментировал с модулем ngx_pagespeed от Google. Он дал прирост 5-10%, но создал кучу проблем: ломалась вёрстка, криво сжимались картинки. Отключил.
Thread pools. На моём сервере с 4 ядрами thread pools не дали прироста. Они полезны на серверах с 16+ ядрами.
Sendfile. Тут забавно: sendfile по умолчанию включён, и я думал, что он работает. Но для Apache-бэкенда sendfile бесполезен — он работает только при раздаче статики самим nginx. Я потратил час на диагностику, пока не прочитал документацию. Оставил как есть, но перестал на него надеяться.
Инструменты, которыми я проверял
GTmetrix показал улучшение с D до A. PageSpeed Insights — с 45 до 92 на мобильных. Но самый честный тест — curl с таймингом: curl -o /dev/null -s -w 'connect: %{time_connect} TTFB: %{time_starttransfer} total: %{time_total}\n' https://mysurik.ru. Именно он показал, что TTFB упал с 0.4 до 0.03 секунды. Ещё смотрел nginx лог в реальном времени: tail -f /var/log/nginx/access.log и сравнивал время ответа до и после правок. Субъективно — страницы начали открываться мгновенно, даже на телефоне через мобильный интернет.
Совет
Начните с кеширования статики и gzip. Это два самых эффективных изменения с минимальными усилиями. Потом HTTP/2 и worker_processes. А Pagespeed не трогайте — больше геморроя, чем пользы.
И ещё: не меняйте всё сразу. Я сделал классическую ошибку — переписал полконфига за раз и не понял, что именно дало прирост. Пришлось откатывать по одному параметру и замерять. Делайте по одной директиве, перезагружайте nginx, смотрите curl и лог. Так вы точно будете знать, что работает, а что — просто красивая цифра на бумаге.