воскресенье, 24 января 2010 г.

Кеширование страниц - ускоряем сайт в 100 раз (Varnish + ESI)

varnish-logo-red-64


В этой статье поговорим о кешировании страниц и их частей, а также о том, какие плюсы это дает.


Если на Вашем сайте практически нет динамики (например, новостной сайт или блог), то Вы легко можете складывать все его страницы в кеш и практически не делать запросов к бекенду. Но что делать если на сайте есть авторизация, и зависящая от этого логика?


В этой статье речь пойдет о том, как кешировать страницы с персональными данными используя Varnish и язык ESI.



ESI


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




<HTML>
<BODY>

Всем привет!

<esi:include src="/news.php?UID"/>

Всем пока!!!

</BODY>
</HTML>

Веб сервер, поддерживаюий ESI вызовы, встретив ESI вызов, просто сделает дополнительный запрос, а результат вставит на его место. Допустим наш скрипт “news.php” содержит такой код:



<?
echo "<ul><li>News title 1</li></ul>";

После обработки первого примера, клиенту вернется такая страница:




<HTML>
<BODY>

Всем привет!

<ul><li>News title 1</li></ul>

Всем пока!!!

</BODY>
</HTML>

Зачем это нужно, если это только увеличивает количество запросов к серверу?

Все просто, если использовать кеширование — у Вас появляется удобное средство кешировать различные куски страниц (с разной логикой и временем жизни кеша). В нашем примере мы могли бы, например, закешировать главную страницу на 1 день, а скрипт новостей на 1 час. Тогда в сутки мы бы получали только 25 запросов к PHP. Это позволило бы значительно разгрузить бекенд, а также увеличить скорость загрузки страниц.


Как все работает?


Web сервер, который поддерживает ESI, делает запрос к бекенду (в нашем случае, PHP). Далее получив страницу, обрабатывает все ESI вызовы, делая на каждый из них дополнительный запрос для получения содержимого. Далее, следуя правилам кеширования, складывает (или не складывает) все это в кеш и генерирует страницу. Последующие вызовы страницы приведут к тому, что она будет отдана клиенту из кеша.


Допустим на странице три ESI вызова (например, шапка, подвал и блок новостей). В этом случае первое обращение к серверу будет выглядеть где-то так:



esi11



Обратите внимание, что один запрос генерирует еще 3 запроса к бекенду (т.к. у нас три ESI вызова на странице). После первого запроса все части закешировались, и теперь (при последующих запросах) картинка будет выглядеть так:



esi21



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


Что необходимо сделать, чтобы все это заработало:



  1. Выделить блоки на странице, сделать их доступными по отдельным HTTP запросам

  2. Описать ESI вызовы на месте выделенных блоков

  3. Описать логику кеширования на сервере (время жизни кеша, параметры кеширования и т.п.)


Персональные данные


Самая сложная часть в частичном кешировании, это выделение персональных частей. Например: блок логина после авторизации превращается в блок с персональными ссылками (типа “выйти”, “настройки” и т.п.). Для того, чтобы получить правильный результат нужно для начала разделить страницу на такие блоки. Важно понять отличие персонализированных блоков (например, блок с фоткой, именем и фамилией залогиненного пользователя) от блоков для авторизированных пользователей (например, кнопка “выйти” вместо формы логина). В первом случае объект нужно сохранить в кеш для каждого пользователя, а во втором у объекта будет всего две версии в кеше (для залогиненных пользователей и нет).


В случае персонализированных блоков необходимо кешировать их на основе идентификатора пользователя (либо сессии), тогда для каждого пользователя будет создан объект в кеше. Учитывая это, важно, чтобы этих объектов было очень мало и сами они были как можно меньше. В остальных случаях необходимо просто кешировать объекты на основе статуса пользователя (залогинен или нет). Далее — детальный пример.


Пример


Для примера возьмем такую задачу. У нас есть сайт с новостями. Новости обновляются каждый час. На сайте также есть блок авторизации и скрытые ссылки для авторизированных пользователей. Исходя из этого логическое разбиение на блоки будет следующим:



  • Блок авторизации

  • Меню

  • Блок новостей


Исходный код стартовой страницы:



<HTML>
<BODY>

<h1>Тестируем ESI</h1>

<esi:include src="/app/auth.php?UID"/>
<esi:include src="/app/menu.php?AUTH"/>

<h3>Новости</h3>
<esi:include src="/app/news.php"/>

</BODY>
</HTML>

Все скрипты для ESI вызовов будут находиться в папке app. Их исходники:


Скрипты бекенда

Скрипт авторизации



<?

session_start();

if ( $_POST['user'] )
{
$_SESSION['user'] = $_POST['user'];
header('Location: /'); exit;
}

$user = $_SESSION['user'];

?>

<? if ( $user ) { ?>
<div>Привет, <b><?=$user?></b>!</div>
<? } else { ?>
<form method="post" action="/app/auth.php">
Войдите в систему
<input type="text" name="user" />
<input type="submit" name="Войти">
</form>
<? } ?>


Скрипт меню



<? session_start(); ?>

<ul>
<? if ( $_SESSION['user'] ) { ?>
<li><a href="#">Пункт меню только для пользователей</a></li>
<? } ?>
<li><a href="#">Публичный пункт меню</a></li>
</ul>


Скрипт новостей



<?

$rss = file_get_contents('http://feeds.nytimes.com/nyt/rss/HomePage');
$xml = simplexml_load_string($rss);

echo "<ul>";
foreach ( $xml->channel->item as $item )
{
echo "<li><a href=\"{$item->link}\">{$item->title}</a>";
}
echo "</ul>";

