Вопросы 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)


  1. DeLuxis
    03.08.2016 11:31
    +25

    Так вы не ускорили, а закешировали. Вместо РНР можно написать что угодно.
    Я думал в статье будут продемонстрированы какие-то новые практики. Ну или хотя бы пример выноса PHP в C модуль.

    Попробуйте сделать кеши на стороне nginx для динамических страниц. Вот это бы было интересно.
    ИМХО.


    1. wrike_ops
      03.08.2016 12:28
      -4

      Кэш ускорил проекты, что и являлось главной задачей. Нашей целью были минимальные сроки и минимальные изменения кода приложений. Собственно этого мы и достигли, а цена оказалась минимальной. Кроме того в статье написали не только про кэш.


      1. hlogeon
        03.08.2016 13:23

        но по факту название не соответствует действительности. Вы написали в духе «желтых» газет. Вместо того, что бы написать: «Как мы ускорили проекты компании», вы пишите об ускорении PHP, к которому, по факту статья никакого отношения не имеет.


        1. wrike_ops
          03.08.2016 14:10

          Мы не писали об ускорении PHP. Мы писали об ускорении работы PHP-проектов.


          1. landy
            04.08.2016 08:26

            Написали бы Web-проектов тогда. Статья хорошая, но не о том.

            Согласен с первым комментарием, вы ввели заблуждение заголовком.
            Я хотел почитать интересную статью о кешировании в PHP (например для нераспределенного проекта).

            Ну и в том числе вы породили кучу очередного холивара про PHP.


    1. Rathil
      04.08.2016 09:00

      Я когда-то делал проще: при запросе я на РНР писал в мемкеш и отдавал контент, а при повторном запросе сам nginx вычитывал данные из мемкэша и отдавал их. Дополнительно использовал SSI.


    1. bosha
      04.08.2016 13:09

      Попробуйте сделать кеши на стороне nginx для динамических страниц. Вот это бы было интересно.

      Так? В этом так то тоже нет никакого rocket-science. Совсем.


  1. KAndy
    03.08.2016 11:48
    -2

    Как ускорить PHP преложение — не запускать его ;)


    1. Rory
      03.08.2016 12:06
      +12

      Свежая грамотная шутка.


    1. wrike_ops
      03.08.2016 12:42

      Вцелом так и получилось: когда проект, по большей части, покрыт кэшем — генерация страниц через php становится минимальной. По сути остались только те части приложения, в которых кэш применить просто нельзя.


      1. asiros
        03.08.2016 14:09

        Прям как-то всё печально получилось конечно с этой шуткой.


  1. alekciy
    03.08.2016 11:59
    +3

    if-ы в 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-ов.


    1. wrike_ops
      03.08.2016 12:37

      Да, map-ы работают быстрее «if», но это не решило бы наших задач. Логики на LUA возложено достаточно много, например, эксперименты с распределением запросов на разные лендинги по одному URL, принятие решения о кэширование, редиректы по условию и т.д.


  1. nikitasius
    03.08.2016 12:30
    -2

    Как мы ускорили PHP-проекты в 40 раз

    Ожидал увидеть статью о том, как вы переписали 40 проектов с php на…


    1. ElectricHeart
      03.08.2016 12:46
      +2

      Ассемблер?


      1. nikitasius
        03.08.2016 17:30

        Я думаю для некоторых сайтов тетка с перфокартами будет быстрее, чем текущая реализация.


        1. ElectricHeart
          04.08.2016 14:48

          Но с языком ведь это связано довольно мало?)


  1. ZaEzzz
    03.08.2016 13:23
    +2

    Открыли для себя кеширование… Молодцы…
    А теперь ускорьте наконец-то работу фронтенда, а то постоянно идут тормоза в интерфейсе.


  1. 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 :)


    1. Nickmob
      03.08.2016 16:58

      Могли бы переехать на cloudflare :)

      А чем бы он помог в этом случае?


      1. sumanai
        03.08.2016 17:05

        Кешированием?


        1. Nickmob
          03.08.2016 17:25

          Он не рассчитан на кеширование динамики, только статика. Можно по-моему через Page Rules принудительно включить, но это уже будет извращение.


          1. G-M-A-X
            03.08.2016 21:15

            Вы просто не умеете его готовить :)

            Хотя там вроде порядка 40 серверов.
            Кеш на каждом отдельный.


          1. nikitasius
            03.08.2016 23:51

            Ничего не извращение.
            CloudFlare + nginx = кешируем всё на бесплатном плане
            Смысл в том, что можно все кешировать. Правда для кеширование динамики надо будет добавлять что-нибудь к адресу (чтобы кеш был каждый раз новый для группы юзеров, или баловаться с чисткой кеша через API раз в 15 минут (если там новостной сайт со средний временем обновления 15 минут).


  1. 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 подошёл.

    Может нужно было просто прописать:
    fastcgi_cache_key $uri;
    это привело бы к тому, что если пришел первым HEAD запрос, то он бы попал в кэш и для GET запросов отдавался кэш от HEAD.


    1. G-M-A-X
      03.08.2016 14:12

      >это привело бы к тому, что если пришел первым HEAD запрос, то он бы попал в кэш и для GET запросов отдавался кэш от HEAD.

      Можно было добавить в ключ метод :)

      Не знаю, как для фастцги, но прокси позволяет кешировать GET и HEAD (любые другие методы) вместе, отправляя 1 GET запрос на бекэнд.

      да, именно так — у каждого свой fastcgi cache

      Вы сами себе создали проблему, а потом ее решили костылями :)


  1. wrike_ops
    03.08.2016 14:27

    Можно было добавить в ключ метод :)

    Мы так и делаем.


  1. mrmoney
    03.08.2016 15:11
    -1

    отличная статья!


  1. Nickmob
    03.08.2016 17:00

    Интересно, что должен делать upstream, чтобы upstream response time был 3-5 секунд?


    1. G-M-A-X
      03.08.2016 21:34

      Вроде написано было 1-3.

      +Это же вордпресс со множеством плагинов :)


      1. Nickmob
        03.08.2016 21:58

        Я сужу по приведённому графику с response time


        1. wrike_ops
          04.08.2016 07:58

          График приведен для одного из приложений (1-3 секунды это среднее по всем проектам), но замечено правильно — wp + плагины.


  1. darken99
    03.08.2016 19:48

    Я надеюсь что заголовок для skip cache в статье был изменен? А то вы сильно рискуете если кто-то решит получить DoS на свои бекенды.


    1. wrike_ops
      04.08.2016 07:55

      Заголовок другой.


  1. akubintsev
    04.08.2016 16:02

    Эх, костыль костылём погоняет…


  1. G-M-A-X
    04.08.2016 22:07

    Так у вас используется lsyncd для дистрибуции кеша или кеш генерируется обходом бота?
    Сколько у вас nginx серверов?
    Я понял из предыдущего ответа, что 1.


    1. sumanai
      04.08.2016 22:22

      Сколько у вас nginx серверов?
      Я понял из предыдущего ответа, что 1.

      Как раз таки из их ответа следует, что у них на каждом сервере по nginx.


      1. G-M-A-X
        04.08.2016 22:54

        Сорри, немного натупил :)
        Но первый вопрос актуален.