Многим разработчикам знакома ситуация когда кешировать страницы сайта, скажем, на 5-10 минут нельзя всего из-за одного небольшого блочка, актуальность которого нужно поддерживать если не в реальном времени, то с временем «старения» не больше 5-10 секунд. При этом посещаемость сайта продолжает расти, растет время генерации страниц и c этим надо что-то делать…
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:
Можно еще прописать путь к лог-файлу в php.ini (error_log = /var/log/fastcgi/fastcgi.log), но при этом придется перезагружать php-cgi.
Делаем:
и запускаем все по-новой
Более продвинутый вариант запуска fastcgi — установка php-fpm.
Можно ставить стандартный из репозитория/портов… Но если хотите чтоб работала возможность «почистить» любой файл в кеше, придется компилировать.
Нам понадобиться модуль: ngx_cache_purge
Я подробно опишу, как это можно сделать для redhat-подобной системы, а вы уж по аналогии компилируйте под вашу систему.
редактируем файл 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.
Все можно компилировать:
Это не совсем красивый способ, т.к. полученный в результате .src.rpm не будет содержать файла с модулем ngx_cache_purge. Если для вас, все же, это критично, то здесь можете загрузить «правильный» вариант nginx .src.rpm для ветки 8.xx. Правда я часть ненужных мне модулей закомментировал.
Устанавливаем пересобранный nginx на наш сервер:
# rpm -ivh nginx-0.7.65-1.fc12.x86_64.rpm
В файл /etc/hosts (добавляем):
В основном конфиге /etc/nginx/nginx.conf в секцию http добавляем:
(Не забудьте создать папку /var/spool/nginx/cache и установить для нее пользователя, под которым запускается nginx)
В папке /etc/nginx/conf.d/ создаем конфиги для виртуальных хостов
Пример кофига (/etc/nginx/conf.d/myproject.conf):
Устанавливаем тестовый проект на php в /var/www/myproject. Исходный код примера можно посмотреть и скачать здесь.
Запускаем nginx. Для RedHat-подобных систем это выглядит приблизительно так:
Все, система готова к работе! Пробуем запустить http://myproject/
Дело в том, что в nginx время кеширования указывается в параметре fastcgi_cache_valid 200 0m; и распространяется на все страницы, в которых заголовком оно не переопределено.
В конфиге «по умолчанию» время кеширования я указал равным 0, т.е. кеширование отключено. Но если бекенд сгенерирует заголовок приблизительно такого вида:
То страница nginx-ом будет закеширована на 20 секунд. В php заголовок можно поменять с помощью функции header() (Со слов автора nginx самым приоритетным является «X-Accel-Cache-Control», потом «Cache-Control», потом «Expires»).
Напишем небольшую функцию. котрая будет управлять временем кеширования:
Блоком будем называть любую логически выделенную часть html-кода без стандартных заголовков html-старницы, например:
Чтоб визуально контролировать состояние свежести каждого из блоков добавим код, наших тестовых блоков вывод времени.
Смотрим рабочий пример с использованием SSI блоков.
К сожалению у nginx-а пока что нету родного (штатного) способа удаления страничек из кеша. Иногда это может создавать неудобства.
Если вы добавили при компиляции модуль ngx_cache_purge, то в конфиг (/etc/nginx/conf.d/myproject.conf) добавим приблизительно такую секцию, перед секцией «location / {...» :
Для того чтоб удалить закешированную страницу: 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.
- Вариант решения 1: Подкрутить то, до чего не доходили руки последнее полгода. Все Вас поймут и передвинут сроки на другие задачи. Вы будете в роли «Супермена» один спасать сайт от непомерной нагрузки, решая проблему «бесплатно» (без доп. вливаний в оборудование). Вам может пригодиться статья «Тюнинг nginx».
- Вариант решения 2: Улучшить техническую базу (докупить мозгов на сервер, улучшить дисковую систему, поставить под БД отдельный сервер). В принципе проблема не решена, а скорее отложена. Теперь у Вас есть время «окопаться» и подготовиться ко второй волне наплыва нагрузки, она будет больше и накроет сильнее.
- Вариант решения 3: Ваш вариант, о котором я, вероятно, узнаю из комментариев.
Как это должно работать
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.