@
JasonLaw 以下描述不针对负载均衡类应用,而针对广义的抽象的 IO+业务处理
想象收银台(IO 模块)和厨房(业务模块)是两个独立的部门,对彼此是黑盒,收银台收到订单后将菜单交给厨房制作(读到数据后调用业务模块),厨房制作完成后将食品让收银台交给用户(业务处理完成后结果写回客户端),针对你的问题“当咖啡处理好了之后,是收银员将咖啡拿给顾客吗”,答案是是的,只有收银台才能和顾客打交道(持有连接)
那么每个部门内部可以分别独立地作出以下选择
收银台:
1. 一个顾客首次到来(建立连接)后指派一个专门的收银员(IO 线程)全程服务这个顾客直到他离开(断开连接),这就是阻塞式 IO,在这个过程中顾客完全可以点一样东西,思考几分钟,再点下一样,他思考的时候收银员就在等待(阻塞)
2. 一个收银员服务轮流服务所有顾客,并且只服务那些已经想好了(数据就绪)的顾客,如果顾客点了一样东西然后卡壳了,需要继续思考(就绪数据已读完),那么对不起,请你马上让开给下一个想好的顾客,等你想好了再来(下一轮 select/poll/epoll),当然收银员有职责记住每个顾客已点的部分食品,下一轮继续回来点的东西能够拼接上,最终形成完整订单(即上层协议的分割和解析,当然这一步严格来说到底算 IO 模块还是算业务模块暂时存疑,具体取决于期望 IO 模块的输出是 TCP 流还是上层协议内容),这就是非阻塞 IO
厨房:
1. 收银员递交订单后需要一直等着,直到拿到做完的东西后才能转身继续服务顾客,这就是同步调用
2. 收银员递交完订单后就转身继续服务顾客,厨房有需要的时候再把东西交给收银台,这就是异步调用
建立完这个模型后再来逐一进行整体的分析
1. 收银台采用阻塞式
优点
1) 和厨房打交道时既可以选择同步方式也可以选择异步方式,当然实际情况多采用同步,因为即使使用了异步,仍然会搭配 wait 等调用,本质上还是转化成同步了
2) 在描述收集整个订单(即上层协议解析)时可以站在收银员的角度,在写一些复杂协议时比较符合人的直觉,即当读完某一部分数据后可以根据需要主动地进行读操作,如果无数据可读就进入阻塞
缺点
1) 要么保证需要的收银员数量完全等于此刻所有处在点单过程中的顾客数量(即一客户端一线程),造成了大量的资源消耗(线程的内存等资源开销),甚至耗尽资源,要么给收银员人数设上限,从而导致了某些顾客无法分配到收银员(客户端连接被阻塞)
2) 即使收银员数量是充足的,但是收银员只有在收银台才能和顾客交互,所有的收银员只能轮流使用收银台(CPU 时间片),而收银台数量是有限的,且一个收银员用完收银台换下一个收银员用时必须要进行一些交接工作(线程上下文切换),那么当收银员数量多到一定程度时可能花在交接收银台上的时间比真正使用收银台的时间还要多
2. 收银台采用非阻塞式
优点
只需要一个或少数几个收银员就可以服务大量顾客
缺点
1) 通常来说和厨房打交道时就只能选异步方式了,当然这个没有任何强制,开发者也可以自行选择同步方式,但是如果业务是耗时操作,那带来的灾难就远大于阻塞式,因为这时不单阻塞了一个顾客,而是阻塞了所有顾客,这其实是一种典型的误用,我个人觉得真出现了这种情况,需要为之负责的是开发者自己,但是却有人觉得这是非阻塞式 IO 的缺陷,我是难以认同的
2) 协议解析、业务处理等部分不能再站在收银员的角度,即收银员无权主动要求读下一块数据,只能被动地接收数据,由顾客来驱动,这有点违反直觉,在写一些复杂协议的解析时需要人工改写为状态机,这也是有的人不习惯使用非阻塞 IO 的原因,无法扭转这个视角
回到你的问题来,我没深入研究过负载均衡类系统,所以不敢妄下结论,只能说说自己的猜测,即如果我自己来实现一个负载均衡系统的话会选择怎么做
还是老话,IO 和业务独立分析,IO 部分既然它们自己说了是非阻塞那就是非阻塞了,所以重点看业务部分,作为一个负载均衡系统,假设它的业务就是选择合适的后端->透传数据,从这个角度讲其实就是把两个反向的 IO 模块背靠背连接起来,并且以无状态的方式透传数据,那么可以认为它的业务是非常轻量的,不好用厨师的例子来做类比