论坛首页 综合技术论坛

SPServer : 一个基于线程池(包括HAHS和LF)的高并发 server 框架

浏览 57733 次
该帖已经被评为精华帖
作者 正文
   发表时间:2007-05-22  
aio在linux下的实现采用NPTL应当跟windows的iocp差不多了,都是用用户线程池来模拟的。linux下比较高效的并发模式还是采用epoll,aio只是个posix标准,在linux下没有真正实现。不过即便如此,epoll的并发能力比windows的iocp也要高。
0 请登录后投票
   发表时间:2007-05-22  
有了 epoll 和 iocp 之后,要做到高并发已经有了很好的基础了。但是真正要做好一个高并发的服务器却不是一件容易的事。

因为 epoll 和 iocp 只是提供了一个高效的事件通知机制,对于实现者来说,就是可以同时处理很多的句柄(比如 Socket,File等)而不用担心操作系统在事件通知上的消耗。

但同时我们可以看到,直接使用 epoll 或者 iocp ,要求是要直接使用底层的句柄,对于一些已经封装过的 api (比如 mysql client api),是没有办法使用 epoll 或者 iocp 来处理的。如果在高并发服务器中,用到了这些 api 的话,那么就需要做一些特殊的处理,否则就会由于这些 api 会阻塞,造成整个服务器阻塞。

fredzhang 写道
不过即便如此,epoll的并发能力比windows的iocp也要高。

在没有任何背景的情况下,这样说有点不合适。如果说服务器是一个什么都不做的,或者只是做一个简单的 echo server 的话,那么 epoll 可能比 iocp 的并发高。但是这样的比较没什么意义。
如果这个服务器要做一些很具体的业务处理,那么高并发的的瓶颈往往已经不在 epoll 或者 iocp 上,而是在怎么处理具体的业务上。包括怎样集成那些会造成 block 的 api ,怎么使用更好的算法,等等。
即使以 memcached 为例,估计用 epoll 或者 iocp 都能支持到同样的并发度。因为在并发数不断升高的时候, memcached 的瓶颈并不在 epoll 或者 iocp 的调度上,而是在于网络的传输速度和 hash table 的查找。即在达到能够区分出 epoll 和 iocp 谁的并发度更高的时刻之前,memcached 就已经因为其他的瓶颈而无法再支持更高的并发。
0 请登录后投票
   发表时间:2007-06-07  
不错,我感觉稍稍重了一点,实现一个Message Decoder要写的代码稍稍多了些。另外有没有考虑使用libevent的bufferevent来做?我最近也在做空上,参照twisted的接口来做的,不过包装得比较浅。你的msgqueue对我非常有帮助,原来正为libevent 多线程困扰呢,把任务放到队列容易,要在完成时得到通知就比较麻烦,刚好看到你的实现。另外,没考虑过跨平台吗?
0 请登录后投票
   发表时间:2007-06-07  
另一个问题,没考虑监听多个端口?多种协议?我目前实现的参照twisted接口,已经把多个端口、协议考虑在内,目前只实现了基本的单线程reactor,执行线程池还在计划之中。

另外还可以考虑一下connector的实现,twisted里面有些思想非常好,比如解析host也是异步的,这在libevent里面也有,我有时候还想是不是这2个不同语言的库也相互参照呢?
0 请登录后投票
   发表时间:2007-06-07  
补充一下,说decoder写代码比较多,实际上也因为状态机的解析方式,虽然高效,写出来感觉并不好看,当然有些协议也只能这么写。我在自己做协议时喜欢用长度前缀,先确定长度字节,收齐了这几字节就解出长度,再判断是否收齐剩下的,直接交给线程池就行了,中间只有一个状态,比较简单。

另外我有一个需求,你看看用你的架构是否有不便之处:

一个上传服务器协议格式:
[header][file content]

header是长度前缀的,包括4个字段,协议类型、要写入的路径、文件长度、MD5。header解析比较简单不讨论了,主要是在收齐header以后,要开始根据文件长度字段收文件内容,边收边写文件,如果用你的spserver架构,实现这个东东,如何才能有效利用起线程池?仅作讨论。
0 请登录后投票
   发表时间:2007-06-08  
引用
你的msgqueue对我非常有帮助,原来正为libevent 多线程困扰呢,把任务放到队列容易,要在完成时得到通知就比较麻烦


呵呵,的确是从 event loop 转到线程池容易,从线程池要转到 event loop 就比较麻烦了。

