上次讲到lru与缓存重建,这次主要讲一下关于过期处理的一些主要问题。
在讨论这个问题之前,有个相关的问题须要大家有所了解。
就是对于一个缓存如期仅仅来说,什么东西应该缓存,什么不应该缓存。这是一个比較复杂的问题。涉及到http协议的诸多细节。
这里赵永明大哥写了一篇文章。讲得非常具体,尽管是以ATS为背景讲的,可是思路是想通的,大家能够点击这里去看一下,文章名字非常骚气叫“to
cache or not to cache,一直是个大问题”。
在缓存server里。分hit和miss两种行为。前面的文章已经讲过了。server本地有缓存叫hit处理(也会由于if-modified-since转成miss处理。这个后面讲),无缓存是miss处理。过期处理自然发生的本地有对应文件的基础上,miss情况下根本没有校验文件过期与否的动作可言。
这里我以我们cacheserver为例讲一下主要的过期处理流程,当然开源的squid,ats之类的在这块也是大同小异。
一个文件缓存与否。包含缓存多长时间。通常取决于取源返回的响应中关于缓存相关的一些头部,比如:Expires,Cache-Control,Pragma, Last-Modified,Etag, Age。Vary等。这些头的解释。大家能够去翻看协议,不想翻协议的,能够从这里找到一个总结,这里就不浪费口舌了。
可是有一种情况必须考虑,对于CDN来说,他们服务的客户运营水平參差不齐,非常多都没有相关的响应头来告诉CDN厂商,他们想缓存多长时间。通常在这样的情况下,CDN厂商有这样一些规则。要么缓存指定的一段时间,这个时间CDN厂商自己控制。要么针对不同的文件后缀。对缓存时间做更仔细化的控制,这样的情况就不再讨论了。我们接下来重点讨论有正规缓存头部的情况。
当请求到来,cacheserver在本地找到了相应的文件,这里所谓的“找到的文件”多数都是所谓的文件在内存中的索引结构,由于这个结构中通常包含相应磁盘文件的全部信息。
为了方便讨论,我们这里暂且把这个索引结构叫做store。
首先store结构中有一个成员。我们暂且叫cache_time,它记录的是缓存server在最開始存储这个文件时的时间,通俗点说就是取源回来開始缓存这个文件,就把当前时间记录在cache_time这个成员里。也能够说这个时间觉得是缓存開始在本地构建这个文件的时间。
在store结构中通常有一个成员来保存源响应头中Date字段相应的时间戳,我们暂且将这个成员叫做backend_date。这个头通常是后端server在发送响应时拿它的当前时间戳构建的。
为了方便讨论,我们把缓存server的当前时间称为current_time。
- 首先处理Cache-Control的相关头部。假设cache_time+ max-age < current_time,说明这个文件过期了。这里的max-age是Cache-Control中的一个字段。
否则当前的验证说明未过期,须要进一步检查其它过期信息。
- 假设存在源响应带有Expires头,那么比較expires - backend_date与current_time - cache_time的大小。
首先前者表示这个文件在源server还有多长时间过期,后者表示这个文件在本机创建,当如今已经过去了多长时间。
假设前者大于后者,意味文件能够继续使用,未过期。当然这是基于第一点中max-age检查。未过期的基础之上的。假设前者小于后者呢?我们就觉得是过期了。可是会出现这样的情况吗?后者的时间差来自于本地server,我们假定是准确的,可是前者的时间来自于其它后端server,不一定准确。就可能出现这样的情况。
出现了这样的情况,仅仅能当做过期处理。我们也碰到过自己的机器时间不准了导致文件异常过期的bug。
所以缓存server通常在启动时要确认机器时间是否准确,这点非常重要。
在以上这里点检查通过之后,未标记为过期的,会进到兴许的hit处理流程。过期的就须要取源。
所以取源有两种,第一种是本地没有缓存该文件,还有一种是文件已经缓存过,可是如今过期了。过期回源是,涉及到回源验证的问题。什么是回源验证?当一个文件过期了,你回源又一次取的时候,可能这个文件在源server上并没有改动过,那么这时缓存server再取一遍是非常傻,也是浪费带宽。减少性能的事情。
这个时候,在store结构中有个成员往往保存了一个时间戳,我们暂且叫做mod_time。这个时间戳来自于最開始构建文件时。源给的响应中的Last-Modified头,这个头中的时间告诉了我们。在我们取源获取这个文件时,该文件在源server最后一次改动的时间。
那么我们在回源验证时,将这个mod_time放到我们取源构建的请求头的If-Modified-Since中,源收到这个头之后。就会去查看文件在这个时间以后。有没有被改动过。假设改动过。那么源通过回应304响应。告知下游文件未发送变化,文件能够重用。否则。就通过200响应。发送最新文件。
一般的情况,缓存server都会把响应头跟响应体保存在一个文件里。接下来作为hit处理的兴许流程,往往须要先去读取保存文件里的响应头。当中我们须要关心的是Last-Modified头。由于client可能携带着If-Modified-Since头。我们须要对照两者是否相等,假设相等,那么我们觉得文件在client询问的时候之后没有改变过,我们给他304响应。这里有两个问题:
- 为什么不把Last-Modified头放在store结构中,这样我们就不须要去读文件来获取这个头了。
由于不同于一般的webserver。cache服务器给出的正常响应必须忠于源给出的响应,而非常少有权利自己打包响应头。除非本机发生像5xx等这类情况,所以既然一定要读文件。就没有必要单独拿出来了。其次store结构作为每一个文件的索引。会占用大量的内存。所以降低哪怕一个成员,在大量文件存储时。也能节省不少空间。
- 假设client的If-Modified-Since跟我们的Last-Modified不相等,而是大于或者小于,该怎么处理?
假设前者小于后者,毫无疑问应该回应200,由于我们的最后修改时间比client问的时间要新。
假设大于呢?我们是否应该去回源验证呢?这样的情况我们直接使用本地文件响应200处理。非常多人觉得这样处理不太合理,毕竟你作为缓存代理。无法知道在“未来”的时间点上。源有没有修改过文件。可是假设你去做回源验证,别人可能因此攻击你。每次都发带有If-Modified-Since将来非常长时间的一个时间点,让你的缓存产生回源,极大的消耗你的性能,由于作为cache,降低回源是最核心的功能。CDN厂商之间的相互攻击早已不是什么新奇事。我们都是交过学费的。
上述提到的这些仅仅是一种最简单的情景。即client -- cache -- origin。缓存直接跟源站交互。
可是假设cache是分级的,比方client -- cache1 -- cache2 -- origin这样的情况。假设我们cache2对origin给的响应头不做不论什么处理直接转发给cache1。那么对于cache1来说,他收到的结果跟cache2感知的是一样的,那么原来cache2将某文件缓存了多长时间,到什么时间过期这些信息。在cache1这边看来是一样的。这样就会出现故障。比方origin告诉cache2。最大缓存时间max-age为1个小时。那么cache2在缓存了半个小时的时候。收到了cache1的请求,这时cache2将该文件发送给它,那么对于cache1来说,它也会把文件缓存一个小时。
这样一个本应该在CDN中缓存1小时的文件。却被缓存了至少(1小时+半小时)长的时间。
对于使用了CDN的origin来说。无论CDN服务商的cache是几级,架构是什么样。1小时的缓存时长是绝对的,不同意被CDN放大。
这样的情况下CDN必须採取一定的措施来规避这个问题。
普通情况下origin在响应头中携带的Expires。Date这些跟缓存时间相关的头是不同意改动的。除非origin有些个性化的需求。主动告知CDN去做哪些改动。事实上这是主要的规则。特别是CDN在做反向代理的时候,应该对client来说是透明的,不能说请求经过CDN和不经过,origin的某些原始响应头会发生变化。那怎么办?最主要的就是使用Age这个头。只是这个头在CDN里用的比較少。由于这个头是在HTTP1.1里才支持的,对于现阶段网络上大量HTTP1.0的请求来说。这个头就没法用了。所以CDN往往干脆自己加一个私有的头来替代Age的作用。Age事实上就是记录一个文件在一个server上从产生到如今,经过了多长时间,所以字如其名。“年龄”的意思。除了这个头这外还须要另外一个相对的max-age。
什么叫相对的max-age?我们能够觉得origin给出的max-age是CDN系统内存活的绝对时间。而在一个多级cache的缓存系统里,当前cache中文件能存活的最大时间。应该是绝对的max-age时间减掉在上级(多层)cache已经存活过的时间,得到的差值就能够叫做相对max-age。
?一般CDN自定义的头通常都使用X-xxx这种形式。所以这里我们就把相对的max-age时间用X-max-age来表示。Age时间用X-age来表示。origin给max-age用Max-Age。这样每层cache在发送响应头的时候应该携带X-max-age。即X-max-age(要发给下级的)=X-max-age(上级给的) - X-age。在这个公式里,假设上级没有给X-max-age,那么意味这当前cache的上级是origin。那么此时等式变为
X-max-age = Max-Age(origin给的) - X-age。至于X-age的计算就非常easy了。用前面提到current_time - cache_time就可以。
这里须要注意一点。假设cache被杀掉重新启动。那么X-max-age,X-age这些信息应该是延续的,而不能重置。所以这就要求cache的设计者把这些信息持久化,仅仅是简单的留在内存中是不行的。
?