给大家分享下我目前在项目中(需要开发一个monitor进程,用于监控大量逻辑SERVER,每个dms服务需要与后端做交互,性能较低)实现的异步网络IO架构,希望对大家解决相同场景的问题时能有所帮助。
这个架构用来解决“高性能场景下,当我的SERVER还需要访问拉取其他SERVER的数据时,我的SERVER性能已经达到最大了,可是这台machine所有硬件(比如所有CPU都没有达到100%,machine负载还是个位数)都没有达到瓶颈”这种场景。
还有一点需要补充,如果第三方服务或者后端SERVER仅提供同步接口给我们,没有提供任何异步接口,并且不支持我们在协议级别上实现异步访问,这个方案就无能为力了。
一般来说,对其他SERVER发出大量并发请求的耗时会存在两个问题:
1、 响应时间不稳定;
2、 平均响应时间可能过长。
举个例子,当逻辑SERVER需要从其他第三方服务里拉取数据,受网络和第三方服务的稳定性影响,性能会有波动,假定平均请求时间为1S,可能99%的请求都可以正常应答,但是可能会有1%的请求延时会达到5S。
某后端SERVER对大量请求的响应时间通常会有这种长尾效果,虽然这种长时间响应的请求数量很少,但是如果设计不当,会对SERVER的整体性能产生较大影响,正常响应用户的请求也会被延迟。
一般我们可能是通过设置网络套接字的超时时间来降低这种影响,但是超时时间过短又可能造成失败率上升。
所以,我们必须防止这1%的请求对其他正常请求的响应造成影响,又不能增加失败率。
第2个问题跟后端SERVER的性能有关,比如说,dms拉取某file cache server,当cache server达到1万次每秒访问量时,平均单次响应需要100ms,那么如果我们设计不良好的系统会出现什么问题呢?首先把非网络因素都剔除,我们假定只有网络IO占了主要请求响应时间的消耗(一般情况下都是如此)。
假定下面这种较常见的网络设计模式:我们对每个网络套接字设定超时时间为1S,当从接入系统中(通常是一个队列)获取到一个完整的请求后,交给一组线程池去实现业务。每个线程分解业务后,向后端SERVER发请求等待响应,收到响应后分析结果,最后应答给client。
首先只看第2个问题。如果平均一个请求100ms,那么每个线程可以处理10次每秒,如果我们开100个线程,可以处理1000个请求每秒。理论上似乎无限。但实际上,当一台PC机器上运行的线程数达到200时,进程间切换已经占到很大的比重,如果有共享资源用到锁,那么这种加解锁的系统调用也会占用系统资源。根据我的经验,通常情况下如果一台PC机线程达到200个左右后,继续增加线程数毫无意义,只能降低性能。
如果再把第1个问题算进去,那么那些少量的长时间响应请求,就会占用线程池的大量业务处理线程,造成SERVER的并发处理能力下降。
我在设计dms_monitor时,假定我一台dms_monitor监控多台dms,比如很极端的配置下,每秒需要轮询一台dms,一共需要管理5000台dms(仅举例),那么就需要每秒发送5000个请求,从不间断(因为需要灵敏的检测到不工作的DMS),并且每个请求都要等待响应。这些请求的响应时间肯定很不均匀,就遇到了上面所说的问题。
我的解决方案是这样的:
略去其他设计,仅谈这一块网络IO模式。我的原则就是软件上无等待,使进程最大化的运行,直到达到硬件资源的瓶颈(通常最直接的表现是处理网卡中断的CPU达到100%)。所以思路就是,仅保留核心的线程,除了没办法优化的资源锁外,去除所有的阻塞操作。
主要设计如下:1+N线程数,用于保存中间过程的共享容器(比如排序二叉树,存储所有请求未处理完的中间数据),下面简要解释下:
1、1个发送线程,负责创建(或者从连接池中获取)非阻塞网络套接字,向后端SERVER发送请求,将发送信息存入共享容器,将套接字放进多路复用实现对象中(例如epoll,select,poll等)。
2、N个处理线程。N>=1,有些情形下实际1个线程应该是效率最高。并且这N个线程可以是几组线程池,例如N=30时,其中10个线程用于处理大数据量(中断较多)请求,20个线程用于处理小数据量请求。这N个处理线程被多路复用实现对象激活,比如:DMS收到DB回的响应包。激活后从网络IO取出数据,接下来从共享容器中取出正确的中间数据,判断业务完整性,不完整,则继续放入多路复用实现对象中,处理下一次激活。
3、保存中间过程的共享容器,应当是适合add和get操作非常多的容易,一般用关系容器实现,不太可能用顺序容器实现。所有的中间过程都必须保存下来,举个例子,client端发来的必要信息必须保存,如果需要多次接收后端SERVER发来的回包(比如回包超大在2K以上大小),或者该业务需要多次向后端SERVER发送请求获取回包。
这种设计肯定增大了CODE复杂度,因为原本一次完成的业务请求,被分成数次甚至被几个线程分别处理了。对于小数据量的应用,该网络设计模式没意义,高并发高性能下则可以几倍的提高同等machine的性能。
我用C++实现了这种结构的简单封装,复用时在解决下面几个问题后就可以正常使用了:
1、 业务分解。根据业务特性来实现,如果只需要对后端SERVER做一次交互,就会很简单。需要多次交互,比如DMS先访问PM去加锁,然后再访问PM获取数据,这就是两次交互,需要设计好中间过程的数据。
2、 后端SERVER必须支持异步接口,协议级别同样可以。
3、 实现可以分析后端SERVER回包的回调方法。比如,判断PM回包是否完整,当一个回包1M以上时,可能需要十几次的网络调用才能完整接收。
4、 如果占用内存大,并且每个请求占用的内存大小分布极不均匀,建议用内存池来存储一个请求处理时的中间数据。
该设计在client访问达到server所能容纳的峰值时,所有线程都不会有阻塞,基本瓶颈在网卡,建议使用千兆网卡多个,同时每颗CPU单独绑定网卡,避免业务操作抢占网卡CPU。