Ламер с 20 летнем стажем
0,1
рейтинг
30 ноября 2010 в 02:27

Разработка → Повышение производительности за счет блочного кеширования

Тема блочного кеширования и ssi не раз проскакивала на Хабре. Ниже я представлю еще одну реализацию, использующего блочное кеширование, а также исходники фреймворка, использующего эти принципы, которые можно найти тут. А как это работает — прочитать ниже.
схема кеширования блоками

Чуть-чуть об MVC подходах


В основу построения фреймворка лег принцип «as simple as better». Если рассмотреть ход сборки выходного HTML по паттерну MVC, то существует два подхода: push & poll.

При первом подходе фронт-контроллер вызывает множество контроллеров, которые запускают модели и с помощью View формируют множество блоков, которые потом собираются в фронт View.

Второй подход — обрабатывается один View шаблон, в котором присутствуют функции обратного вывода (callback), которые вызывают соответствующие контроллеры, а они в свою очередь формируют HTML блока. Такой подход, например, использован в ZendFramework.

В данном случае использован первый подход, но с тем отличием, что сборкой занимается не РНР-скрипт, а непосредственно WEB-сервер (nginx) посредством ssi (Server side include). Используются SSI директивы: include и echo. Это позволяет:
  • упростить код контроллера, снизить нагрузку на РНР скрипты
  • выполнять несколько скриптов одновременно
  • уменьшить поток байт, передаваемый между WEB сервером и бэкэндом
  • кешировать блоки средствами сервера, не дергая скрипты бэкэнд(РНР)
Сам фреймворк (дал ему название quickly, так как он по производительности в 4-ре раза быстрее ZF) тесно интегрирован с nginx, и часть nginx конфига — является непосредственной частью проекта. (кому-то это не нравится… На вкус и цвет товарища нет) Для этого в nginx.conf должн быть включена директива include /path/to/project/conf/local.nginx.conf

Немного о том как это работает вообще


Классическая схема MVC в WEB — каждый контролер привязан к какой-то определенной части url. Часть url привязана к action и часть к параметрам.

Как упоминалось выше, часть функций контроллера берет на себя nginx. Разруливание по url осуществляется с помощью директив location. Можно все разрулить по контроллерам (страницам), действиям ( actions)и параметрам. Но это более гибче, чем в том же ZF. Обязательно в директиве location должен присутстввовать fastcgi параметр page, по значению которого выбирается соответствующий класс. Считаем, что вызвали соответствующий контроллер блока.
Пример части конфига: set $app_script run_app.php;
. . .
location ~ ^/catalog/(\w+)/? {
fastcgi_pass localhost:9000;
fastcgi_param page catalog;
fastcgi_param cat_name $1;
include fastcgi_params;
}

В данном примере показано, что будет передан fcgi-параметры page=catalog, cat_name равен последней части url. Будет вызван РНР скрипт: run_app.php, который из каталога page инстанцирует класс catalogPage (расположение page/catalogPage.php) и запустит метод run(). В переменной окружения cat_name для урла /catalog/bmv примет значение bmv.

Как это работает с SSI


Разруливание по location делятся на две части: внешние и внутренние. Внешние — это выбор соответствующего ssi шаблона с использованием реврайта. Внутренние — это локейшены частных контроллеров.

Пример SSI шаблона (index.tpl):
<script>
  <!--# include virtual="$js" -->
</script>
<table >
  <tr >
    <td >left block
    <!--#include virtual="$top" -->
    </td>
    content
    <td valign="top">content block<br>
    <!--#include virtual="$int" -->
    </td>
  </tr>
</table>

* This source code was highlighted with Source Code Highlighter.

Пример части конфига:
  set $int "/ssi$request_uri";
  set $top "/ssi/top10$request_uri";
  . . .
  location /catalog {      
    set $js "js/catalog.js";
    rewrite ^(.*)$ /index.tpl;
  }

  location /ssi {
    internal; # ставим директиву на продакшене, при отладке убираем
    location /ssi/catalog/(\w+)/? {
      fastcgi_pass localhost:9000;
      fastcgi_param page catalog;
      fastcgi_param cat_name $1;
      fastcgi_param ssi 1;
      include fastcgi_params;
    }

    location /ssi/top10/(\w+)/? {
      fastcgi_pass localhost:9000;
      fastcgi_param page top10;
      fastcgi_param top_name $1;
      fastcgi_param ssi 1;
      include fastcgi_params;
    }
  }

* This source code was highlighted with Source Code Highlighter.


