Статьи Архив статей

Автор: Мациевский Николай aka sunnybear
Опубликована: 7 июля 2009

Автоматизация кэширования

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

Кэширование на клиентском уровне

Настройка кэширования для предотвращения дополнительных запросов из браузера к серверу осуществляется достаточно просто: нужно всего лишь знать наиболее характерные случаи использования:

Статические ресурсы без сжатия

Форсирование кэширования для статических ресурсов без сжатия. В данном случае мы ничем не рискуем, выставляя не только максимальное время кэширования, но и предлагая кэшировать ресурсы на локальных прокси-серверах (директива Cache-Control: public). Для PHP у нас получится следующий код (в Expires прописана дата на 10 лет вперед относительно текущего времени на сервере):

<?php
    header("Cache-Control: public, max-age=315360000");
    header("Expires: Mon, 01 Jul 2019 00:00:00");
?>

В случае выставления директив для Apache:

<FilesMatch \.(bmp|png|gif|jpe?g|ico|swf|flv|pdf|tiff)$>
    Header append Cache-Control public
    ExpiresActive On
    ExpiresDefault "access plus 10 years"
</FilesMatch>

И в случае nginx:

location ~* ^.+\.(bmp|gif|jpg|jpeg|png|swf|tiff|swf|flv)$ {
    expires   10y;
    header set Cache-Control public;
}

10-летний срок кэширования здесь вполне оправдан: таким образом мы сообщаем пользователям, что ресурсы в течение этого времени перезапрашивать не нужно. Если ресурсы будут изменены, то нам все равно придется форсировать сброс кэша (об этом чуть ниже), чтобы гарантировать корректное отображение материалов сайта во всех браузерах.

Статические ресурсы со сжатием

Все отличие от предыдущего случая заключается в том, что разрешить локальным прокси-серверам кэшировать такие ресурсы (обычно это CSS-, JavaScript- или ICO-файлы) нельзя. Даже больше: нам нужно запретить кэширование сжатых версий файлов, чтобы избежать выдачи сжатого файла тем пользователям, которые сжатия не поддерживают.

Код для PHP:

<?php
    header("Cache-Control: private, max-age=315360000");
    header("Expires: Mon, 01 Jul 2019 00:00:00");
?>

Для Apache:

<FilesMatch \.(css|js|ico)$>
    Header append Cache-Control private
    ExpiresActive On
    ExpiresDefault "access plus 10 years"
</FilesMatch>

И для nginx:

location ~* ^.+\.(css|js|ico)$ {
    expires   10y;
    header set Cache-Control private;
}

Очевидно, что в некоторых случаях директивы можно объединить с предыдущим случаем.

Если нам нужно добавить кэширование на определенный срок для HTML-документов, то достаточно прописать в директиве FilesMatch расширения файлов, указанные чуть ниже.

Запрет кэширования динамических ресурсов

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

Для того чтобы запретить кэширование во всех браузерах HTML-документов, нужно написать с помощью PHP (в Expires прописано текущее время на сервере):

<?php
    header("Expires: Wed, 01 July 2009 00:00:00");
    header("Cache-Control: no-store, no-cache, must-revalidate, private");
    header("Pragma: no-cache");
?>

Для Apache:

<FilesMatch \.(php|phtml|shtml|html|xml|htm)$>
    ExpiresActive Off
    Header append Cache-Control "no-store, no-cache, must-revalidate, private"
    Header append Pragma "no-cache"
</FilesMatch>

Для nginx:

location ~* ^.+\.(php|phtml|shtml|html|xml|htm)$ {
    expires   0;
    header set Cache-Control "no-store, no-cache, must-revalidate, private";
    header set Pragma no-cache;
}

Сброс кэша

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

Строка запроса

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

Рассмотрим следующий вызов CSS-файла:

<link rel="stylesheet" href="/css/main.css" type="text/css"/>

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

Если мы используем метку времени и у нас есть, предположим, имя файла, который мы хотим включить в наш документ как таблицу стилей, то следующий PHP-код обеспечивает уникальность кэширования для нашего случая:

<?php
/* получаем метку времени, равную времени изменения файла */
$timestamp = filemtime($file);
/* выводим ссылку на файл в HTML-документе */
echo '<link rel="stylesheet" href="/css/main.css?". $timestamp .
    "' type="text/css"/>';
?>

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

Физическое имя файла

Указанное выше решение обладает еще одним небольшим недостатком: некоторые прокси-сервера не будут кэшировать файлы со строкой запроса, считая их динамическими. Мы можем обойти данную ситуацию через Rewrite-правило в конфигурации Apache:

RewriteRule ^(.*)(\.v. +)?\.(css|js)$ $1.$2 [QSA,L]

Какой оно несет смысл? А тот, что при указании в HTML-документе ссылки на файл

main.layout.v123456.css

сервер отдаст физический файл

main.layout.css

Таким образом мы элегантно обходим проблему прокси-серверов одной строкой в конфигурации сервера. Соответствующий PHP-код будет выглядеть так:

<?php
/* получаем метку времени, равную времени изменения файла */
$timestamp = filemtime($file);
/* выводим ссылку на файл в HTML-документе */
echo '<link rel="stylesheet" href="/css/main.css.v". $timestamp .
    "' type="text/css"/>';
