HTTP缓存控制及在GAE上的应用

标签:Google App Engine

以前曾写过一篇《一些使用HTTP头提高性能的方法》,讲述了怎样用Cache-Control头来控制浏览器缓存策略。不过那篇文章没有考虑代理服务器的情况,因此今天把这部分也补上。

想起研究这事的起因是在GAE论坛发现有人问Cache-Control为什么会对请求数造成如此大的影响

上图中可以看到,同样都是将缓存时间设为10800秒,但为public时几乎没有请求,而为private时就猛增了10多倍。

我查了半天后找到一篇《Proxy caching on Google Appengine》,才发现这是GAE的缓存服务器在起作用。
简单来说,用户代理(一般是浏览器)向GAE请求一个资源(网页或图片等)时,实际上会访问一个边缘缓存服务器。这个服务器起了反向代理的作用,它会去访问应用服务器,取得内容,然后输出给用户代理。
而当应用服务器返回了Cache-Control头,并允许保存缓存时,这个代理服务器就会缓存这个请求;而在缓存失效前,如果有任何用户代理再次请求这个资源,代理服务器并不会去访问应用服务器,而是直接输出缓存中保存的内容。
也就是说,缓存不但存在于用户的浏览器中,还存在于代理服务器上。不同的是浏览器的缓存一般只对使用这个浏览器的用户有效,而代理服务器的缓存可以对所有使用它的用户有效,因此后者也称为shared cache(共享缓存)。但如果应用服务器将Cache-Control设为private,代理服务器就不会将其保存在共享缓存里。
于是上图就很好解释了:当应用服务器设定缓存为public时,代理服务器会保存它们至共享缓存,其他用户代理访问时就不会请求应用服务器;而设为private时,这个缓存只对当前请求它的用户代理有效,所以其他用户代理访问时会继续请求应用服务器。
那么未指定是public还是private时会是怎样的呢?关于这点我没找到文档来描述,但估计是靠代理服务器自己去处理,例如HTTPS和需要Authorization的响应是不会缓存的,而很多代理服务器也不缓存带有cookie的响应。
此外,Google为了区分是不是被边缘缓存服务器缓存了,会在访问记录里加上一条状态为204的请求(这点和标准的代理服务器不同),并且控制面板的Request by Type/Second图表也会显示Cached Requests。此外,Ikai Lan也指出:只有启用支付的应用,Google才会对其启用边缘缓存服务器

当然,本文不可能这么简单就结束了,不然也太不专业了,于是继续对其他情况进行分析。

要保证用户拿到的响应足够新,还可以用no-cache和must-revalidate指令。其中no-cache非常严格,不允许任何缓存,就连按后退按钮都必须访问源服务器。而must-revalidate则允许缓存,但如果“不新鲜”时,缓存必须重新访问源服务器去验证;如果通过代理服务器,则代理服务器也必须访问源服务器去验证(使用它的原因是,有些代理服务器可能会自作主张地设置过期时间,而不采用源服务器的设置,这个指令则能确保按照源服务器的要求来缓存)。

上文的处理是不区分浏览器和代理服务器的缓存的,而要只针对代理服务器的话,就需要用到其他指令了。
其中proxy-revalidate就是这样一个指令,它和must-revalidate相似,但允许在代理服务器之间相互分享缓存。此外还有个s-maxage,它和max-age的不同在于,它只指定共享缓存(一般就是代理服务器)的缓存时间。

另外,源服务器还可以输出一个Vary头字段,它可以指定一组用于区分是否能直接输出缓存内容的头字段。
举例来说,当Vary为Accept-Language和User-Agent时,一个用户以zh-cn的Accept-Language和iPhone的User-Agent来访问,代理服务器可能会保存一份缓存;而另一个用户以en-us的Accept-Language和Chrome的User-Agent来访问时,由于Vary中所指定的头字段变了,因此代理服务器就不会使用前一份缓存,而必须像源服务器请求一个符合要求的响应。

而用户想要获取最新资源时,也是可以强行要求代理服务器进行验证的。你可以拿Firefox+Firebug试验一下:按F5刷新时,请求头里会包含一个“Cache-Control: max-age=0”;而按Ctrl+F5时,会包含“Pragma: no-cache”和“Cache-Control: no-cache”。
前者是用于端到端验证(end-to-end revalidation),也就是从用户到源服务器之间,各个代理服务器都必须向下一层服务器进行验证,保证自己的缓存有效;而后者是用于端到端重载(End-to-end reload),也就是路径中的任何一个代理服务器都不能使用缓存来响应,而必须请求源服务器的响应。

感觉写着写着就偏题了,为此再补充一下GAE方面的话题吧。
从上面的分析可以看出,对于时效性要求不是很高的页面,输出一个“Cache-Control: public, max-age=xxx”头,可以有效减少应用服务器的压力,因为请求完全不会送达到它。
但是这对于需要自行统计页面访问次数的应用却不太适合,因为应用服务器完全不知道这些请求的存在。但如果你是用自己的反向代理服务器的话,也可以在这个服务器上统计PV。
此外,这也说明动态页面最好不要设置过长的max-age,以免被代理服务器缓存后,资源要过很长时间才会更新;不过也可以指定为private,这样就只会影响到小部分用户了。
另外,GAE的边缘缓存服务器似乎有bug,不能正确处理Vary头。不过我没有开启支付,所以不能验证。
而特别要注意的是,如果一个页面对不同人生成不同的结果,千万不要让它保存在共享缓存中(也就是确保Cache-Control为private,如果你不自行设置的话,应用服务器会自动设置为private;或包含Vary头字段)。这种情况很常见,例如有的页面会对管理员显示额外的链接和debug语句,有的会对已登录用户显示额外内容,有的只给已登录用户或管理员查看,有的会对手机用户或AJAX请求展示不同的页面,有的会对不同用户展示不同语言的页面等。

最后,想了解更多信息的可以读读《Caching Tutorial》这篇文章,而REDbot也是一个用于检查HTTP头字段的好东西。
而对一般用户来说,如果不是为了清除缓存,没事别老按F5刷新,因为代价是非常高的(页面本身和引用的资源全部都要重新向源服务器验证)。

2010年12月16日更新:
据我测试,未开启支付的app,只要使用Google Apps域名访问,并且输出“Cache-Control: public, max-age=xxx”(也就是必须为public,且过期时间大于0),且不包含Vary头字段,就也会使用反向代理服务器(因为我观察到响应带有Age头字段)。

注意,这个反向代理服务器并不识别s-maxage,因此必须使用max-age。此外,包含Vary头的并不进行检查,而是直接访问源服务器,这也说明它并不智能,只能为一个资源保存一份缓存。
这对动态页面和静态文件都有效,也就是说,它不但节省了请求数,还能大幅减少流量(流量主要是静态文件消耗的)。
不过这个反向代理不会在后台记录一个204请求,也不会在统计图里显示成cached request。

还要注意一点,它存在一个严重的bug:一旦被缓存,在过期前都不会重新请求资源——就算按Ctrl+F5,也仍然输出缓存的内容,而不会更新。
如果急需更新的话,可以在引入静态文件时加上query string。不想每次都手动设置版本号的话,可以使用os.environ['CURRENT_VERSION_ID'],它可以保证每次部署的版本号都不一样。

2条评论 你不来一发么↓ 顺序排列 倒序排列

    向下滚动可载入更多评论,或者点这里禁止自动加载

    想说点什么呢?