HTTP缓存

3.3 version
维护中的版本

Diff 本文源自BOOK,不同于官方现有文档。本文在某些地方解释得更深入、更细致。因此我们没有强行与官方同步。

富网络应用程序的天然属性是,它们是动态的。不管你的程序多有效率,每次请求始终承受着远远大过静态文件的开销。

而更多的web程序,并没有受大的影响。Symfony闪电般快,除非你在做一些超级重载,每一次请求都会很快恢复,而没有把过多压力留给服务器。

但你的网站在成长,过载有可能成为问题。针对通常请求的处理,只应完成一次。而这正是缓存锁定的目标。

缓存于巨人的肩膀 

改善一套程序的性能,最有效的方式是缓存页面的全部输出,然后无视整个后续请求。当然,对于高动态网站而言,不可能总是这样。本章,你将了解Symfony的缓存系统是如何运作的,以及为何这是最佳方案。

Symfony缓存系统与众不同,因为它依靠的是HTTP specification所定义的HTTP cache之简单与强大。不同于重新发明一套缓存方法,Symfony强调的是定义了web基本通信的标准。一旦你掌握了“HTTP验证”,以及“缓存models的过期”等基本知识,你已经可以去掌握Symfony的缓存系统。

学习Symfony缓存的过程,可分为四个步骤:

  1. 网关缓存(gateway cache),或者反向代理(reverse proxy),是位于你程序前面的独立层。反向代理,缓存的是响应,因为它们被你的程序返回;还能在请求到达你的程序之前,通过缓存的响应来回应请求。Symfony提供了自己的反向代理,但是任何反向代理都可以使用。

  2. HTTP cacheHTTP缓存头,在你的程序和客户端之间,被用于同网关缓存或其他缓存进行通信。Symfony提供了合理的默认配置,以及强大的接口,用于与缓存头(cache headers)进行互动。

  3. HTTP过期与验证(expiration and validation),这两个模型被用于决定缓存的内容是否新鲜/fresh(可从cache中复用),或者是否陈旧/stale(应当被程序重新生成)

  4. Edge Side Includes(ESI),边缘端包容允许HTTP cache被用于页面局部(甚至嵌套片段)的独立缓存。在ESI的帮助下,你甚至可以“缓存整个页面60分钟,但侧边栏只缓存5分钟”。

由于HTTP cache并非Symfony专用,有很多相关文章。如果你对HTTP缓存不太熟,强烈推荐阅读Ryan Tomayko的缓存能做什么(Things Caches Do)。另一个深度好文是Mark Nottingham的缓存教程(Cache Tutorial)

使用Gateway Cache 

当用HTTP缓存时,cache是完全与你的程序分开的,它居于你的程序与发动请求的客户端的之间。

缓存的任务,就是接收客户端请求,然后把它们再传回你的程序,跟着推送回客户端。这里的缓存是程序与浏览器之间的“请求-响应”通信过程的“中间人”。

随着时间推移,这些缓存将存储每一次被认为“可以缓存(cacheable)”的响应(参考HTTP缓存介绍)。如果相同的资源被再次请求,cache将发送缓存了的响应至客户端,完全无视你的程序。

这种类型的缓存即是HTTP gateway cache(网关缓存),存在于诸如Varnish反向代理模式下的Squid以及Symfony的反向代理之中。

缓存类型 

但是Gateway缓存并非唯一的缓存类型。实际上,你的程序发送的HTTP缓存头,被假定于被最多三种方式的缓存所解释:

  • 浏览器缓存(Browser caches):每个浏览器都内置了自己的本地缓存,用于你点击“回退”时使用,或者用于图片和其他assets资源。浏览器缓存是私有(private)缓存,因为缓存的资源不能被其他人使用;

  • 代理缓存(Proxy caches):代理,是指共享(shared)缓存,因为很多人可以跟在某个人的后面(来使用)。通常被大公司或ISP所使用,以减低访问延迟和网络流量。

  • 网关缓存(Gateway caches):类似代理,它也是共享缓存,但却是在服务器端。常为网络管理员所用,令网站更易升级、更可靠、性能更高。

Gateway caches有时特指反向代理缓存,surrogate caches(代理缓存),甚至HTTP加速器。

当缓存的响应包含了某个特定用户的内容(比如账号信息)这种情况被讨论时,私有(private)缓存和共享(shared)缓存的重要性与日俱增。

程序的每一次响应,将会经历前两种缓存类型中的一种或两种。这些缓存是在你的(程序)控制之外,却遵守响应中设置好的HTTP缓存的指令。

