6 июня 2010 в 11:29

Кеширование блоков с помощью nginx

nginx + SSIМногим разработчикам знакома ситуация когда кешировать страницы сайта, скажем, на 5-10 минут нельзя всего из-за одного небольшого блочка, актуальность которого нужно поддерживать если не в реальном времени, то с временем «старения» не больше 5-10 секунд. При этом посещаемость сайта продолжает расти, растет время генерации страниц и c этим надо что-то делать…
  • Вариант решения 1: Подкрутить то, до чего не доходили руки последнее полгода. Все Вас поймут и передвинут сроки на другие задачи. Вы будете в роли «Супермена» один спасать сайт от непомерной нагрузки, решая проблему «бесплатно» (без доп. вливаний в оборудование). Вам может пригодиться статья «Тюнинг nginx».
     
  • Вариант решения 2: Улучшить техническую базу (докупить мозгов на сервер, улучшить дисковую систему, поставить под БД отдельный сервер). В принципе проблема не решена, а скорее отложена. Теперь у Вас есть время «окопаться» и подготовиться ко второй волне наплыва нагрузки, она будет больше и накроет сильнее.
     
  • Вариант решения 3: Ваш вариант, о котором я, вероятно, узнаю из комментариев.
     
Позвольте предложить и мне проверенное и относительно простое решение на базе одной из старейших технологий в Web-разработке.


Как это должно работать


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

При этом сборкой блоков в единое целое занимается некий «сборщик» и если любой из блоков по какой-то причине не создан за отведенное ему время, то это еще не повод выдавать клиенту «Gateway timeout» или «Internal Server Error». Можно собрать успешно созданные блоки, а на месте «сбойных» показывать устаревший контент из кеша.

Для реализации такой модели нам понадобиться технология-ветеран Web-разработки: ssi. В качестве «сборщика», как ясно из названия статьи, выступает nginx. «Чудеса» станут возможны благодаря модулю fastcgi_cache.

Итак, поехали:


Исключаем лишнее звено


Нам не пригодиться apache, наличие которого, как правило объясняется использованием RewriteRules. В nginx есть аналог mod_rewrite или комбинация location/alias с регулярными выражениями, возможности которых позволяют написать аналог любому RewriteRule от apache. Кроме того в современных фреймворках разбором входного URL может заниматься сам движек (например Zend_Controller_Router_Rewrite в Zend Framework)

В качестве fastcgi-бекенда может использоваться любая платформа. Примеры будут на php, но это не означает что нельзя написать аналогичный код на python-е или perl-е.

Запускаем php в режиме fastcgi:
# /bin/su -m www_user -c "PHP_FCGI_CHILDREN=8 /usr/bin/php-cgi -q -b 127.0.0.1:7777 &"

Можно еще прописать путь к лог-файлу в php.ini (error_log = /var/log/fastcgi/fastcgi.log), но при этом придется перезагружать php-cgi.

Делаем:
# killall php-cgi
и запускаем все по-новой

Более продвинутый вариант запуска fastcgi — установка php-fpm.


Устанавливаем nginx


Можно ставить стандартный из репозитория/портов… Но если хотите чтоб работала возможность «почистить» любой файл в кеше, придется компилировать.

Нам понадобиться модуль: ngx_cache_purge

Я подробно опишу, как это можно сделать для redhat-подобной системы, а вы уж по аналогии компилируйте под вашу систему.

# cd ~/rpmbuild/SRPMS
# yumdownloader --source nginx
# rpm -ivh nginx-0.7.65-1.fc12.src.rpm

редактируем файл nginx.spec, где-нибудь в список ./configure вставляем строчку--add-module=/root/rpmbuild/BUILD/ngx_cache_purge-1.0 \. Тут же можно удалить строчки с ненужными модулями (например --with-ipv6 \, --with-http_dav_module \, --with-mail \, --with-mail_ssl_module \ ...)

теперь распаковываем содержимое http://labs.frickle.com/files/ngx_cache_purge-1.0.tar.gz в папку /root/rpmbuild/BUILD/ngx_cache_purge-1.0.
Все можно компилировать:

