引子:昨天中文站出现了大量的用户投诉,投诉内容是运费发生变化,运费金额不正确。可是我们本地怎么测试都没有问题。但是考虑到问题唯一的可能是昨天的一个关于运费模版优化的项目引发的。因为,我昨天中午发布了一个style应用版本,发布时间是11点20分左右,运费模版优化项目的发布是在晚上9点钟左右。但是由于运费模版优化项目在正式发布阶段发现了问题,于是在第二天的凌晨3点左右响应了正式发布失败操作,style应用一起做了回滚处理(即用中午11点20左右那个版本覆盖服务器现有版本)。那么,经过分析很有可能是用户端的前端js文件并没有正常的回滚到中午11点20分左右发布的版本(后续均用merge-ump-old.js来代替此版本),也就是说该用户在当天晚上9点至3点左右(运费模板优化项目正式发布期间)访问过立即订购页面,所以浏览器缓存了运费模版优化项目所产生的版本(后续均用merge-ump-new.js来代替此版本),而浏览器在第二天再次访问立即订购页面的时候,请求服务器的style文件,而服务器直接告诉浏览器缓存文件比我服务器的文件还要新(因为服务器已经回滚merge-ump-old.js,而用户还缓存的是merge-ump-new.js),所以直接返回Status Code:304,浏览器也就仍然在用昨晚缓存的文件,于是就悲剧了,并且是大量的悲剧。
那为什么我们自己测试都是没有任何问题的呢?到底浏览器和服务器间的缓存是怎样协商控制的呢?通过在网络查找参考了几篇文献,得出了以下的一个缓存控制流程图:
简单描述一下整个流程,并对重点控制节点做一个简要概述,具体不展开讨论,可以参考扩展阅读链接咨询具体情况。由于浏览器对于post的请求始终都不会缓存的,所以只是针对get方式请求文件的流程做出本文的描述,如有不准确的地方还望指正。
A流程。第一次访问或者强制刷新页面时(和第一次访问类似),服务器将会直接返回状态码Status Code:200,并附带该文件的相关信息Response Headers,所有信息都是服务器自行配置按照需求增减。浏览器收到服务器返回的Response Headers之后,会根据这些信息来判断是否要缓存该文件,缓存有效期是多久等,以便再次访问参考。
从上面的几个关键来看,在浏览器和服务器端协商控制缓存的主要信息为:
1 Pragma值 决定是否需要浏览器缓存(HTTP1.0规范,HTTP1.1兼容,不是所有浏览器都支持)
2 Cache-Control 决定浏览器是否缓存或者设置缓存时间(秒数),一般用这个较多
3 Expires 过期时间值,但是服务器和客户端可能不同步时区之类导致不准确
4 Etag 类似于该文件在服务器的唯一hash值,由服务器自己按规则生成
5 Last-Modified 文件最后修改时间,只能到秒级大都配合ETag处理
大致还是存在这么一个顺序或者优先级的:
浏览器端行为:Pragma值校验》Cache-Control值校验》Expires值校验
服务器行为:Etag校验》Last-Modified校验(大多数服务器会在二者都通过时才能返回304的,所以没这么严格的顺序关系,当然具体校验哪个或者校验与否都与自己的服务器配置有关)
平常我们按键F5就是停止了浏览器本地的缓存校验,让Cache-Control和Expires值无效但Last-Modified和ETag值仍然有效(如果存在的话),浏览器直接带上缓存文件的信息去请求服务器,交由服务器来判断是否返回304还是200的状态码,从而决定浏览器是否直接使用浏览器端的缓存文件。
而对于强制刷新页面的情况,也就是我们常说的Ctrl+F5,浏览器端直接当第一次访问文件处理,让与缓存设置相关的所有值无效,包括Last-Modified和ETag值都失效,所以直接走上图中的【A】流程,就不会产生浏览器和服务器的协商控制过程。而很多时候我们强刷还没用,这个就是某些特定浏览器的问题了,具体情况有位同学做了详细的评测分析,可以参考一下参考文献【3】。
接下来我们以下单页面引用了mere-ump.js这个文件为例,模拟一位投诉用户的访问过程。
第一阶段:该用户在11日晚上9点至12日凌晨3点之间(运费模版优化项目正式发布与回滚期间)第一次访问了立即订购页面
1.进入页面,浏览器分析到引用了merge-ump.js这个文件,并且本地已有了一份对应的merge-ump-old.js文件缓存(假设该用户9点之前访问过)。
2、浏览器判断缓存文件是否过期。由于中文站的style应用并没有特意的设置pragma值(该值在HTTP 1.1规范中规定的,但是很多浏览器并没有实现,所以意义不是特别大),也没有特意设置Cache-Control值来控制缓存,连Expires都没有设置(这个时间是服务器时间,但是浏览器和服务器时区不一致等原因会导致时间根本不一致,所以不是很靠谱),只是通过文件的ETag和Last-Modified时间来控制返回给浏览器。所以,浏览器每次都是要请求服务器的,缓存控制始终交由服务器来判断。如图所示:
中文站style文件的请求头和返回header信息(点击查看大图)
另外,从服务器返回的时间来看服务器的时区是不对的,和我们刚好相差8个小时!
3、从上图可见,浏览器携带的Request Header信息应该是merge-ump-old.js的相关信息,其中包含ETag值和modified时间,以IF-None-Match和If-Modified-Since的key发送。因此浏览器发送请求到服务器时,服务器会比较传入的值是否和服务器上的merge-ump-new.js的Etag值一致(如果校验的话),以及merge-ump-old.js最后修改时间是否早于merge-ump-new.js最后修改时间(如果校验的话)。最后浏览器对比发现merge-ump-new.js要新一些,所以就返回Status Code:200,并返回最新的merge-ump-new.js文件给浏览器,浏览器将会再次根据Response Headers 缓存该文件。
4.由于项目发布失败,需要回滚。回滚操作,虽然不是特别清楚,但是肯定的是:直接将服务器上上一个版本的所有文件copy之后覆盖现有的文件,因此文件的Last-Modified时间将变为上一版本发布时的修改时间。而Etag生成的规则和校验规则都是由服务器自行判断的,并且ETag的生成和校验对于大型网站的静态服务资源来说是一个极大的成本,因为所有静态文件很少有1秒内更改N次的场景,所以采用Last-Modified时间足矣。所以很多像Yahoo!,Google,Facebook等大型网站就禁用了此功能,仅仅依靠文件的修改时间来判断,提高服务器性能。所以,中文站的还不肯定,需要和运维人员确认一下是否启用了ETag的验证功能。目前看来是启用了生成的功能,不知道校验是否启用了(后来和米志文同学确认并没有校验ETag值,只是采用默认的Last-Modified来校验)。
第二阶段 用户12日上午9点再次访问立即订购页面(回滚之后第一次访问)。
浏览器重复第一阶段中的步骤1和2,只不过此时本地的缓存文件是昨天晚上下载的merge-ump-new.js文件了,因此Request Headers所携带的信息也是该文件的信息。服务器接收到请求之后,对比请求头信息中的Last-Modified-Since和服务器中的merge-ump-old.js文件(因为回滚到此版本了)的修改时间,发现缓存的文件修改时间居然还比服务器的新,所以就pass掉了,直接返回了Status Code304状态码到Response Headers中,浏览器接收到了304状态码之后就直接使用了本地缓存文件merge-ump-new.js,而立即订购页面本身是动态页面并且不会缓存(设置了Pragma:no-cache和Cache-Control:max-age=0)。所以,页面回滚了,但是用户本地的js文件还是运费模板优化项目所产生的版本,从而导致了运算逻辑前后端的不一致,最终产生用户投诉。
当时联系了一位杭州的本地用户,远程协助到用户的电脑进行操作,通过他的IE9浏览器下载了立即订购页引用的merge-ump.js文件,发现此文件版本为merge-ump-new.js文件,而服务器上经过排查都已经是merge-ump-old.js的内容,从而表明服务器直接返回了304,我们下载的也只是浏览器本地的缓存。由于当时未能记录对应的Etag值,所以没办法考证。
最后,为了尽快解决问题,我们立即采用了空发一次未经改动的style应用,从而达到修改线上服务器文件的Last-Modified时间的目的。结果表明非常有效果,发布上线1分钟之后用户的下单操作就能成功了。所以,从以上推断可以表明以下两种可能,才能合理解释此次故障产生的原因。
1)style服务器并未验证Etag值;(经过确认,服务器并没有验证)
2)回滚操作并未修改对应的Etag值;
小结:
通过以上的现象分析以及问题的产生和解决过程来看,浏览器到服务器间的缓存协商控制大体分为两个方面:
1)服务器通过Response Headers告诉浏览器是否需要缓存该文件以及缓存多久,或者是否需要直接利用本地缓存文件。
2)浏览器根据上次请求缓存文件时接收到的Response Header信息来决定是否缓存,缓存多久,以及下次访问时是否需要发起HTTP请求来再次请求文件服务器,并让服务器来告诉浏览器是否可以利用本地缓存文件。
建议意见:
因此结合实际情况,应当具体问题具体分析。
1。动态的内容页面,实时性要求高的内容。应当利用Pragma和Cache-Control/Expires来禁止浏览器缓存。
2。对于图片等从来不会变更的静态文件,则应当通过设置较大的Cache-Control值(建议用这个值)或者Expires值来告诉浏览器直接用本地缓存而无需发送请求到服务器。从而减少HTTP请求,减小服务器的开销,提高性能,也能提高用户访问速度(因为浏览器同时并发请求URI是由限制的)。
3。对于style应用中的Css和JS文件这类静态文件,虽然变化不多,但是有时候又经常发布,需要及时生效。为了在及时更新文件内容和减小服务器压力两个方面达到一个平衡,需要浏览器缓存文件,但又不能太长,需要在新的发布之后能够及时的更新文件。最好的办法就是设置一个较小的Cache-Control值或者Expires值减少HTTP请求,当浏览器发出请求文件的Request Header信息时,充分利用Etag和Last-Modified时间来校验,决定是否返回新内容,从而保证服务器始终将会将服务器端最新的文件内容响应给用户,不管回滚还是发布阶段都应该及时更新Etag值和Last-Modified时间。与此同时,style应用的发布则应当选择在用户活跃度低的深夜时间段,从而减少浏览器缓存时间带来的可用性下降的问题。当然如果style服务器性能足够好或者不用担心压力的情况下,就不用设置浏览器缓存过期时间,每次直接访问服务器,将是否使用缓存内容的决定权交给服务器来处理。目前中文站还有一个平滑发布的方案,但是同样应当注意Etag和modified时间的值在回滚和发布中都应当有所更新才能防止同样的悲剧。
以上就是本次故障的一个详细分析,如果大家有什么不同的看法,或者对本文讲述的内容有所不同意见,欢迎批评指正。
参考文献列表:
【1】Hypertext Transfer Protocol — HTTP/1.1
【2】浏览器缓存机制
【3】【总结】浏览器的缓存机制