由Scalable IO引发的思考

前言

前面我们一起研究了NIO,了解了NIO对TCP通信过程做了哪些优化,以及这些优化的实现原理。今天我们一起来看一看在利用NIO进行应用开发时,线程模型的演化。让我们一起来学习一下Java并发包的作者Doug Lea的Scalable IO in Java。

一起读一读

NIO通过操作系统的epoll机制,帮助用户在通信条件就绪时能够获得相应的Event。帮助上层应用准确的调度线程启动计算。这种模式跟我们接触过的很多设计都很相似,比如在Java图形界面编程中AWT通过界面的按钮触发相应事件从而完成功能计算。

 

这种事件驱动计算执行的模式就是响应模式(Reactor Pattern)。一个最简单的响应式线程模型如下:

 

在响应模式中有以下几个要素: 

  • 事件(Event)。事件是计算条件就绪的信号,他是所有计算行为的起点。不同的事件会驱动不同的计算执行。事件都是由不同的客户端行为生成的。

  • 响应器(Reactor)。响应器是计算的协调和调度者。是事件和计算行为之间的桥梁。Reactor收集所有的事件,同时根据不同的Event调用不同的处理器进行处理。所以他又相当于任务收集与分发(dispatch)角色。

  • 处理器(Handler)。处理器是具体任务的执行,所有的事件处理逻辑都集成在Handler内部。

从上面的过程描述可以看出在Reactor内部存在这样一个while循环,不停的探测事件,同时根据不同的事件new出对应的Handler去process事件。对于网络IO来说事件主要有如下几种:

  • ACCEPT事件。对应着客户端的连接请求,TCP三次握手。

  • READ事件。对应着客户端的数据发送条件据需。(客户端断开连接后也会不停触发READ事件,这个不再赘述)。

  • WRITE事件。对应着服务端向客户端发送数据条件就绪。

在READ和WRITE之间就是我们的业务处理过程PROCESS,这个不需要事件触发。通常情况下我们的通信过程运行上面的处理循环中就OK了。到目前为止,我们的所有处理逻辑运行在一个线程当中。但是现实中我们常常面临着客户端的并发压力,因此我们的模型需要进行改变。解决并发计算问题的惯用手段就是分治(Devide and Conquer),加多线程。这就是我们的业务逻辑多线程模型。

 

加多线程只是解决问题的指导思想,但是具体落地实施时是有一些讲究的。多线程应该怎么加,在哪个粒度加是有讲究的。一个请求的处理链路可以看作是read,decode,compute,encode,send等多个处理节点(Handler)串联而成。有些节点在事件响应模式下造成性能问题的可能性是比较小的,而有些节点是重型计算节点。而整个机器的线程计算资源是有限的,我们需要将有限的资源分发给最需要的模块,这样才能最大可能的提高整机的吞吐。从上图中可以看出,我们将多线程提供给了业务逻辑处理节点上。因为这部分的计算压力是比较大的。这一个改进的实现比较容易的,只要将业务处理的部分通过线程池提交,同时在线程处理完毕后把通信的SocketChannel重新注册Send事件,这样Reactor就能够重新dispatch send任务了。

同理,如果随着业务并发的增长,计算瓶颈逐渐转移到read和send节点上(此时我们的Reactor还是单线程执行)。我们就需要在Reactor上加持多线程资源。这部分的改进稍微麻烦一点,因为事件是通过注册和探测实现的。事件必须注册(绑定)到特定的Selector,后续的响应也由对应的Selector探测分发。因此如果维持一个Selector的状态,探测分发模块将无法进行分治。所以需要引入多个Selector,在注册事件时可以采取随机或者轮转的方式进行注册分治。每个Selector运行在独立的探测线程中,从而达到分治的目的。这个模型就是主从Reactor的雏形。

 

在Reactor内部创建多个Selector,为每个Selector创建单独的运行线程。这种模式也是向重型节点进行资源分配倾斜的分治思路。因为accept相对read和send来说是一个及其轻量的操作,不会出现性能问题。还是那句话,线程资源是有限的,只有好钢用在刀刃上才能尽可能的提升整台机器的吞吐。通过将accept和read,send的Selector分离,实现数据传输的异步化和多线程化,这就是主从Reactor模式。负责ACCEPT逻辑的Reactor被称为主Reactor,负责read和send的称为从Reactor的模式。从Reactor可以是多实例多线程运行的,从而具备可伸缩的扩展能力(而主Reactor是不行的,因为ServerSocketChannel只有一个只能单线程运行,对于Accept来说也不需要,这个操作很轻量)。主从Reactor线程模型如下:

从上面的演化过程我们可以看出,整个模型的演变和优化过程就是不停的细化拆解每个模块。不断将重型节点划分出来形成独立运行模块。寻找计算模型中的性能瓶颈点,进行模块隔离,进行计算资源倾斜配置。这就是分治法最重要的落地原则。

一些思考

模型认知是重要的知识沉淀。从上面的介绍可以看出React模型是一个优秀的线程模型。在不同的性能场景下都可以方便的进行扩展优化。我常常思考,一个经验丰富的工程师比一个小鲜肉工程师到底应该强在哪里。对优秀模型认知理解一定是非常重要的一项。如果我们仔细去研究,一定会发现很多优秀的框架本质的模型抽象是非常惊人的相似(比如TensorFlow的OP流和Flink的算子流非常相似,都是流水线的解题思路)。对这些优秀模型的认识和思考对我们去面对新的问题会有很大的帮助。 

程序设计是一门管理科学。Reactor是一种优秀的IO模型,扩展性非常强。我觉得这更像是一种IO场景下的线程管理模式。我们在程序设计的过程中,一定碰到过当需求发生部分扩散之后,整个架构需要进行翻天覆地的改变才能满足新的功能需求。在我看来,这是设计者没有能够管理好我们的工程,没有划分好功能模块,没有为我们面对的需求设计好一套相互独立,有序配合的执行流程和协作机制。试想一下,如果今天没有MVC分层思想,让我们来写一个Web应用,我想大部分人会苦不堪言。因为MVC思想本质上是针对Web场景的工程管理思路,他对Web应用设计了明确角色层次,职责划分和协作依赖准则。这跟管理世界里的组织架构设定,部门职能分配,人员分工和组织协同配合是一个道理。

欢迎关注个人公众号讨论交流。ScalableIO in Java的原文可以到我的Git上进行下载:https://github.com/SharkWater/blog.git

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章