Пятница. Вечер. Хорошая погода. Сижу, отдыхаю, никого не трогаю… Приходит SMS от системы мониторинга: «Хватит расслабляться! Сайт компании лежит». Небольшое отступление: сайт использует CMS Drupal 7 и работает на самом дешёвом VPS с 1 ядром, 1 ГиБ памяти и 10 ГиБ на SSD. Ему по сути больше и не надо, ведь 500 посещений в день просто несерьёзно. Быстрое расследование показало, что зависшие процессы PHP-FPM отобрали всю память и MariaDB от такой наглости решила взять выходной. Быстро приводим сайт в чувство:
# systemctl restart php-fpm # systemctl start mariadb
Допустим, с кривым PHP-кодом программисты когда-нибудь разберутся. Но что я могу сделать со своей стороны в этот прекрасный вечер пятницы? Полагаю, что более чем на 10 строк меня не хватит. 😉
Для начала с помощью ApacheBench проверим, на что вообще способен сайт в текущем состоянии. Здесь и далее под словом «сайт» следует понимать всю совокупность программно-аппаратных средств, которые позволяют отобразить в окне браузера необходимую информацию. Для 1000 запросов при 100 одновременных подключений получаем следующий результат:
Concurrency Level: 100 Time taken for tests: 33.492 seconds Complete requests: 1000 Failed requests: 0 Total transferred: 63059272 bytes HTML transferred: 62486000 bytes Requests per second: 29.86 [#/sec] (mean) Time per request: 3349.179 [ms] (mean) Time per request: 33.492 [ms] (mean, across all concurrent requests) Transfer rate: 1838.70 [Kbytes/sec] received
30 запросов в секунду. Интересно, во сколько раз можно улучшить этот показатель изменив не более 10 строк? Обратимся к мощи nginx и попробуем это выяснить. Но для начала в админке Drupal зайдём в раздел «Конфигурация → Разработка → Производительность» и задействуем кэширование страниц для анонимных пользователей. Время жизни кэша выбираем исходя из своих потребностей. Я поставил 1 день, так как на сайте довольно редко происходят изменения.
Строка первая
Для ускорения будем использовать механизм кэширования ответов FastCGI. Для этого потребуется задать путь для хранения кэша и его параметры. У меня получилось так:
http { fastcgi_cache_path /var/cache/nginx levels=1 keys_zone=drupal:1m inactive=1d; }
Первый параметр директивы fastcgi_cache_path задаёт путь для хранения кэша (/var/cache/nginx). Так как на сайте менее 100 страниц, то я не стал раздувать уровни иерархии кэша и ограничился levels=1. Если предполагается значительно большее количество страниц в кэше, то можно добавить ещё один уровень записав levels=1:2. Параметром keys_zone задаётся имя зоны разделяемой памяти для хранения активных ключей и её размер. Одного мегабайта хватает примерно на 8000 ключей. Если у вас больше, то следует увеличить этот параметр, а в моём случае и его более чем достаточно. Один из ключевых параметров — inactive — задаёт время, по истечению которого данные из кэша будут удаляться при отсутствии обращений. По умолчанию 10 минут, я поставил в соответствии с настройками кэша Drupal (1 день) — это позволит сайту продолжать работать даже если остановить PHP-FPM.
Строка вторая и третья
У нас практически всё готово и можно задействовать кэширование:
http { fastcgi_cache_path /var/cache/nginx levels=1 keys_zone=drupal:1m inactive=1d; server { location ~ \.php$ { fastcgi_cache drupal; fastcgi_cache_key $request_uri; } } }
Директивой fastcgi_cache указываем заданную ранее зону разделяемой памяти, которая будет использоваться для кэширования ответов. Директивой fastcgi_cache_key задаём уникальный ключ для кэширования, переменные можно добавить по вкусу и в зависимости от задач. Не забываем применять внесённые изменения:
# nginx -s reload
Строки с четвёртой по восьмую
Кэширование для анонимных пользователей это конечно же хорошо. Но у нас есть авторизованные пользователи и если ничего не предпринять, то они не смогут полноценно пользоваться сайтом. Добавим для них специальную переменную, которая будет отслеживать наличие активной сессии:
http { fastcgi_cache_path /var/cache/nginx levels=1 keys_zone=drupal:1m inactive=1d; map $http_cookie $no_cache { default 0; "~SESS" 1; } server { location ~ \.php$ { fastcgi_cache drupal; fastcgi_cache_bypass $no_cache; fastcgi_cache_key $request_uri; } } }
Что происходит: директивой map объявляется пользовательская переменная $no_cache, значение которой зависит от значения встроенной переменной $http_cookie. По умолчанию $no_cache = 0, но если в переменной $http_cookie встречается строка «SESS», то $no_cache = 1. Директива fastcgi_cache_bypass задаёт условие, при котором ответ не будет браться из кэша. Собственно на этом можно было закончить, но я же обещал 10 строк.
Строка девятая
На тот случай, если на сервере откажут все службы (кроме nginx), можно какое-то время скрывать это от пользователей за счёт кэша:
http { fastcgi_cache_path /var/cache/nginx levels=1 keys_zone=drupal:1m inactive=1d; map $http_cookie $no_cache { default 0; "~SESS" 1; } server { location ~ \.php$ { fastcgi_cache drupal; fastcgi_cache_bypass $no_cache; fastcgi_cache_key $request_uri; fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503; } } }
Директивой fastcgi_cache_use_stale указываем nginx в каких случаях можно использовать устаревший закэшированный ответ, если при работе с FastCGI-сервером возникла ошибка.
Строка десятая
PHP-FPM нам практически не нужен, так что сэкономим оперативную память и не будет держать его процессы в боевой готовности:
pm = ondemand
Всё, 10 строк. Проверим результат:
Concurrency Level: 100 Time taken for tests: 6.191 seconds Complete requests: 1000 Failed requests: 0 Total transferred: 63162000 bytes HTML transferred: 62589000 bytes Requests per second: 161.53 [#/sec] (mean) Time per request: 619.074 [ms] (mean) Time per request: 6.191 [ms] (mean, across all concurrent requests) Transfer rate: 9963.53 [Kbytes/sec] received
161 запрос в секунду против 30 до внесения изменений. На мой взгляд вполне достойно. Бонусом снизилось потребление оперативной памяти с ≈700 МиБ до ≈300 МиБ, так что можно переходить на более дешёвый тариф 😀