HTTP缓存
Diff本文源自BOOK,不同于官方现有文档。本文在某些地方解释得更深入、更细致。因此我们没有强行与官方同步。
富网络应用程序的天然属性是,它们是动态的。不管你的程序多有效率,每次请求始终承受着远远大过静态文件的开销。
而更多的web程序,并没有受大的影响。Symfony闪电般快,除非你在做一些超级重载,每一次请求都会很快恢复,而没有把过多压力留给服务器。
但你的网站在成长,过载有可能成为问题。针对通常请求的处理,只应完成一次。而这正是缓存锁定的目标。
缓存于巨人的肩膀 ¶
改善一套程序的性能,最有效的方式是缓存页面的全部输出,然后无视整个后续请求。当然,对于高动态网站而言,不可能总是这样。本章,你将了解Symfony的缓存系统是如何运作的,以及为何这是最佳方案。
Symfony缓存系统与众不同,因为它依靠的是HTTP specification所定义的HTTP cache之简单与强大。不同于重新发明一套缓存方法,Symfony强调的是定义了web基本通信的标准。一旦你掌握了“HTTP验证”,以及“缓存models的过期”等基本知识,你已经可以去掌握Symfony的缓存系统。
学习Symfony缓存的过程,可分为四个步骤:
网关缓存(gateway cache),或者反向代理(reverse proxy),是位于你程序前面的独立层。反向代理,缓存的是响应,因为它们被你的程序返回;还能在请求到达你的程序之前,通过缓存的响应来回应请求。Symfony提供了自己的反向代理,但是任何反向代理都可以使用。
HTTP cacheHTTP缓存头,在你的程序和客户端之间,被用于同网关缓存或其他缓存进行通信。Symfony提供了合理的默认配置,以及强大的接口,用于与缓存头(cache headers)进行互动。
HTTP过期与验证(expiration and validation),这两个模型被用于决定缓存的内容是否新鲜/fresh(可从cache中复用),或者是否陈旧/stale(应当被程序重新生成)
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
环境添加缓存:
// web/app.phpuse 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()
方法来设置一组选项,该对象即可被精细调整。
// app/AppCache.phpuse 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-Control
或Expires
头,可以覆写这个值(默认是0
)。
private_headers
一组请求头,在没有“通过Cache-Control
指令(默认是Authorization
和Cookie
)明确声明当前响应是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
头到响应中,里面有关于缓存命中和丢失的有用信息。
Symfony反向代理的性能是独立于程序复杂程度之外的。这是因为程序内核只在“request需要被发送给它”时才会启动。
令你的响应成为HTTP缓存 ¶
为了利用可用的缓存层,你的程序应该与以下信息进行通信:1、哪些响应可被缓存。2、能够决定缓存“何时/如何变成不新鲜”的规则。
记得,“HTTP”就是一种语言(简单文本)而已,被客户端和和服务器用来进行相互通信之用。而HTTP缓存就是这种语言的一部分,允许客户端和服务器交换关于缓存的信息。
HTTP指定了以下四种用于响应的缓存头:
Cache-Control
Expires
ETag
Last-Modified
其中最为重要和功能最强的当属Cache-Control
头,它可说是多种缓存信息的集合。
每种头都在HTTP Expiration,Validation和Invalidation小节中进行了详解。
Cache-Control头 ¶
Cache-Control
头是特殊的,它包含不止一条,而是很多条和响应的缓存能力相关的信息。每种信息被以英文逗号分隔开来:
Cache-Control: private, max-age=0, must-revalidate Cache-Control: max-age=3600, must-revalidate
Symfony提供了一个关于Cache-Control
头的抽象层,以便令它的创建更加易于管理:
// ... 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-Control
、Expires
、ETag
或Last-Modified
),Cache-Control
将被设为no-cache
,代表响应将不被缓存;如果
Cache-Control
是空(但是另外一个缓存头有被设置),其值将被设为private, must-revalidate
;但是如果至少有一个
Cache-Control
指令被设置,而且没有public
或private
指令被显式添加的话,Symfony会自动添加private
指令(除了当s-maxage
被设置时)
HTTP Expiration,Validation和Invalidation ¶
HTTP协议定义了两种缓存模型:
利用expiration model(过期模型),通过包容
Cache-Control
头和/或Expires
头,即可直接指定一个响应应该被认为“新鲜”的时长。缓存能够理解过期时间,不再制造相同请求,直到该缓存版本抵达过期时间,而且变得“不新鲜(stale)”。当页面是真动态时(展现层经常改变),则validation model(验证模型)的使用就十分有必要。利用这个模型,缓存把响应存储起来,但会在每次请求时向服务器“提问”——是否缓存了的响应仍然有效?程序使用了一个独立的响应识别器(即
Etag
头)和/或一个时间戳(即Last-Modified
头),来检查当前页面自被缓存之后,是否发生了改变。
Expiration(过期) ¶
expiration model,是两个缓存模型里效率更高、更直接的一个,因此应该被尽可能多地使用。当一个响应通过expiration被缓存时,缓存将保存响应,并且在过期之前直接返回它,而毋须命中程序。
过期模型,可以通过以下几乎一样的两种HTTP头之一来实现:Expires
或Cache-Control
。
使用Expires头控制过期 ¶
根据HTTP specification,“Expires
头字段将在response被认为是stale之后给出date/time。”。这里的Expires
头可以被设为Response
方法:setExpires()
。它使用DateTime
实例作为参数:
$date = new DateTime(); $date->modify('+600 seconds'); $response->setExpires($date);
该响应的HTTP头信息类似这种:
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-age
和s-maxage
。第一个用于全部缓存,而第二个仅在共享缓存时用到。
// 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
类提供了很多方法以应对缓存。下面是几个特别有用的:
// Marks the Response stale 标记响应过期$response->expire(); // Force the response to return a proper 304 response with no content // 强制响应返回一个没有内容的恰当的304响应$response->setNotModified();
另外,多数与缓存相关的HTTP头可以单独使用setCache()
方法来完成设置:
// 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的世界。