上次讲到lru与缓存重建,这次主要讲一下关于过期处理的一些主要问题。在讨论这个问题之前,有个相关的问题需要大家有所了解。就是对于一个缓存如期只来说,什么东西应该缓存,什么不应该缓存。这是一个比较复杂的问题,涉及到http协议的诸多细节。这里赵永明大哥写了一篇文章,讲得很详细,虽然是以ATS为背景讲的,但是思路是想通的,大家可以点击[这里](https://blog.zymlinux.net/index.php/archives/1090)去看一下,文章名字很骚气叫*“to
cache or not to cache,一直是个大问题”*。
在缓存服务器里,分hit和miss两种行为。前面的文章已经讲过了,服务器本地有缓存叫hit处理(*也会因为if-modified-since转成miss处理,这个后面讲*),无缓存是miss处理。过期处理自然发生的本地有相应文件的基础上,miss情况下根本没有校验文件过期与否的动作可言。这里我以我们cache服务器为例讲一下基本的过期处理流程,当然开源的squid,ats之类的在这块也是大同小异。
一个文件缓存与否,包括缓存多长时间,通常取决于取源返回的响应中关于缓存相关的一些头部,例如:Expires,Cache-Control,Pragma, Last-Modified,Etag, Age,Vary等。这些头的解释,大家可以去翻看协议,不想翻协议的,可以从[这里](http://www.cnblogs.com/_franky/archive/2011/11/23/2260109.html)找到一个总结,这里就不浪费口舌了。
但是有一种情况必须考虑,对于CDN来说,他们服务的客户运营水平参差不齐,很多都没有相关的响应头来告诉CDN厂商,他们想缓存多长时间。通常在这种情况下,CDN厂商有这样一些规则,要么缓存指定的一段时间,这个时间CDN厂商自己控制。要么针对不同的文件后缀,对缓存时间做更细致化的控制,这种情况就不再讨论了。我们接下来重点讨论有正规缓存头部的情况。
当请求到来,cache服务器在本地找到了相应的文件,这里所谓的“找到的文件”多数都是所谓的文件在内存中的索引结构,因为这个结构中通常包括对应磁盘文件的所有信息。为了方便讨论,我们这里暂且把这个索引结构叫做store。
首先store结构中有一个成员,我们暂且叫cache_time,它记录的是缓存服务器在最开始存储这个文件时的时间,通俗点说就是取源回来开始缓存这个文件,就把当前时间记录在cache_time这个成员里。也可以说这个时间认为是缓存开始在本地构建这个文件的时间。
在store结构中通常有一个成员来保存源响应头中Date字段对应的时间戳,我们暂且将这个成员叫做backend_date,这个头一般是后端服务器在发送响应时拿它的当前时间戳构建的。
为了方便讨论,我们把缓存服务器的当前时间称为current_time。
- 首先处理Cache-Control的相关头部。如果cache_time+ max-age < current_time,说明这个文件过期了。这里的max-age是Cache-Control中的一个字段。否则当前的验证说明未过期,需要进一步检查其他过期信息。
- 如果存在源响应带有Expires头,那么比较expires - backend_date与current_time - cache_time的大小。首先前者表示这个文件在源服务器还有多长时间过期,后者表示这个文件在本机创建,当现在已经过去了多长时间。如果前者大于后者,意味文件可以继续使用,未过期。当然这是基于第一点中max-age检查,未过期的基础之上的。如果前者小于后者呢?我们就认为是过期了,但是会出现这种情况吗?后者的时间差来自于本地服务器,我们假定是准确的,但是前者的时间来自于其他后端服务器,不一定准确,就可能出现这种情况。出现了这种情况,只能当做过期处理。我们也碰到过自己的机器时间不准了导致文件异常过期的bug。所以缓存服务器通常在启动时要确认机器时间是否准确,这点很重要。
在以上这里点检查通过之后,未标记为过期的,会进到后续的hit处理流程。过期的就需要取源。所以取源有两种,第一种是本地没有缓存该文件,另一种是文件已经缓存过,但是现在过期了。过期回源是,涉及到回源验证的问题。什么是回源验证?当一个文件过期了,你回源重新取的时候,可能这个文件在源服务器上并没有改动过,那么这时缓存服务器再取一遍是很傻,也是浪费带宽,降低性能的事情。这个时候,在store结构中有个成员往往保存了一个时间戳,我们暂且叫做mod_time,这个时间戳来自于最开始构建文件时,源给的响应中的Last-Modified头,这个头中的时间告诉了我们,在我们取源获取这个文件时,该文件在源服务器最后一次修改的时间。那么我们在回源验证时,将这个mod_time放到我们取源构建的请求头的If-Modified-Since中,源收到这个头之后,就会去查看文件在这个时间以后,有没有被修改过,如果修改过,那么源通过回应304响应,告知下游文件未发送变化,文件可以重用。否则,就通过200响应,发送最新文件。
一般的情况,缓存服务器都会把响应头跟响应体保存在一个文件中。接下来作为hit处理的后续流程,往往需要先去读取保存文件中的响应头,其中我们需要关心的是Last-Modified头。因为客户端可能携带着If-Modified-Since头,我们需要对比两者是否相等,如果相等,那么我们认为文件在客户端询问的时候之后没有改变过,我们给他304响应。这里有两个问题:
1. 为什么不把Last-Modified头放在store结构中,这样我们就不需要去读文件来获取这个头了。
> 因为对于缓存服务器一般的webserver,它给出的响应必须忠于源给出的响应,而很少有权利自己打包响应头,除非本机发生像5xx等这类情况,所以既然一定要读文件,就没有必要单独拿出来了。其次store结构作为每个文件的索引,会占用大量的内存。所以减少哪怕一个成员,在大量文件存储时,也能节省不少空间。
2. 如果客户端的If-Modified-Since跟我们的Last-Modified不相等,而是大于或者小于,该怎么处理?
> 如果前者小于后者,毫无疑问应该回应200,因为我们的最后修改时间比客户端问的时间要新。如果大于呢?我们是否应该去回源验证呢?这种情况我们之间也直接使用本地文件响应200处理。很多人认为这样处理不太合理,毕竟你作为缓存代理,无法知道在*“未来”*的时间点上,源有没有改动过文件。但是如果你去做回源验证,别人可能因此攻击你,每次都发带有If-Modified-Since将来很长时间的一个时间点,让你的缓存产生回源,极大的消耗你的性能,因为作为cache,减少回源是最核心的功能。CDN厂商之间的相互攻击早已不是什么新鲜事,我们都是交过学费的。