前言
近期看了很多中间件的文章,RocketMQ,Dubbo 这些中间件内部的rpc通信都用的是非阻塞的模型。(Netty),这里从 Socket 的角度总结一下。
1. 阻塞模型
一个线程处理socket的连接和读写,如果使用的是阻塞IO,程序会在读取前后傻傻的呆着。比如:
- 文件上传
服务端阻塞读,没数据就等着。由于存在着TCP的拥塞控制,会产生空闲时间 - 文件下载
服务端阻塞写,网卡有其他数据排队就等着。
2. 非阻塞模型
引入 Netty 的主从 Reactor 模型实现,使用上述例子,处理流程大致如下
- boss 线程 (如图一个线程,4个事件在循环)
- 单线程
- 入队文件上传 socket,事件循环不断捞出可连接的 socket
- 入队文件下载 socket,事件循环不断捞出可连接的 socket
- 连接 文件上传 socket 完成,交给worker 线程池 (可换序)
- 连接 文件下载 socket 完成,交给worker 线程池(可换序)
- 单线程
- worker 线程池 (如图2个线程,每个线程各有2个事件在循环)
- 默认是 CPU * 2 的线程数量
- 文件上传 socket 客户端数据准备就绪,申请读(可换序)
- 文件下载 socket 服务端数据准备就绪,申请写(可换序)
- 读/写 socket (可并发)
- 释放 socket
- 默认是 CPU * 2 的线程数量
- worker 与 boss 中的 socket 后续同时可以被虚拟机回收。
2.1 Reactor 模型优势
- 同样的请求数,线程数减少了,不需要同步或文本切换带来的低开销。
- 默认基于 Tcp 而不是 Http,对于开发RPC协议是很轻量的选择。
- 方便模块化,能写出形如以下的声明式代码。
// 主从 Reactor 模式
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializerImpl());
// worker 中的流水线定义
ch.pipeline().addLast(
new HttpServerCodec(),
new HttpObjectAggregator(65536),
new WebSocketServerProtocolHandler("/websocket"),
new TextFrameHandler(),
new BinaryFrameHandler(),
new ContinuationFrameHandler());
2.2 Reactor 模型劣势
- 编程模型复杂。
- 业务开发应用场景少。
后记
非阻塞模型可以将 Socket 的连接和读写过程拆开,读写时不阻塞(数据准备好了再操作,而不用傻傻等着)。优化了IO等待时间。当然更深层次的原因是使用了 Linux 的 epoll 指令,该指令能让操作系统做到及时发送 “数据准备就绪” 的通知,可以让 Socket 立马到就绪状态,原本的IO等待时间就可以空出来给操作系统做其他事情 。