?>

Создание хэша

Как уже было указано выше, для автоматизации процесса сброса кэша на клиенте нам нужно каждый раз запрашивать файловую систему на предмет изменения файла. Если файлов у нас несколько, то дополнительная нагрузка многократно возрастает. Но принимая во внимание то, что мы собираемся все файла объединить в один, мы можем каким-либо образом контролировать изменение итогового файла, не проверяя при этом всех, входящих в него.

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

$hash = ;
foreach ($this->initial_files as $file) {
    $hash .= $file[file_raw];
}
$new_file_name = md5($hash);

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

Использование разделенной памяти

Собственно решение проблемы проверки физических файлов на изменение лежит на поверхности. Для этого нам нужно обеспечить:

  • Подключение библиотек разделяемой памяти (APC, eAccelerator, memcache).
  • Возможность управлять состоянием кэша (редактирование проверяемых файлов через веб-интерфейс, частичный сброс кэша либо полный сброс закэшированных файлов).

На примере APC описанный алгоритм выглядит следующим образом:

<?php
/* удаляем (ставим время истечения равным 1с) из APC запись относительно */
/* текущего файла, при изменении каких-либо включенных в него файлов */
if ($changed) {
    apc_store($new_file_name, apc_fetch($new_file_name), 1);
}

/* при выдаче закэшированного файла проверяем, нужно ли  его пересобирать */
$mtime = apc_fetch($new_file_name);
if (!$mtime) {
    ...
/* если нужно, то при создании файла записываем текущее время в APC */
    $mtime = time();
    echo '<link rel="stylesheet" href="/css/main.css?". $mtime .
	"' type="text/css"/>';
    apc_store($new_file_name, $mtime);
} else {
/* если нет, то у нас уже получено время изменения файла, которое */
/* можно использовать для метки кэша */
    echo '<link rel="stylesheet" href="/css/main.css?". $mtime .
	"' type="text/css"/>';
}
?>

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

Таким образом кэширование на клиентском уровне может ускорить загрузку последующих страниц (или посещений) вашего сайта на 80-90% (в 5-10 раз), а правильное управление кэшированием гарантирует вам, что информация, получаемая пользователями, всегда будет актуальной.

Кэширование на серверном уровне

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

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

На виртуальных хостингах (когда ресурсы одного сервера могут делить десятки или сотни различных сайтов) для относительно простого сайта (который не предполагает значительного взаимодействия с пользователем) стоит рассмотреть возможность создания кэша готовых HTML-страниц. Что это такое?

Отдаем закэшированный документ

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

Для более детального примера давайте рассмотри следующим код, отвечающий за простейшее серверное кэширование, из Web Optimizer:

/* проверяем, можем ли отдать закэшированный документ */
if (!empty($this->cache_me)) {
/* переводим адрес документа в реальное имя файла */
    $this->uri = $this->convert_request_uri();
    $file = $this->options['page']['cachedir'] . '/' . $this->uri;
/* проверяем, существует ли файл и достаточно ли он актуальный */
    if (is_file($file) &&
	time() - filemtime($file) < $this->options['page']['cache_timeout']) {
	    $content = @file_get_contents($file);
/* проверяем, сжат ли файл */
	    if (!empty($this->options['page']['gzip']) &&
		substr($content, 0, 8) == "\x1f\x8b\x08\x00\x00\x00\x00\x00") {
/* если сжат, то выставляем соответствующие заголовки */
		    $this->set_gzip_header();
	    }
/* отдает закэшированное содержимое */
	    echo $content;
/* и закрываем PHP-процес */
	    die();
    }
}

Переменная $cache_me может формироваться на основе множества параметров (в том числе, части URL, которые нужно или не нужно кэшировать, пользовательские агенты и роботы, для которых можно отдавать кэшированные версии страниц, и т.д.). Стоит также отметить, что просто создать файла с именем, равным текущему URL страницы невозможно: в нем встречаются недопустимые символы (/, ?), которые нужно трансформировать при сохранении на файловой системе.

Создаем закэшированный документ

Но мы рассмотрели процесс выдачи закэшированного документа, а каким образом он появляется на жестком диске? Процедура сохранения файла немного проще, и может быть записана следующим образом (код из Web Optimizer):

/* определяем, нужно ли нам сохранять закэшированную версию документа */
if (!empty($this->cache_me)) {
/* формируем имя файла */
    $file = $options['cachedir'] . '/' . $this->uri;
/* проверяем, есть ли такой файл и устарел ли он */
    if (!is_file($file) ||
	time() - filemtime($file) > $options['cache_timeout']) {
/* записываем новое содержимое в файл */
	    $fp = @fopen($file, "w");
	    if ($fp) {
		@fwrite($fp, $this->content);
		@fclose($fp);
	    }
    }
}

Правильно настроенное кэширование на серверном уровне способно сэкономить время ваших посетителей (и тем самым поднять конверсию сайта) и сэкономить серверные ресурсы (при использовании каких-либо распределенных мощностей).

Читать дальше

Все комментарии (habrahabr.ru)