CSDN的第一篇技术博客就从这里开始吧。
对于常见的几种服务器模型,有过服务器端开发经验或研究了解过这个领域的可能都清楚。类似介绍这些模型,分析各自特点优劣的文章不胜枚举,本来在这个方向班门弄斧的挥上一笔似乎已经没有太大价值。但是,正由于以前都是看其他牛人的分析结果,虽然都基本认同,但却没有自己去验证过。前段心血来潮,就略花了点时间,自己写代码简单的实现了下几种服务器模型,然后自己写了些客户端的测试用例,得到些定量的分析结果,在整个过程中,感觉还是有点收获,所以一方面跟大家分享一下,另一方面也算是记录下自己的成果的。
开始,先说明下自己到底做了些什么吧。
1. 用Python实现了以下几种服务器模型,后面会再介绍这几种模型的
- 循环服务器模型(后面简称SingleSync)
- 多线程服务器模型(后面简称MultiSync)
- 线程池服务器模型(后面简称MultiSyncWithPool)
- 基于事件驱动IO多路复用的服务器模型(单线程版)(后面简称SingleAsync)
- 基于事件驱动IO多路复用的服务器模型(多线程版)(后面简称MultiAsync)
(这里声明下,只是基于各个模型的分析需要,实现了最简单的功能,而且都是基于TCP的,至于为什么用Python,只有一个理由,时间和效率,网上有句行话:life is short,you need python)
2. 还是用Python实现了客户端,对于不同模型的服务器,使用相同的客户端程序
3. 在多种不同并发负载情况下,客户端采取三种连接时间来进行测试,测试具体情况和最后结果在后文中一一说明。
(声明:一下所有实现只是处于验证目的而已,并非真正服务器架构模型)
首先,简单介绍下自己实现的几种模型。
1. 循环服务器模型(SingleSync)
TCP循环服务器模型,应该已经灭绝了,毕竟没有人能忍受同一时刻只能有一个客户端占有服务器,它也是同步阻塞型的一种,之所以实现它,是为了作为一种情况来进行对比。通过图1-1的活动图,应该就明白何谓循环服务器了。这里不做其他解释,看了图还不清楚的就请google一下吧。
缺点,不支持并发,同一时间,一个客户端独占服务器
2. 多线程服务器模型(MultiSync)
这算是比较常见的一种模型,其实也属于同步阻塞,但是如果使用这种模型,希望大家小心了,并发不高时可能没什么问题,但当并发达到一定程度时,它会让你非常失望的。这里就不多做说明了,后面的测试数据会说明一切的。如果不了解的,请先参考下面这个活动图来帮助理解吧。
缺点,多线程的启动以及线程间的切换会带来额外的开销,高并发时对性能影响比较大
3. 线程池服务器模型(MultiSyncWithPool)
这其实就是上一个种模型的改进版,预设的线程池以及线程数的限制有效的减少了线程启动以及过多线程切换带来的开销。虽然带来了一定同步控制上得开销,但总体来说,在高并发状态下,不管是性能,效率还是稳定性都要比上一种好。如图所示
缺点,进程池的同步控制会增加一些开销
4. 基于事件驱动IO多路复用的服务器模型(单线程)(SingleAsync)
通过select或poll或epoll这些IO多路复用API实现事件驱动的服务器模型貌似是当下比较受欢迎的一种,这些API的说明不是本文的目的,如果你不了解就google下吧。调用Select这些接口本身还是会被阻塞的,但是结合它们支持IO多路复用的特点,即使单线程也可以支撑起支持高并发的服务器。让我们看如下活动图吧。
缺点,windows平台下不支持poll和epoll,而select本身IO轮询效率不是很高。
5. 基于事件驱动IO多路复用的服务器模型(多线程)(MultiAsync)
这还是延续使用IO多路复用API的方式,不过把监听处理和连接后的处理分在不同的线程中,当然这些处理都是基于异步IO多路复用来实现的。这种思想类似于一种分布式类的设计模式--半同步半异步模式,关于半同步半异步模式可以参考面向模式系列书籍的第二卷。大概处理可以参考下图。这里有一点说明下,进行Client IO poll的线程并不一定是一个,也可以是个线程池,我测试的时候只起了一个Client IO poll线程。
缺点,监听线程和连接后处理的线程池间会带来一定的同步开销。(本来我也觉得这种模型应该是以上这些模型里最优的一个,但测试结果如何在后面揭晓吧)
好了,验证用的模型都说明完了,接着进入主题,把我的测试结果show给大家吧。在给出结果前,先说明下测试验证时的环境。Windows平台,双核CPU,服务器客户端测试代码都是Python。
结果数据如下,
1. 客户端短连接时间的结果(作为基准每次连接时间为x)
| 10 | 100 | 1000 | 10000 |
SingleSync | 5ms | 45ms | 440ms | 4300ms |
MultiSync | 7ms | 52ms | 470ms | 4650ms |
MultiSyncWithPool | 6ms | 48ms | 440ms | 4360ms |
SingleAsync | 5ms | 45ms | 430ms | 4200ms |
MultiAsync | 6ms | 47ms | 430ms | 4250ms |
2. 客户端3倍连接时间的结果(每次连接时间为3x)
| 10 | 100 | 1000 | 10000 |
SingleSync | 6ms | 550ms | 1000ms | N/A |
MultiSync | 8ms | 60ms | 550ms | 5500ms |
MultiSyncWithPool | 6ms | 55ms | 525ms | 5120ms |
SingleAsync | 6ms | 50ms | 470ms | 4600ms |
MultiAsync | 7ms | 52ms | 500ms | 4800ms |
3. 客户端6倍连接时间的结果(每次连接时间为6x)
| 10 | 100 | 1000 | 10000 | 20000 |
SingleSync | 500ms | 1040ms | N/A | N/A | N/A |
MultiSync | 10ms | 70~550ms | 650~1250ms | 6700ms | N/A |
MultiSyncWithPool | 7ms | 65ms | 620ms | 6150ms | 12450ms |
SingleAsync | 8ms | 60ms | 550ms | 5500ms | 11190ms |
MultiAsync | 9ms | 65ms | 565ms | 5650ms | 11980ms |
看到这些数据谁会一头雾水,让我先来解释一下各个测试的参数吧。
每个表格title里的10,100,1000,10000是指客户端并发连接的次数,当然这只是模拟的效果不是真实的并发数(真正的并发远低于这个数值),所以数字本身没有任何意义,只是用作在不同并发负荷下比较来用。
上面说到的连接时间x,3x,6x指并发连接的客户端中每次连接处理所花的时间,这里也是个模拟值,所以只是用x来表达倍数的关系。其实这里的连接时间都是很短的,只是几次简单的收发消息而已。
表格左边一栏当然就是各种模式的简写,对应关系前面提到过了。结果就是整个处理所花费的时间。N/A代表出现过载异常了。类似70~550ms的范围数据,代表结果很不稳定,在这个数据范围内。
说下我得出的结论吧。
1. 在连接处理不是非常短的状态下,绝对不能循环服务器模式(这是句废话,我想现在应该没有人会这么用的)。
2. 对于多线程服务器模型,在并发不高时性能比后面几种略低,高并发状态下也出现了过载异常,而且一定程度上稳定性也不是很好。
3. 后三种模型从结果上看差别不大,IO多路复用的略好于线程池的模型。但我是在widnows下验证的,所以IO多路复用只支持select,效率不高。Linux平台下应该后两种的优势会更明显。
4. 总体来说,后三种都是比较稳定,而且性能也不错。可以根据需求来进行选择。虽然异步的好于同步的,但毕竟考虑的易实现,易使用的话,同步的操作性还是好于异步的。
5. 本来认为最后一种应该是最优的,但实际结果来看,单线程IO多路复用模型反而最好,多线程来分载IO多路复用可能由于同步控制上带来了一些开销所以反而效果不是很明显(也可能是我实现上的问题)。怪不得看到一些Web异步服务器框架的实现会采用单线程的多路复用。本来自己还持有异议,现在看来牛人们早就有远见了。
6. 这点是上面表格之外的,针对IO多路复用我做了些额外的尝试,Write事件独立出来和Read事件都进行监听比只监听Read事件(Write事件只是作为Read事件响应后的处理)要高。
【2012/3/15 】
追加一点,当在多核CPU并且并发低于一定程度时,线程池模型的性能会好于异步IO多路复用模型。而这里的所谓一定程度,是需要根据实际情况进行验证的。