Symfony反向代理(Reverse Proxy) 

Symfony内置了用PHP写的反向代理(也被称为gateway缓存)。它并非Varnish这种全功能的反向代理缓存,但却是一个很好的起步。

关于Varnish设置的更多细节,参考 如何使用Varnish加速我的网站

开启代理很容易:Symfony程序都预建了一个cache kernel缓存核心(AppCache),它把默认的核心(AppKernel)给打包。这个缓存核心就是 反向代理。

开启缓存很容易,修改你的前端控制器代码。你也可以在app_dev.php中做出这些改变,即可为dev环境添加缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// web/app.php
use Symfony\Component\HttpFoundation\Request;
 
// ...
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
 
// add (or uncomment) this new line! / 添加下面新行!
// wrap the default AppKernel with the AppCache one
// 用AppCache打包默认的AppKernel
$kernel = new AppCache($kernel);
 
$request = Request::createFromGlobals();
 
$response = $kernel->handle($request);
$response->send();
 
$kernel->terminate($request, $response);

上面的缓存核心,将立即作为反向代理来运作——从你的程序中缓存响应,然后把它们返回到客户端。

如果你正使用framework.http_method_override选项,来从_method参数中读取HTTP方法,参考上面链接来调整到你需要的程度。

缓存核心有一个特殊的getLog()方法,返回一个字符串,用以表明缓存层中到底发生了什么。在开发环境下,可以使用它来除错,或者验证你的缓存战略。

1
error_log($kernel->getLog());

AppCache对象有一个合适的默认配置,但是通过覆写getOptions()方法来设置一组选项,该对象即可被精细调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app/AppCache.php
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
 
class AppCache extends HttpCache
{
    protected function getOptions()
    {
        return array(
            'debug'                  => false,
            'default_ttl'            => 0,
            'private_headers'        => array('Authorization', 'Cookie'),
            'allow_reload'           => false,
            'allow_revalidate'       => false,
            'stale_while_revalidate' => 2,
            'stale_if_error'         => 60,
        );
    }
}

除非在getOptions()方法中进行覆写,否则debub选项将被自动设成“被剥离出来的AppKernel”中的debug值。

下面是一些主要选项:

default_ttl

数值是秒,表达的是当响应中没有提供明确的新鲜度信息时,一个缓存入口被认为是fresh的时长。显式指定Cache-ControlExpires头,可以覆写这个值(默认是0)。


private_headers

一组请求头,在没有“通过Cache-Control指令(默认是AuthorizationCookie)明确声明当前响应是public还是private状态”的响应中,触发“private”Cache-Control行为。


allow_reload

指定客户端是否可以在请求中包容一个Cache-Control的“no-cache”指令来强制重新加载缓存。设为true即可遵守RFC2616(默认是false)。


allow_revalidate

指定客户端是否可以在请求中包容一个来Cache-Control的“max-age=0”来强制重新验证。设为true即可遵守RFC2616(默认是false)。


stale_while_revalidate

指定的默认秒数(以秒为间隔是因为Response的TTL精度是秒),在此期间,尽管缓存在后台对响应正进行重新验证,但它能够立即返回一个不新鲜的响应(默认值是2);本设置可被HTTPCache-Control扩展的stale-while-revalidate覆写(参考RFC 5861)。


stale_if_error

指定的默认秒数(间隔是秒),在此期间,缓存可以对遇到错误的响应提供服务(默认值是60)。本设置可被HTTPCache-Control扩展的stale-if-error覆写(参考RFC 5861)。


如果debug被设为true,Symfony将自动添加一个X-Symfony-Cache头到响应中,里面有关于缓存命中和丢失的有用信息。

从一个反向代理切换到另一个

在开发网站时,或者在部署网站到“除了php代码什么都不能安装”的共享主机过程中,Symfony的反向代理是个极为好用的工具。但由于是用PHP写成,它不如用C写的代理快。这也是为什么强烈推荐你在生产环境的服务器上尽可能地使用Vanish或Squid。好消息是,从一个代理服务器切换到另一个是很容易的,而且过程透明,因为你的程序中并没有代码要修改。你可以安心使用Symfony反向代理,日后访问量增长时可以随时升级到Varnish。

Symfony反向代理的性能是独立于程序复杂程度之外的。这是因为程序内核只在“request需要被发送给它”时才会启动。

令你的响应成为HTTP缓存 

为了利用可用的缓存层,你的程序应该与以下信息进行通信:1、哪些响应可被缓存。2、能够决定缓存“何时/如何变成不新鲜”的规则。

