Кэширование с Varnish + ESI

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

ESI

Блоки страниц

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

<HTML>
<BODY>

Всем привет!

<esi:include src=&quot;/news.php?UID&quot;/>

Всем пока!!!

</BODY>
</HTML>
Web-сервер, поддерживающий ESI вызовы, просто сделает дополнительный запрос, а результат вставит на место ESI инструкции. Допустим наш скрипт "news.php" содержит такой код:
<?
echo "<ul><li>News title 1</li></ul>";

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

<HTML>
<BODY>

Всем привет!

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

Всем пока!!!

</BODY>
</HTML>

ESI

Запросы ESI можно кэшировать. Следовательно, у Вас появляется удобное средство работы с динамическим контентом. Достаточно разделить их на разные блоки и закэшировать те, которые не изменяются (либо изменяются редко).

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

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

Порядок подключения ESI

  1. Сначала нужно определить блоки на странице, вынести их в отдельные скрипты (каждый блок должен иметь свой адрес)
  2. Вставить ESI инструкции на месте вынесенных блоков
  3. Включить кэширование ESI блоков на Web сервере

Персонализированные блоки

ESI

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

Стоит учитывать, что количество таких блоков в кэше будет пропорционально количеству пользователей.

общее количество = количество пользователей х количество блоков на страницу

Подробный пример

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

  • Блок авторизации
  • Меню
  • Блок новостей
Стартовая страница
<HTML>
<BODY>

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

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

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

</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=&quot;#&quot;>Пункт меню только для пользователей</a></li>
	<? } ?>
	<li><a href=&quot;#&quot;>Публичный пункт меню</a></li>
</ul>
Скрипт новостей

<?

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

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

Настройка Web сервера

Для приложения будем использовать Nginx, Varnish будет направлять запросы ему (8090 порт):

server {
    listen 8090;

    # Если включен 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

Давайте настроим кэш по таким правилам:

  • Главную страницу кэшируем на 24 часа, блоки — на 1 час
  • Кэшировать будем все запросы кроме POST
  • Для кэширования персонального блока будем использовать значение сессионных Cookies (PHPSESSID) — для установки ключа
  • Для отделения персонализированных блоков от обычных блоков для авторизированных пользователей будем использовать соотв. приставки к запросам: UID (персонализированные блоки) и AUTH (обычные блоки, учитывающие только статус пользователя)
Varnish ESI
Конфигурация:

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;
}

После проверки скорости получим такие результаты:

ab -n 100 -c 5 http://127.0.0.1/
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 раз!

Самое важное

ESI позволяет использовать кэширование на динамических сайтах с высокой персонализацией. Обратите внимание на альтернативу — SSI в связке с Nginx.


Подпишитесь на Хайлоад с помощью Google аккаунта
или закройте эту хрень