错配的nginx——web缓存问题探究

写在前面

随着项目的推进,渐渐的要上https,要从tomcat服务器转换到nginx+tomcat组合的方式了。 nginx有个问题就是缓存太”严重”,我认为这其实是一个大的优化,如果用好了可以对项目体验起到很好的催化,这也是nginx作为服务器的前端的意义所在。 但是往往当开发人员对http理解薄弱的,就会对此很搓火。因为过强的缓存会导致页面滞后,debug时候很多东西无法及时出来,只能动不动强制清缓存,大大破坏了开发体验。

由来

其实一直一来都在做强缓存。新起的项目都做好了资源的hash。但是如果不是这次错配的nginx导致个人寻根结底找原因,也不会有这篇关于http缓存的文章。 HTTP的文章其实网上已经有很多了。个人完全不认为会比那些无比详细和成系列的雄文对比。仅仅是个人对知识体系的一个总结吧。

言归正传。问题的由来其实是因为nginx的响应头配置有问题,它里面既没有 Cache-Control,也没有Expires。导致很多资源无端200(from cache)而不304,使得资源存在更新不及时的问题.

举个栗子,这是清空缓存后第一次访问资源的http响应头:

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Accept-Ranges: bytes
ETag: W/"2477-1467900381000"
Last-Modified: Thu, 07 Jul 2016 14:06:21 GMT
Content-Type: image/png
Content-Length: 2477
Date: Fri, 15 Jul 2016 14:00:41 GMT

这是对应的请求头:

1
2
3
4
5
6
7
8
9
Accept:image/webp,image/*,*/*;q=0.8
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4
Cache-Control:max-age=0
Connection:keep-alive
Cookie:CNZZDATA1256284005=647609082-1468586434-%7C1468586434
Host:www.xxx.com
Referer:http://www.xxx.com/
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36

然后在地址栏敲回车以后以后,返回的200(from cache),而我不是猜想的304 总之一句话就是这个问题的关键在于:响应头里既没有 Cache-Control也没有Expires的时候,是如何在新鲜度检测阶段直接认为新鲜度没有问题的。

问题的抛出

在搞清整个问题之前我觉得还是要将HTTP缓存机制好好整理处理作为问题解决的前提和疑问抛出的前提。

用最简单的话来说,服务器缓存启用是存在两个判断机制的:

  1. 新鲜度检测阶段 需要依赖响应头的Cache-Control和Expires,通过才会返回200(from cache)
  2. 资源二次校验阶段 校验资源一致性,如果一致返回304,如果不一致返回200,当然,如果没有找到返回404

现在的问题就抛出来了,为什么,响应头里面没有Cache-Control和Expires,那么按照上诉的缓存启用机制,返回的状态吗可能是304,可能是200,可能是404或者更多的,但是就是不可能是200(from cache)。但是现实用惨淡的现实告诉我们,结果就是200(from cache)!这是为什么呢?

HTTP缓存

用户操作对缓存的影响

用户不同的操作是对缓存存在固有影响,正常链接点入、新tab打开网址/窗口、F5刷新、前进后退、Ctrl+F5强刷,对缓存都会不同的表现

用户操作Exprires/Cache-ControlLast-Modified/Etag
地址栏回车有效有效
页面链接转跳有效有效
新开窗口有效有效
前进后退无效有效
Ctrl+F5强刷无效无效

缓存机制

缓存总体上的流程是这样的(原图来自《http权威指南》,个人有涂鸦):

新鲜度检测

Cache-Control与Expires 是新鲜度检测的关键:

Cache-Control与Expires的作用一致,都是指明当前资源的有效期。只不过Cache-Control的选择更多,设置更细致,如果同时设置的话,其优先级高于Expires。
详细一点说明的话,Expires是http/1.0定义的资源有效期,它是一个历史遗留产物。它的值是一个绝对值,它显示指定了一个日期作为过期时间。然而经过一段时间实践之后,Bug就出来了:当客户端可服务器时间不一致,那么到底怎么处理呢?

Cache-Control是http/1.1定义的,功能很多,但是核心的我认为还是还是改变了资源有效期的表达方式,我认为最大的意义是Cache-Control是Expires的bugfix产物(作为bugfix优先级当然会更高)。它使用max-age来根据最后时间加上这个max-age动态计算了过期时间,这样就不存在Expires的Bug了。