记得,“HTTP”就是一种语言(简单文本)而已,被客户端和和服务器用来进行相互通信之用。而HTTP缓存就是这种语言的一部分,允许客户端和服务器交换关于缓存的信息。

HTTP指定了以下四种用于响应的缓存头:

  • Cache-Control

  • Expires

  • ETag

  • Last-Modified

其中最为重要和功能最强的当属Cache-Control头,它可说是多种缓存信息的集合。

每种头都在HTTP Expiration,Validation和Invalidation小节中进行了详解。

Cache-Control头 

Cache-Control头是特殊的,它包含不止一条,而是很多条和响应的缓存能力相关的信息。每种信息被以英文逗号分隔开来:

1
2
3
Cache-Control: private, max-age=0, must-revalidate
 
Cache-Control: max-age=3600, must-revalidate

Symfony提供了一个关于Cache-Control头的抽象层,以便令它的创建更加易于管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
 
use Symfony\Component\HttpFoundation\Response;
 
$response = new Response();
 
// mark the response as either public or private 标记响应是公有还是私有
$response->setPublic();
$response->setPrivate();
 
// set the private or shared max age 设置私有或公有的最大周期
$response->setMaxAge(600);
$response->setSharedMaxAge(600);
 
// set a custom Cache-Control directive 设置一个自定义Cache-Control命令
$response->headers->addCacheControlDirective('must-revalidate', true)

如果你要为控制器中不同的action设置缓存头,你也许需要看看FOSHttpCacheBundle。它提供了一种基于URL模式匹配和其他请求属性的方式来定义缓存头。

Public响应和Private响应 

不管是gateway还是proxy缓存,都被认为是“shared”共享缓存,因为缓存内容被更多用户分享。如果一个“特定用户专有”响应被错误地置于共享缓存中,它可能在后面的时间里被返回给多位不同用户。试想你的账号信息被缓存,然后发送给所有后续请求了自己账号页面的用户是个什么场面!

为应对这种情形,每一个响应应当被设为public或private:

public

指示响应应该被同时缓存为public和private缓存。


private

指示所有或部分响应信息仅针对某一用户,因此禁止缓存为public缓存。


Symfony保守的设置每一次响应为private。为了利用好共享缓存(比如Symfony反向代理),响应必须显式设定为public。

安全方法(Safe Method) 

HTTP缓存只工作在“安全”HTTP方法下(比如GET或HEAD)。所谓安全,是指你在对请求提供服务时(诸如记录日志,处理缓存信息等)永远不能改变服务器上的程序状态。这就产生两个极为有说服力的重要结论:

  • 你永远不应该在GET或HEAD请求的响应中改变程序状态。就算你不使用gateway cache,然而代理缓存的本质是,任何GET或HEAD请求,可能或并没有真正hit到你的服务器;

  • 不要预期对PUT、POST或DELETE方法进行缓存。这些方法意味着被用于你的程序状态发生改变时(比如删除一篇博客)。缓存它们将阻止特定的请求命中或改变你的程序。

缓存规则和默认设置 

HTTP1.1允许默认缓存任何内容,除非显式指定了Cache-Control头。实践中,多数缓存在请求中包含cookie时、包含authorization头时、使用了一个非安全方法时(比如PUT、POST或DELETE)或当响应有一个重定向状态码时,什么也不做。

当开发者在响应头中什么也没设置时,Symfony依据以下规则,自动设置了有意义的而且是偏保守的Cache-Header头。

  • 如果没有缓存头信息被定义(Cache-ControlExpiresETagLast-Modified),Cache-Control将被设为no-cache,代表响应将不被缓存;

  • 如果Cache-Control是空(但是另外一个缓存头有被设置),其值将被设为private, must-revalidate

  • 但是如果至少有一个Cache-Control指令被设置,而且没有publicprivate指令被显式添加的话,Symfony会自动添加private指令(除了当s-maxage被设置时)

HTTP Expiration,Validation和Invalidation 

HTTP协议定义了两种缓存模型:

  • 利用expiration model(过期模型),通过包容Cache-Control头和/或Expires头,即可直接指定一个响应应该被认为“新鲜”的时长。缓存能够理解过期时间,不再制造相同请求,直到该缓存版本抵达过期时间,而且变得“不新鲜(stale)”。

  • 当页面是真动态时(展现层经常改变),则validation model(验证模型)的使用就十分有必要。利用这个模型,缓存把响应存储起来,但会在每次请求时向服务器“提问”——是否缓存了的响应仍然有效?程序使用了一个独立的响应识别器(即Etag头)和/或一个时间戳(即Last-Modified头),来检查当前页面自被缓存之后,是否发生了改变。

