我们现在讨论算是最简单的情景,即服务器还没有文件缓存,第一个需要缓存的请求的处理过程。当然需要关注的情景有很多,一个一个来吧。
在缓存服务器设计与实现(一)中讨论的都是一些准备工作,我们接下来要关注从后端机器取回数据以后进行缓存的情景。首先来探讨一个问题,以nginx为例,它是在取后端数据之前就创建了缓存对象,那么从整个系统的角度来看,创建缓存对象的过程包括在内存中建立相应的控制结构,并且在磁盘上创建实体(文件的形式)。那么我们需要关注的是这两部分都有些什么成分?先看磁盘上的文件,它应该存什么。存储实际文件内容是必然的,这就够了吗?
我们知道作为一个http缓存系统,首先它是一个完整的http服务器。所以在响应一个客户端的请求时,必须先给出一个http响应头,然后才是内容。当nginx作为一个静态http服务器工作的时候,响应头是nginx自己构造的,想怎么搞都可以。但是当它作为一个缓存服务器使用时,响应头应该尽量跟被代理的后端服务器一致,甚至严格一致。那么此时,响应头神马的就不能自己杜撰了。从这个角度上来讲把响应头头跟文件内容同时缓存起来是合适,也是必要的。你可以尝试打开一个nginx缓存好的文件,就会发现在具体内容之前确实是保存着相应的响应头的。不过在文件的最开始(响应头之前)貌似还有一些东西,关于这些神秘的东西,随着我们的讨论,都会搞清楚的。其实几乎所有的http缓存服务器都是这么做的,即将http响应头和内容都缓存在文件中。
刚才我们关注的是缓存对象在磁盘上的内容组织,下面我们再看一下内存中的控制结构。
在nginx中每个文件都在内存中都有相应的控制结构,称为一个node。这个结构是在共享内存中申请和管理的,为什么用共享内存?nginx作为多进程模型,我们希望在worker A中缓存的文件对象,在worker B中相同请求到来时也能够hit该对象,要不然就太废了。当然互斥也是不可避免的。
在nginx具体实现中,这个node是结构体ngx_http_file_cache_node_t。关于这个结构中各个成员的说明,可以参考,这里就不多说了。当一个node首次创建之后,需要放入到系统的缓存管理体系中,nginx用到的是红黑树,所有的node都被插入到树里面,然后还要放到lru队列中,作用就是在存储空间不够的时候,通过lru来删掉一些对象。我们的cache在lru方面跟nginx是一致的,但是所有node是通过hash表来管理的。
其实细节需要讨论的东西实在是太多。现在还是先转到重点上来,来看一下nginx如何将收到的后端数据写到本地磁盘文件里去。
核心思想是这样的,nginx首先会将数据写到一个临时文件中去,然后内容收完之后,再将这个临时文件rename到实际的目标文件。这是主要框架,至于如何管理临时文件,又该如何去实现,nginx有它的处理,我们自己实现一套也可以,这都是一些无关紧要的细节了。重点的问题其实是要维护我们的缓存系统中有关当前缓存文件的状态,缓存过程是OK的,该怎么处理,出现异常之后,又该如何处理。
先看当这个文件缓存ok的时候,该如何更新缓存信息,来声明文件已经缓存完毕,可供使用了。这里还是以nginx为例:
在文件获取完成之前,缓存对象的node节点结构中exists成员一直为0,当缓存正常结束时exists就会被置1。所以这个成员的含义就很明确了。这里注意的是,缓存对象的node节点信息位于共享内存中红黑树中,是唯一的。而每个request通过一个cache成员,来跟这个node发生联系。
现在有一个相同的请求过来,首先要做的事情就是查找是否有已经缓存的目标文件。如果文件存在,一般需要先将文件开始部分的一些控制和管理信息读取出来,通过分析这些信息来判断该文件是否是可用的,如未过期或者完好等等。如果可用,那么剩下的工作就是发送内容了,不过在实际发送之前,一般需要先将读取出来的响应头做一些header filter的处理等等,后面的文件内容一般直接发送就好了。
先做一个阶段性的总结吧。到目前为止,我们看到的过程包括:第一个请求到来->去后端取数据缓存;同样的请求到来->发现了刚刚创建的文件->文件可用->发送这个文件。从nginx实现上看,它的ngx_http_file_cache_node_t结构中有两个成员需要注意,一个是count,表示当前正在使用这个node的请求数,另一个成员uses记录了到目前为止,这个node被访问的次数,这个值是一直累加的。
如果一个文件过期了,此时当有请求再次访问到这个文件时,该如何处理呢?这里应该提一下跟cache相关的管理进程(后面会拿出篇幅来重点讨论他们),这其中有一个非常重要的后台程序,它跟普通的worker进程一样,是由master进程fork出来的。ps命令看到的进程title一般为:“nginx: cache manager process”,它的一个重要的作用就是监控过期,做lru等工作。我们把这个manager进程发现过期的情景称为主动发现,worker进程在处理请求时也会发现某个对象过期,这个称为被动发现。能做到这些,它的基础就在于管理缓存对象的控制结构及其信息是由master进程通过共享内存创建并初始化的,这样manager和worker在被fork之后,这些信息就是共享的了。manager进程在运作时,从lru队列的尾部开始,检查是否有文件过期。有过期的就删掉。至于为什么从lru队列的尾部检查,是因为在worker处理对象的时候,每次hit一个文件(一个正常的文件必然位于lru队列中),该文件就从队列中删掉,然后插到队列头部去,所以越靠近尾部的对象,越是较长时间没有被访问到的,LRU的思想也在于此。当然一直以来很多人都在批评这种处理,说它不科学不严谨。是的,这点没错。网上有很多更高级更严谨的lru理论及实现,但是在实际应用中我们不能死磕理论,往往需要结合系统复杂度,实现难易等各方面的考虑。我们自己的cache,包括squid,nginx都是用的这种最简单处理方式,而事实也表明:运行表现不错。
貌似扯远了。我们现在重点关注当worker进程发现了一个对象过期时,它会如何去处理。首先一个问题就是进程如何发现一个文件时过期的?其实在一个文件的开头部分,存放了一些有关文件的控制头信息,前面已经提过了。这个头信息中有相关的变量标记这个对象保鲜时间,所以只需要读出这个变量跟当前时间比较一下就可以了。
从系统的控制结构来看,有个名叫updating的变量此时会被置1。后续的处理就很显然,就是去后端取新文件了。注意此时这个文件相关的管理结构还未被从系统里释放,所以后续的请求还是会hit,不过后面的处理还是会发现过期,这样只要一个请求在更新完成之前,对于该文件的请求,都会去后端取文件(即透传)。前面我们分析过了,nginx会先用临时文件来保存数据,完整取完之后会rename到缓存目录下去。那么当文件过期,但此时有又同一并发请求,那么最后谁去rename呢?说得这里就不得不提另外一个rename,那就是针对一个文件的首次并发请求,各个请求都是独立取源,最后也会出现同时rename的情况。呵呵,看一篇文章吧:
http://www.ibm.com/developerworks/cn/linux/l-cn-fsmeta/
上面讲到nginx会出现并发取源的情况,很多公司对这块进行了定制。最常提到的是所谓取源合并。顾名思义,就是合并回源的请求。这项功能,nginx在比较新的版本里面已经支持了,使用的指令时proxy_cache_lock。官方wiki给出的说明:
syntax: proxy_cache_lock on | off;
default: proxy_cache_lock off;
context: http, server, location
This directive appeared in version 1.1.12.
When enabled, only one request at a time will be allowed to populate a new cache element identified according to the proxy_cache_key directive by passing a request to a proxied server. Other requests of the same cache element will either wait for a response to appear in the cache, or the cache lock for this element to be released, up to the time set by the proxy_cache_lock_timeout directive.
另外一个地方就是当文件过期时,也会产生大量的并发回源量。这点nginx也做了处理,很多公司也对这块做了自己的定制。nginx通过指令proxy_cache_use_stale来控制在文件过期更新过程的回源请求量,让当一个请求在更新文件时,其他请求则暂时使用过期文件。具体配置为proxy_cache_use_stale updating;关于该指令的具体用法,大家可以去官网查阅。
还有一些机制,后面再接着讨论。
阅读(1384) | 评论(0) | 转发(0) |