Cache-Control可选值:(偷懒摘自《翻译:web制作、开发人员需知的Web缓存知》)

  • max-age=[秒]:表示在这个时间范围内缓存是新鲜的无需更新。类似Expires时间,不过这个时间是相对的,而不是绝对的。也就是某次请求成功后多少秒内缓存是新鲜的。
  • s-maxage=[秒]:类似max-age, 除了仅应用于共享缓存(如代理)。
  • public:标记认证的响应才能够被缓存。一般而言,需要认证HTTP请求内容会自动私有化(不会被缓存Add)。
  • private:允许缓存专门为某一个用户存储响应,比方说在浏览器中;共享缓存一般不会,例如在代理中。
  • no-cache:每次在释放缓存副本之前都强制发送请求给源服务器进行验证,这在确保认证有效性上很管用(和public结合使用)或者保证内容必须是即时的,不得无视缓存的所有优点,如国内的微博、twitter等的刷新显示Add。
  • no-store:强制缓存在任何情况下都不要保留任何副本。
  • must-revalidate:告诉缓存,我给你准备了一些关于新鲜度的信息,在表现的时候要严格遵循之。HTTP允许缓存在某些特定情况下返回过期数据,指定了这个属性,相对于告诉缓存,你丫必须严格遵循我的规则。
  • proxy-revalidate:类似must-revalidate,除了只能应用于代理缓存。

个人注:理想的情况下,Cache-Control与Expires可以都标明以达到最优兼容,但是这不是必须都有的,多数可以只有其中一个也不会有什么大问题(如果引起BUG就另说了),但是——不要两个都没有,不然会引起相对诡异的现象,如果本文的例子。
——但是你硬要这么干,确实可以两个都没有.

资源二次校验

Last-Modified/ETag 这两个数据用于确定数据是否改变
Last-Modified嘛,很好理解,文件最后修改时间。如果这个东西那么好用的话其实Etag不过画蛇添足罢了,但是很可惜的Last-Modified的时间度量单位是秒——这意味着一秒内的修改服务器分辨不出来——股票行情系统和金融系统对此有利益攸关的需求。
所以就有了ETag,这个东西就是类似文件hash的东西,在服务器上用于分辨文件是不是同一个。

Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag。一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304

个人注:如果你想用304,Last-Modified和ETag是很重要的属性。如果你还想用304,那么就不要删除它们(删除其中一个会不会有影响个人还未验证)。

新鲜度校验的补充

试探性过期(本章摘自HTTP权威指南Page193)
如果响应中没有 Cache-Control: max-age 首部,也没有 Expires 首部,缓存可 以 计算出一个试探性最大使用期。可以使用任意算法,但如果得到的最大使用期大于24小时,就应该向响应首部添加一个 Heuristic Expiration Warning首部。据我们所知,很少有浏览器会为用户提供这种警告信息。
LM-Factor 算法是一种很常用的试探性过期算法,如果文档中包含了最后修改日期, 就可以使用这种算法。LM-Factor算法 将最后修改日期作为依据,来估计文档有多么易变。算法的逻辑如下所示。
• 如果已缓存文档最后一次修改发生在很久以前,它可能会是一份稳定的文档,不太会突然发生变化,因此将其继续保存在缓存中会比较安全。
• 如果已缓存文挡最近被修改过,就说明它很可能会频繁地发生变化,因此在与服 务器进行再验证之前,只应该将其缓存很短一段时间。

这段描述已经解决了个人的疑惑了,简单说算法猜你近期没有改的不会突然改了,反之认为你会突然改。在《HTTP权威指南》内有简单的公式和图例,有兴趣的可以自行前往翻阅。

个人注:《HTTP权威指南》有这样一段话:

HTTP 有一组非常复杂的新鲜度检测规则,缓存产品支持的大量配置选项,以及与 非 HTTP 新鲜度标准进行互通的需要则使问题变得更加严重了。本章其余的大部分 篇幅都用于解释新鲜度的计算问题。

所以,这个新鲜度校验是非常复杂的,远不是网上大多数文章提到的那么简略,如果以后在新鲜度校验这里出现疑问,不妨翻一翻。

结尾

问题到这里算是解决了,解决方案很简单,加上了Cache-Control和Expires。但是这次的服务器错配的导致的问题的探索,着实是很大程度上提高了个人对HTTP缓存的理解。

参考文章

《翻译:web制作、开发人员需知的Web缓存知识》
【Web缓存机制系列】2 – Web浏览器的缓存机制
《HTTP权威指南》