MyException - 我的异常网
当前位置:我的异常网» 云计算 » 缓存服务器设计与兑现(三)

缓存服务器设计与兑现(三)

www.MyException.Cn  网友分享于:2015-08-26  浏览:5次
缓存服务器设计与实现(三)
        这里我们讨论一个比较重要功能,在之前的文章中提到过,取源合并。为什么要单独把它拿出来讨论呢?其实主要是出于个人工作的角度。之前公司里的cache需要这样一个功能,现有的squid该功能不完善,并且也不太适合我们的业务。然后我们分别在cache和nginx上加了这个功能,不过现在的nginx版本已经原生支持了。呵呵,您也许不知道我们的nginx可是0.7.x的版本,我们当时开发的时候还在想,也许不久官方能做支持,可是谁知道这个不久是多久呢。

        进入正题吧,看看nginx如何去设计和实现的。

        这里我们把取源合并成为fetch merged。在默认情况下,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 theproxy_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.

        如果开启的话,在ngx_http_cache_t结构中有个成员叫lock,它会被置1。这个结构前面已经提过了,在一个cache中,总要有一个结构来沟通请求与其对应的缓存对象。同样的,在nginx中就是通过该结构。

        让我们想想流程。对象在取源的时候,其相应的控制结构已经存在,并且在系统中也已经可见。也就是说在一个请求正在取源的过程中,如果后续的同一请求再次到来,那么必然会找到这个对象。显然此时的对象跟正常的是有区别的,在cache中我们通常用pending和ok状态来区分两者。nginx并没有通过这些状态来处理,当一个对象未缓存完整时,一个名为updating的成员会置1,同时exists成员置0。这两个成员在整个缓存过程中起到了很重要的作用,不仅仅是fetch merged。如果哪位同学要去翻阅代码,那这两个成员的运用一定要注意。

        在开启fetch merged的时候,nginx会让后续的请求等待一段时间(默认是5秒)。在这段时间内,nginx每500ms去检查缓存是否正常结束(updating是否被置0),如果结束了,那么就会在后面的处理流程中发现已经缓存好的文件,即可hit了。如果时间挺长,过了等待的最长时间,那么针对该缓存文件的fetch merged机制就会cancel,这样后续的请求就会跟普通情况下一样,继续去后端fetch文件了。当然这里的cancel处理不会影响其他文件的fetch merged。

        上面说过在fetch merged中,默认的等待时间是5秒,这个时间是可以通过配置来控制的。即proxy_cache_lock_timeout,具体使用方法可以去官方wiki查看。聊到这里,各位可能看出来了。nginx这个东西做的有点鸡肋,因为它为了减轻后端的压力,却给客户端造成了延迟。特别是对于大文件取源时间长,一来你要让客户端去等,二来等待时间过去之后,又要去取源。我认为很多公司可能对nginx这个功能都不太认可,特别是CDN公司。一般公认的比较好的处理方式就是一边接收一边分发,这样子很合理。不过在处理起来有些麻烦,接下来的时间,就跟大家一起来讨论下这个方案吧。

        首先需要第一个请求去后端取数据,在这期间,相同请求过来的时候,需要有种机制来发现那个正在进行取源的请求,进而来完成合并。nginx用的是rbtree,所有的缓存对象都以有一个node的形式登记在树中(关于这点,我了解的比较多的情况是用hash表,那种使用冲突链表的实现)。找到取源的对象才是第一步,后面的步骤就是要通过这个请求来做数据分发,最简单的方式就是这个request的结构中添加一个队列,需要合并的请求挂载到上面去,然后取回来的数据分发给队列上的每个请求。但是问题并不这么简单:当这个取源的请求取源结束,一般的处理是要释放该请求,那么我们需要在释放请求之前保证数据分发完,或者通过其他的机制来让该请求释放之后,能够完成后续的分发工作。对于前者,我们不得不在request结构体中增加引用计数,或者其他的计数方式。只有计数为0,才意味着分发工作,请求可以释放了。这种处理是可以的,但是如果你是在原来的系统上搞二次开发,那么你需要在暴多的地方来添加计数检查的代码,那真是梦魇。我们用的是后者,具体地,是将需要合并的请求挂载一个全局的hash表,需要合并的请求会在表中添加一个条目,我们不妨称为一个node,这个node中管理了一个队列,上面管理着同一个文件的所有合并请求。我们需要做的就是让这个node在数据完全分发完之后,才会被销毁。这点的处理不像那种request中添加计数那样,相对来说代码耦合小很多,而仅仅多了一个hash表的内存消耗。

        分发动作其实很简单。首先的准备工作就是在request中添加一些成员,标记已发多少,下次需要发多少之类的。在事件方面的处理上,客户端关闭的事件,通过注册读事件处理函数就可以发现。对于写,当取源正在进行时,我们不需要设置什么处理函数,此时的发送依赖于取源请求的分发。而取源请求的分发本身依赖于读写的触发,所以当取源结束时,也就不会有事件来驱动分发了。这也好处理,当取源完成以后做最后一次分发的时候,给每个合并的请求设置新的读写事件处理函数,这样一来,后续的分发就可以通过读写事件来驱动,并且大家也有缓存完好的文件可用。当然具体实现方法有很多,但是思路一般就是这样子了。
