半同步-半异步模式谈服务器的设计

原文:http://www.cppblog.com/converse/archive/2009/11/22/101623.html

半同步-半异步模式,最早应该是由ACE的作者提出,原文在这里.

简而言之,所谓的半同步半异步模式分为三个组成模块:同步处理模块,队列模块,异步处理模块.三个模块之间的交互关系如图:


(注:上图出自这里)
几个模块的之间的交互为:异步模块接收可能会异步到来的各种事件(I/O,信号等),然后将它们放入队列中,而同步模块一般只有一种动作,就是不停的从队列中取出消息进行处理.

半同步-半异步模式的出现是为了给服务器的功能进行划分,尽可能将的可能阻塞的操作放在同步模块中,这样不会影响到异步模块的处理.
举个例子说明.
假设现在有一个服务器,在接收完客户端请求之后会去数据库查询,这个查询可能会很慢.这时,如果还是采用的把接收客户端的连接和处理客户端的请求(在这里这个处理就是查询数据库)放在一个模块中来处理,很可能将会有很多连接的处理响应非常慢.
此时,考虑使用半同步半异步的模式,开一个进程,使用多路复用IO(如epoll/select)等监听客户端的连接,接收到新的连接请求之后就将这些请求存放到通过某种IPC方式实现的消息队列中,同时,还有N个处理进程,它们所做的工作就是不停的从消息队列中取出消息进行处理.这样的划分,将接收客户端请求和处理客户端请求划分为不同的模块,相互之间的通过IPC进行通讯,将对彼此功能的影响限制到最小.

然后,不是每种请求下都适合使用半同步半异步模式的.

我之前深入阅读过ligty的代码,它的设计是monitor+worker多进程 + 多路复用IO + 状态机的架构.也就是说,每个worker进程负责接收客户端连接和处理客户端连接的全过程,每个过程都会记录一个状态,比如现在在接收包头,如果这次的接收不是因为连接关闭的原因导致的接收错误,那么就将这个客户端的fd放入多路复用IO中,等待着下一次根据这次保存的状态进入状态机中进行处理.

简单的说,在ligty中,一个worker子进程全权负责了接收和处理的全过程,并没有按照上面半同步半异步的划分来设计.

再后来,我大概看过一些nginx的代码,细节之处可能不一样,但是就服务器总体的架构而言,是与ligty的设计差不多的.

这两个服务器是目前比较快的web服务器了,没有采用多么复杂的模式.

那么为什么对于web服务器而言,不需要使用半同步半异步也可以达到非常高的效率呢?我想,这与服务器的业务有关.对于web服务器而言,大部分的时间都花在了IO处理上,比如监听服务器端口,接收客户端连接,根据客户端的请求发送文本文件内容到客户端去,这里的操作,基本上没有太可能会造成阻塞的地方,也就是说,处理完成一个客户端请求的全过程对web服务器而言是非常快的.

所以,要回答这个问题,需要看具体的业务需求.打个比方,如果处理一个客户端请求需要10s,那么完全有一个模块全部处理不是一个很好的设计;反之,如果处理一个请求只需要10ms,而进程/线程间的切换就需要1s了,还将模块进行划分就不必了.

另外,回到半同步半异步模式的具体实现上,可以使用线程或者进程,而队列层则可以使用不同的IPC方式,有很多关于多线程多进程孰优孰劣的争论,由于我没有太多多线程的编程经验,也就不在这里进一步说明了.


http://www.cppblog.com/converse/archive/2010/03/11/109449.html

memcached采用的网络模型

memcached采用的网络模型是早前提到的半同步半异步的网络模型.

简单的说,大致流程就是:主线程负责接收新的连接,接收到新的连接之后,选择一个worker副线程,将该新连接push到副线程的连接队列中.主副线程之间通过管道进行通讯,因此主线程将新的连接push到工作线程之后,主线程要向该副线程的管道中写一个字符,而每个副线程也都有自己的poll set, 其中会包含自己的管道fd, 副线程也会通过多路复用I/O来监控管道的情况,一旦可读,说明有新的连接到来,此时从连接队列中取出新连接,将其fd加入到自身的poll set中,最后对该连接的业务逻辑处理也全都在该副线程中进行(读数据,处理,发送回应等).

这个模型有以下的好处:
1) 接收操作只在主循环中处理,因此不会出现惊群现象.
2) 主副线程分工明确, 主线程仅负责I/O, 副线程负责业务逻辑处理.我认为这个可以抽象出来作为一般服务器的网络I/O架构, 以后要使用的时候只需要将业务逻辑处理函数传递进行就好了.简单的说,就是主线程负责接客,副线程负责服务.
3) 多个副线程之间不会有影响.因为大家都有各自独立的连接队列.主线程在新连接到来的时候是如何选择处理副线程的呢?很简单,有一个计数器last_thread, 每次将last_thread加一,再模线程数来选择线程ID.

缺点是:
假如业务逻辑是类似于web服务器之类的, 那么一个简单的请求也需要这个比较繁琐的操作的话(最重要的是,很可能一个进程就能处理完的事情,非得从一个线程接收再到另一个线程去处理), 那么显然代价是不值得的.所以说,所谓的服务器网络模型的选择, 其实没有一套通吃的方案, 还是按照具体的业务逻辑具体来分析吧.

需要补充的是,主副线程之间相互通信采用的管道,现在新版的linux内核已经提供一种新的API:eventfd(),简单的说,有以下好处:1)管道需要分配两个fd,一个读一个写,而eventfd一个fd就搞定了. 2) 管道需要不定长的缓冲区,往里面写数据才能通知读一端有数据到来,而eventfd现在可以使用定长的数据了. 3) 最后,听说eventfd性能上比管道要好,这个没有做过测试了.反正, 对于简单的类似上面分析的那样通知机制, 用管道似乎太"重量级"了一点.

eventfd的man page在此.

你可能感兴趣的:(thread,多线程,网络,服务器,web服务,linux内核)