msgqueue 不是我实现的,是借来的。在 msgqueue 文件中保留了作者的声明。之前也发过 mail 给作者询问,在得到他允许之后才一起发布的。作者的原帖在这里
http://monkeymail.org/archives/libevent-users/2006-October/000257.html

引用
另一个问题,没考虑监听多个端口?多种协议?


这个没有问题啊。spserver 这个类有两个方法:
run 启动一个后台线程作为 event loop 线程,然后把执行流程返回给调用者;
runForever 不启动后台线程,直接使用调用者的线程。
如果要监听多个端口,那么只要初始化多个 spserver 对象就行了。每个 spserver 用不同的 HandlerFactory 来初始化。

引用
另外还可以考虑一下connector的实现


开始做的时候就考虑过这个问题,不过没有想到很好的办法,所以暂时先不实现,先把自己想清楚的部分先实现了。等想清楚了再看看是不是加进去。

引用
补充一下,说decoder写代码比较多,实际上也因为状态机的解析方式


的确,现在 spserver 的 msgdecoder 基本上可以认为是一个基于流的协议解释器。比如 spserver 自带的 HttpMsgDecoder 就是一个基于流的 http 协议解释器。因为这个框架最初做的时候是以实现 xmpp 服务器作为目标的,而 xmpp 的协议就是一个基于 xml 流的协议,所以整个框架就是倾向于这种做法。另外在开始实现 spserver 之前,刚好又看到了 mina 的介绍,所以这个 msgdecoder 的名字其实是从 mina 借来的,:)

引用
我在自己做协议时喜欢用长度前缀,先确定长度字节,收齐了这几字节就解出长度,再判断是否收齐剩下的,直接交给线程池就行了,中间只有一个状态,比较简单。


对于这种格式的协议,使用 msgdecoder 同样只有一个状态。原来的 msgdecoder 有一个 getMsg 的接口方法,现在已经去掉了,因为这个方法在框架中并不是必须的。也就是现在 msgdecoder 变成这样了

class SP_MsgDecoder {  
public:  
  virtual ~SP_MsgDecoder();  
   
  enum { eOK, eMoreData };  
  virtual int decode( SP_Buffer * inBuffer ) = 0;  
};  


在这段时间,我也一致在考虑是否有更好的实现 msgdecoder 这部分功能的方法。暂时还没想到更好的方法。只是对 msgdecoder 的认识更清晰了一点。就是这个接口只是定义了框架怎么回调特定的协议解释接口,至于在解释了协议之后,怎么获得解释的结果,就不是框架要关心的了,而是由具体的实现自己来决定。所以就去掉了原来的 getMsg 这个接口。

最近用 spserver 实现了一个 xmpp 服务器,在实现过程中,发现这个 msgdecoder 对协议解释部分和协议处理部分做了很好的解藕。在协议处理部分可以实现到 mina 宣称的“你只需撰写面向POJO的message而不是ByteBuffer的”。

引用
header解析比较简单不讨论了,主要是在收齐header以后,要开始根据文件长度字段收文件内容,边收边写文件


可以分成两个 msgdecoder ,一个用来解释 header ,在收齐 header 之后,decode 接口就返回 eOK 。然后设置 msgdecoder 为 defaultmsgdecoder ,这样就会“边收边调用handler::handle接口”,也就可以在handler::handle 接口中“边写文件”了。
0 请登录后投票
   发表时间:2007-06-14  
你目前的实现方式是收到数据后,由msgdecoder来判断是否完成(是这样没错吧?),然后放入线程池我觉得欠点灵活性。

比如我有这样一个应用A,它非常简单,不需要放入线程池,收到后只需要根据长度就可以判断是否收完了,而且不耗CPU,所以不需要线程池,这时msgdecoder也是多余的了。

另一个应用B收到数据后要计算,所以交由线程池处理。

这样2个应用都兼顾的方式,我自已想了一种:
http://qiezi.iteye.com/blog/72929

写得很乱,个人习惯不好,我大致讲一下使用方式吧。由于C++没有委托,所以实现起来可能要用static方法来做,下面的代码用D语法来演示。

class Protocol
{
	/**
	 * bytes: 要读取的字节数
	 * callback: 读取完成时的回调委托函数
	 * recvall: 是否要读取所有请求的字节数才回调
	 * threading: 是否交由线程池
	 *
	 * void delegate(char[] data, bool finished) 参数:
	 * data: 数据
	 * finished: 是否已经接收完成。对于recvall为true的,回调时这个参数肯定为true,否则可能为true或false
	 */
	void requestRead(size_t bytes, void delegate(char[], bool) callback, bool recvall, bool threading);
}

