Netty-5-线程模型
我们所选择的网络编程模式的内部有多少线程在为我们服务,以及它们是如何协同完成工作的。
上述问题都属于’'线程模型"的范畴,了解“线程模型”对于性能调优至关重要。
NIO 的 3 种 Reactor 模式
Netty的线程模型在本质上与我们选择的网络编程模式息息相关。下面仍然以现实生活中的饭店场景为例进行类比学习。当我们经营一家饭店时,内部分工可能随着饭店规模的扩大而发生不同的变化。
-
一人包揽所有工作:刚开始时,饭店里可能只有你一个人。你需要包揽所有的工作, 包括迎宾、点菜、做菜、上菜、送客等,着实辛苦。
-
招几个伙计一起做事:后来,饭店的规模扩大了,你会招儿个伙计来做事。
-
设立迎宾岗:随着饭店规模的进一步扩大,你发现迎宾非常重要。只要把客人招揽进来,即使上菜稍微慢点,也不会有太大问题;但如果因为比较忙而没有把顾客招揽进来,则得不偿失。因此,你对饭店内部做了进一步分工,从原来的伙计当中挑选或者直接招聘几个颜值比较高的员工,他们专门负责迎宾。
与Reactor的3种模式一一对应:
一人包揽所有工作的方式相当于Reactor单线程模式;
招几个伙计一起做事的方式相当于Reactor多线程模式;
而对饭店内部进行进-步分工, 安排一人或多人专门迎宾的方式就相当于Reactor主从多线程模式。
Reactor的核心流程比较简单,就是注册通道感兴趣的事件到多路复用器,然后由多路复用器判断是否有感兴趣的事件发生。
如果发生了,就做出相应的处理。不同通道感兴趣的事,对于不同类型的通道;监听的事件也不完全相同。
对于客户端的SocketChannel,监听连接、读、写三种事件。
对于服务器端的ServerSocketChannel,则只监听连接事件;而对于服务器端的SocketChannel,则仅监听读、写两种事件,不需要监听连接或被连接事件。
我们有必要先了解一下与BIO对应的thread per connection开发模式。
thread per connection开发模式本身要解决的是I/O阻塞的问题,解决方案是一个线程负责一个连接。
对于同一个连接,读数据、解码、处理、编码、写数据都由同一个线程处理,但BIO读写是阻塞操作,因而问题显而易见:阻塞的连接越多,占用的线程就越多。
即使使用控制线程数量的线程池,也只缓解了线程无限增多的问题,但代价是又多了一种阻塞——等待线程的阻塞。
Reactor单线程模式
Reactor单线程模式比较简单,接收连接、处理读写操作、注册事件、扫描事件等所有操作都由一个线程来完成。显而易见,这个线程将“疲惫不堪”,这种模式很容易成为瓶颈。
Reactor多线程模式
Reactor多线程模式弥补Reactor单线程模式中的缺陷,解码、处理、编码等比较复杂且耗时较长的操作将由线程池来做,显而易见,效率相比Reactor单线程模式提高了很多。
Reactor主从多线程模式
Netty对3种Reactor模式的支持
如何在Netty中使用这3种Reactor模式
Reactor单线程模式
EventLoopGroup eventGroup = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);
Reactor多线程模式
EventLoopGroup eventGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);
Reactor主从多线程模式
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
- Reactor单线程模式需要显式构建一个线程数为1的NioEventLoopGroup,然后传递给ServerBootstrap 使用。
- Reactor多线程模式使用默认的构造器构建NioEventLoopGroup (不能显式指定线程数为1),因为这种Reactor模式会根据默认的CPU内核数对线程数进行计算。如今,使用单核CPU的服务器己经很难再看到了,因此计算出来的线程数肯定大于1。
- Reactor主从多线程模式需要显式地声明两个group,根据命名习惯,它们分别名为boss group和worker group。boss group负责接纳、分配工作,其实也就是接收连接并把创建的连接绑定到 worker group (NioEventLoopGroup)中的一个 worker (NioEventLoop)上,这个worker本身用来处理连接上发生的所有事件。
但在Netty中,它们的线程池是能够处理I/O事件的。 无论是否完全匹配,Reactor主从多线程模式都是项目的首选模式。
Netty在内部是如何支持Reactor模式的
以Reactor主从多线程模式为例,下面分析Netty在内部是如何支持Reactor模式的。
在本质上,Netty所做的其实就是将两种通道分别注册到两种独立的Selector中。
以NIO编程为例, 这两种独立的Selector都是NioEventLoop。每一个NioEventLoop都包含一个Selector,而每一个NioEventLoop又可以当作线程,因此对Reactor模式提供支持其实就是将两种通道分别绑定到两个独立的多线程组。
接下来,我们分析一下Netty是如何对Reactor实现中的几个关键步骤进行支持的。
创建主从Selector
支持Reactor主从多线程模式的第一步是创建主从Selector 在Netty中,这实际上就是创建两种类型的NioEventLoopGroup。对于每个group,这又会创建若干NioEventLoop。
//NioEventLoop.java
// NioEventLoop类的构造方法
// 参数:
// parent:父NioEventLoopGroup对象
// executor:执行器
// selectorProvider:选择器提供者
// strategy:选择策略
// rejectedExecutionHandler:任务拒绝处理器
// taskQueueFactory:任务队列工厂
// tailTaskQueueFactory:尾部任务队列工厂
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
EventLoopTaskQueueFactory taskQueueFactory, EventLoopTaskQueueFactory tailTaskQueueFactory) {
super(parent, executor, false, newTaskQueue(taskQueueFactory), newTaskQueue(tailTaskQueueFactory),
rejectedExecutionHandler);
this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider");
this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy");
// 打开一个选择器
final SelectorTuple selectorTuple = openSelector();
this.selector = selectorTuple.selector;
this.unwrappedSelector = selectorTuple.unwrappedSelector;
}
private SelectorTuple openSelector() {
final Selector unwrappedSelector;
try {
// 使用provider对象打开一个选择器
unwrappedSelector = provider.openSelector();
} catch (IOException e) {
// 抛出ChannelException异常,指示打开一个新的选择器失败
throw new ChannelException("failed to open a new selector", e);
}
}
selectorProvider由NioEventLoopGroup 传入,默认使用的是 SelectorProvider.provider()方法的返回值。
有了 selectorProvider之后,就调用openSelector()方法来创建一个Selector以提供注册功能。
另外,创建的这个Selector会作为NioEventLoop的unwrappedSelector成员变量。
由此可见,Selector和NioEventLoop之间确实存在一一对应关系,考虑到每个NioEventLoopGroup又有多个NioEventLoop,最终我们不是创建两个Selector,而是创建两组Selector。因为如果仅仅创建两个Selector,最终可能存在性能瓶颈。 回到创建SelectorProvider的过程,SelectorProvider.provider()方法的具体实现代码。
static SelectorProvider provider() {
// 创建一个PrivilegedAction<SelectorProvider>对象,用于执行PrivilegedAction动作
PrivilegedAction<SelectorProvider> pa = () -> {
SelectorProvider sp;
// 从属性中加载SelectorProvider对象
if ((sp = loadProviderFromProperty()) != null)
return sp;
// 从服务中加载SelectorProvider对象
if ((sp = loadProviderAsService()) != null)
return sp;
// 使用默认的SelectorProvider对象
return sun.nio.ch.DefaultSelectorProvider.get();
};
// 使用AccessController执行PrivilegedAction动作,并返回结果
return AccessController.doPrivileged(pa);
}
除非显式地指明要使用哪种Provider,否蚓SelectorProvider会使用sun.nio.ch.DefaultSelectorProvider.create()方法创建一个SelectorProvider实例。
创建 ServerSocketChannel
在有了两种类型的Selector之后,我们就可以注Channel 了。但在此之前,需要创建通道。 当通过执行Serverbootstrap的bind 方法来启动服务器程序时,最终会调用AbstractBootstrap的initAndRegister()方法。
//AbstractBootstrap.java
public ChannelFuture bind() {
validate();
SocketAddress localAddress = this.localAddress;
if (localAddress == null) {
throw new IllegalStateException("localAddress not set");
}
return doBind(localAddress);
}
private ChannelFuture doBind(final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
...
}
final ChannelFuture initAndRegister() {
Channel channel = null;
try {
channel = channelFactory.newChannel();
init(channel);
} catch (Throwable t) {
if (channel != null) {
// 如果Channel不为null,则尝试强制关闭Channel
channel.unsafe().closeForcibly();
// 由于Channel尚未注册,需要强制使用GlobalEventExecutor
return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
}
// 由于Channel尚未注册,需要强制使用GlobalEventExecutor
return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
}
// 注册Channel到Group
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
// 如果注册失败,则强制关闭Channel
// 如果注册成功,则可以安全地进行bind()或connect()操作,因为Channel已经注册
// 如果注册被安排在其他线程执行,则可以安全地进行bind()或connect()操作,因为bind()或connect()将在注册任务之后执行
// 这是因为register()、bind()和connect()都绑定在同一个线程上
return regFuture;
}
在上述代码中,channelFactory.newChannel()方法用来创建ServcrSocketChannel,具体是通过 ReflectiveChannelFactory 来实现的。如代码清单 6-5 所示,ReflectiveChannelFactory 的实现用到了反射、泛型等技术,创建的SocketChannel类型则由AbstractBootstrap#channel方法指定。
/**
* 反射型通道工厂类
*/
public class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T> {
private final Constructor<? extends T> constructor;
/**
* 构造方法
* @param clazz 通道类
*/
public ReflectiveChannelFactory(Class<? extends T> clazz) {
ObjectUtil.checkNotNull(clazz, "clazz");
try {
this.constructor = clazz.getConstructor();
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Class " + StringUtil.simpleClassName(clazz) +
" does not have a public non-arg constructor", e);
}
}
/**
* 创建通道对象
* @return 创建的通道对象
* @throws ChannelException 通道创建失败时抛出异常
*/
@Override
public T newChannel() {
try {
return constructor.newInstance();
} catch (Throwable t) {
throw new ChannelException("Unable to create Channel from class " + constructor.getDeclaringClass(), t);
}
}
/**
* 返回该对象的字符串表示
* @return 对象的字符串表示
*/
@Override
public String toString() {
return StringUtil.simpleClassName(ReflectiveChannelFactory.class) +
'(' + StringUtil.simpleClassName(constructor.getDeclaringClass()) + ".class)";
}
}
注册 ServerSocketChannel 给主 Selector
ChannelFuture regFuture = config().group().register(channel);
group()此时指的就是 boss group。
//MultithreadEventLoopGroup.java
@Override
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
这其实就是从boss group中选择(通过调用next 方法)一个NioEventLoop进行注册,最终执行的代码如下。
javaChannel() .register(eventLoop().unwrappedSelector(), 0, this);具体的执行逻辑就是注册ServcrSocketChannel 到 NioEventLoop 中的 unwrappedSelector 成员变量上。
创建 Socketchannel
至此,我们己经完成主Selector的构建以及ServerSocketChannel的创建和注册,因此可以接收连接了。
在服务器接收到连接之后,创建连接的过程就是创建Socketchannel
Socketchannel ch = SocketUtils.accept(javaChannel());
为 Selector 注册 Socketchannel
在有了代表连接实体的SocketChannel之后,我们就可以为Selector SocketChannel,使用的是 ServerBootstrap.ServerBootstrapAcceptor#channelRead 方法,这个方法负责对创建后的连接执行如下语句以完成注册。
childGroup. register (child)至此,我们展示了 Netty是如何对Reactor主从多线程模式进行支持的。
Netty支持跨平台, 并且通过判断是否只有一个NioEventLoopGroup来决定是否使用Reactor主从多线程模式,而
通过判断NioEventLoopGroup中有多少个元素来控制是否采用多线程。
实战实现线程模型
使用Reactor主从多线程模式
下面首先来完成服务器端代码
ServerBootstrap serverBootstrap = new ServerBootstrap();
EventLoopGroup bossGroup = new NioEventLoopGroup ();
EventLoopGroup workerGroup = new NioEventLoopGroup();
serverBootstrap.group(bossGroup, workerGroup);
下面再来看看客户端代码
NioEventLoopGroup group = new NioEventLoopGroup ();
bootstrap.group(group);
使用独立线程池
在演示如何进行优化之前,我们首先实现业务层处理程序,并更改业务处理,使之具有I/O型业务的特点——等待时间较长。
/**
* 订单服务器处理器类,继承自SimpleChannelInboundHandler类
*/
public class OrderServerProcessHandler extends SimpleChannelInboundHandler<RequestMessage> {
/**
* 重写channelRead0方法,处理接收到的消息
*
* @param ctx ChannelHandlerContext对象,表示通道处理上下文
* @param requestMessage RequestMessage对象,表示接收到的请求消息
* @throws Exception 异常信息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, RequestMessage requestMessage) throws Exception {
// 获取请求消息的操作对象
Operation operation = requestMessage.getMessageBody();
// 执行操作并获取操作结果
OperationResult operationResult = operation.execute();
// 创建响应消息对象
ResponseMessage responseMessage = new ResponseMessage();
// 设置响应消息的头部信息为请求消息的头部信息
responseMessage.setMessageHeader(requestMessage.getMessageHeader());
// 设置响应消息的 body 为操作结果
responseMessage.setMessageBody(operationResult);
// 判断通道是否可写
if (ctx.channel().isActive() && ctx.channel().isWritable()) {
// 将响应消息写入通道并刷新
ctx.writeAndFlush(responseMessage);
} else {
log.error("不可写,消息丢失");
}
}
}
接下来,选择核心的业务操作0rderOperation并进行修改(添加3秒的“等待时间”),从而模拟I/O型业务。
public class OrderOperation extends Operation implements Serializable {
// 订单操作类,继承自Operation类,并实现了Serializable接口
private int tableld; // 表编号
private String dish; // 菜品名称
@Override
public OrderOperationResult execute() {
// 执行订单操作的主体方法
log.info("norder's executing startup with orderRequest: " + toString());
// 记录日志,打印订单请求信息
Uninterruptibles.sleepUninterruptibly(3, TimeUnit.SECONDS);
// 线程暂停3秒
log.info("order's executing complete");
// 记录日志,订单执行完成
OrderOperationResult orderResponse = new OrderOperationResult(tableld, dish, true);
// 创建并返回订单操作结果对象
return orderResponse;
// 返回订单操作结果
}
}
最后,把业务处理器添加到处理程序流水线中。
pipeline.addLast(new OrderServerProcessHandler());
在服务器端,最终只需要绑定一个端口并启动即可,实现服务器端。
// 创建一个ServerBootstrap对象
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 设置通道类型为NioServerSocketChannel
serverBootstrap.channel(NioServerSocketChannel.class);
// 设置处理器为LoggingHandler,记录日志级别为INFO
serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
// 创建 bossEventLoopGroup 对象,使用 NioEventLoopGroup
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 创建 workerEventLoopGroup 对象,使用 NioEventLoopGroup
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 设置服务器的 boss 线程组和 worker 线程组
serverBootstrap.group(bossGroup, workerGroup);
// 设置 Channel 的初始化处理器为 ChannelInitializer
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 获取通道的 pipeline 对象
Channelpipeline pipeline = ch.pipeline();
// 添加 OrderFrameDecoder 组件到 pipeline 的头部
pipeline.addLast(new OrderFrameDecoder());
// 添加 OrderFrameEncoder 组件到 pipeline 的尾部
pipeline.addLast(new OrderFrameEncoder());
// 添加 OrderProtocolEncoder 组件到 pipeline 的尾部
pipeline.addLast(new OrderProtocolEncoder());
// 添加 OrderProtocolDecoder 组件到 pipeline 的头部
pipeline.addFirst(new OrderProtocolDecoder());
// 添加 OrderServerProcessHandler 组件到 pipeline 的尾部
pipeline.addLast(new OrderServerProcessHandler());
}
});
// 绑定服务器端通道到 8090 端口,并同步等待绑定操作完成
ChannelFuture channelFuture = serverBootstrap.bind(8090).sync();
// 获取绑定的通道,并同步等待通道关闭
channelFuture.channel().closeFuture().sync();
} finally {
// 关闭 bossEventLoopGroup 和 workerEventLoopGroup
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
实现客户端代码。
Bootstrap bootstrap = new Bootstrap(); // 创建一个Bootstrap对象
bootstrap.channel(NioSocketChannel.class); // 设置通道类型为NioSocketChannel
NioEventLoopGroup group = new NioEventLoopGroup(); // 创建一个NioEventLoopGroup对象
try{
bootstrap.group(group); // 设置事件循环组为当前的NioEventLoopGroup对象
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() { // 设置初始化器为一个ChannelInitializer对象
@Override
protected void initChannel(NioSocketChannel ch) throws Exception { // 初始化NioSocketChannel通道
ChannelPipeline pipeline = ch.pipeline(); // 获取通道的处理管道
pipeline.addLast(new OrderFrameDecoder()); // 添加一个OrderFrameDecoder处理器到处理管道的最后
pipeline.addLast(new OrderFrameEncoder()); // 添加一个OrderFrameEncoder处理器到处理管道的最后
pipeline.addLast(new OrderProtocolEncoder()); // 添加一个OrderProtocolEncoder处理器到处理管道的最后
pipeline.addLast(new OrderProtocolDecoder()); // 添加一个OrderProtocolDecoder处理器到处理管道的最后
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8090); // 建立到指定主机和端口的连接
channelFuture.sync(); // 等待连接完成
RequestMessage requestMessage = new RequestMessage(IdUtil.nextId(), new OrderOperation(1001, "tudou")); // 创建一个RequestMessage对象
channelFuture.channel().writeAndFlush(requestMessage); // 向连接通道写入请求消息
channelFuture.channel().closeFuture().sync(); // 等待通道关闭
} finally {
group.shutdownGracefully(); // 关闭事件循环组
}
I/O事件处理和业务处理共享同一个线程池(worker group) 此时,修改线程模型,将业务处理线程独立出来。
UnorderedThreadPoolEventExecutor businessGroup = new UnorderedThreadPoolEventExecutor(10);
//省略其他非核心代码
pipeline.addLast(businessGroup, new OrderServerProcessHandler());
常见疑问
当前的Netty线程模型能支持多少个连接
传统的thread per connection开发模式无法在单台机器上支持海量的连接,这就是人们常说的C10K问题。
解决这个问题的关键就在于引入Reactor模式,那么支持Reactor模式的Netty线程模型到底能支持多少个连接呢?
这个问题其实并非Netty专属,对于网络编程框架,都可能存在类似的问题。
解决此类问题的关键是我们需要先知道单个客户端到底能创建多少个连接。
以Linux平台为例。连接是由客户端IP 客户端端口、服务器IP 服务器端口唯一标识的,其中服务器IP和服务器端口一般是固定的,因此对于能够支持多少个连接,我们可以进行简单的估算。
单个客户端可以创建的连接数
理论值取决于本地可用端口的数量,在不考虑其他应用端口占用的情况下,大约为64511,计算方式如下。
65535 (报文中端口占用的字节数是16,所以本地可用端口的最大数量为65535) - 1024 (保留端口数)
实际值主要受以下3个因素的影响。
- TCP 层:可调整的 ipjocaljiort raiige 配置(参考/proc/sys/nct/ipv4/ip_local_port_range),调整范围即为可用端口数。
- 系统限制可调整的最大文件句柄数(参考/etc/security/Iimits.conf),最大为21亿。
- 资源限制:诸如内存等资源是有限的,而连接本身也是需要占用资源的(Netty本身的Socket相关对象也要占用JVM)。
在综合考虑理论和实际情况之后,我们得出如下结论:如果把配置调整得足够大,并且资源充足(实际上,即使64 511个连接也并不占用多少资源),单个客户端所能创建的连接数是可以达到理论值的。
Netty的NioEventLoopGroup为什么不改为默认只有一个线程
存在需要使用多个线程的场景
并不是所有的应用程序都只监听一个端口。在一个应用需要监听多个端口,而boss group又共亨的情况下,你就可以使用boss group中的多个线程。比如用于Http服务器的9864端口和用于HTTPS服务器的9865端口。
NioEventLoopGroup 是共用的
boss group在本质上就是NioEventLoopGroup,并且与worker group是共用的。
因此,如果修改boss group的构造器,就等于修改worker group的默认行为,而worker group在多数情况下是需要使用多个线程的。
综合以上因素,NioEventLoopGroup的默认线程数不能改为1。另外,用不上的NioEventLoop并没有真正启动线程工作,因而并无太大影响。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!