简述 Netty 线程模型,Netty 为什么如此高效?

netty线程模型采用“服务端监听线程”和“IO线程”分离的方式,与多线程Reactor模型类似

非阻塞IO

Netty采用了IO多路复用技术,让多个IO的阻塞复用到一个select线程阻塞上,能够有效的应对大量的并发请求

高效的Reactor线程模型

Netty服务端采用Reactor主从多线程模型
主线程:Acceptor 线程池用于监听Client 的TCP 连接请求
从线程:Client 的IO 操作都由一个特定的NIO 线程池负责,负责消息的读取、解码、编码和发送
Client连接有很多,但是NIO 线程数是比较少的,一个NIO 线程可以同时绑定到多个Client,同时一个Client只能对应一个线程,避免出现线程安全问题

无锁化串行设计

串行设计:消息的处理尽可能在一个线程内完成,期间不进行线程切换,避免了多线程竞争和同步锁的使用

高效的并发编程

volatile 的大量、正确使用
CAS 和原子类的广泛使用
线程安全容器的使用
通过读写锁提升并发性能

高性能的序列化框架

Netty 默认提供了对Google Protobuf 的支持,通过扩展Netty 的编解码接口,可以实现其它的高性能序列化框架

零拷贝

Netty 的接收和发送ByteBuffer 采用DirectByteBuffer,使用堆外直接内存进行Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HeapByteBuffer)进行Socket 读写,JVM 会将堆内存Buffer 拷贝一份到直接内存中,然后才写入Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝
Netty 提供了组合Buffer 对象,可以聚合多个ByteBuffer 对象,用户可以像操作一个Buffer 那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer 合并成一个大的Buffer。
Netty 的文件传输采用了transferTo()方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write()方式导致的内存拷贝问题。

内存池

基于对象池的 ByteBuf可以重用 ByteBuf对象,内部维护了一个内存池,可以循环利用已创建的 ByteBuf,提升内存的使用效率,降低由于高负载导致的频繁GC。测试表明使用内存池后的Nety在高负载、大并发的冲击下内存和GC更加平稳

灵活的TCP 参数配置能力

合理设置TCP 参数在某些场景下对于性能的提升可以起到显著的效果,例如SO_RCVBUF 和SO_SNDBUF。如果设置不当,对性能的影响是非常大的
SO_RCVBUF 和SO_SNDBUF:通常建议值为128K 或者256K;
SO_TCPNODELAY:NAGLE 算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法;
软中断:如果Linux 内核版本支持RPS(2.6.35 以上版本),开启RPS 后可以实现软中断,提升网络吞吐量。RPS根据数据包的源地址,目的地址以及目的和源端口,计算出一个hash 值,然后根据这个hash 值来选择软中断运行的cpu,从上层来看,也就是说将每个连接和cpu 绑定,并通过这个hash 值,来均衡软中断在多个cpu 上,提升网络并行处理性能

Netty 通过 Reactor 模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss
线程池和 work 线程池,其中 boss 线程池的线程负责处理请求的 accept 事件,当接收到 accept
事件的请求时,把对应的 socket 封装到一个 NioSocketChannel 中,并交给 work 线程池,其
中 work 线程池负责请求的 read 和 write 事件,由对应的 Handler 处理。
单线程模型:所有 I/O 操作都由一个线程完成,即多路复用、事件分发和处理都是在一个
更多关注 Java 大后端公众号Reactor 线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请求
或应答/响应消息。一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,速度慢,若
线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。
多线程模型:有一个 NIO 线程(Acceptor) 只负责监听服务端,接收客户端的 TCP 连接请
求;NIO 线程池负责网络 IO 的操作,即消息的读取、解码、编码和发送;1 个 NIO 线程可
以同时处理 N 条链路,但是 1 个链路只对应 1 个 NIO 线程,这是为了防止发生并发操作
问题。但在并发百万客户端连接或需要安全认证时,一个 Acceptor 线程可能会存在性能不
足问题。
主从多线程模型:Acceptor 线程用于绑定监听端口,接收客户端连接,将 SocketChannel 从
主线程池的 Reactor 线程的多路复用器上移除,重新注册到 Sub 线程池的线程上,用于处
理 I/O 的读写等操作,从而保证 mainReactor 只负责接入认证、握手等操作;

TCP 粘包/拆包的原因及解决方法?

TCP 是以流的方式来处理数据,一个完整的包可能会被 TCP 拆分成多个包进行发送,也可能
把小的封装成一个大的数据包发送。
TCP 粘包/分包的原因:
应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入
数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现
象;
进行 MSS 大小的 TCP 分段,当 TCP 报文长度-TCP 头部长度>MSS 的时候将发生拆包
以太网帧的 payload(净荷)大于 MTU(1500 字节)进行 ip 分片。
解决方法
消息定长:FixedLengthFrameDecoder 类
包尾增加特殊字符分割:行分隔符类:LineBasedFrameDecoder 或自定义分隔符类 :
DelimiterBasedFrameDecoder
将消息分为消息头和消息体:LengthFieldBasedFrameDecoder 类。分为有头部的拆包与粘包、
长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。

Selector BUG:若 Selector 的轮询结果为空,也没有 wakeup 或新消息处理,则发生空轮询, CPU 使用率 100%,

Netty 的解决办法:对 Selector 的 select 操作周期进行统计,每完成一次空的 select 操作进行
一次计数,若在某个周期内连续发生 N 次空轮询,则触发了 epoll 死循环 bug。重建 Selector,
判断是否是其他线程发起的重建请求,若不是则将原 SocketChannel 从旧的 Selector 上去除注册,重新注册到新的 Selector 上,并将原来的 Selector 关闭。

Java 缓冲流 buffer 的用途和原理是什么?