Netty学习路线
前言:
本文列出学习Netty的路线,从Java Buffer到Netty项目、故障定位技巧。
摘录自《Netty进阶之路:跟着案例学Netty》20.1 Netty学习策略和20.2 Netty故障定位技巧
20.1.1 入门知识准备
1.Java NIO类库
需要熟悉和掌握的类库主要包括:(1)缓冲区Buffer。(2)通道Channel。(3)多路复用器Selector。首先介绍缓冲区(Buffer)的概念,Buffer 是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的 I/O 中,可以将数据直接写入或者将数据直接读到 Stream对象中。在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区的;在写入数据时,直接写入缓冲区。在任何时候访问NIO中的数据,都通过缓冲区进行操作。
缓冲区实质上是一个数组。通常它是一个字节数组(ByteBuffer),也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问及维护读写位置(limit)等信息。
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。比较常用的就是get和put系列方法,如图20-1所示。

Channel 是一个通道,可以通过它读取和写入数据,它就像自来水管一样,网络数据通过Channel读取和写入。通道与流的不同之处在于,通道是双向的,流只是在一个方向上移动(流必须是InputStream或者OutputStream的子类),而且通道可以用于读或写,或者同时用于读和写。因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读和写操作。
比较常用的Channel是SocketChannel和ServerSocketChannel,其中SocketChannel的继承关系如图20-2所示。
Selector是Java NIO编程的基础,熟练地掌握 Selector对于掌握NIO编程至关重要。多路复用器提供选择已经就绪的任务的能力。简单来讲,Selector 会不断地轮询注册在其上的 Channel,如果某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个Channel就处于就绪状态,会被 Selector轮询出来,然后通过 SelectionKey获取就绪Channel的集合,进行后续的I/O操作。

2.Java多线程编程
作为异步事件驱动、高性能的NIO框架,Netty代码中大量运用了Java多线程编程技巧,熟练掌握多线程编程知识是掌握Netty的必备条件。
需要掌握的多线程编程相关知识包括:
(1)Java内存模型。
(2)关键字synchronized。
(3)读写锁。
(4)volatile的正确使用。
(5)CAS指令和原子类。
(6)JDK线程池及各种默认实现。
20.1.2 Netty入门学习
通过Netty官方文档,以及Netty自带的例子(example)来进行学习,可以先看自带的例子,然后动手编写和调试代码。
把 echo 客户端连接创建、服务端连接接入、客户端消息发送和读取、服务端消息发送和读取流程及机制搞清楚,再学习其他的协议栈,例如HTTP、MQTT等。
20.1.3 项目实践
完成 Netty 基础知识的学习之后,就可以在项目中使用 Netty 了,只有通过实践才能真正掌握 Netty,如果项目暂时用不到 Netty,可以学习一些开源的 RPC 或者服务框架,看这些框架是怎么集成并使用Netty的,例如:
(1)gRPC,主要用到了Netty的HTTP/2协议栈。
(2)Vert.x,主要用到了Netty的NIO通信框架和HTTP协议栈,以及EventLoop Reactive线程模型。
建议通过“源码阅读+调试”的方式来学习,因为 Netty 的很多处理都是异步的,单纯地靠阅读代码很难真正掌握消息的处理流程。
20.1.4 Netty源码阅读策略当前Netty的源码规模已经非常庞大,全部阅读并掌握需要很长时间,建议读者根据自己的需要选择性地阅读。
首先需要掌握 Netty的内核工作流程,主要包括客户端连接创建、客户端消息读写、服务端监听和客户端接入、服务端消息读写,涉及的核心类库如下。
(1)ByteBuf和各种子类。
(2)Channel、Unsafe及各种子类。
(3)ChannelPipeline和ChannelHandler。
(4)EventLoop和EventLoopGroup及各种子类实现。
(5)Future和Promise及各种子类实现。
作为入门知识,Netty 客户端和服务端的创建流程需要熟练掌握,以服务端的创建流程为例,画出流程创建图,然后结合源码阅读,并进行调试,可以更高效地掌握相关知识点,如图20-3所示。

掌握了 Netty内核工作原理之后,再结合业务的实际需求选择性地学习其他协议栈,例如HTTP。
20.2 Netty故障定位技巧
尽管 Netty 应用广泛,非常成熟,但是由于对 Netty 底层机制不太了解,用户在实际使用中还是会经常遇到各种问题,大部分问题都是业务使用不当导致的。Netty 使用者需要学习Netty的故障定位技巧,以便出了问题能够独立、快速地解决。
20.2.1 接收不到消息
如果业务的ChannelHandler接收不到消息,可能的原因如下。
(1)业务的解码ChannelHandler存在缺陷,导致消息解码失败,没有投递到后端。
(2)业务发送的是畸形或者错误码流(例如长度错误),导致业务解码ChannelHandler无法正确解码业务消息。
(3)业务ChannelHandler执行了一些耗时或者阻塞操作,导致Netty的NioEventLoop被挂住,无法读取消息。
(4)执行业务ChannelHandler的线程池队列积压,导致新接收的消息排队,没有得到及时处理。
(5)对方确实没有发送消息。
定位策略如下。
(1)在业务的首个 ChannelHandler的channelRead方法中设置断点进行调试,看是否能读取到消息。
(2)在ChannelHandler中添加LoggingHandler,打印接口日志。
(3)查看NioEventLoop线程的状态,看是否发生了阻塞。
(4)通过tcpdump抓包查看消息是否发送成功。
20.2.2 内存泄漏
通过“jmap-dump:format=b,file=xx pid”命令打印内存堆栈,然后使用MemoryAnalyzer工具对内存占用情况进行分析,查找内存泄漏点,再结合代码进行分析,定位内存泄漏的具体原因,如图20-4所示。

20.2.3 性能问题
如果出现性能问题,首先需要确认是 Netty的问题还是业务的问题,通过 jstack命令或者jvisualvm工具打印线程堆栈,按照线程CPU使用情况进行排序(top-Hp命令采集),看线程在忙什么。通常如果采集几次都发现 Netty 的 NIO 线程堆栈停留在select 操作上,说明I/O比较空闲,性能瓶颈不是Netty,如图20-5所示。

如果发现性能瓶颈在网络I/O读写上,可以适当调大NioEventLoopGroup中的work I/O线程数,直到I/O处理性能能够满足业务需求。
作者:A-Persimmons
声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
Previous
JDK 21虚拟线程
Next
常见性能优化策略的总结