做为一名IT工程师,网络通讯编程相信都会接触到,好比Web开发的HTTP库,Java中的Netty,或者C/C++中的Libevent,Libev等第三方通讯库,甚至是直接使用Socket API,可是不少程序员都仅限于使用,对于使用的方式是否合理并无特别深的理解,好比有一股脑的使用线程池解决问题的(虽然大部分状况采用多线程方案不会有什么问题,可是编程复杂度比起单线程提高了不少,线程开的太多也会致使切换过于频繁,性能未必有太大提高),也有始终用一条线程处理全部业务的,而后上线以后常常出现各类服务响应慢等问题。
在介绍TCP的网络通讯编程时,不得不提到同步,异步,阻塞,非阻塞这几个概念,C++系和Java系沟通网络IO相关时,常常把这几种混在一块儿描述,好比同步阻塞,同步非阻塞,异步非阻塞等等,实际上,Linux AIO相关的API不多有使用在网络编程上,用同步异步描述网络IO并不许确,对于咱们经常使用的Socket API,好比:Connect、Send、 Recv、Close等,只有阻塞非阻塞之分,没有同步异步之分,而各类应用提供的API接口能够分同步异步,好比Redis,MySQL官方提供的库大都是同步接口,Aerospike则既提供了同步接口,也提供了异步接口。python
下面咱们列一下在阻塞与非阻塞下,这些API都是如何表现的
看到了上面阻塞API执行的表现,那么咱们假设一些异常状况程序员
Connect: 在网络状况差的状况下进行Connect操做 Send:Server端由于各类缘由不Recv数据,致使Client端发送缓冲区满,Send没法写数据入缓冲区
按照上面的情景很容易想到,函数都会Blocking住,此时这条线程就被OS挂起,让出CPU资源,而且该线程没法处理其它业务,若是此时正处于请求高峰期,结果可想而知。
那么如何解决这个问题呢,可能不少人首先想到的是多线程,可是多线程也会带来一个问题,这个线程池建立多少合适,太少不够用,太多资源占用多,并且线程切换频繁带来的损耗也不小。
正确答案是使用Socket的非阻塞模式,如今的通讯框架基本都采用这种模式,好比一些成熟的第三方库,Libevent, Libev等。
当使用非阻塞模式,再结合多路复用Epoll,一个解决C10k问题的高并发网络框架基本就造成了。
接下来就介绍几种基于多路复用的非阻塞服务端模型数据库
常见的服务端模型
1.单进程单线程编程
好比事件驱动通讯框架Libevent,Libev,应用有知名的Redis,Memcache,比较适合没有太多耗时任务的状况。
简单高效的代名词,对于网络IO来讲,就是哪一个Socket Fd有读/写事件触发了,就执行它的逻辑,缺点是当一个业务逻辑涉及到不少RPC调用时,业务代码会分散在各处,可读性比较差,后面会与常见的非事件驱动Web框架作个对比。后端
适合有比较耗时的业务的状况,好比流媒体,文件传输服务器,数据库代理等,其中又能够划分出以下两种比较典型的状况。网络
图一多线程
图二并发
对于图一,是比较常见的状况,好比DB Proxy使用线程池的方式创建多个链接以提供并行处理的能力。
对于图二,Accept IO Loop 只负责Accpet Client端的Fd, 而后将Fd传递给 Recv IO Loop,这样有一个好处,每一个Client的请求都只会在一个Recv IO Loop中处理,从而保证了单个Client请求的有序性,而不像图一中须要其它手段保证有序。框架
3.多进程
这种模式和单进程多线程的有点相似,并且若是Work是单线程的话,就能够不用考虑多线程带来的锁问题。
Master进程能够负责Accpet Client的链接,同时Recv数据并经过IPC的方式将数据包交予Work进程处理。
另外一种,Master只负责Accept Client端的连接,而后将Fd传递给Work,让Work进行数据的收发与逻辑处理。
使用Send,Recv编写简单,适合于同步接口的封装,好比Aerospike,HTTP,的同步接口均可以使用这种方式,问题是不适合作成异步接口,在Recv的时候该线程不能处理其它业务
2.阻塞/非阻塞的多路复用模式
既可封装成同步接口,也能够封装成异步CallBack接口,扩展性更强,好比Aerospike的异步CallBack接口,优点是能够进行多个请求发送,有数据可Recv时才处理
Client库的并行调用
编写一个应用时,咱们常常会遇到同时发送多个Req至服务端的场景,好比MySQL,HTTP,Redis或者自定义的协议,常用的一个方式是链接池,不一样的Req分别用一个链接进行处理,这样作的缘由是协议的特性决定的,由于使用一个链接,对于Rsp咱们没法回溯哪一条Req,因此只能使用链接池方式,而咱们本身设计协议时,通常都会在协议头都会增长一个惟一序列号,这样Rsp返回就能够经过该序列号找到对应的Req,了解了这一点在作Client端的并发调用时就能够更清楚的选择如下哪种模式了。
1.线程池:
比较常见的就是使用MySQL,Redis等开源产品的同步库,线程池使用比较方便,可是问题也比较明显,依赖线程的数量,设置太少,并发处理能力太弱,设置太多,线程切换频繁。
2.链接池:
经过建立多个链接,并结合多路复用的方式进行操做。好比自行解析MySQL,Redis,HTTP等协议,直接操做Socket Fd,Aerospike的异步链接池就是使用这种方式,好处是能够避免频繁的线程切换,问题就是若是官方的库没有提供这种功能的话,就只能本身去解析协议,没有线程池使用起来方便快捷。
3.链接复用:
通常咱们自定义的二进制协议,协议设计时都会带有一个惟一的序列号,Rsp经过这个序列号来找到对应哪一个Req,这样就能够复用一条连接进行屡次发送,而无需使用上面提到的线程池和链接池方式了。
事件驱动型框架
在上面的服务器模型介绍中提到过事件驱动,简单介绍了事件驱动的原理,就是利用多路复用Select/Epoll监听一堆Socket Fd,当哪一个Socket Fd有读/写事件后,就处理它的事件。
如上图,是一个很常见的请求流程,对于不少Web框架的用法来讲,ServerA对于Client的请求代码都是以下写法:
def DoReqClient(req): res1 = Call_RPC_B() res2 = Call_RPC_C() Send_Rsp_To_Client()
ServerA对ServerB与ServerC的RPC都是同步的,ServerC的RPC须要等到ServerB完成后在执行,假如一个请求ServerB处理很慢,则处理这个任务的线程/进程就必须等待,若是ServerA是PHP-FPM就至关于一个进程堵住,彻底不能处理其余任务,假如开启了500个进程,则表示PHP-FPM最多只能同时有500个这样的请求,以后该机就彻底没法处理新的请求,直到任务完成释放一个进程。
可是咱们能够看到对于这台机器来讲,他的单机性能彻底没有机会发挥,所有堵塞在了网络IO上,要解决并发量的问题,要么用机器堆(钱多),要么优化业务,看看如何提升后端业务的处理能力,还有一种就是采用事件驱动型框架,有网络IO事件了才处理,这样就不会有任何的网络IO阻塞,惟一的缺点是业务代码逻辑分散,好比上图的ServerA若是换成事件驱动的写法就会以下面的样子。
def DoReqClient(req): ... Send_Req_To_ServerB() def DoRspServerB(rspB): ... Send_Req_To_ServerC() def DoRspServerC(rspC): ... Send_Rsp_To_Client()
然而坏消息是大部分Web框架并不支持这种事件驱动的模式,更多的都是使用第一种同步写法,简单快捷,便于快速开发出产品,由于初期的时候性能并非第一要务,快速产出才是关键,毕竟经过堆机器有时候也能够提升网站的并发能力,而当你开始考虑以更少的机器支撑更大的并发时,基于事件驱动模型的框架是一个不错的选择。
想要优化事件驱动逻辑分散的写法能够用到如今比较流行的协程,用同步的代码实现异步的流程,如今不少语言都开始支持协程的语法,本文就不在具体展开了,有兴趣的同窗能够去了解一下,好比PHP同窗能够看看咱们公司的Zan框架,Python同窗能够了解一下tornado框架,或者直接学习一下Golang。
结语
经过以上的总结,咱们能够知道服务器和客户端的网络通讯模型并无一个固定的模式,而是须要结合具体的协议,使用场景来判断,何时单进程单线程就能知足需求,何时必须使用多线程,链接池。只有采用了合适的模型,才能为一个高并发高性能应用打好坚实的基础。