По первому location осуществляется реврайт на index.tpl с установкой переменной $js = js/catalog.js
В шаблоне index.tpl осуществляется подстановка нужного js скрипта, а также вызов нужных блоков путем использования ssi-директив #include. В нашем примере сработает внутренний location /ssi/catalog/ и вызовит PHP скрипт run_app, который синстанцирует класс catalogPage и запустит метод run(), а также аналогичным образом отработает блок top10.

Как это работает с memcached


смотрим рисунок. там все предельно ясно: вместо обращения по location /top10 мы обращаемся напрямую к мемкешу по location /mc. Если кеш инвалиден (пустой), то модуль ngx_memcache_module дает нам 404 ошибку. Обрабатываем 404 ошибку и делаем внутренний редирект на именованный location mcb. РНР-скрипт должен сформировать HTML и положить его в кеш. Особо беспокоиться об этом не надо, это происходит в базовом классе, если указать в нашем классе параметры:
protected $_Cached = true;
public $CachingKey = '/top_$top_name';

Пример конфига:
  location ~ ^/catalog/(\w+) {
     rewrite ^(.*)$ /index.tpl;
     set $memkey "top_$1";
   }

  location /mc {
      set $memcached_key $memkey;
    default_type text/html;
    memcached_pass localhost:11211;
    error_page 404 @mcb; // если данные по ключу отсутствуют,
          //то идет переход на именованный локейшен 
    }

  location @mcb {
    fastcgi_pass localhost:9000;
    fastcgi_param page block;
       fastcgi_param blocknum $blocknum;
    include fastcgi_params;
  }

* This source code was highlighted with Source Code Highlighter.


Особенности кеширования:
если вы используете расширение php_memcache, то ни каких особенностей нет.

Если используется библиотека libmemcached и php_memcached, то по умолчанию отрабатывает сжатие контента.
Возможны следующие варианты:
  • не использовать сжатие, установить параметр Memcached::OPT_COMPRESSION = false.
  • установить в location default_type gzip/deflate; Но при маленьких объемах, где-то до 64 байт сжатие не производится.
  • пропатчить ngx_memcache_module. В зависимости от значения принятого из memcache параметра выдавать заголовок text/html или gzip/deflate (патч 10 строк)

Благодарности


В первую очередь хочется высказать благодарность в адрес Игоря Сысоева sysoev.ru, без него не было бы и этого кода да и многих высокопроизводительных проектов рунета.
А также спасибо Константину Барышникову (fixxxer) за идеи использования location в качестве фронт-контроллера.
Хочется отблагодарить Алексея Рыбака ( fisher) за его blitz, который я активно использую в своих проектах, в частности и в этом фреймворке, уже более трех лет.
Ну и автору php-fpm Андрею Нигматулину (anight), который своим проектом внес не малый вклад в hiload рунета.

