Netty的常见面试题

160 阅读9分钟

内容大纲

  • Netty基础和组件
  • 粘包半包问题
  • Netty解决JDK NIO的epoll空转问题
  • Netty面试题

1.Netty

  • 对NIO的高度封装,API简单,下限低上限高。
  • 功能丰富,预支了多种协议的编解码功能,性能高。
  • 基于Java的NIO,而并非AIO。
  • 应用十分广泛,例如RockteMQ、Dubbo、Hadoop......

2.Netty常用组件

2.1 ServerBootstrap和Bootstrap

网络通信本质上就是多个进程间的连接通信,Netty对服务器和客户端的应用引导类程序做了两个抽象:ServerBootstrap和Bootstrap。 ServerBootstrp需要绑定一个端口,监听客户端的连接。Bootstrap需要把绑定一个远程服务器端口和IP地址。其次,ServerBootstrap将包含2组Channel:ServerChannel作为服务器自身绑定到某端口正在监听的socket;第二个就是包含所有已经创建的用来处理客户端连接的channel。

2.2 EventLoop和EventLoopGroup

EventLoop是Netty中处理网络事件的抽象,一个EventLoop对应着一个Thread,线程任务的提交也是EventLoop处理。多个EventLoop就是一个EventLoopGroup,Netty可以用少量的EventLoop来支撑大量的Channel,而不是为每一个Channel都创建一个线程。 同一个EventLoop可能分配给多个Channel共享利用,一旦为某个Channel分配了EventLoop那么这个关系就绑定下来了,不会改变。 image.png

2.3 Channel

Netty的Channel屏蔽了Socket对IO的绑定、读写等操作,Channel用简单的API来实现IO的操作。Channel的生命周期:

     - Channel被创建,但是还未注册到EventLoop中。
     - Channel注册到EventLoop成功。
     - Channe连接远程成功,处于活动状态,可以随时收发数据。

Channel在这些状态扭转时,就会产生相应的事件,这些事件会转发给ChannelPipeline中的ChannelHandler来处理。

2.4 ChannelPipeline

当Channel被创建时,它将会被自动地分配一个新的ChannelPipelint,每一个Channel都有自己专属的ChannelPipeline,这个关联关系是永久的。 ChannelPipeline顾名思义是一个管道结构,类似于责任链模式;pipeline里面装载了各种各样的ChannelHandler,使得事件可以通过这个责任链让各种ChannelHandler来处理事件。

2.5 ChannelHandler

负责处理ChannelPipeline责任链的事件处理器,各种各样的ChannelHandler的执行顺序依赖它们添加到ChannelPipeline的位置决定。 ChannelHandler在添加到ChannelPipeline或者从中移除时,会触发一些方法执行,每一个方法都会接受一个ChannelHandlerContext

     - handlerAdded:当有一个ChannelHandler添加到ChannelPipeline中时触发。
     - handlerRemoved:同上,移除时触发。
     - exceptionCaught:ChannelHandler处理事件过程中发生异常时触发。

image.png

2.6 ChannelHanderAdapter

Netty提供了一些ChannelHander的适配器,在接口里面定义了许多方法的默认实现,我们自己开发可以实现这些接口来快速对接。

2.7 ByteBuf

ByteBuf的优点:

     - 用户可以自定义缓冲区类型扩展。
     - 通过内置的复合缓冲区实现了零拷贝。
     - Buffer容量可以按需增长。

2.7.1 使用模式

     - 堆缓冲区:ByteBuf存储在Jvm的堆内存中。
     - 直接缓冲区:基于直接内存,避免GC带来的STW问题,但是申请空间和释放代价较大。
     - 复合缓冲区:由多个ByteBuf聚合成为一个整体的ByteBuf。

2.7.2 分配

     - ByteBufAllocator
     - Unpooled

对于入站请求,Netty的EventLoop在处理Channel的读操作时进行ByteBuf的分配,对于这类Buffer需要我们自己释放。 对于出站请求,当调用了write、writeAndFlush后,Netty会自动释放ByteBuf。

Netty通信的一些问题

3.粘包和半包问题

客户端与服务端通信基于字节序列,客户端将数据包发往服务器,可能每一个数据包大小不一而且语义不以,那么在服务端对字节序列的接收判断情况可能就会造成粘包或半包的问题。

3.1 通信粘包和半包发生的原因

TCP作为传输层协议,它本身传输的就是字节序列,其本身并没有粘包和半包的说法。但是应用层开发基于TCP作为可靠性传输的手段,就存在粘包或半包的问题。 客户端和服务器进行socket通信,如果发送的数据包太小,TCP就会采用Nagle算法对数据包进行合并再发送,那么这种情况可能会造成粘包的问题。 半包的发生情况就是一个完整语义的数据包被分成了多次的数据包接收,就存在半包的问题,如果不做控制,服务端并不知道接收到的数据包是一个不完整的半包状态。半包发生的原因就是应用层写入数据的字节大小 > socket的buffer换从去大小。 UDP是无连接的不可靠传输协议,它本身没有数据包的合并流程,所以UDP本身没有粘包的说法。

3.2 粘包和半包的解决方案