class AProtocol : public Protocol
{
    void connectionMade() // 连接建立时调用
	{
		// 请求接收4字节包头长度,必须收满4字节才回调,不放入线程池
		requestRead(4, &readPackageLength, true, false);
	}

	void readPackageLength(char[] data, bool finished)
	in {
		assert(data.length == 4);
		assert(finished);
	}
	body {
		int len = ntohl(*cast(int*)data.ptr);

		writefln("Received package length: %d", len);

		// 请求读取包内容
		requestRead(len, &readPackageData, true, false);
	}

	void readPackageData(char[] data, bool finished)
	{
		// send back
		send(data); // 此时没有放入线程池,可以直接设置EV_WRITE
	}
}


class BProtocol : public Protocol
{
    void connectionMade() // 连接建立时调用
	{
		// 请求接收4字节包头长度,必须收满4字节才回调,不放入线程池
		requestRead(4, &readPackageLength, true, false);
	}

	void readPackageLength(char[] data, bool finished)
	in {
		assert(data.length == 4);
		assert(finished);
	}
	body {
		int len = ntohl(*cast(int*)data.ptr);

		writefln("Received package length: %d", len);

		// 请求读取包内容,交由线程池处理
		requestRead(len, &readPackageData, true, true);
	}

	void readPackageData(char[] data, bool finished)
	{
		// 解出包头,密集型计算
		// ....
		send(data); // 此时不直接发送,放入发送缓冲区
	}
}

接口方式类似于twisted,改进了一些,算是reactor + proactor。目前还在构思阶段,实现还没开始。
0 请登录后投票
   发表时间:2007-06-14  
应该算是reactor + proactor + 你这个半同步/半异步。

实现方式是reactor,反应式的;用户接口则是以proactor方式来调用,请求/回调;处理部分算是半同步/半异步。
0 请登录后投票
   发表时间:2007-06-16  
qiezi 写道
你目前的实现方式是收到数据后,由msgdecoder来判断是否完成(是这样没错吧?),然后放入线程池我觉得欠点灵活性。

比如我有这样一个应用A,它非常简单,不需要放入线程池,收到后只需要根据长度就可以判断是否收完了,而且不耗CPU,所以不需要线程池,这时msgdecoder也是多余的了。

另一个应用B收到数据后要计算,所以交由线程池处理。



对于不需要线程池的应用,直接使用 libevent 就是了。
spserver 是基于 libevent 来实现的,对于单线程高并发的应用,直接使用 libevent 就非常好了。
spserver 的主要作用是在 libevent 的基础上,增加一个线程池,使得能够方便地实现需要线程来处理的应用。

前几天和一个朋友讨论到这个问题,他也是强调这一点,认为 spserver 的结构不够灵活,不能很好地优化不需要线程池的应用。但如果了解 spserver 和 libevent 各自的侧重点,那么对于不需要线程池的应用,直接使用 libevent 就是行了。只有需要线程池的应用才使用 spserver 。
0 请登录后投票
   发表时间:2007-06-17  
引用
应该算是reactor + proactor + 你这个半同步/半异步。

实现方式是reactor,反应式的;用户接口则是以proactor方式来调用,请求/回调;处理部分算是半同步/半异步。


关于 proactor ,按 <POSA2> 的说法,“主动器模式是同步反应器模式的异步变体”。在这本书的描述中,主动器专门指封装了 windows 的 IOCP 和 POSIX 的 aio_* API 系列异步 I/O 机制的框架。而在我们这里讨论的上下文中,都没有提到使用 IOCP 或者 aio_* ,严格来说,这里体现的不是 proactor 模式,仍然就是 reactor 模式。

不过楼上的说法是“用户接口是proactor方式”。如果从这个角度来说,可以认为是 proactor 。<POSA2>中提到,“如果操作系统不支持异步操作,可以在实现中使用多线程模拟主动器模式的语意”。

在 spserver 的结构中, event loop 线程可以认为就是用来模拟异步操作的。线程池就是异步操作的发起者,一方面,msgdecoder 可以看成是线程池向 event loop 发起的一个读操作,msgdecoder 判断到读操作完成时就回调线程池;另一方面,response 就是由线程池向 event loop 发起的写操作,写操作完成之后,通过 completionhandler 来回调线程池。
0 请登录后投票
论坛首页 综合技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics