Netty原理源码总结01-异步,BIO,NIO
Netty是Java网络一个大杀器,在很多行业都有广泛的应用,连Spring也在5版本提供了默认基于Netty的webflux,总之深入学习一下Netty一定会受益良多
Netty,异步
说到Netty往往不能避开Java的io模型,一般的文章会先说到BIO,再说到NIO,但是这里我想要先说一下异步这个概念.
对于开发者来说,异步是一种程序设计的思想,使用异步模式设计的程序可以显著减少线程等待,从而在高吞吐量的场景中,极大提升系统的整体性能,显著降低时延。
为什么异步能极大提升系统整体性能呢?我用一个钱包系统来举例.
同步
假定一个转账系统,同步的核心代码如下:
public void transfer(accountFrom, accountTo, amount){
add(accountFrom,-amount);//黑盒,耗时50ms
add(accountTo,amount);
}
上面的伪代码从 accountFrom 的钱包扣除 amount,再把 amount 转到 accountTo 的钱包里面去,这是同步的实现方式,但是性能如何呢?
假设微服务 add 的平均响应时间是50ms,那么整个 transfer 的耗时就是100ms,也就是说一个线程一秒可以处理十笔转账请求,假设服务器线程最多是100,也就是说,整个服务器,每秒能处理1000个请求,超出的请求就只能进入阻塞,或者等待延时了.
但是1000是不是系统极限呢,如果我们监测一下服务器的各项指标,会发现服务器没有一项是到了性能瓶颈的.这是因为, transfer 方法虽然耗时100ms,但是真正用在发送,接收和处理数据的时间都很短,绝大多数时间都用在等待add服务返回和网络传输上了.
也就是说,采用同步实现的方式,整个服务器的所有线程大部分时间都没有在工作,而是在等待
异步
如果是异步要怎么处理呢?核心代码如下:
public void transfer(accountFrom, accountTo, amount){
CompletableFuture.runAsync(()->{
add(accountFrom,-amount);
return;
).whenComplete(()->add(accountTo,amount));
}
这里借助了Java提供的CompletableFuture来实现异步的调用,如果没有用过没有关系,关键是思想.这段代码的流程是:执行给accountFrom扣除金额的方法,在成功的时候,再执行给accountTo添加金额的方法;
如果这个时候对transfer方法进行耗时统计,会发现每次执行时间只需要几ms,系统每秒的处理数量也会远远的超过同步的方式,直到达到服务器的物理性能上限
个人理解,异步的本质是提高cpu的利用率,同步时线程等待同样会占用大量的cpu时间片,这样的占用绝大多数是无意义的,而异步会减少线程等待占用的CPU时间片,从而提高了CPU时间片的利用率.
当然这个例子里面没有对add失败进行处理,真实开发的时候,异步会加大程序的复杂性,所以异步应该应用在性能敏感并且有io等耗时操作的地方.
BIO,NIO对比
明明是Netty,明明是讲BIO,NIO,为什么我要先说异步呢,其实这个世界上新技术有很多,一味地追逐上层的技术,人的精力有限,是做不到的.但是有很多东西其实是基石,讲NIO之前我先讲异步,等到我真的讲NIO的时候,你会发现NIO超越BIO,理所应当啊,也不需要去背什么概念了,再到以后,你或许学到了消息队列,Kafka,RocketMq,一个个产品花里胡哨,但是你会知道,他们最大的作用之一就是提供进程间的异步,为什么要异步,跟你今天理解的异步,其实是没有区别的.这个时候你会意识到,知识是成体系的,一个简单的异步,就可以串联起很多东西,这才是学习更重要的东西.
回到BIO和NIO,有一点需要认识到,网络并非时刻可读可写的,BIO就是不管不顾一直往Channel流读写数据,即使无数据可读,无数据缓冲可用的时候,也把持着线程资源. 我们用NIO就是在解决这个问题,NIO其实在读写操作的时候还是阻塞的,但是当没有数据可读,没有缓冲区可写的时候就会让渡出线程资源,等到有数据可读可写的时候在操作io.
举个例子:
有一个养鸡的农场,里面养着来自各个农户(Thread)的鸡(Socket),每家农户都在农场中建立了自己的鸡舍(SocketChannel)
- BIO:Block IO,每个农户盯着自己的鸡舍,一旦有鸡下蛋,就去做捡蛋处理;
- NIO:No-Block IO-单Selector,农户们花钱请了一个饲养员(Selector),并告诉饲养员(register)如果哪家的鸡有任何情况(下蛋)均要向这家农户报告(select keys);
- NIO:No-Block IO-多Selector,当农场中的鸡舍逐渐增多时,一个饲养员巡视(轮询)一次所需时间就会不断地加长,这样农户知道自己家的鸡有下蛋的情况就会发生较大的延迟。怎么解决呢?没错,多请几个饲养员(多Selector),每个饲养员分配管理鸡舍,这样就可以减轻一个饲养员的工作量,同时农户们可以更快的知晓自己家的鸡是否下蛋了;
- Epoll模式:如果采用Epoll方式,农场问题应该如何改进呢?其实就是饲养员不需要再巡视鸡舍,而是听到哪间鸡舍的鸡打鸣了(活跃连接),就知道哪家农户的鸡下蛋了;
在连接数不多的时候,其实BIO的性能并不差,因为BIO实现非常简单,这是它的优点,所以我们也没有必要去使用NIO.但是连接数多的时候,BIO就会存在很大的性能问题,也就是NIO发挥作用的时候了.
NIO和Reactor
上面的例子很生动的说明了BIO和NIO的特性,NIO思路就是,创建一个Selector,Channel告诉Selector自己关心的事件(SelectKey),Selector自己循环判断哪个Channel关心的事件可以触发了,当有事件的时候就通知Channel,Channel进行读写创建连接等操作.这就是Reactor模式,同时一个Selector可以注册很多Channel,这就是多路复用机制.
在上面的情况下,只有一个Selector,也就是在农场例子中的单Selector的情况,也称为Reactor单线程模式,但是在并发量很大的时候,单线程未必够用,所以我们还可以创建多个Selectot,每个Selector负责一部分channel,这就是Reactor多线程模式
走到这一步,Reactor模式还可不可以再优化呢?答案是可以的,对于服务器来说,接收连接是非常重要的事情,如果超长的读写操作影响了连接创建,这是不太能接收的,所以最主流的处理模式是Reactor主从模式,boss线程负责连接的创建,然后将创建好的连接交给子线程组,子线程组选取一个线程处理这个连接读写事件通知.
在Netty中,三种Reactor模式都是支持,但是除非选了一个垃圾的单核cpu的服务器,选主从模式就可以了,三种实现方式在下面:
//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);
最后再补充一下,Netty的boosGroup大多时候并不能用到一个线程组,只会用到线程组中的一个,在Netty服务器启动的时候,会绑定地址和端口,一般来说我们服务器只会绑定一个地址和端口,所以实际上也只用到了bossGroup中的一个线程.如果我在文章中描述有什么不对的地方,欢迎指正.