论坛首页 综合技术论坛

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

浏览 57734 次
该帖已经被评为精华帖
作者 正文
   发表时间:2007-06-29  
现在好像默认就使用proactor方式了,用户在connectionMade里面写入requestRead。有一个int process()方法,返回值是1时表明用户已经处理,返回0表明用户想放入线程池处理,返回-1是错误,需要断开。所以想使用reactor方式时,必须要重写int process()了。我在想另一种方式,如果没有requestRead,则process直接调用dataReceived来作reactor方式的处理。细节上有些绕,还没想好,现在基本可以工作,从线程池里面回到libevent用的是event_msgqueue,还比较好用。
0 请登录后投票
   发表时间:2007-06-29  
qiezi 写道
现在最不自然的地方,就是Protocol来处理是否放入线程池,因为read请求也是它发起的,问题在于线程池在reactor里面,所以现在是通过protocol找到factory再找到reactor,虽然直接用指针就定位到了(factory->reactor->addTask(this)),不过感觉很不舒服。这类问题在twisted和ace里面可能是使用全局变量或singleton来访问,但我想在一个系统里面同时跑起多个reactor(虽然不一定有用),更重要的原因是我极度讨厌全局变量和singleton。


使用 libevent 和线程池,一般很自然地就把线程池和 reactor 混在一起了。
主要的原因是 libevent 的 event_loop 的实现,当底层的 select/poll/epoll 探测到有事件的时候,event_loop 是逐个处理所有的事件,在 event_loop 内部调用所有的 callback 。而一般就是在 callback 里面直接处理事件了。

为了把 reactor/proactor 和线程池分开,可以用一个间接的方法。在 callback 中不直接处理事件,把事件保存到一个列表(eventList)里面,这样就可以使得 event_loop 要做的事情很简单。等到 event_loop 运行完之后,就可以使用相应的线程池策略来处理 eventList 了。

在这里有类似的描述
Modification of the leader-follower pattern for Proactor

代码大概是这样的
class Reactor {
public:
    int handle_events() {
        if( mEventList->getCount() <= 0 ) {
            // 把所有的 event 保存到 mEventList 中
            event_loop( xxx );
        }

        Event * event = mEventList->remove( 0 );
        if( NULL != event ) {
            // process event
        }
    }

private:
    List * mEventList;
};

int main( ... )
{
    Reactor reactor;

    // 可以使用不同的线程池模型来处理这个循环
    // 可以是 Half-Async/Half-Sync ,也可以是 Leader/Follower
    for( ; ; ) {
        reactor.handle_events();
    }

    return 0;
}



我也正准备按这个思路来尝试修改 spserver ,然后对比一下 HAHS 和 LF 线程池模型的性能差异。
从目前想象到的运行过程来看,LF 的线程切换应该比 HAHS 少很多,性能应该会更好。
关键的地方是
引用
(Please, see Collections of results below how to avoid/minimize the cost of operations on collections).
0 请登录后投票
   发表时间:2007-06-29  
LF线程切换会比HAHS少?目前你的HAHS实现是怎样的?我是做了一个同步队列,所以这个线池里面的线程是阻塞在同步队列上的,这些线程的关系应该也是LF。这种情况下,HAHS可能同时运行的线程数比LF多一个,在多CPU上感觉差异并不太大。不过我那个实现用了同步队列,线程锁会影响性能。改用LF也会有锁的问题,必须保证在一个线程取到一个socket事件时,从libevent里面移除该socket的所有事件,以防止多个线程跑同一个socket,这时锁是免不了的。虽然libevent会在发生事件时移除该句柄上的事件,但似乎只是移除一个事件,而且这个过程也不是线程安全的,所以我对于实际性能会不会提高还有疑问。另一个LF方式的缺点,就是各个处理线程都忙时,不会再接受连接,而HAHS方式是先接下来再说,接下来的连接还可以先接收数据,至于哪种方式更合理,我也不能确定。

不知道我对于这2种模式有没有理解上的偏差。

我也订了一套POSA2,周末到货,看看再说。
0 请登录后投票
   发表时间:2007-06-29  
引用
LF线程切换会比HAHS少?目前你的HAHS实现是怎样的?我是做了一个同步队列,所以这个线池里面的线程是阻塞在同步队列上的,这些线程的关系应该也是LF。


目前是实现了一个 Executor ,这个 Executor 对象是一个主动对象,内部维持一个任务队列和一个线程池( worker )。创建 Executor 对象的时候,自动会创建一个线程(称为 scheduler 线程 ),这个线程复杂分派队列中的任务给线程池中的线程。

Executor 的使用场景类似这样:
void main_thread( .... )
{
    Executor executor;

    for( ; ; ) {
        Task * task = xxxxx;
        executor.execute( task );
    }
}


这样带来的好处是 Executor 可以用 lazy 的方式来创建线程池里面的线程,也可以在必要的时候回收线程。
另外就是使用者容易使用,接口容易理解。这种线程池的使用和普通的 connection pool 之类的池很类似。
过量的任务只会阻塞 scheduler ,不会阻塞 main_thread 。

坏处就是多了线程的切换。executor.execute 的时候,从 main_thread 切换到 scheduler 线程,然后 scheduler 线程分派任务,又从 scheduler 切换到 worker 线程。

引用
另一个LF方式的缺点,就是各个处理线程都忙时,不会再接受连接,而HAHS方式是先接下来再说,接下来的连接还可以先接收数据,至于哪种方式更合理,我也不能确定。


LF 的确有这种问题。所以可能要做到由用户来选择,把 HAHS 和 LF 封装起来,可以方便地切换。
0 请登录后投票
   发表时间:2007-07-01  