PS. Если у Вас что-то не запустилось, не беда. Получится в другой раз, главное не падать духом. А пока передохните и почитайте Хабр.
Александр Календарев @akalend
карма
76,5
рейтинг 0,1
Ламер с 20 летнем стажем
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (60)

  • +3
    Вроде как Varnish для этого более приспособлен
    • +3
      I ♥ nginx
      • 0
        Я тоже его люблю, но ssi в nginx 0.6.x, 0.7.x, 0.8.x глючен. Иногда, вместо вставляемого блока он отдает кусок случайных символов (налицо какая то проблема с указателями на память).
        И нам так же пришлось перейти на varnish в качестве esi-процессора.
        • 0
          это потому что SSI ради скорости сделан как upstream модуль.
          если бэкенд тупит, то может и сглюконуть.
          Делаем быстрый бэкенд и проблемы нет.
          • 0
            это слабо тянет на оправдание для такого нарушения целостности, imho.
          • 0
            Не вижу логической связи.
    • 0
      в Varnish так красиво не раскидаешь по контроллерам
      не задашь параметры и экшены для бэкенда.

      Может это возможно, просто у меня не получилось
    • 0
      поддерживаю, хотя в реализации esi:include гибкости оччень не достаяет…
      • 0
        как вариант в nginx
        можно использовать ssi + xslt
        я этот вариант рассматриваю как альтернатива
        гибкости будет достаточно
        единственное меня смущает скорость libxslt
        • +1
          очень правильно смущает, это вообще не вариант в реальной жизни.
          • 0
            согл
  • +2
    Из int повлиять на то, что покажется в top (самый простой пример из body повлиять на title) мы с таким подходом не сможем?
    • 0
      ну почему же можем?

      можно использовать shm для хранения общих объектов
      или тот же мемкеш…

      у меня, например, строка поиска сделана в обном ssi а вывод контента (по поиску) в другом
      все конечно зависит от проекта, но блоки не должны быть взаимосвязаны
      блок выдачи перечня товаров и блок топ10 товаров. блок голосования и рекламный блок.

      конечно ssi должно быть использовано в меру. По мимо ssi, для формирования контента я использую и первый и второй подходы.
      • 0
        Средствами «шаблонизатора» получается не можем. То есть для вывода одного блока информации в двух блоках страницы в разном виде нам нужно вызывать два разных контроллера.

        По-моему, ситуация, когда блоки взаимосвязаны (а вернее зависят от общих данных) довольно часта («с этим товаром также покупают», «похожие материалы», форма коммента для залогиненных пользователей и т. п.).

        В общем выходит, что перед вызовом шаблона нужно вызвать какой-то «суперконтроллер», который подготовит в shm, мемкэше или ещё в каком-то месте доступном разным процессам/потокам все данные, а затем из шаблона вызывать «миниконтроллеры», форматирующие данные как нужно в данном блоке. Или разгребать кучу зависимостей при изменении вёрстки, чтобы быть уверенным, что данные в общей памяти есть и актуальны. Для примера title и body мы должны в контроллере title получить данные и для title и для body, выведя title и поместив body в общую память, откуда их потом возьмёт контроллер body. А если мы откажемся от вывода title, то нужно будет переносить логику получения данных в контроллер body…

        В общем, по-моему, налицо жёсткая и хрупкая связь между логикой приложения, бизнес-логикой и отображением в таком фреймворке и его применение должно быть оправдано (например скорость работы куда важнее простоты поддержки).
        • +1
          Блок «с этим товаром покупают» ни как не должен зависить от блока «карточка товара». Есть общая часть урла от нее и пляшем: goods/10235
          для карточки товара формируем урл /int/goods/10235 а для блока «с этим товаром покупают» урл /int/with/10235. Первый блок тянет из БД таблицу goods id=10235. Второй блок тянет из таблицы withGoods id= 10235. Все!!! Ни каих зависимостей! Хотим чтоб работало быстро — готовим данные заранее в отдельную таблицу. Хотим делать как все — тогда игнорируем этот топик!
          • 0
            тогда мы не интересуемся высокими нагрузками.
        • 0
          то перед вызовом шаблона нужно вызвать какой-то «суперконтроллер», который подготовит в shm, мемкэше или ещё в каком-то месте доступном разным процессам/потокам все данные
          У меня это понадобилось лишь один раз в одном месте, и я сделал так называемые «отложенные вычисления». Проверяем если есть в кеше (любом) — если нет, то готовим данные. Все и ни каких контроллеров. Будьте проще, не надо городить кучу кода.
          • +1
            Отвечаю сразу на все:

            Каюсь, пример неудачный, в общем когда один блок зависит от динамически вычисляемого содержимого (а не id) другого блока. А готовить данные в отдельную таблицу смысла может не иметь, т. к. число чтений и записей примерно равно, а может быть и вредно, если записей намного больше.

            «Контроллер блока» вы в посте употребили. И зависимость останется даже при отложенных вычислениях или надо будет дублировать код для подготовки данных.

            В общем идея интересная, но, по-моему, и большей гибкости, и большей производительности можно добиться комбинируя сборку шаблона из блоков средствами SSI и подготовку динамических блоков, которых нет в кэше, одним вызовом PHP
            • 0
              у меня так и есть!
              есть два класса Application & ApplicationSSI
              первоночально использовался класс Application
              в котором предусмотрено использование и блоков, и хелперов (1-й и 2-й подход MVC), потом я усложнил до ssi включений. Я не стал накручивать на класс Application, а продублировал часть функционала в ApplicationSSI.
              разруливается это в location установкой переменной ssi=1 (вызов ApplicationSSI)

              Для отладки можно вызвать блок напрямую, тогда сработает класс Application
  • +5
    Вот вы сравнивали с ZF, а сравните с Yii ;)
    • 0
      я работал в нескольких компаниях
      и там Технические Директора считали ZF образцом кодинга
      по этому с ним и сравниваю
      • +3
        Просто они не видели Yii/Symphony :) No holy wars plz
        • –1
          Просто они не видели Rails :) No holy wars plz
          • +3
            я думаю, что они видели и Yii/Symphony, и Rails, но не видели Django
            Let the sratch begin!!!
      • +5
        по поводу исходников ZF я бы поспорил — там хватает всякого, я бы за образец взял Symfony2
        но архитектура ZF вполне неплоха, и предлагаемые coding standards — весьма удобны
        • 0
          согл
          хватает всякого!
          некоторые части исходников я использую в своих целях.
  • +1
    Спасибо))) Все таки вы написали статью для нас))
    • +2
      спасибо Вам что меня подтолкнули…
      пол года как исходники валяются на гитхабе

      у меня есть еще пара интересного материала.
  • 0
    Действительно ли SSI в этом случае быстрый?
  • +2
    С каких пор у нас ZF стал колбэк из вьюх дергать? В последний раз контроллеры инжектили данные во вью
  • 0
    Вполне интересная и жизнеспособная идея.
    • 0
      Идея не жизнеспособна. Простой фреймворк, называемый SSI, усложнили до безобразия реврайтами, пожертвовав простотой ради эфемерной гибкости.

      В стандартном виде SSI шаблон реализуется в HTML файле так (это внутренности страницы, например /index.html):

      <!--#include virtual="/templates/header_skin1.html?mode=1" -->
      <!--#include virtual="/content/bmv.html" -->
      <!--#include virtual="/templates/footer_skin1.html?mode=1" -->

      Как видим, получилась SSI страница, которую можно смело назвать конфигом HTML страницы. Для каждой страницы (класса страниц) на сайте можно сделать свой конфиг. И верстальщику будет довольно просто управлять этим хозяйством — проверено практикой :)
      • 0
        конечно конфиг громоздкий, но в моем подходе формируются индивидуальные переменные для каждого контоллера

        если url = /code/delete/123 то в моей схеме можно разрулить так:




        если бы этого не надо было делать, то Ваше решение вполне подошло бы;
      • 0
        хм…
        все исчезло
      • 0
        <!--#include virtual="/templates/header_skin1.html?mode=code" -->
        <!--#include virtual="/content/bmv.html?action=delete&id=123" -->
        • 0
          это кусок к первому комментарию
        • 0
          э-э-э-м-м-м-м…
          Так бы и сказали, что Вам нужен ЧПУ…
          Для таких случаев действительно используют реврайт, но используют как-то так:

          location /catalog/ {
          rewrite ^/catalog/([^/]+)/([^/]+)/? /templates/catalog.ssi?action=$1&id=$2 last;
          }

          И в /templates/catalog.ssi пишут:

          <!--#include virtual="/templates/header_skin1.html?mode=1" -->
          <!--#include virtual="/fastcgi/script.php?action=${arg_action}&id=${arg_id}" -->
          <!--#include virtual="/templates/footer_skin1.html?mode=1" -->

          А если еще использовать конструкцию <--#if -->, то на одном SSI такое можно наворотить :)
          • 0
            хорошее замечание
          • 0
            Не вижу необходимости в реврайте задавать вид адреса и правила для парсинга переменных. При наличии десятка контроллеров с десятком экшенов каждый ваш конфиг или .htaccess в случае апача будет страшным монстром.
            • –1
              1) упрощает код контроллера
              2) увеличивает производительность кода
              3) апач в игноре, наверно забыли что находитесь в блоге «Высокие нагрузки»
  • +11
    SSI в принципе работает достаточно быстро. НО, некоторые господа забывают одну прекрасную вещь. Разбив свою страницу на блоки, которые например не кэшируются, или кэшируются на очень маленькое время, подключая их через virtual, нужно помнить, что это всё подзапросы. То есть, если на вашей странице («mysite.ru/main/») есть блок с профайлом пользователя (инклуд virtual="/userprfile/?id=123"), и блок последние комментарии пользователя (инклуд virtual="/lastcomment/?userid=123"), которые вы не кэшируете по тем или иным причинам, в результате выполнится три запроса к вашему скрипту обработчику.
    В логах так и увидите
    "/userprfile/?id=123"
    "/lastcomment/?userid=123"
    «mysite.ru/main/»
    И, если вы установили в глобальный обработчик функцию проверки аутентификации, она выполнится три раза. Вы на один запрос пользователя будете его авторизовывать три раза.

    Нужно очень внимательно смотреть на то, что вы подключаете как SSI. Такая вот горе оптимизация, ведет к тому, что вы усложняете работу вашего движка/скрипта…
    • +1
      а потом во вьюхе появляется куча условий и переменных х)
    • +4
      самый дельный комментатор!!!
      ssi надо использовать там где это действительно дает эффект
      пихать микрошаблоны для формирования одной строчки таблицы большой — это тупизм!!!
    • –1
      И, если вы установили в глобальный обработчик функцию проверки аутентификации, она выполнится три раза. Вы на один запрос пользователя будете его авторизовывать три раза.

      если конкретно аутентификация — то используем серверную, например ngx_accesskey_module, и не надо нагружать бэкенд всякой ерундой. Кeep it simple — Девиз *nix ! 

      Если что-то хотим некий объект (результаты работы класса) использовать несколько раз, то используем кеш APC, memcache или  shm — выбо ограничен вашими знаниями.
  • +1
    Неужели ssi в nginx работает настолько медленно? Я к тому, что если _это_ только в 4 раза быстрее ZF…
    Решение оригинальное и классное. Но хочется, чтобы вы провели нормальные тесты по скорости. Я не верю, что у вас ZF такой шустрый :)
    • 0
      тесты см в src
    • 0
      по тестам без SSI на одном блоке
      на простом запросе выбор по PrimariKey = 0,1 мкс (БД на производительность не влияет)
      мой — 17-21 мкс
      ZF 79-84 мкс

      грубо говоря в 4 раза.
      сделать тоже самое (реализовать 5 блоков) на ZF ради теста не поднимается рука.

      на 2х -3х блоках (запросы простые аналогичные) — 24-34 мкс

      кеширование, акселераторы и опкоды не использовались
      отдача кешировонного контента напрямую в 10 раз быстрее, отдачи кешированного контента через РНР скрипт.
  • +2
    Еще отличный момент, например пробрасывать какие-то запросы на другой бекенд. Например где у вас статистика, или к примеру биллинг. Когда дублировать эти данные на основном бекенде не хочется.

    Еще вариант использования, который я вижу для себя это генерация JSON-а в RIA.
    Например имеем такую структуру
    {
    «user»: {«messages»: 10},
    «page»: {«goods»: [...]}
    }
    Если разнести это в ssi и закешировать обе части, то у нас вообще не будет уходить ни одного запроса на бекенд.
    • 0
      о том и речь…
      кешируем то что нам нужно с какими нужно ключами

      если надо — тянем бэкенд с другого сервера (у меня пока второго сервера нет по этому я про это промолчал)
    • 0
      по аяксу — отдельная тема
  • 0
    Программирование на Nginx ))

    И еще момент, не уверен на 100%, но вроде желательно (если та часть, которая выделяется (\w+), далее использоваться не будет) вместо
    location ~ ^/catalog/(\w+)/? {
    сделать
    location /catalog/ {
    и будет шустрее работать. Недавно была на хабре ссылка на доклад Сысоева на эту тему habrahabr.ru/blogs/nginx/108703/
    • 0
      несомненно будет быстрее работать но Вы просмотрели одну деталь
      location ~ ^/catalog/(\w+) {
      rewrite ^(.*)$ /index.tpl;
      set $memkey «top_$1»;
      }

      Догадались какую или кто подскажет???
      • 0
        Да, ключ мемкеша. Думаю можно в качестве set $memkey использовать целиком $uri, хотя это уже изврат получается.

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

        Хотя, пожалуй, я бы в итоге все равно сделал как у вас)
        • 0
          см конфиг conf/local.nginx.conf
          там использованы вложенные локейшены.

          можно использовать весь ури, но есть следующая ситуация:
          отображаем топ 10 всех компьютеров,
          т.е есть ури /top10/computer/p1, /top10/computer/p2 и /top10/computer/p3
          но для них должно быть одно и тоже отображение, которое формируется и кешируется результатами исполнения /top10/computer

          нам не надо кешировать три раза одно и тоже
  • 0
    Зачем холиварить о скорости работы языков программирования? Что быстрее? Что надежнее? Что лучше?
    Давайте, ребята, кодить на чистом nginx. :) Нужны только пара библиотек а-ля nginx_mongo, nginx_memcached итп. Нужно же где-то хранить данные.
    • 0
      а не лучше ли кодить на чистом Си, минуя nginx?
      • 0
        Разумеется, в чем-то лучше, а в чем-то и хуже. Задачи разные бывают. Например, у меня есть проекты, где бэк-эндом у меня моё приложение на Си, а фронт-эндом стоит nginx, которые раздает статику итп. Это очень удобно.
        • 0
          тоже есть подобные задачи,
          считаю клики на Си — производительность 2.5 К запросов в сек.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.