理解HTTP Specification

HTTP specification定义了一种简单但却强大的语言,可以令客户端和服务器进行通信。作为web开发者,HTTP specification所拥有的request-response model(“请求-响应”模型)将支配你的缓存工作。不幸的是,HTTP协议的真正文档:RFC2616,是难以读懂的。

不过有一个正在进行中的HTTP Bis要覆写RFC 2616。它并不描述新版本的HTTP,更多的是对原有HTTP协议进行疏理。文档的组织结构也随着HTTP协议被分为七个部分而有所改进;关于HTTP caching的每一部分,都可以在以下两个独立章节找到(P4 - Conditional RequestsP6 - Caching: Browser and intermediary caches)。

作为一名web开发者,你被我们Symfony官方团队 最强烈力劝 来阅读HTTP协议相关内容。它是如此明晰与强大——哪怕是在它被创造出来的10年之后——Http Specification是无价的。我们特别提醒您,千万别对这些协议的表象敷衍了事——其内容之美丽,百千万亿倍于其封面。

Expiration(过期) 

expiration model,是两个缓存模型里效率更高、更直接的一个,因此应该被尽可能多地使用。当一个响应通过expiration被缓存时,缓存将保存响应,并且在过期之前直接返回它,而毋须命中程序。

过期模型,可以通过以下几乎一样的两种HTTP头之一来实现:ExpiresCache-Control

使用Expires头控制过期 

根据HTTP specification,“Expires头字段将在response被认为是stale之后给出date/time。”。这里的Expires头可以被设为Response方法:setExpires()。它使用DateTime实例作为参数:

1
2
3
4
$date = new DateTime();
$date->modify('+600 seconds');
 
$response->setExpires($date);

该响应的HTTP头信息类似这种:

1
Expires: Thu, 01 Mar 2011 16:00:00 GMT

setExpires()方法将自动转换日期为GMT时区,因为这是HTTP specification的要求。

注意,在HTTP 1.1版之前,并不需要原始服务器来发送Date头。因此,缓存(比如浏览器的)就需要本地时钟来评估Expires头,进而令缓存周期的计算因时间倾斜而变得脆弱不堪。另外一个Expires头限制是,正如HTTP协议中所描述的,“HTTP/1.1 不得发送Expires的日期超过一年。”

使用Cache-Control头控制过期 

因为Expires头的限制,多数情况下,你应该使用Cache-Control头来替代。记得,Cache-Control头被用于多种不同的缓存指令。例如,max-ages-maxage。第一个用于全部缓存,而第二个仅在共享缓存时用到。

1
2
3
4
5
6
7
8
// Sets the number of seconds after which the response
// should no longer be considered fresh
// 设置“响应过期”的秒数
$response->setMaxAge(600);
 
// Same as above but only for shared caches
// 同上,但仅用于共享缓存
$response->setSharedMaxAge(600);

Cache-Control头一般是下述格式(但有时也会有其他指令):

1
Cache-Control: max-age=600, s-maxage=600

过期和验证(Expiration and Validation) 

你当然可以对同一个Response同时使用validation和expiration。因为expiration的优势大过validation,你能很容易地从两个世界中好的一面受益。也就是说,同时使用过期和验证,你可以命令缓存来服务于已缓存的内容,同时还能在某些区间(expiration)向后检查以确认缓存内容仍然有效。

你也可以通过annotation来为expiration和validation去定义HTTP缓存头。参考FrameworkExtraBundle文档。

更多Response方法 

Response类提供了很多方法以应对缓存。下面是几个特别有用的:

1
2
3
4
5
6
// Marks the Response stale 标记响应过期
$response->expire();
 
// Force the response to return a proper 304 response with no content
// 强制响应返回一个没有内容的恰当的304响应
$response->setNotModified();

另外,多数与缓存相关的HTTP头可以单独使用setCache()方法来完成设置:

1
2
3
4
5
6
7
8
9
// Set cache settings in one call
$response->setCache(array(
    'etag'          => $etag,
    'last_modified' => $date,
    'max_age'       => 10,
    's_maxage'      => 10,
    'public'        => true,
    // 'private'    => true,
));

总结 

Symfony的设计思想即是遵循业界公认标准:HTTP。缓存功能也不例外。掌握Symfony的缓存系统意味着你已然熟悉了HTTP cache模型并且能够高效地使用它。换句话说,毋须依赖Symfony文档和例程,你可以驰骋于HTTP caching和以Varnish为代表的gateway caches的世界。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)