# cd ~/rpmbuild/SRPMS
# rpmbuild -ba nginx.spec

Это не совсем красивый способ, т.к. полученный в результате .src.rpm не будет содержать файла с модулем ngx_cache_purge. Если для вас, все же, это критично, то здесь можете загрузить «правильный» вариант nginx .src.rpm для ветки 8.xx. Правда я часть ненужных мне модулей закомментировал.

Устанавливаем пересобранный nginx на наш сервер:

# rpm -ivh nginx-0.7.65-1.fc12.x86_64.rpm


Настройка nginx для проекта на php


В файл /etc/hosts (добавляем):

# Virtual hosts 
127.0.0.1 myproject

В основном конфиге /etc/nginx/nginx.conf в секцию http добавляем:

fastcgi_cache_path /var/spool/nginx/cache levels=1:2 keys_zone=mycache:64m;
include /etc/nginx/conf.d/*.conf;

(Не забудьте создать папку /var/spool/nginx/cache и установить для нее пользователя, под которым запускается nginx)

В папке /etc/nginx/conf.d/ создаем конфиги для виртуальных хостов

Пример кофига (/etc/nginx/conf.d/myproject.conf):

server {
        listen       80;
        server_name  myproject;

        root   /var/www/myproject/public;
        ssi on;

        # Включаем кеш если есть такая необходимость
        fastcgi_cache mycache;
        fastcgi_cache_min_uses 1;
        # Время кеширования равно нулю. кеш включен но кеширования нет
        # Время кеширования для конкретных страниц указиваем в заголовке "Cache-Control"
        fastcgi_cache_valid 200 0m;
        fastcgi_cache_valid 404 1m;
        fastcgi_cache_valid 500 0m;
        fastcgi_cache_use_stale updating error timeout invalid_header http_500; # Используем вариант из кеша (даже если он устарел) в случае ошибки
        fastcgi_cache_key $uri$is_args$args;

        # Раскоментируйте эту секцию если nginx собран с модулем ngx_cache_purge
#         location ~ ^/purge(/.*) {
#               fastcgi_cache_purge   mycache   $1$is_args$args;
#         }

        location ~ /(img|css|js|assets) {
#               access_log  off;
                access_log  /var/log/nginx/myproject_img_access.log  main;
                expires 1h;
        }

        location / {
                access_log  /var/log/nginx/myproject_main_access.log  main;
                error_log  /var/log/nginx/myproject_error.log;

                fastcgi_pass 127.0.0.1:7777;
                fastcgi_index    index.php;

                include         fastcgi.conf;
        }
}

Устанавливаем тестовый проект на php в /var/www/myproject. Исходный код примера можно посмотреть и скачать здесь.

Запускаем nginx. Для RedHat-подобных систем это выглядит приблизительно так:

# service nginx start

Все, система готова к работе! Пробуем запустить http://myproject/


Учим backend управлять временем кеширования


Дело в том, что в nginx время кеширования указывается в параметре fastcgi_cache_valid 200 0m; и распространяется на все страницы, в которых заголовком оно не переопределено.

В конфиге «по умолчанию» время кеширования я указал равным 0, т.е. кеширование отключено. Но если бекенд сгенерирует заголовок приблизительно такого вида:

Cache-Control: public, max-age=20
либо
Expires: Thu, 18 Mar 2010 20:57:07 GMT

То страница nginx-ом будет закеширована на 20 секунд. В php заголовок можно поменять с помощью функции header() (Со слов автора nginx самым приоритетным является «X-Accel-Cache-Control», потом «Cache-Control», потом «Expires»).

Напишем небольшую функцию. котрая будет управлять временем кеширования:
function cacheHeaders($lifetime=0) {
#        $date = gmdate("D, d M Y H:i:s", time() + $lifetime);
#        header('Expires: ' . $date . ' GMT');
        header('Cache-Control: public, max-age=' . $lifetime);
}



Мастерим блоки


Блоком будем называть любую логически выделенную часть html-кода без стандартных заголовков html-старницы, например:

<div>
    Это простой блок
</div>

Чтоб визуально контролировать состояние свежести каждого из блоков добавим код, наших тестовых блоков вывод времени.

<?php echo date('G:i:s')?>

Смотрим рабочий пример с использованием SSI блоков.


Удаляем страницы из кеша


К сожалению у nginx-а пока что нету родного (штатного) способа удаления страничек из кеша. Иногда это может создавать неудобства.

Если вы добавили при компиляции модуль ngx_cache_purge, то в конфиг (/etc/nginx/conf.d/myproject.conf) добавим приблизительно такую секцию, перед секцией «location / {...» :

location ~ ^/purge(/.*) {
        #allow     127.0.0.1;
        #allow     10.1.1.0/24;
        #deny     all;
        fastcgi_cache_purge   mycache   $1$is_args$args;
}

Для того чтоб удалить закешированную страницу: http://myproject/mypage.php?lang=ru, мне достаточно загрузить страницу http://myproject/purge/mypage.php?lang=ru

В php это можно сделать командой file_get_contents(«http://myproject/purge/mypage.php?lang=ru»);

С помощью директив allow и deny можно ограничить круг хостов с которых можно «чистить» кеш.


Тестируем


Напоминаю, ссылка для тестов http://linux.ria.ua/SsiBlocks/src/bin/index.php.

Обратите внимание, «каркас» страницы обновляется раз в 10 секунд, остальные блоки обновляются согласно примечаниям под временем создания блока.

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

Кроме того, помните, что вы не одни сейчас проводите эксперименты с этой страничкой, если хотите поэкспериментировать — самостоятельно настройте локальную копию примера.


Делаем выводы


Даже если такой подход покажется Вам примитивным, и функциональность его сильно ограниченной, обратите внимание на то, что это работает не просто быстро, а очень быстро!

Узким местом может быть только дисковая система, если кеш «распухнет» до больших размеров и не будет помещаться в дисковый кеш.

PS: Если эта статья будет интересна читателям, я планирую написать вторую часть о применении описанного подхода к кешированию блоков на Zend Framework.
Олег Черний @apelsyn
карма
179,1
рейтинг 0,0
Разработчик
Похожие публикации
Самое читаемое Администрирование

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

  • +1
    Есть еще Varnish, не пробывали его?
  • +6
    Имею мнение, что рациональнее заходить с другой стороны — встраивать через ssi отдельные блоки, и кэшировать каждый блок по отдельности. Это позволяет более гибко работать с временем кэширования, и избавиться от костыля с пуржингом кэша.
    • 0
      IMHO, это самый прозрачный способ.
    • 0
      Я с Вами согласен. В данном случае каждый блок кешируется именно по отдельности, сборка блоков в готовую страницу происходит при каждом запросе к серверу. Просто алгоритм сборки настолько заоптимизирован, что этот процес отъедает очень малую долю процессорного времени.
      • +1
        Если fastcgi_cache написал на весь location / {} — где ж тут по-отдельности?

        • 0
          Возможно вы не привыкли к стилю конфтгов nginx, в данном конкретном случае в кеш не попадает «по умолчанию» ничего, т.к. время кеширования равно 0.

          По какому принципу ложить в кеш указано в директиве fastcgi_cache_key $uri$is_args$args; Т.е., например, сранички /menu.php и /menu.php?key=value будут размещены в разных кеш-файлах.

          Кроме того в кеше не происходит подмены ssi-инструкций. Если в index.php есть "", то в кеше эта инструкция так и остается. Подмена происходит в менент запроса и если menu.php нету сейчас в кеше то происходит обращение к бекенду и размещение в кеш только menu.php, где время размещения в кеше определяется по заголовку «Cache-control».

          Объяснил как умел, если неумело, то настройте локальную копию и убедитесь в этом сами.

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

          Но если запрашивается сраничка index.php, menu.php,… то в кеше создается отдельная страничка
          • 0
            Там хабракат кое-что вырезал, в скобочках было <!--# include virtual=«menu.php» -->
          • 0
            Мама, роди меня обратно…

            Я как бы так помягче сказать, использую nginx в продакшне уже года четыре. Примерно на 800-та серверах. Зачастую, с собственными патчами. С трафиком в пределе около 30к rps на nginx.
            Но это так, к слову.

            Собственно, я хотел обратить внимание на то, что существенно дешевле не делать кэш для всего с нулевым таймаутом, а кэшировать только те куски, которые вставляются через ssi. Чтобы не строить кэш-кэй по целой простыне параметров. Чтобы не городить огород с use_stale.

            Это позволит также избавиться от «если ваш бекенд сгенерит ошибку». Можно перехватить ошибку бэкэнда для каждого отдельного блока.

            • 0
              Я с Вами согласен что схему можно селать более оптимальной, но согласитесь что при этом конфиг будет выглядеть сложнее для понимания схемы.
  • НЛО прилетело и опубликовало эту надпись здесь
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Можно поидее ж примонтировать часть ОП как папку Ж…
      • НЛО прилетело и опубликовало эту надпись здесь
  • +5
    Структура старницы
    Збойный блок
    Поправьте, глаза режет.
    • 0
      «движек» туда же
  • +5
    Че за херня? Тема: Кеширование блоков с помощью nginx, а в статье уже рассказывается как его заинсталить и как заинсталить РНР, че за бред?

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

      А если видите что-то лишнее для себя, переходите с следующему подзаголовку.

  • +5
    Поправьте, бога ради, окончания «тся» и «ться».
  • +1
    Не рассматривали ли вы такой случай, когда сайт реализован на каком-то относительно тяжелом фреймворке (для которого время «разогрева» сравнимо или даже превышает время создания содержимого блока), и при этом страница содержит более одного блока с нулевым или близким к нулю временем жизни? Получается, что может возникнуть ситуация, когда время двух запросов к бэкенду за содержимым некэшируемых блоков превысит время создания всей страницы без использования кэша.
    • 0
      В любом деле нужно действовать без фанатизма. Я с Вами согласен, что в таком случае все переводить на блоки нельзя.

      Вобще-то можно, частично, неужели все блоки должны быть актуальны? Посто каркас страницы кешировать уже не получится, а вот ленту новостей, курсы валют, блок с счетчиками/информерами,… можно поробовать.
  • 0
    Способ слишком не гибкий — применяли уже кое-что подобное.
    Почему? Потому что могут быть принципиально не кэшируемые блочки: те что зависят от текущего пользователя, например, «Спасибо, что заглянули, %username%! У вас %msg_count% новых сообщений». Каждый такой блочок будет выражаться в запросе к backend-у. Допустим таких блочков у нас 5 (вполне возможная ситуация: приветствия, формы, контролы над своими сообщениями и т.п.), это означает, что на каждую страницу на нужно делать 5 запросов к backend-у, пусть все они будут простыми, но с каждого мы получим оверхед сети, fastcgi запроса/разбора, проверки текущего пользователя. В общем, ужасно.

    Куда эффективнее да ещё и гибче разруливать такие вещи на самом backend-е
    • +2
      почитайте доки по nginx ssi. там есть поддержка переменных и выражения. так что при должном старании, можно и это сделать. я так понял, автор хотел только поверхностно ознакомить с темой поэтому и не рассмотрел этот аспект.
      • 0
        Причём тут поддержка переменных и выражения?
        Мне нужно прочитать сессию пользователя из, скажем, редиса, запросить сколько у него сообщений ещё откуда-нибудь (PostgreSQL) и вывести соответствующую фразу. ngnix же это за меня не сделает?
        Или, например, мы показываем список постов, для текущего пользователя нужно показать кнопки редактирования рядом с его постами, что же мне кэшировать этот список постов отдельно для каждого юзера?
        • +1
          Вверху страницы делается одна ssi-вставка типа /usersettings/, которая кешируется по сессионной куке uid.
          Эта вставка выдет список из set var с id постов (последних N постов) юзера (и любых других его данных).
          Ниже, при выводе списка для каждого поста с помошью ssi if проверяем существование переменной с id поста среди списка постов юзера. Внутри каждого if — кнопка.

          И не забываем про internal для локейшена /usersettings/ и параметр wait для его вставки.
          Еще можно для него указать таймаут в несколько секунд, чтобы все хоть как-то работало, когда база сдохнет.
    • +1
      получаем сессию отдельным блоком, на результат накладываем xslt, который раскидает данные сессии по документу.
      • 0
        Во-первых, это гемор, во-вторых xslt — это не так уж быстро, в третьих остаются блочки некэшируемые по другим причинам: число сообщений у юзера, статус онлайн кого-нибудь, просто часто обновляющаяся инфа, в-четвёртых, libxml/libxslt текут, в-пятых, чтобы делать xsl-трансформацию нужно чтобы ssi-скрипты выдавали валидный xml.

        Ну и в конце концов, что у нас получится? Бекэнд со своим движком и какими-то шаблонами (предположительно), ssi-шаблончики, xsl-трансформации. жуткий зоопарк.
        • 0
          1. это ни разу не гемор. гемор — это писать тучи сси-ифов
          2. и не так уж и медленно.
          3. какая разница по каким причинам они не кэшируются?
          4. откуда такие данные? это ведь не какие-то левые пропиетарные библиотеки.
          5. это так сложно выдавать валидный xml?

          а зачем нам «какие-то шаблоны», если мы всё-равно накладываем xslt? ;-) пусть движок отдаёт чистый xml, ssi объединяет несколько xml-ек в одну и xslt приводит это дело к удобочитаемому виду. кстати, nginx случайно не поддерживает xi:include?
          • 0
            1. SSI + XSLT + шаблоны в бэкенде (без последних если используем только xslt) шаблоны двух видов — гемор. xslt сам по себе не самый удобный шаблонизатор.
            3. Такая разница, что если блок не кэшируется принципиально, то придётся запрашивать его с бекэнда каждый раз, и если таких блоков больше одного, то вся эта солянка теряет смысл. И даже если один, она становится очень сомнительной.
            4. Тестировали, некоторые трансформации текут. Возможно, если просто подсовывать значения, то с этим не столкнёшься.
            5. Намного сложнее, чем html, любой неэкранированный символ, любая неопредённая entity, неправильный utf символ и всё, весь документ сломался.
            • 0
              1. уж по удобней, чем большинство
              3. одним запросом можно получить все некэшируемые блоки
              4. багрепорт конечно же не написали?
              5. любой баг вылезает сразу, а не когда его найдёт кулхацкер. это плохо?
              • 0
                1. Не используйте большинство, используйте удобный.
                3. Почему тогда просто не посчитать эти блоки на бекэнде всунуть их в страницу точно также как нгинкс, подтянуть что-то из кеша, тоже всунуть и выдать страницу?
                4. Я писал другой багрепорт для libxslt раньше, понял, что всем похуй.
                5. Не сразу, а когда возникнет такая ситуация, и в такой ситуации у тебя тупо перестанет отображаться вся страница.
                • 0
                  1. это о чём речь?
                  3. идея сабжа как раз и заключается в том, чтобы не изобретать велосипед, а воспользоваться кэшированием энжиникса.
                  4. м… печальненько.
                  5. лучше ничего не показать, чем показать xss
                  • 0
                    1. Выбирайте на свой вкус, мне, например, нравится jinja2.
                    3. Если уж говорить о велосипедах, то скорее идея сабжа «а вот есть ещё такой быстрый велосипед». Но есть, то и дургие, ничего изобретать не надо.
                    5. Да там нет никакой xss, данные могут и не от пользователей браться, в конце концов. Просто если заворачивать данные в xml простой конкатенацией строк, то неизбежно будешь ловить такие баги время от времени, а как-то хитрее уже медленнее. Вообще, необходимость все данные заворачивать в xml не особо удобна.

                    Я не говорю же, что такой подход нельзя использовать, я говорю, что он негибкий, а значит для каких-то типов сайтов будет неэффективен, неудобен и/или порождать кучу извращений, чтобы втиснутся в рамки.
                    • 0
                      1. бугога х))) спасибо, такого мне не надо
                      3. но тогда у нас не получится map-reduce архитектуры
                      5. не надо заворачивать данные в xml конкатенацией строк. это вредная привычка. и именно благодаря ей xss и просачивается.
                      • 0
                        3. Захочешь — получиться, не захочешь — сделаешь такую какую захочешь.
                        5. Чем-то приходиться жертвовать ради скорости, чтобы не было xss достаточно проверять данные на входе.
                        • 0
                          5. ага, типа как на хабре — каверкать любое упоминание слова javascript? x)
                      • 0
                        Кстати, вопрос на засыпку. Вы сами-то использовали такое кэширование с ssi?
                        • 0
                          нет
                          • 0
                            Ну вот, а я гавна такого сорта уже поел, и xslt тоже.
                            • –1
                              у тебя нет вкуса
                              • 0
                                Ты даже не понял смысла моей фразы, и при этом ещё бросаешь безосновательные ощущения.
                                • 0
                                  я прекрасно всё понял.
  • 0
    простите за глупый вопрос. а как нгикс понимает что див кешировать, а не Спан? можно для примеру кусок пхп кода, чтоб понять как страница на пхп выглядит.
    • 0
      nginx парсит SSI-инструкции, ему неважно какой кусок html вы упаковали в блок. В статье есть ссылка на исходный код linux.ria.ua/SsiBlocks/. Если в коде не разберетесь пишите.
      • 0
        Все врубил.
  • 0
    я однажды от скуки померял сайт на ssi, лежащий на локалхасте, утилитой ab и обнаружил, что медленнее Server Side Includes что-то еще поискать надо! как я понял, он при каждом инклюде форкает новый процесс апача/nginx, а эта операция в юниксе чрезвычайно затратная и по времени, и по памяти
    • –2
      Только что проверил сабж (ваш пример, который, как я понял, особо ничего и не делает) и свой собственный сайт, на тяжелом движке с большим количеством запросов к базе, к тому же на хостинге в другой стране, — так и есть:

      ваш сайт
      HTML transferred: 3109309 bytes
      Requests per second: 143.95 [#/sec] (mean)
      Time per request: 69.468 [ms] (mean)
      Time per request: 6.947 [ms] (mean, across all concurrent requests)
      Transfer rate: 462.66 [Kbytes/sec] received

      мой
      HTML transferred: 24322000 bytes
      Requests per second: 226.20 [#/sec] (mean)
      Time per request: 44.209 [ms] (mean)
      Time per request: 4.421 [ms] (mean, across all concurrent requests)
      Transfer rate: 5452.79 [Kbytes/sec] received
      • +1
        Я думаю твой тест может претендовать на звание «самый объективный тест в рунете», тут даже нету смысла говорить о чистоте эксперимента — результат очевиден. Ура!
        • –1
          Наркоман штоле?
    • 0
      nginx новый процесс не форкает.
      • 0
        Тем не менее, результаты удручающие.
        • +1
          сравнение не корректное, приложения работают в разных окружениях.
          • 0
            Я бы согласился с вами, если б не огромная разница в Transfer rate.
            • 0
              Я тут протестил, оказывается хабр работает намного медленее, чем мой собственный сайт-визитка в локалке :)
              • 0
                Я кажется написал, что сайт находится в другой стране, а не в локалке, ок?
                • 0
                  Да, и это полностью сводит на нет всю объективность вашего измерения…
    • +1
      при каждом инклюде форкает новый процесс апача/nginx

      nginx устроен по другому
  • 0
    не всегда в случае сбоя нужно показывает кэшированную версию. зачастую требуется заглушка или хотябы ремарка о том, что эти данные могут быть не актуальны…

    а ещё лучше делать так: habrahabr.ru/blogs/client_side_optimization/90481/
  • 0
    «There are only two hard problems in Computer Science: cache invalidation and naming things.» Phil Karlton
  • 0
    наткнулся случайно,
    про такую возможность писал пять лет назад
    habrahabr.ru/post/109050
    а вообще было интересно почитать, спасибо
    • 0
      :)

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