Вопросы 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 на протяжении внедрения решения выглядит следующим образом:
Расхождения в графиках закономерны, так как консоль показывает динамику всех проектов домена. Само кэширование не принесло значительных неудобств, так как инструменты для его ревалидации оказались весьма простыми.
Таким образом мы достигли значительного увеличения скорости наших веб-проектов, практически не затратив ресурсов разработчиков на модернизацию приложений, и они могли продолжать заниматься разработкой фич. Данный способ хорош скоростью и удобством реализации, однако слабая его сторона в том, что кэш, хоть и решает проблему медленного рендеринга страниц, но не устраняет корень проблем — медленную работы самих скриптов.
Комментарии (38)
KAndy
03.08.2016 11:48-2Как ускорить PHP преложение — не запускать его ;)
wrike_ops
03.08.2016 12:42Вцелом так и получилось: когда проект, по большей части, покрыт кэшем — генерация страниц через php становится минимальной. По сути остались только те части приложения, в которых кэш применить просто нельзя.
alekciy
03.08.2016 11:59+3if-ы в nginx прекрасно заменяются на map (более православный вариант). Приведенный скрипт на LUA вполне заменяем на что-то в духе:
map "$request_uri" $skip {
default 0;
"test.php" 1;
}
в основном конфиге nginx. Можно делать и более сложные варианты. К примеру у меня такой был: «все запросы на https редиректить на http кроме некоторых адресов»:
map "$server_port:$uri" $https_redirect {
default 0;
"443:/clean/RbkMoneyCallback" 0;
"443:/robots.txt" 0;
"~^443:.+$" 1;
}
Теперь достаточно в одном месте дописывать эти адреса и не копаться в if-ах разных location-ов.wrike_ops
03.08.2016 12:37Да, map-ы работают быстрее «if», но это не решило бы наших задач. Логики на LUA возложено достаточно много, например, эксперименты с распределением запросов на разные лендинги по одному URL, принятие решения о кэширование, редиректы по условию и т.д.
nikitasius
03.08.2016 12:30-2Как мы ускорили PHP-проекты в 40 раз
Ожидал увидеть статью о том, как вы переписали 40 проектов с php на…ElectricHeart
03.08.2016 12:46+2Ассемблер?
nikitasius
03.08.2016 17:30Я думаю для некоторых сайтов тетка с перфокартами будет быстрее, чем текущая реализация.
ZaEzzz
03.08.2016 13:23+2Открыли для себя кеширование… Молодцы…
А теперь ускорьте наконец-то работу фронтенда, а то постоянно идут тормоза в интерфейсе.
G-M-A-X
03.08.2016 14:03кэш на каждом из серверов для одного и того же приложения был свой, пусть и одинаковый.
То есть было примерно так:
upstream php-fpm7.0 { server 10.0.0.1; server 10.0.0.2; }
?
И для каждого сервера-бекэнда был свой фастцги-кеш?
использовать lsyncd для дистрибьюции кэша между нодами апстрима
Или выше Вы говорили не о фастцги-кеше, а о кеше приложения?..
Может стоило использовать мемкеш/сетевую фс?
Или у Вас несколько nginx + несколько php?
Соответственно были подтюнены опции fastcgi_cache_path и fastcgi_cache_valid.
Может нужно было просто прописать:
fastcgi_cache_key $uri;
Могли бы переехать на cloudflare :)Nickmob
03.08.2016 16:58Могли бы переехать на cloudflare :)
А чем бы он помог в этом случае?sumanai
03.08.2016 17:05Кешированием?
Nickmob
03.08.2016 17:25Он не рассчитан на кеширование динамики, только статика. Можно по-моему через Page Rules принудительно включить, но это уже будет извращение.
G-M-A-X
03.08.2016 21:15Вы просто не умеете его готовить :)
Хотя там вроде порядка 40 серверов.
Кеш на каждом отдельный.
nikitasius
03.08.2016 23:51Ничего не извращение.
CloudFlare + nginx = кешируем всё на бесплатном плане
Смысл в том, что можно все кешировать. Правда для кеширование динамики надо будет добавлять что-нибудь к адресу (чтобы кеш был каждый раз новый для группы юзеров, или баловаться с чисткой кеша через API раз в 15 минут (если там новостной сайт со средний временем обновления 15 минут).
wrike_ops
03.08.2016 14:07То есть было примерно так:
upstream php-fpm7.0 {
server 10.0.0.1;
server 10.0.0.2;
}
Да. На каждом сервере Nginx + PHP-FPM.
И для каждого сервера-бекэнда был свой фастцги-кеш?
да, именно так — у каждого свой fastcgi cache.
Может стоило использовать мемкеш/сетевую фс?
Сетевую файловую систему использовать не хотелось. Хотели использовать простое и проверенное решение, lsync подошёл.
Может нужно было просто прописать:
это привело бы к тому, что если пришел первым HEAD запрос, то он бы попал в кэш и для GET запросов отдавался кэш от HEAD.
fastcgi_cache_key $uri;G-M-A-X
03.08.2016 14:12>это привело бы к тому, что если пришел первым HEAD запрос, то он бы попал в кэш и для GET запросов отдавался кэш от HEAD.
Можно было добавить в ключ метод :)
Не знаю, как для фастцги, но прокси позволяет кешировать GET и HEAD (любые другие методы) вместе, отправляя 1 GET запрос на бекэнд.
да, именно так — у каждого свой fastcgi cache
Вы сами себе создали проблему, а потом ее решили костылями :)
G-M-A-X
04.08.2016 22:07Так у вас используется lsyncd для дистрибуции кеша или кеш генерируется обходом бота?
Сколько у вас nginx серверов?
Я понял из предыдущего ответа, что 1.
DeLuxis
Так вы не ускорили, а закешировали. Вместо РНР можно написать что угодно.
Я думал в статье будут продемонстрированы какие-то новые практики. Ну или хотя бы пример выноса PHP в C модуль.
Попробуйте сделать кеши на стороне nginx для динамических страниц. Вот это бы было интересно.
ИМХО.
wrike_ops
Кэш ускорил проекты, что и являлось главной задачей. Нашей целью были минимальные сроки и минимальные изменения кода приложений. Собственно этого мы и достигли, а цена оказалась минимальной. Кроме того в статье написали не только про кэш.
hlogeon
но по факту название не соответствует действительности. Вы написали в духе «желтых» газет. Вместо того, что бы написать: «Как мы ускорили проекты компании», вы пишите об ускорении PHP, к которому, по факту статья никакого отношения не имеет.
wrike_ops
Мы не писали об ускорении PHP. Мы писали об ускорении работы PHP-проектов.
landy
Написали бы Web-проектов тогда. Статья хорошая, но не о том.
Согласен с первым комментарием, вы ввели заблуждение заголовком.
Я хотел почитать интересную статью о кешировании в PHP (например для нераспределенного проекта).
Ну и в том числе вы породили кучу очередного холивара про PHP.
Rathil
Я когда-то делал проще: при запросе я на РНР писал в мемкеш и отдавал контент, а при повторном запросе сам nginx вычитывал данные из мемкэша и отдавал их. Дополнительно использовал SSI.
bosha
Попробуйте сделать кеши на стороне nginx для динамических страниц. Вот это бы было интересно.
Так? В этом так то тоже нет никакого rocket-science. Совсем.