那么在这种模型下,怎么合理正确的处理异常呢?我们的处理遵循简单粗暴的原则:如果客户端关了,那么没啥好说的,直接将它从队列里踢掉就行了。如果取源的连接出现了问题,则依次关闭队列上的各个请求连接。这里其实不需要考虑什么更友好的处理方式,因为源宕机一般不能瞬间恢复,给这些合并的请求找个“后妈”,往往也于事无补。不过像nginx这种有负载均衡机制的,可以去尝试找个“后妈”。

        这里提一下关于多进程取源合并的两种实现,第一种是进程内合并,另一种是进程间合并。对于进程内的,合并请求node使用的hash表是进程内的,所以同一文件会有最大进程个数数量的并发连接取源,这个并发量是完全可以接受的。如果我们将这个hash放到共享内存中,就有希望实现进程间完美的合并。但是仅仅这样就可以了吗?进程间合并是个棘手的事。首当其冲的问题就是谁负责分发?进程内合并,取源请求可以肩负起分发的重任,因为队列上的请求都是属于本进程的。如果我们将hash表及其队列放到了共享内存,分发的时候就要区分哪些是属于自己进程的。哪些不属于自己进程的呢?对于基于磁盘文件的cache,相对好处理一些。因为各个进程内的请求都可以在每次有可写事件时,去磁盘上查看是否有新内容,从而将数据发送出去。万一没有新数据的话,又要重新添加事件,这样每次epoll_wait都会报很多事件。这仅仅一种实现。

        再看全内存cache,它的文件内容全部放在内存中。那么通过查看磁盘文件来发送数据的路彻底没戏了,那么内容如何分发,换句话说,除了取源请求所在的进程,其他进程中的合并请求如何被告知,何时有新内容要发送?之前我们讨论过一个方案,就是这些请求设置一些定时器,过一段时间去查看内容是否有更新,但这样跟nginx的实现类似了,由于定时器的存在,客户端增加了延迟。加事件的方式前面讨论了,也不行。所以无论磁盘缓存还是内存缓存,通知何时可分发是个关键问题,也是最棘手的问题。我们的目标就是要让其他进程异步得到通知,而不是忙等(定时器和事件在这里就是),所以信号貌似可以:取源的进程向其他进程发送信号,告诉他们有内容更新了。收到信号的进程在信号处理函数中完成数据的分发,但是这样做会导致系统内信号满天飞,加上信号处理函数的特殊性,这种方案的可行性有待商榷。我们的程序经过调研之后还是选择了进程内的合并,目前看来完全满足需求。nginx虽然解决了进程间合并的这个问题,但是它以客户端的的延迟做代价,这点在我们的CDN业务上是难以接受的。

        所以既不过多影响性能,有能实时异步的实现进程间合并,还是有很多问题要解决的。这里留着跟大家一起讨论吧。

文章评论

程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
漫画:程序员的工作
漫画:程序员的工作
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
总结2014中国互联网十大段子
总结2014中国互联网十大段子
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
代码女神横空出世
代码女神横空出世
老程序员的下场
老程序员的下场
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
一个程序员的时间管理
一个程序员的时间管理
那些争议最大的编程观点
那些争议最大的编程观点
那些性感的让人尖叫的程序员
那些性感的让人尖叫的程序员
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
程序员的鄙视链
程序员的鄙视链
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
旅行,写作,编程
旅行,写作,编程
我是如何打败拖延症的
我是如何打败拖延症的
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
程序员必看的十大电影
程序员必看的十大电影
 程序员的样子
程序员的样子
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
10个调试和排错的小建议
10个调试和排错的小建议
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
编程语言是女人
编程语言是女人
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
Java程序员必看电影
Java程序员必看电影
为什么程序员都是夜猫子
为什么程序员都是夜猫子
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
中美印日四国程序员比较
中美印日四国程序员比较
程序员和编码员之间的区别
程序员和编码员之间的区别
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
如何成为一名黑客
如何成为一名黑客
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有