前言:tomcat一度是web容器的标准,但是tomcat的并发量却只有200-400之间,即使现在有了aio模式,也没有提升太多。
所以现在大部分都是使用netty作为高性能服务器框架,在dubbo,vert.x,gateway等等开源项目中都使用了,那么netty为什么深受喜爱?下文将带你寻找答案
目录
(1) IO模型
(2) zero-copy
(3) 堆外内存
(4) 高性能对象池
阅读netty会发现netty对于java有着很多改进,并适配了不同版本,不用升jdk版本就实现了相关jdk功能,及时优化了Java的bug,epoll的cpu100%空转。netty也封装了java的nio,简化了代码操作。netty本身也提供很多的附加功能,比如流量整形,黑白名单,安全认证等,极大的方便了网络开发。
IO模型
首先这个问题绕不开io,java有bio,nio,多路复用io,aio
同步异步区别:是否立即返回结果
阻塞非阻塞区别:线程是否需要等待任务完成。
多路复用:使用linux底层的select、poll、epoll非阻塞api进行调用
信号驱动:类似事件机制进行,向linux发送系统需要调用信号,系统返回调用信号准备结果,期间主线程还是能接收其他请求过来。当返回调用信号成功后,等待数据从内核态复制到用户态,最终完成
bio 、nio、多路复用的解释
在java的nio当中,当线程读取数据的时候,当没有数据可以读取的时候,会立即返回-1给线程,此时线程就知道现在没有多余的数据可以读了,线程就可以继续往下执行。但是在bio中,一旦没有数据可以读取,此时不会返回给线程结果,而是一直阻塞在那里,线程也就无法继续执行代码了。
nio 解决了线程阻塞的问题,就是一旦没有数据可以读,就可以往下执行,但是还是有个问题,就是虽然现在没数据可以读,但是你怎么知道接下来会没有数据读写呢,所以一般都是类似于死循环这种模式去读,读不到就进行下一次循环。虽然不是阻塞,但是还是基本上属于一个线程对一个socket读写的模式。
对于io多路复用,整体的大概是这样的,就是一个或几个线程可以管理一堆socket,socket一旦有读写请求,就会通知你,然后你就可以进行io读写操作了。这些都是依赖操作系统层面。在java中也这种模型api的封装,比如 Selector , SocketChannel ,SocketChannel 可以往 Selector 注册, 一个 Selector 可以管理一堆 SocketChannel ,其实底层最后都是基于操作系统的机制操作的。io多路复用的意思也就是 多个socket 复用一个或多个线程。
所以一般都是nio + io多路复用 一起使用的。当一个线程A来管理一堆Socket,不断去选择有可以进行读写操作的socket(Selector 的 select方法就是干这个事的) ,一旦发现有读写(上面说过,操作系统会通知你哪些socket有读写操作),就将socket交给其他的线程去处理,其他线程处理完了,发现没有读写了,因为是nio,所以不会阻塞,可以继续执行,由于有通知机制,所以这个线程不需要一直循环去判断有没有数据要读取,因为一旦这个socket将来还有数据,还是会通知到线程A,线程A会再次交给子线程处理。基于这种模型,可以实现一定数量的线程就可以完成一大堆socket的io读写操作。
面试补充:
select 缺点:
用户态拷贝到内核态
内核遍历fd(文件描述符号)
支持的文件描述符限制1024
poll 改进:
fd(文件描述符号)没有限制
缺点:
和select一样,poll返回后,需要遍历fd(文件描述符号)来获取就绪的描述符
调用poll都需要大把大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降
epoll 改进:
用户态和内核态共享
回调解决轮训
从表格上来看aio无疑是最佳选择,但是实际上aio并没有带来大量性能提升,而具有reactor的netty框架是当前的性能杀招。
上图是主从reactor的多线程的原理,通过boss和work作为分发器分发给线程池处理。
boss负责接受请求,什么叫接收请求,就是当有客户端需要跟服务端进行通信的时候,客户端需要跟服务端进行tcp三次握手,之后服务端会创建一个跟客户端通信的Socket,java中的api是叫SocketChannel,这些事都是boss负责的。
work负责读写处理,就是当客户端和服务端进行tcp三次握手之后,成功建立连接,此时客户端向服务端发来请求,work线程就会负责从连接中读取数据,处理请求,然后响应给客户端。
2.零拷贝
有了reactor,netty还有杀招-零拷贝
所谓零拷贝指的是零cpu拷贝,相比普通的拷贝,减少了用户态和内核态的切换,也减少了2次cpu拷贝。而用户态和内核态的切换是很好性能的。
正常的io会经过如下过程
而linux进行升级mmp,sendfile api,最终诞生了零拷贝,通过共享用户态避免了向用户态的切换。
使用文件描述符,代替了内核态的修改,只需传输标识地址,无需修改大量内容。
文件描述符号(简称呼fd):标识打开的文件的记录表
堆内存创建快,读写慢。堆外内存,创建慢,读写快。学过c和c++的都知道如何申请一块内存,堆外内存也是一样使用,一般可以安全的使用如下api申请。
ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
在netty中同样支持堆内存,堆外内存,池化,非池化的选择,这也是其高性能利器
4.高性能对象池
在java中对象池往往都非常重,但netty对于这块做了专门的优化,实现了Recycler做对象池,用来做池化内存。
Recycler提供了3个方法
get():获取一个对象。
recycle(T, Handle):回收一个对象,T为对象泛型。
newObject(Handle):当没有可用对象时创建对象的实现方法。
读取netty源码,发现recycler主要依靠DefaultHandler,WeakOrderQueue,Stack实现,如果threadlocal有就拿,没有就新建,DefaultHandler的pop是拿去的核心方法。