代码改变世界

半同步半异步(HSHA)模式的服务器模型

2011-12-05 05:47  j.cheen  阅读(2169)  评论(5编辑  收藏  举报

半年前偶然看到一叫spserver的服务器框架,它将windows下IOCP移植到到libevent,并且以HSHA,LF两种模式实现了服务器框架.做了点功课,写点心得.

一般来说在设计一个服务器网络框架的时候,需要用到线程池,里面的线程负责执行服务端所有代码.这些代码总的来说可以分为两类:

  • 一类负责网络IO部分,也就是从网络读取和发送数据
  • 另一类负责处理各种业务逻辑.

通常情况下他们是分离的,网络IO部分不需要管理业务逻辑具体做什么工作,而后者也不关心数据怎么得来,怎么送到网络上.两者就是一个生产者消费者关系.

这里讨论的HSHA就是网络IO部分为异步模式,而业务逻辑部分为同步模式.即:

  • 网络IO请求为非阻塞的异步操作,只要调用了,就立即返回,操作结果不用调用方不断的去问:”发送/接收成功了吗?”.网上有一个比较形象的比喻,叫经典的好莱坞法则:你不用给我们打电话,我们会通知你的(you dont call me,i will call you).
  • 业务逻辑部分是同步的,虽然不确定服务端线程池中,到底是哪一个线程在执行,但同一时刻只会有一个线程在执行这个网络会话上的业务逻辑.

同步和异步两部分采用一个作业队列(jobQueue)来实现结合,一些细节:

  • 服务端定义一组回调函数,用于将网络连接上的OnAccept,OnRecv,OnSend,OnClose等事件通知业务逻辑层,而业务逻辑层的实现代码正是在这些回调函数中实现.
  • 网络连接建立时,服务端会创建一个网络会话对象,并保存有相关回调函数的指针.将回调函数OnAccept和相关上下文打包成一个任务push到作业队列.
  • 网络连接收到数据后,服务端将回调函数OnRecv和相关上下文打包成一个任务push到作业队列.
  • 网络连接发送数据后,服务端将回调函数OnSend和相关上下文打包成一个任务push到作业队列.
  • 网络连接关闭后,服务端将回调函数OnClose和相关上下文打包成一个任务push到作业队列.
  • 负责执行作业的某个线程被唤醒,取出这些作业,并执行.

以下代码说明大概的处理流程:

   1: class job_queue;
   2: class msg_free_queue;
   3: class thread_pool;
   4: int main()
   5: {
   6:     job_queue jobs;
   7:     msg_free_queue msg_to_free;
   8:     thread_pool        job_thread;
   9:     thread_pool        clean_thread;
  10:     server_init_and_listen();
  11:     run_accept_thread();
  12:     while(!exit_falg){
  13:         run_iocp_event_loop(&jobs,&msg_to_free);
  14:         while(job_queue.size() > 0){
  15:             job_thread.push(job_queue.pop_front());
  16:         }
  17:         while(msg_to_free.size() > 0){
  18:             clean_thread.push(msg_to_free.pop_front());
  19:         }
  20:     };
  21: }
 
 
主线程负责完成端口事件的处理,并完成作业任务的打包,保存在一个容器中.
以上代码只说明了服务端如何把IO层收到的数据交给逻辑层,并没有解释逻辑层怎么样把数据交给IO层发送,实际上这个工作是在负责调用回调函数的那个线程完成,当回调函数返回后,作业线程直接将需要发送的数据打包成一个完成事件,post到完成端口句柄上,这样就建立起关系了.
由于是异步的,逻辑层发送的数据不能确定什么时候发送出去,因此需要保存起来,当完成事件到达后,会把已经发送的数据指针放到一个容器中,然后由主线程负责处理善后问题.

下面是一些题外话:

你也看到了,只有一个线程负责处理IO部分,那么性能如何呢?

这个吧,其实大多数时候,你的服务器性能瓶颈不再网络IO部分,而在业务逻辑处理部分,在那里可能产生磁盘IO,数据库操作等.我个人觉得但线程IO已经足够了.采用IOCP这样高效的IO模型是为业务逻辑处理腾出资源,因为一个高效的服务端不是看你每秒能收发多少数据,而是看业务处理能力.另外单线程的IOCP也有很多好处,首先就是锁的问题解决了,其次就是资源的管理简单了,这两个问题都是比较棘手的东西.

我编译了spserver的代码,用asio的乒乓测试客户端程序做了一下压力测试,结果你可能也猜到了,IO吞吐量并不算很高,而且多核CPU上,只有一个CPU达到了100%利用率,而另外几个CPU则利用率不足50%.如果你跟我一样,是一个完美主义者,我们可以这样改进它:

将上面的代码中,监听工作和Accept线程分离出来,主循环那的部分(while那一块和相关的上下文环境)封装成一个模块,每一个模块都有自己完成端口句柄,当连接建立时,做一个简单的负载均衡,为新建立的连接的套接字句柄选择一个完成端口句柄并绑定.于是就变成了下面的样子:

   1: class job_queue;
   2: class msg_free_queue;
   3: class thread_pool;
   4: int nod();
   5: int main()
   6: {
   7:     thread_pool io_thread_nods;
   8:     io_thread_nods.set_thread_count(4);
   9:     server_init_and_listen();
  10:     run_accept_thread();
  11:     while(true){
  12:         io_thread_nods.run(nod);
  13:     }
  14: }
  15: int nod()
  16: {
  17:     job_queue jobs;
  18:     msg_free_queue msg_to_free;
  19:     thread_pool        job_thread;
  20:     thread_pool        clean_thread;
  21:     while(!exit_falg){
  22:         run_iocp_event_loop(&jobs,&msg_to_free);
  23:         while(job_queue.size() > 0){
  24:             job_thread.push(job_queue.pop_front());
  25:         }
  26:         while(msg_to_free.size() > 0){
  27:             clean_thread.push(msg_to_free.pop_front());
  28:         }
  29:     };
  30: }

 

OK,完工.