一、同步异步,阻塞非阻塞讲起,常见线程模型设计,
二、JAVA BIO & NIO
三、SOFABolt 中对 Netty 的模型使用,Reactor
三、详解 SOFARPC 在一次调用过程中各个步骤执行的线程。
一、几种常见的 IO 模型
Linux 几种 IO 模型,进程从 Socket 中读取数据为例。进程最终通过 recvfrom 系统调用来读取数据。系统内核收到后, IO 模型不同,处理不同。
1. 阻塞 I/O(红色表示阻塞时间)
最流行,最简单易I/O 模型,使请求进程阻塞,直到请求完成或出错。
2. 非阻塞 I/O
如果 I/O 操作会导致请求进程休眠,不要把它挂起,不会让出 CPU,返回错误告诉它(可能是 EWOULDBLOCK 或者 EAGAIN)。
3. I/O 复用
I/O 多路复用(I/O multiplexing)用 select 或 poll 或 epoll 函数,这几个函数也会使进程阻塞,和阻塞 I/O 所不同:函数可同时阻塞多个 I/O 操作。且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。
4. 信号驱动式 I/O
内核在描述符就绪时发送 SIGIO 信号通知我们进行处理,开始真正的读。
5. 异步 I/O
由 POSIX 规范定义,包含一系列以 aio 开头的接口。工作机制:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核空间拷贝到用户空间)完成后通知我们。
与信号驱动模型主要区别是:信号驱动 I/O由内核通知何时可以启动 I/O 操作,异步 I/O 模型是由内核通知我们 I/O 操作何时完成。
五种常见的 IO 模型汇总
二、JAVA BIO & NIO
1. JAVA BIO
直接使用 JAVA BIO 写得服务端:传统BIO里socket.read(),如TCP RecvBuffer里没有数据,调用会一直阻塞,直到收到数据,返回读到的数据。
2. JAVA NIO
如果 TCP 的 buffer 中有数据,把数据从网卡读到内存,返回给用户;反之直接返回 0,永远不会阻塞。典型的 NIO 的处理代码:
NIO 和多路复用结合。是最简单的 Reactor 模式:注册所有感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。新 NIO 好处:
(1)事件驱动模型
(2)单线程处理多任务
(3)非阻塞 I/O,I/O 读写不再阻塞,而是返回 0
(4)基于块的传输,比基于流的传输更高效
(5)更高级IO 函数,零拷贝
(6)允许 IO 多路复用
三、Reactor 线程模型
诞生场景:直接用 NIO多路复用,技术问题解决,工程层面,高效没问题架构依然难。因此有 Reactor 编程模型。
原因:I/O 复用机制需事件分发器,太简单,实际有性能问题。目前比较流行的是 Reactor 和 Proactor。标准/典型的 Reactor 中定义了三个角色:
步骤1:等待事件到来(Reactor 负责)。
步骤2:将读就绪事件分发给用户定义的处理器(Reactor 负责)。
步骤3:读数据、然后处理数据(用户处理器负责)。
Reactor 重点是 IO部分:建立连接和 IO读写
1. 单线程模型
所有IO操作都在同一个NIO线程上完成,职责:
NIO 服务端,接收客户端 TCP 连接;
NIO 客户端,向服务端发起 TCP 连接;
读取通信对端的请求或者应答消息;向通信对端发送消息请求或者应答消息。
Reactor 线程:多路分离套接字,新连接到来触发 connect 事件之后,交由 Acceptor 进行处理,有 IO 读写事件之后交给 hanlder 处理。
Acceptor 主要任务:构建 handler ,获取和 client 相关的 SocketChannel 之后 ,绑定到相应 handler,对应的 SocketChannel 有读写事件之后,reactor 分发hanlder 处理(所有 IO 事件都绑定到selector 上,由 Reactor 分发)。
场景:快速完成的场景,实际使用不多,单线程模型不能充分利用多核资源,。
2. 多线程模型
与单线程模型最大区别:将 IO 操作和非 IO 操作做了分离,效率提高
有专门NIO线程-Acceptor 线程用于监听服务端,接收客户端的 TCP 连接请求;
网络 IO 操作-读、写等由单独的 NIO 线程池负责,JDK 线程池实现,包含一个任务队列和 N 个可用线程,这些 NIO 线程负责消息的解码、处理和编码;
3. 主从多线程模型
大部分 RPC 框架,服务端处理主要选择。
服务端独立的 NIO 线程池接收客户端连接,不再是1个单独 NIO 线程
流程:
MainReactor 将连接事件分发给 Acceptor
Acceptor 接收到客户端 TCP 连接请求处理完成后(可能包含接入认证,黑名单等),将新创建的 SocketChannel 注册到 IO 线程池(sub reactor线程池)的某个IO线程上,Acceptor 线程池仅用于:客户端登陆、握手和安全认证。
SubReactor 负责 SocketChannel 的读写和编解码工作。其 IO 线程负责后续的 IO 操作。
四、 SOFARPC 线程模型
SOFARPC 和底层的 SOFABolt 一起,使用 Netty 的 Reactor 主从模型的基础上,支持业务线程池的选择。
4.1线程模型
压测数据支撑情况下,选主从线程模型,
BizThreadPool(自定义线程池):对序列化和业务代码执行使用 (可对核心线程数(线程池),队列等调整)。提高系统整体吞吐量。
对 header部分,将反序列化放 Worker线程中,在对性能影响极低的情况下,提供好处:允许业务配置接口对应的线程池。
4.2默认执行步骤
完整 RPC 调用默认执行线程:
(1)客户端
长连接:Netty-Worker 线程
序列化请求/反序列化响应:发起请求的线程,如果是 callback,是新线程
心跳:Netty-Worker 线程
(2)服务端
端口:Netty-Boss 线程
长连接:Netty-Worker 线程
心跳:Netty-Worker 线程
反序列化请求Header:Netty-Worker 线程
反序列化请求Body/序列化响应:SOFARPC 业务线程池
4.3自定义业务线程池
SOFARPC 支持自定义业务线程池,可以为指定服务设置一个独立的业务线程池,和 SOFARPC 自身的业务线程池是隔离的。多个服务可以共用一个独立的线程池。
4.4实现原理
自定义线程池管理器封装服务接口和自定义线程池映射关系,用户创建配置自定义线程池,提供指定服务注册自定义线程池。
BOLT 支持部分反序列化,所以框架会在 IO 线程池提前反序列化请求的 Header 头部数据,注意,这部分一个普通的 Map,操作很快,一般不会成为瓶颈,Body 数据还是在业务线程内反序列化。
核心代码在自定义线程池管理器里:
com.alipay.remoting.rpc.protocol.RpcRequestProcessor#process 选择线程池
UserThreadPoolManager 注册线程池。
4.5使用方式
请求处理过程,默认是一个线程池(造成整体吞吐量降低)。而有些业务场景,希望对核心的请求处理过程单独分配一个线程池。SOFARPC 提供线程池选择器设置到用户请求处理器里,调用过程即可根据选择器逻辑来选择对应的线程池避免不同请求互相影响。
通过sofa:global-attrs元素的 thread-pool-ref 属性为该服务设置自定义线程池。
总结
常见的 IO 模型,JAVA 中的 IO和 NIO,IO 模型在工程上实践不错的 Reactor 模型。
https://mp.weixin.qq.com/s/yEu1RedULcljHsyY--F0Ww