Вам непременно нужно понимать, как работает
HTTP-кэширование. Честное слово, нужно. Я много раз упоминал,
что выбор HTTP-методов крайне важен при проектировании
веб-сервиса, — в частности, выбор метода
GET
позволяет получить выигрыш от использования
HTTP-кэширования. Так вот: для того, чтобы получить от
использования метода
GET
все те преимущества, которые он способен
обеспечить, нужно понимать, как HTTP-кэширование работает, и как
можно, эффективно его задействовав, увеличить производительность
вашего сервиса.
В этой статье я не буду объяснять, как настроить кэширование в том веб-сервере, который вы используете, и не буду описывать различные виды кэширования. Если вам всё это интересно, то рекомендую ознакомиться с замечательным учебником по HTTP-кэшированию от Марка Ноттингема.
Самое первое, что вам нужно понимать — какие цели преследуются моделью кэширования, используемой в HTTP. Одна из таких целей — это позволить и клиенту, и серверу задавать условия, в которых передаваемый документ может браться из кэша. Легко видеть, что одно это уже привносит в модель кэширования определённую сложность.
Основу модели кэширования, используемой в HTTP, образуют валидаторы — части запроса, используемые клиентом для того, чтобы убедиться, что кэшированный документ всё ещё действителен (т.е. не устарел). Использование валидаторов позволяет клиенту или промежуточному серверу проверять текущее состояние документа, не передавая на сервер его кэшированную копию целиком. Сервер же, в свою очередь, передаёт в ответе документ только в том случае, если полученный им валидатор свидетельствует о наличии в кэше клиента устаревшей (недействительной) копии.
Один из валидаторов, используемых в HTTP — это
ETag
.
ETag
представляет собой хеш («отпечаток») байтов
документа: если в документе изменится хоть один байт, то изменится
и
ETag
.
Перед тем, как использовать этот валидатор, нужно хоть раз
запросить документ методом
GET
: если сервер поддерживает эту возможность, то
заголовок
ETag
в его ответе будет содержать хеш передаваемой
версии документа. Клиент в этом случае сохраняет в кэше вместе с
самим документом его
ETag
, и в дальнейших запросах того же документа
использует сохранённый
ETag
в качестве валидатора.
Например, пусть я запросил документ с сервера example.org, и получил такой ответ:
HTTP/1.1 200 OK
Date: Fri, 30 Dec 2005 17:30:56 GMT
Server: Apache
ETag: "11c415a-8206-243aea40"
Accept-Ranges: bytes
Content-Length: 33286
Vary: Accept-Encoding,User-Agent
Cache-Control: max-age=7200
Expires: Fri, 30 Dec 2005 19:30:56 GMT
Content-Type: image/png
-- binary data --
Тогда в следующий раз, когда я запрашиваю этот же документ
методом
GET
, я смогу использовать валидатор. Обратите
внимание: сохранённое значение
ETag
передаётся в заголовке
If-None-Match
.
GET / HTTP/1.1
Host: example.org
If-None-Match: "11c415a-8206-243aea40"
Если за время между запросами документ не изменялся, то сервер
вернёт код
304 Not Modified
.
HTTP/1.1 304 Not Modified
Date: Fri, 30 Dec 2005 17:32:47 GMT
Если же документ менялся, то сервер вернёт код
200
и передаст в ответе новую версию документа, а
также значение
ETag
этой новой версии.
HTTP/1.1 200 OK
Date: Fri, 30 Dec 2005 17:32:47 GMT
Server: Apache
ETag: "0192384-9023-1a929893"
Accept-Ranges: bytes
Content-Length: 33286
Vary: Accept-Encoding,User-Agent
Cache-Control: max-age=7200
Expires: Fri, 30 Dec 2005 19:30:56 GMT
Content-Type: image/png
-- binary data --
Валидаторы используются для проверки того, не изменился ли
документ; для того, чтобы управлять сохранением его копии в кэше,
используется заголовок
Cache-Control
. Самая главная из директив управления кэшем — это
max-age
: она указывает, что сохранённая в кэше копия
документа устаревает через
max-age
секунд. Эта директива может использоваться как
в запросе, так и в ответе, — так что и клиент, и сервер могут
решать, сколько времени будут действительны передаваемые ими
документы. Если на сервере есть достаточно новый кэшированный
ответ, то его можно вернуть прямо из кэша; иначе же происходит
описанная выше процедура валидации.
Рассмотрим ещё раз полученный нами от сервера ответ. В нём
заголовок
Cache-Control
задаёт
max-age=7200
, т.е. кэшированная копия документа
перестаёт быть действительной через 2 часа.
HTTP/1.1 200 OK
Date: Fri, 30 Dec 2005 17:32:47 GMT
Server: Apache
ETag: "0192384-9023-1a929893"
Accept-Ranges: bytes
Content-Length: 33286
Vary: Accept-Encoding,User-Agent
Cache-Control: max-age=7200
Expires: Fri, 30 Dec 2005 19:30:56 GMT
Content-Type: text/xml
Кроме
max-age
, в заголовке
Cache-Control
допускается множество других директив.
Каждая из них может использоваться либо в запросах, либо в ответах,
либо — как
max-age
— и в запросах, и в ответах.
Директива | Описание |
---|---|
no-cache
|
Сервер не должен использовать кэшированный ответ. |
no-store
|
Ответ на этот запрос не должен кэшироваться. |
max-age=delta-seconds
|
Клиент допускает кэшированный ответ, если его возраст не
превышает
delta-seconds секунд; клиент не требует его
валидации. |
max-stale=delta-seconds
|
Клиент допускает кэшированный ответ, если его возраст не
превышает
delta-seconds секунд. |
min-fresh=delta-seconds
|
Клиент допускает кэшированный ответ, если он будет оставаться
действительным не менее
delta-seconds секунд от момента запроса. |
no-transform
|
К запрашиваемому документу не должны применяться преобразования. |
only-if-cached
|
Допускается только кэшированный ответ. Если подходящего ответа нет в кэше, то не нужна ни валидация старого ответа, ни получение нового. |
Директива | Описание |
---|---|
public
|
Ответ разрешается сохранять в любом кэше. |
private
|
Ответ разрешается сохранять только в закрытом кэше (т.е. только для этого пользователя). |
no-cache
|
Кэшированный ответ не должен использоваться без предварительной его валидации. |
no-store
|
Ответ не разрешается сохранять в кэше. |
no-transform
|
К передаваемому документу не должны применяться преобразования. |
must-revalidate
|
Если кэшированный ответ устарел, то к нему
должна применяться процедура валидации. Эта директива
отменяет действие
max-stale . |
max-age=delta-seconds
|
Клиент допускает кэшированный ответ, если его возраст не
превышает
delta-seconds секунд; клиент не требует его
валидации. |
s-maxage=delta-seconds
|
То же, что
max-age , но действует только на открытые кэши. |
proxy-revalidate
|
То же, что
must-revalidate , но действует только на
прокси-сервера. |
Рассмотрим примеры использования заголовка
Cache-Control
:
Cache-Control: private, max-age=3600
Допустимо в ответе от сервера; означает, что ответ может храниться только в закрытом кэше на протяжении часа.
Cache-Control: public, must-revalidate, max-age=7200
Означает, что ответ может храниться в открытом кэше на протяжении двух часов; после их истечения действительность сохранённой копии должна перепроверяться при каждом запросе.
Cache-Control: must-revalidate, max-age=0
Вынуждает клиента перепроверять действительность сохранённой
копии при каждом запросе;
max-age=0
указывает, что документ устаревает сразу же
после его получения клиентом. В заметке Марка Ноттингема «Leveraging the Web: Caching» приведён любопытный пример случая, в котором
можно использовать такой заголовок.
Cache-Control: no-cache
Практически то же самое, что и в предыдущем примере;
единственное отличие — что клиент мог использовать в запросе
директиву
max-stale
и получить устаревший ответ, тогда как
директива
must-revalidate
отменила бы действие
max-stale
. Такая сложная система необходима потому,
что, как я отмечал раньше, и сервер, и клиент могут одновременно
влиять на использование кэша.
Все эти примеры использования заголовка
Cache-Control
рассматривают его применение в ответе от
сервера. Теперь рассмотрим примеры, когда этот заголовок
используется в запросе.
Cache-Control: no-cache
Вызывает «полное» («end-to-end») обновление документа: кэш, к которому обращается клиент, должен вновь запросить документ у сервера, с которого он был получен.
Cache-Control: min-fresh=200
Таким заголовком клиент запрашивает документ, который будет оставаться действительным на протяжении 200 секунд после запроса.
Наверное, вам интересно, не запутывается ли кэш во всех этих
ситуациях. Например, что он должен делать, если сервер может
возвращать различные документы для одного и того же URI? Для таких
случаев в HTTP предусмотрен заголовок
Vary
. Этот заголовок указывает кэшу названия всех тех
заголовков запроса, при изменении которых сервер вернул бы другой
документ.
Предположим, что сервер согласовывает с клиентом тип
возвращаемого документа — тогда разные ответы для одного и того же
URI могли бы иметь разный заголовок
Content-Type
, в зависимости от согласованного типа
документа. Тогда сервер может добавить в ответ заголовок
Vary: accept
, и кэш сохранял бы отдельно ответы на
запросы с разными значениями заголовка
Accept
.
Date: Mon, 23 Jan 2006 15:37:34 GMT
Server: Apache
Accept-Ranges: bytes
Vary: Accept-Encoding,User-Agent
Content-Encoding: gzip
Cache-Control: max-age=7200
Expires: Mon, 23 Jan 2006 17:37:34 GMT
Content-Length: 5073
Content-Type: text/html; charset=utf-8
В этом примере сервер указывает, что ответ можно сохранять в
кэше на протяжении двух часов, причём по отдельности для запросов с
разными значениями заголовков
Accept-Encoding
и
User-Agent
.
После того, как сервер успешно проверит действительность
кэшированного результата, например при помощи валидатора в
заголовке
If-None-Match
, — он возвращает код
304 Not Modified
. Больше в этом случае ничего не
происходит, так? На самом деле, не совсем: сервер может послать
вместе с ответом
304 Not Modified
значения заголовков документа, чтобы
обновить их в кэше. Кроме этого, сервер может в заголовке
Connection
перечислить названия тех заголовков,
которые
не надо обновлять в кэше.
По умолчанию, некоторые заголовки не сохраняются в кэше — это
так называемые «заголовки соединения» («hop-by-hop»):
Connection, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, TE, Trailers, Transfer-Encoding
и
Upgrade
. Значения всех остальных заголовков —
«заголовков документа» («end-to-end») — по умолчанию
кэшируются.
HTTP/1.1 304 Not Modified
Content-Length: 647
Server: Apache
Connection: close
Date: Mon, 23 Jan 2006 16:10:52 GMT
Content-Type: text/html; charset=iso-8859-1
...
В этом примере значение заголовка
Date
, не являющегося заголовком соединения и не
указанного в заголовке
Connection
, будет сохранено в кэше.
Хотя все описанные механизмы кэширования представляют определённую сложность для восприятия, по крайней мере они образуют стройную логичную систему. Естественно, всё усложняется необходимостью работы с существующими серверами и клиентами, использующими протокол HTTP 1.0. В этом более старом протоколе для управления кэшированием используются другие заголовки — оперирующие временем; все эти более старые заголовки поддерживаются в HTTP 1.1 для обратной совместимости.
Используемая в HTTP 1.0 модель кэширования построена вокруг
времени устаревания документа. Валидатор
Last-Modified
проверяет время последнего изменения
документа. Кэш использует для проверки действительности документа
заголовки
Date
,
Expires
,
Last-Modified
и
If-Modified-Since
.
Если вы разрабатываете HTTP-клиент, то лучше всегда использовать оба этих валидатора, потому что неизвестно заранее, не окажется ли на пути между вами и сервером кэш, работающий по протоколу HTTP 1.0. Безусловно, все эти старые сервера уже давно могли бы обновить — ведь от даты опубликования протокола HTTP 1.1 прошло семь лет, — но они всё ещё попадаются. Рекомендуемая мной тактика даёт большую гарантию успеха, чем использование средств только какой-то одной версии HTTP, — всё равно, как если бы вы носили одновременно и ремень, и подтяжки.
Теперь, когда вы понимаете, как работает кэширование в HTTP, вам наверняка интересно, нет ли для вашего любимого языка готовых библиотек с поддержкой кэширования. Я могу ответить про Python: к сожалению, пока что нет. Мне очень обидно, что ни одна из лучших реализаций HTTP-клиентов не написана на моём любимом языке. И нужно это недоразумение исправить.
Я рад представить вам библиотеку
httplib2
— полнофункциональный HTTP-клиент, написанный на Python. Эта
библиотека поддерживает локальный закрытый кэш и понимает все
описанные в этой статье операции над ним. Кроме этого, в ней есть
много возможностей, обычно отсутствующих в других
HTTP-библиотеках:
Connection: Keep-alive
, позволяющий через одно
соединение выполнить несколько HTTP-запросов.Cache-Control
, причём используется оба валидатора — и
ETag
, и
Last-Modified
.GET
и
POST
.GET
автоматически обрабатываются перенаправления (коды
ответа
3ХХ
).compress
и
gzip
.ETag
автоматически вставляются в запросы
PUT
к документам, имеющимся в кэше, — в исполнение
раздела 3.2 статьи «
Обнаружение потерянных обновлений при помощи немонопольной блокировки».Об остальных возможностях вы можете узнать на
странице проекта httplib2
.
В следующей статье я рассмотрю HTTP-авторизацию,
перенаправления, устойчивые соединения и сжатие в HTTP-ответах, а
также поддержку всех этих возможностей в
httplib2
. Ещё вам наверняка будет интересно, как
реализуют кэширование «крутые парни», — этому будет посвящена
целиком отдельная статья.