粘包和半包本身不是TCP的问题,所以只能在引用层处理,方案如下:

     - `包尾添加分隔标识符`:在每一个数据包末尾添加一个可识别的分隔符,FTP协议就是这么做的。
     - `消息数据定长`:将数据包的大小固定,不足的可以用空格填充。
     - `消息封装为固定格式,例如添加消息头、消息体`:通过消息头就可以得知消息体的字节大小等。

4.编解码问题

网络通信传输的字节序列需要通过一定的形式进行数据格式的转换才能得到目的消息。编解码流程涉及到编码器和解码器,其中粘包和半包问题也属于是编解码问题之一。

4.1 解码器

Netty提供了两种解码方式:ByteToMessageDecoder将字节数据包解码为消息对象。MessageToMessageDecoder将一种消息对象解码转换为另一种格式的消息对象。

4.2 编码器

和解码器相反,编码器的目的就是将应用程序要发送的消息编码为字节序列,Netty也提供了消息转字节和消息转消息的接口。

5.序列化问题(略)

后续我会专门写一篇序列化技术相关的文章,涉及到常见的JDK序列化、Kryo、Protobuf等。

6.epoll死循环空转问题

这个问题的本质是JDK NIO包实现上的一个Bug:Selector空轮询,最终导致CPU飙升%。大致原因如下:

     1. 在Linux的kernel中,poll和epoll对于socket的中断信号会对返回的EventSet事件状态修改。
     1. EventSet集合发生了变化,Selector会被唤醒,但是Selector#select( )返回的numKeys数量为0,代表没有事件可处理。
     1. selectwhile(true)往复循环处理,出现了selector的空转。

6.1 Netty的解决方案

对于Selector的#select( )进行周期性的计数,当发生依稀空转就进行一次计数,如果在某一个周期内连续法身了n次空转,说明就发生了epoll的空转问题。将Selector重建,将原SocketChannel从旧的Selector踢出,重新注册到新的Selector上,将原来旧的Selector关闭。

Netty的一些面试题

7.Netty的特点?

  • 高性能、异步事件驱动的NIO框架。
  • 支持多种编解码,对TCP粘包半包问题进行自动化处理。
  • 高效Reactor模型,大量使用Volatile、CAS、读写锁等提高效率。

8.Netty的线程模型?

  • Netty的线程模型指的是它的Reactor模型,Netty的Reactor模型基于IO多路复用器用2个线程池来处理请求和读写操作。Boss线程池负责处理accept连接事件,当接收到accept事件是将对应的socket交给work线程池处理,work线程池负责处理读、写动作,由对应的Handler具体处理。

8.1 单线程模型

所有的IO操作都由1个线程完成,也就是说IO多路复用器、事件分发、读写处理都由1Reactor线程处理。

8.2 多线程模型

一个IO线程负责监听服务端的Acceptor,接受客户端的连接请求事件。NIO线程池则是负责处理消息的IO任务,即编解码、读写发送等。

8.3 主从多线程模型

Acceptor负责监听连接请求,客户端连接后,将channel从主线程池的Reactor多路复用器上移除,重新注册到sub线程池的selector上。从而形成:主线程池负责连接请求,sub线程池负责数据处理、读写发送等。

9.Netty的重要组件有哪些?

组件作用
EventLoop事件线程,配合Channel处理IO事件。
Channel对socket的高级抽象,包含了网络IO的具体操作方法。
ChannelFutureNetty中所有的IO操作都是异步的,类似于JDK的Future,当IO处理完毕后通过执行注册的监听事件来通知返回结果。
ChannelHandler处理各种事件,例如连接、数据接收、异常、转换等。
ChannelPipelineChannelHandler的容器,链式调用ChannelHandler处理事件。

10.Netty的高性能体现在哪儿?

  - NIO、多路复用,高效的Reactor模型。
  - 零拷贝技术。
  - 使用直接内存来作为内存池管理,内部使用二叉查找树来管理内存。
  - 读写串行化,避免加锁同步开销,虽然CPU利用率不高,但是可以通过调整NIO线程池的线程数量来并行处理事件。
  - 支持多种序列化协议,例如Protobuf等。
  - Volatil、CAS、读写锁的引入。

11.Netty的内存管理机制?

在直接内存中申请一块内存Arena,这个Arena由多个Chunk组成,每一个Chunk又是由2048个Page页组成。Chunk通过ALV数来管理组织这些Page。

12.ByteBuf的特点?

  - 支持自动扩容,默认4M。
  - 读写索引分离,无需调用JDK 的#flip( )来切换。
  - 缓冲池支持复用,避免内存的分配、释放问题。

13.如何实现单机Netty支持百万长链接?

13.1 OS层面优化

  - 修改进程同时打开的文件描述符数量上限(默认1024个,而且包含了Linux默认强制打开的一些FD)。
  - 解除OS限制的最大打开文件数。

13.2 Netty参数调优

  - Boss NioEventLoop Group、Work NioEventLoop Group 配置合理线程池大小。
  - 连接心跳的优化,即时检测失效的连接并踢除,设置合理的心跳周期。
  - 调整TCP接受和发送缓冲区大小,来控制TCP连接资源的占用情况。
  - 合理使用内存池,实现ByteBuf的复用。

13.3 JVM层面优化

  - 结合通信业务场景来调整堆内存和内存结构比例。
  - 选择合适的垃圾收集器,尽量避免长时间的STW。