Кэширование и сжатие: теория

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

Когда клиент скачивает по протоколу HTTP какую-либо полезную информацию, он сохраняет её в памяти и затем отображает пользователю. При разработке стандарта HTTP Тим Бернерс Ли предложил способ сократить количество передаваемых данных за счёт системы кэширования, целью которой было предотвращение повторной загрузки ресурса, который уже был загружен клиентом и не изменился.

Основа кэширования

Кэширование всегда инициируется сервером, и поддерживается клиентом. Самый простой способ кэширования выглядит так: Клиент запрашивает какой-то ресурс, сервер включает в заголовки ответа заголовок Expires со значением какой-либо даты. Этот заголовок "говорит" клиенту о том, что данный ресурс будет актуальным до даты, указанной в нём. Если указать некоторую дату в будущем, клиент будет вправе до этой даты при запросе пользователя выдавать ресурс из своего кэша. Если указать 0, текущую дату или дату в прошлом, клиент будет при каждом запросе заново скачивать весь ресурс, так как будет полагать, что "срок годности" ресурса истёк и следует получить его свежую версию.

Механизм метода устаревания документа

В данном примере ресурс index.html устареет ровно через день.

В стандарте HTTP/1.1 был добавлен заголовок Cache-control, контролирующий механизм кэширования (о нём я расскажу ниже). Одним из его возможных атрибутов является атрибут max-age, явно указывающий срок годности ресурса. В случае, если клиент поддерживает HTTP/1.1 и видит в заголовке Cache-control, принятом от сервера, атрибут max-age с каким-либо значением, клиент принимает срок годности, указанный в max-age, даже если есть заголок Expires с другим значением. Другими словами, max-age имеет бóльший приоритет, чем Expires, что позволяет, например, предоставлять разные возможности кэширования HTTP/1.0 и HTTP/1.1-клиентам.

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

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

Необходимо заметить, что большинство серверов для статических файлов автоматически реализуют кэширование, основанное на методе "GET-запроса с условием" (см. ниже), что даёт экономию в трафике, но всё равно заставляет клиента каждый раз выполнять множество запросов, что, в свою очередь, вынуждает сервер проверять все условия и отвечать клиенту либо 304 Not Modified, либо отдавать сам ресурс. Наиболее оптимально совмещать эти два способа, потому я и предлагаю заняться кэшированием вручную.

Резюмируем метод кэширования, основанного на устаревании ресурса:

Так как в большинстве браузеров по нажатию кнопки "Обновить" такие ресурсы запрашиваются заново, поэтому я и предлагаю сочетать этот метод с "GET-запросом с условием".

"GET-запрос с условием" (conditional GET)

Не всегда авторам известна дата устаревания ресурса, потому был создан механизм "GET-запроса с условием".

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

"GET-запрос с условием" в HTTP/1.0

При запросе клиентом ресурса, определенного URI, сервер может добавить к стандартным заголовкам ответа заголовок Last-Modified со значением даты последней модификации ресурса. В случае запроса статического файла в качестве времени модификации удобно использовать время его последней модификации в файловой системе.

Клиент получает ответ, и если он умеет работать с кэшем, сохраняет скачанный документ вместе с полученной датой. При следующем запросе ресурса с тем же URI клиент смотрит в свой кэш, и если находит ресурс с сохранённой датой (полученной из заголовка Last-Modified), добавляет к заголовкам запроса заголовок If-Modified-Since, куда подставляет сохранённую дату.

Сервер, получая такой заголовок, сравнивает дату модификации ресурса у себя с датой, полученной в заголовке If-Modified-Since. Если даты совпали (или дата, присланная клиентом, "больше"), сервер отдаёт клиенту заголовок 304 Not Modified, таким образом оповещая клиента о том, что ресурс не изменился и клиент может отобразить его из своего кэша. Если у клиента дата старее, чем дата модификации ресурса, сервер отдаёт клиенту ресурс заново вместе с новой датой модификации.

Механизм работы "GET-запроса с условием" HTTP/1.0:

  1. клиент запрашивает какой-либо ресурс:

     GET http://somehost.ru/index.html HTTP/1.0
    
  2. Сервер, поддерживающий кэширование для данного ресурса, добавляет в ответ заголовок Last-Modified.

     HTTP/1.0 200 OK
     Date: Fri, 30 Dec 2003 13:20:12 GMT
     Last-Modified: Fri, 29 Dec 2003 13:20:12 GMT
     Content-Length: 4
     Content-Type: text/html
    
     test
    