Вся логика крайне упрощена, но тем не менее сохраняет суть стандартных задач подобного рода.


Настройка Nginx

Для обслуживания будем использовать Nginx. Конфигурация включает два виртуальных сервера. Первый (80 порт) принимает и обрабатывает запросы от клиента. Все запросы проксирует на Varnish. Второй сервер (порт 8090) нужен для запросов от Varnish’a:



server {
listen 80;
server_name esi.dev;
}

server {
listen 8090;

root /home/golotyuk/www/localhost/esi;

# Если включен gzip, обязательно нужно выключить!
gzip off;

location / {
index index.php;
}

location ~* \.(php)$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /home/golotyuk/www/localhost/esi/$fastcgi_script_name;
}
}

Настройка varnish + esi

Перед конфигурацией varnish’a определяем логику кеширования:



  • Главную страницу кешируем на 24 часа, блоки — на 1 час

  • Кешировать будем все запросы кроме POST

  • Для кеширования персонального блока будем использовать значение сессионных cookie (PHPSESSID) — для установки ключа кеширования

  • Для разделения персонализированных блоков от обычных блоков для авторизированных пользователей будем использовать соотв. приставки к запросам: UID (персонализированные блоки) и AUTH (обычные блоки, учитывающие только статус пользователя)


Конфигурация:



# Бекенд сервер — виртуальный сервер Nginx
backend default { .host = "127.0.0.1"; .port = "8090"; }

# Процедура формирования ключа для кеширования
sub vcl_hash {
# Стандартные параметры — имя сервера и URL
set req.hash += req.url;
set req.hash += req.http.host;

# Если установлена сессионная кука, сохраняем ее значение в переменную
if( req.http.cookie ~ "PHPSESSID" ) {
set req.http.X-Varnish-Hashed-On =
regsub( req.http.cookie, "^.*?PHPSESSID=([^;]*?);*.*$", "\1" );
}

# Если в строке запроса мы находим "UID", то необходимо добавить
# значение сессии в параметры кеширования
if( req.url ~ "/app/.*UID" && req.http.X-Varnish-Hashed-On ) {
set req.hash += req.http.X-Varnish-Hashed-On;
}

# Если в строке запроса мы находим "AUTH", то необходимо добавить
# флаг статуса (logged in) в параметры кеширования
if( req.url ~ "/app/.*AUTH" && req.http.X-Varnish-Hashed-On ) {
set req.hash += "logged in";
}

hash;
}

sub vcl_recv {
# Если тип запрос не POST, то ищем объект в кеше
if ( req.request != "POST" )
{
lookup;
}
}

sub vcl_fetch {
# Для запроса "/" используем обработку esi и кешируем на 1 сутки
if (req.url == "/") {
esi;
set obj.ttl = 24h;
}
# Для запросов "/app" (ESI вызовы) кешируем результат на 1 час
elseif (req.url ~ "^/app/") {
set obj.ttl = 1h;
}

deliver;
}

После конфигурации и запуска, получим приблизительно следующее:



esi-screen-shot


Для анализа производительности воспользуемся утилитой ab (из пакета apache2-utils):


ab -n 100 -c 5 http://esi.dev/

Результат:




Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 0
Processing: 2 5 3.8 3 18
Waiting: 2 5 3.8 3 18
Total: 2 5 3.8 4 19

Percentage of the requests served within a certain time (ms)
50% 4
66% 4
75% 5
80% 8
90% 12
95% 15
98% 17
99% 19
100% 19 (longest request)

Для сравнения запустим тест на аналогичном скрипте без ESI, который содержит всю туже логику внутри и каждый раз вызывает PHP:


ab -n 100 -c 5 http://127.0.0.1:8090/index_standard.php

Результаты:



Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 354 579 666.2 458 5484
Waiting: 354 579 666.2 458 5483
Total: 354 579 666.2 458 5484

Percentage of the requests served within a certain time (ms)
50% 458
66% 492
75% 517
80% 539
90% 602
95% 667
98% 3572
99% 5484
100% 5484 (longest request)

Как видим скорость обслуживания отличается в 100 раз!


Возможно тесты покажуться слишком хитрыми (новости мы получаем из внешнего источника, что очень медленно). Но следует понимать, что пример содержит очень мало логики и для симуляции реального примера был использован 1 внешний запрос. По существу это тоже самое, что несколько десятков запросов к MySQL + несколько сотен запросов к memcached + логика приложения + логика отображения.


Итог


Применение принципа частичного кеширования страниц позволяет кешировать страницы с динамическими и персональными данными с нетривиальной логикой. Плюсы такого подхода:



  1. Существенно увеличивается скорость отдачи страниц

  2. Сильно разгружаются бекенд сервера от повторяющейся логики


Текущие проблемы


  • Применение подобного подхода требует достаточно серъезных изменений в структуре приложения

  • Очень большая персонализация страниц на больших объемах будет требовать больших объемов оперативной памяти

  • Данный подход требует редко изменяющейся разметки данных, т.к. частое ее изменение будет приводить к постоянному сбросу кеша, что малоэффективно


В качестве альтернативных решений следует привести связку Nginx + SSI + Memcache, которая будет рассмотрена в будущих статьях.


Ссылки


Описание работы ESI в Varnish

Исходники примеров


Какие еще есть решения частичного кеширования страниц?



Google Bookmarks Digg I.ua Ru-marks Ruspace Zakladok.net Reddit delicious Technorati Yahoo My Web News2.ru БобрДобр.ru Memori.ru rucity.com



Related posts:

  1. Varnish — быстрый старт
  2. Кеширование тяжелых запросов (на примере memcache)
  3. Varnish

Комментариев нет:

Отправить комментарий