引用
为了把 reactor/proactor 和线程池分开,可以用一个间接的方法。在 callback 中不直接处理事件,把事件保存到一个列表(eventList)里面,这样就可以使得 event_loop 要做的事情很简单。等到 event_loop 运行完之后,就可以使用相应的线程池策略来处理 eventList 了。

从目前想象到的运行过程来看,LF 的线程切换应该比 HAHS 少很多,性能应该会更好。


今天试着用这个思路来实现 Leader/Follower ,为 SPServer 增加了一个 SP_LFServer 的类,接口和 SP_Server 一样。

最新代码
http://spserver.googlecode.com/files/spserver-0.6.src.tar.gz

然后修改了 testhttp.cpp 的代码,使得可以在命令行指定使用的线程池模型,然后用 ab 做了一些简单的测试。
testhttp.cpp 的功能很简单,只是把收到的 http 请求解释出来,然后把解释到的内容作为 response 返回给客户端。由于 testhttp.cpp 的功能足够简单,所以用来测试线程的切换的消耗还是比较理想的。

从初步的测试结果来看,在 testhttp.cpp 这种应用中, LF 比 HAHS 快。

测试不是很严格,针对 HAHS 和 LF 分别测试使用 1 个线程和 2 个线程的情况,每个 case 运行 5 次,取中间的结果。性能最好的是 LF 使用 1 个线程的时候,因为这里完全没有线程的切换,

对比上次 测试memcached 得到的数据,LF 使用 1 线程的时候,处理请求的速度和 memcached 差不多。memcached 是单线程的。

对于 HAHS 模型的测试结果

bash-2.05a$ ./testhttp -s hahs -t 1

bash-2.05a$ ./ab -n 10000 -c 100 -k http://127.0.0.1:8080/ 

Requests per second:    3464.96 [#/sec] (mean)




bash-2.05a$ ./testhttp -s hahs -t 2

bash-2.05a$ ./ab -n 10000 -c 100 -k http://127.0.0.1:8080/ 

Requests per second:    2768.06 [#/sec] (mean)




对于 LF 模型的测试结果


bash-2.05a$ ./testhttp -s lf -t 1

bash-2.05a$ ./ab -n 10000 -c 100 -k http://127.0.0.1:8080/ 

Requests per second:    4576.40 [#/sec] (mean)




bash-2.05a$ ./testhttp -s lf -t 2

bash-2.05a$ ./ab -n 10000 -c 100 -k http://127.0.0.1:8080/

Requests per second:    2951.07 [#/sec] (mean)


0 请登录后投票
   发表时间:2007-07-01  
有没有测试多CPU?结果可能会不同吧。

现在无论是使用LF还是HAHS,都有个锁的问题。底层无论是使用select还是epoll,都无法在不加锁的情况下,在一个原子操作中完成取出事件并取消已经注册的事件(select当然是操作fd_set),这就给LF方式带来了不方便,必须用线程锁了。我感觉要做出最高效的应用,可能还是要自己去整底层API,依赖libevent可能还是有局限性,自己做比较麻烦的是超时,有时间看看libevent是如何实现的。感觉每种方案都有不完善的地方,不知道linux内核中实现的aio怎么样。
0 请登录后投票
   发表时间:2007-07-01  
引用
有没有测试多CPU?结果可能会不同吧。


测试的主要关键点是在测试案例的选取上。
就目前这个 testhttp.cpp 的案例来说,多 CPU 对性能的提升应该不大了。对于这种案例,如果要在多 CPU 上获得更好的性能,应该使用 memcached 的方法,每个线程使用一个 event_base 。

引用
底层无论是使用select还是epoll,都无法在不加锁的情况下


POSA2 书中,多次提到 win32 WaitForMultiObjects 函数允许线程池在同一个句柄集上同时等待来支持并发句柄集。

引用
可能还是要自己去整底层API,依赖libevent可能还是有局限性


对于 event-driven 编程方式的封装,libevent 其实做得很好了。libevent 主要的特点是使用 callback ,但这个也是由 event-driven 决定的。这种实现方式就相当于设计模式中的“被动迭代器”。

通过上面提到的方法可以把“被动迭代器”转换为“主动迭代器”。
引用
为了把 reactor/proactor 和线程池分开,可以用一个间接的方法。在 callback 中不直接处理事件,把事件保存到一个列表(eventList)里面,这样就可以使得 event_loop 要做的事情很简单。等到 event_loop 运行完之后,就可以使用相应的线程池策略来处理 eventList 了。


如果要改进,应该就是要考虑如何一步到位地把 event-driven 编程方式封装为“主动迭代器”的方式。

0 请登录后投票
   发表时间:2007-08-08  
SP_MsgDecoder 实现时,COPY缓冲能不能很好的解决?
0 请登录后投票
   发表时间:2007-08-08  
linux网络内核没有实现aio
0 请登录后投票
   发表时间:2007-08-08  
qiezi 写道
底层无论是使用select还是epoll,都无法在不加锁的情况下,在一个原子操作中完成取出事件并取消已经注册的事件。

经过测试,我这个说法是错误的。epoll有个EPOLLONESHOT可以完成一次性事件,可以让epoll_wait每次只取一个事件,从而达到没有线程锁的情况下实现LF模式。一个线程调用epoll_wait时还允许其它线程调用epoll_ctl来注册事件。多个线程在同一个epoll描述符上执行epoll_wait内核会进行排队,不会出现一个事件在有EPOLLONESHOT时还被多个线程得到的情况,所以可以在无锁的情况下完成整个过程,目前简单测试通过了。
0 请登录后投票
论坛首页 综合技术版

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