Если же на сервере документ актуален (т.е. даты равны), то сервер отдаст заголовок HTTP/1.0 304 Not Modified, но уже не отдаст тело ответа

    HTTP/1.0 304 Not Modified
    Date: Sat, 31 Dec 2003 11:20:22 GMT

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

Дата, указанная в Last-Modified, должна быть именно в таком виде и в формате GMT. Строго говоря, и HTTP/1.0 и HTTP/1.1 обязывают серверы понимать три формата времени:

    Sun, 06 Nov 1994 08:49:37 GMT
    Sunday, 06-Nov-94 08:49:37 GMT
    Sun Nov  6 08:49:37 1994

Но HTTP/1.0 добавляет, что сами серверы не имеют права использовать последний формат, а HTTP/1.1 требует от серверов использовать только первый формат времени.

Таким образом реализуется замечательная возможность — передавать только те документы, которые действительно изменились. Но этот метод далёк от совершенства — бывает сложно однозначно идентифицировать изменённость документа только по дате модификации файла, а в случае динамически генерируемого содержимого и вовсе не всегда возможно. Потому в спецификации HTTP/1.1 механизм "GET-запроса с условием" был расширен и дополнен.

Резюмируем:

"GET-запрос с условием" в HTTP/1.1

В HTTP/1.1 в механизм "GET-запроса с условием" было добавлено ещё одно "условие" т.н. ETag — сущность, однозначно определяющая содержимое ресурса.

Это может быть хэш всего ресурса, его контрольная сумма или другая функция, однозначно идентифицирующая содержимое. В ситуации, когда документ генерируется шаблонной системой и CMS, бывает сложно определить время модификации ресурса, особенно если он "собирается" из нескольких частей. Вот в таких случаях удобнее определить ETag и сравнить его с присланным клиентом. Механизм сравнения похож на реализованный в HTTP/1.0 механизм сравнения даты модификации.

Только если в HTTP/1.0 использовалась пара заголовков Last-Modified(сервер)/If-Modified-Since(клиент), то в в HTTP/1.1 используется пара ETag(сервер)/If-None-Match(клиент), использующаяся таким же образом — сервер идентифицирует ресурс и шлёт клиенту в заголовке ETag этот идентификатор. Клиент в следующий раз в запрос того же ресурса добавляет заголовок If-None-Match с сохраненным значением присланного сервером заголовка ETag. Сервер сверяет идентификаторы и либо возвращает 304 Not Modified в случае, если ресурс не изменился, или же возвращает заново весь ресурс с новым значением ETag.

Механизм работы "GET-запроса с условием" HTTP/1.1:

  1. клиент запрашивает какой-либо ресурс.

     GET /index.html HTTP/1.1
     Host: somehost.ru
    
  2. Сервер, поддерживающий кэширование для данного ресурса, добавляет в ответ заголовок ETag.

     HTTP/1.1 200 OK
     Date: Fri, 30 Dec 2003 13:20:12 GMT
     ETag: "328e-1d-1b63fda6"
     Content-Length: 4
     Content-Type: text/html
    
     test
    
  3. Клиент сохраняет в кэше документ вместе со значением заголовка ETag, полученным с сервера.

  4. В следующий раз клиент при запросе того же ресурса в заголовок запроса добавит сохранённое значение ETag.

     GET /index.html HTTP/1.1
     Host: somehost.ru
     If-None-Match: "328e-1d-1b63fda6"
    
  5. Сервер сравнит полученное значение идентификатора ETag с собственным. Если идентификаторы различаются, сервер отдаст документ вместе с новым значением идентификатора ETag.

     HTTP/1.1 200 OK
     Date: Sat, 31 Dec 2003 11:20:22 GMT
     ETag: "435b-4t-jla890l9"
     Content-Length: 7
     Content-Type: text/html
    
     newtest
    

    Если же значение идентификатора ETag, присланное клиентом совпадает со значением серверным, то сервер отдаст заголовок HTTP/1.1 304 Not Modified, но уже не отдаст тело ответа.

     HTTP/1.1 304 Not Modified
     Date: Sat, 31 Dec 2003 11:20:22 GMT
    

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

Резюмируем:

Сжатие

