Как мы ускорили PHP-проекты в 40 раз
Вопросы SEO-оптимизации и улучшения User eXperience, которые в определенный момент встали перед командой Wrike, потребовали значительного увеличения скорости работы наших веб-проектов. На тот момент их было порядка десяти (основной сайт, блог, справочный центр и т. д.). Решение по ускорению проектов было выполнено на основе связки Nginx + fastcgi cache + LUA + LSYNC.
Дано
На большинстве проектов для удобства, универсальности и расширяемости плагинами мы использовали связку Wordpress + themosis, на некоторых — просто Wordpress. Естественно, на Wordpress еще было «навешано» множество плагинов + наша тема: на серверные ноды с веб-проектами — Nginx + php-fpm, а перед — Entry Point (Nginx + proxy_pass на них).
Каждое из приложение находилось на своем сервере апстрима, на котором был proxy_pass по round-robin. Сами понимаете, ждать от такой связки хороших показателей не приходилось.
На тот момент TTFB (Time To First Byte) и Upstream Response Time в большинстве случаев составляли от 1 до 3 секунд. Такие показатели нас не устраивали.
Прежде чем заняться поиском решения, мы определили, что нас устроит 50 мс для upstream response time. Upstream response time выбрали как наиболее показательную величину, которая показывала исключительно время ответа сервера с веб-приложением и не зависела от интернет-соединения.
Шаг 1: fastcgi
По результатам ресерча остановились на fastcgi-кэше. Штука оказалась действительно хорошая, настраиваемая и замечательно справляется со своей задачей.
После ее включения и настройки на нодах показатели улучшились, но незначительно. Весомых результатов добиться не удалось из-за того, что Entry Point раскидывал запросы по round-robin алгоритму внутри апстрима, и, соответственно, кэш на каждом из серверов для одного и того же приложения был свой, пусть и одинаковый. Наша архитектура не позволяла нам складывать кэш на наш Entry Point, поэтому пришлось думать дальше.
Шаг 2: lsyncd
Решение было выбрано следующее: использовать lsyncd для дистрибьюции кэша между нодами апстрима по событию inotify. Сказано — сделано: кэш сразу же во время создания на одной ноде по inotify начинал «улетать» на остальные ноды, но к успеху это, конечно, не привело. О странице в кэше знал только Nginx той ноды, запрос в которой был обработан.
Мы немного подумали и нашли способ, которым можно и остальные ноды научить работать с кэшем, полученным через lsyncd. Способ оказался не изящным — это рестарт Nginx, после которого он запускает cache loader (спустя минуту), а тот в свою очередь начинает загружать в зону кэша информацию о закэшированных данных — тем самым он узнает о кэше, который был синхронизирован с других нод. На этом этапе также было принято решение о том, что кэш должен жить очень долго и генерироваться в большинстве случаев через специального бота, который бы ходил по нужным страницам, а не через посетителей сайта и поисковых ботов. Соответственно были подтюнены опции fastcgi_cache_path и fastcgi_cache_valid.
Все хорошо, но как ревалидировать кэш, который, например, необходим после каждого деплоя. Вопрос ревалидации решили с помощью специального заголовка в опции типа fastcgi_cache_bypass:
fastcgi_cache_bypass $skip $http_x_specialheader;
Теперь оставалось сделать так, чтобы наш бот после деплоя начинал ревалидацию проекта, используя такой заголовок:
--header='x-specialheader: 1'
В ходе процесса ревалидации кэш сразу же «разлетался» на все ноды (lsyncd), а так как время жизни кэша у нас большое и Nginx знает, что страницы закэшированы, то он начинает отдавать посетителям уже новый кэш. Да, на всякий случай добавляем опцию:
fastcgi_cache_use_stale error timeout updating invalid_header http_500;
Опция пригодится, если, например, php-fpm вдруг случайно отвалился, либо в продакшн приехал код, который по каким-то невероятным причинам возвращает 500-ки. Теперь Nginx будет возвращать не 500-ку, а вернет старый «рабочий» кэш.
Также схема ревалидации с помощью заголовка позволила нам сделать веб-интерфейс для ревалидации определенных урлов. Он был сделан на основе php-скриптов, которые отправляли специальный заголовок на требуемый URL и ревалидировали его.
Здесь мы ощутили нужный прирост скорости отдачи страниц. Дело пошло в нужное русло :)
Шаг 3: LUA
Но оставалось одно «но»: нам необходимо было управлять логикой кэширования в зависимости от тех или иных условий: запросы с определенным параметром, кукой и т.д… Работать с «if» в Nginx не хотелось, да и не решил бы он всех тех задач с логикой, которые перед нами стояли.
Начался новый ресерч, и в качестве прослойки для управления логикой кэширования был выбран LUA.
Язык оказался весьма простым, быстрым и, главное, через модуль хорошо интегрировался с Nginx. Процесс сборки отлично задокументирован здесь.
Оценив возможности связки Nginx + LUA, мы решили возложить на него следующие обязанности:
редиректы с несколькими условиями;
эксперименты с распределением запросов на разные лендинги по одному URL (разный процент запросов на разные лендинги);
блокировки с условиями;
принятие решение о необходимости кэширования той или иной страницы. Это делалось по заранее заданным условиям, конструкциями вида:
location ~ \.php {
set $skip 0;
set_by_lua $skip '
local skip = ngx.var.skip;
if string.find(ngx.var.request_uri, "test.php") then
app = "1";
end
return app;
';
...
fastcgi_cache_bypass $skip $http_x_specialheader;
fastcgi_no_cache $skip;
...
}
Проделанная работа позволила получить такие результаты:
- Upstream response time для подавляющего большинства запросов перестало выходить за пределы 50 мс, а в большинстве случаев оно ещё меньше.
- Также это отметилось в консоли Google ~25% снижением Time spent downloading a page (работы).
- Значительно улучшились Apdex-показатели по Request Time.
- Бонусом стала опция fastcgi_cache_use_stale, которая послужит своеобразным защитником от 500-к в случае неудачного деплоя или проблем с php-fpm.
- Возможность держать гораздо больше RPS за счёт того, что обращения к php минимизировались, а кэш, грубо говоря, является статическим html, который отдаётся прямо с диска.
На наиболее показательном примере upstream response time одного из приложений динамика выглядела так:
Динамика в консоли Google на протяжении внедрения решения выглядит следующим образом:
Расхождения в графиках закономерны, так как консоль показывает динамику всех проектов домена. Само кэширование не принесло значительных неудобств, так как инструменты для его ревалидации оказались весьма простыми.
Таким образом мы достигли значительного увеличения скорости наших веб-проектов, практически не затратив ресурсов разработчиков на модернизацию приложений, и они могли продолжать заниматься разработкой фич. Данный способ хорош скоростью и удобством реализации, однако слабая его сторона в том, что кэш, хоть и решает проблему медленного рендеринга страниц, но не устраняет корень проблем — медленную работы самих скриптов.
Комментарии (1)
3 августа 2016 в 11:31
0↑
↓
Так вы не ускорили, а закешировали. Вместо РНР можно написать что угодно.
Я думал в статье будут продемонстрированы какие-то новые практики. Ну или хотя бы пример выноса PHP в C модуль.Попробуйте сделать кеши на стороне nginx для динамических страниц. Вот это бы было интересно.
ИМХО.