Сжатие ответа было представлено в HTTP/1.0. Оно выполняется просто. Если клиент поддерживает какой-то механизм кодирования, он к каждому запросу может приложить заголовок Accept-Encoding с перечислением поддерживающихся механизмов кодирования. Сервер может закодировать ресурс одним из методов, поддерживающихся клиентом.

В HTTP/1.0 были введены два типа кодирования — x-gzip и x-compress (но было сказано, что gzip и compress им эквивалентны для совместимости с будущими реализациями протокола). В HTTP/1.1 уже просто используются gzip и compress (и говорится, что программы должны понимать x-gzip и x-compress для совместимости с предыдущими реализациями протокола:)). Также в HTTP/1.1 был введён метод deflate, но из-за кучи проблем с реализацией в разных браузерах этот метод используется гораздо реже. Чаще всего используется gzip. Его поддерживают все браузеры, начиная c IE4, Opera5.12, NN4.06 и ранних версий Firefox. Сжимать можно любой тип данных, но большинство браузеров понимает только text/plain, text/html, text/css, text/javascript. Необходимо заметить, что при сжатии в заголовке Content-Length указывается длина сжатого тела сообщения.

Последовательность работы механизма сжатия:

Необходимо осветить также заголовок вариантности кэширования.

Заголовок вариантности Vary

Vary: Accept-Encoding нужен для того, чтобы дать понять кэширующему механизму, что ответ сервера зависит oт принимаемого клиентом метода кодирования. Предположим ситуацию: есть два клиента, один поддерживает gzip, другой нет.

Поддерживающий gzip клиент шлёт в запросе Accept-Encoding: gzip, не поддерживающий не шлёт, соответственно :)

Всё замечательно работает — сервер шлёт одному клиенту пожатый контент, другому несжатый. Но если на пути к серверу попадётся прокси-сервер, поддерживающий кэширование, возникает проблема. Если первый клиент, поддерживающий gzip, получает от сервера пожатый контент, прокси эти данные кэширует. При обращении к тому же ресурсу второго клиента(не поддерживающего сжатие), прокси-сервер отдаёт этому клиенту пожатый контент! Клиент ничего не понимает :)

Эта проблема решается с помощью посылаемого сервером заголовка Vary. Его значением может быть любое HTTP-поле. Фактически этот заголовок говорит, что ответ на запрос будет разным в зависимости от некоторых заголовков.

В результате в кэше будут храниться все варианты ответов. Если скрипт отдаёт разные данные в зависимости от Cookie, нужно отдавать Vary: Cookie, таким образом кэш отдаст закэшированный вариант ответа только для конкретной куки.

Например, если два клиента обращались к ресурсу с Cookie: name=Vasya и Cookie: name=Petya, будут закэшированы отдельно ответы для обоих клиентов, соответственно Васе будет отдан его ответ, Пете — его.

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

Нужно сказать, что существуют серьёзные проблемы при использовании сжатия. Во-первых, это нагрузка на процессор сервера.

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

Проблемы со сжатием в различных браузерах:

Лучше всего себя в работе с кэшированием и сжатием показали Firefox и IE7b3.

Стратегии кэширования

Кэширование служебных изображений

Кэширование таблиц стилей и скриптов

Если сайт "устоялся" и не планируются частые изменения, то стратегия кэширования идентична кэшированию изображений, за исключением того, что к таблицам стилей и скриптам можно также применить сжатие. Правда, нужно решить, что для Вас важнее — неудобство пользователей Opera, теряющих из-за сжатия возможность кэширования или же удобство пользователей остальных браузеров, умеющих кэшировать сжатые таблицы стилей и скрипты. В принципе, можно совместить требования.

Получается следующее:

Иногда (x)html-код генерируется автоматически, потому использование времени последней модификации невозможно. Остаётся ETag. Cледовательно, учитывая неумение IE6 кэшировать сжатый контент на основе ETag, от сжатия, скорее всего, придётся отказаться; или же слать сжатое содержимое только тем браузерам, которые не забывают про Etag. Если же (x)html-код находится в статических файлах, можно и для IE6 использовать сжатие совместно с кэшированием на основе Last-Modified.

Также, как и в случае с кэшированием таблиц стилей и скриптов, статические файлы можно заранее сжать, чтобы потом не тратить ресурсы на сжатие на лету.

comments powered by Disqus