选择Netty而不使用原生NIO编程原因
- NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、 ServerSocketChannel、SocketChannel、ByteBuffer等。
- 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为 NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能 编写出高质量的NIO程序。
- 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网 络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都 非常大。
- JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询, 最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但 是直到JDK1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而 已,它并没有被根本解决。该BUG以及与该BUG相关的问题单可以参见以下 链接内容。
Netty优点
- API使用简单,开发门槛低;
- 功能强大,预置了多种编解码功能,支持多种主流协议;
- 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
- 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;
- 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不
需要再为NIO的BUG而烦恼; - 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的
新功能会加入; - 经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络
游戏、企业应用、电信软件等众多行业得到成功商用,证明了它已经完全
能够满足不同行业的商业应用了。
Netty架构图
Netty线程模型
Reactor单线程模型
Reactor 单线程模型,是指所有的 I/O 操作都在同一个 NIO 线程上面完成。 NIO 线程的职责如下。
- 作为NIO服务端,接收客户端的TCP连接;
- 作为NIO客户端,向服务端发起TCP连接;
- 读取通信对端的请求或者应答消息;
- 向通信对端发送消息请求或者应答消息。
在一些小容量应用场景下,可以使用单线程模型。但是这对于高负载、大并
发的应用场景却不合适,主要原因如下:
- 一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的 CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送。
- 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超
时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致
大量消息积压和处理超时,成为系统的性能瓶颈。 - 可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统
通信模块不可用,不能接收和处理外部消息,造成节点故障。
Reactor多线程模型
Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程来处理 I/O操作
Reactor 多线程模型的特点如下:
- 有专门一个NIO线程——Acceptor线程用于监听服务端,接收客户端的TCP连接请求
- 网络I/O操作——读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送。
- 一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题。
主从Reactor多线程模型
主从 Reactor 线程模型的特点是:服务端用于接收客户端连接的不再是一个单独的 NIO 线程,而是一个独立的 NIO 线程池。Acceptor 接收到客户端 TCP连接请求并处理完成后(可能包含接入认证等),将新创建的 SocketChannel 注册到I/O线程池(sub reactor线程池)的某个I/O线程上,由它负责 SocketChannel 的读写和编解码工作。Acceptor 线程池仅仅用于客户端的登录、 握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池 的 I/O 线程上,由 I/O 线程负责后续的 I/O 操作。
Netty设计原理
为了尽可能地提升性能,Netty 在很多地方进行了无锁化的设计,例如在 I/O 线程内部进行串行操作,避免多线程竞争导致的性能下降问题。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线 程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程 设计相比一个队列—多个工作线程的模型性能更优。
Netty 的 NioEventLoop 读 取 到 消 息 之 后, 直 接 调 用 ChannelPipeline 的 fireChannelRead (Object msg)。只要用户不主动切换线程,一直都是由 NioEventLoop 调用用户的 Handler,期间不进行线程切换。这种串行化处理方式 避免了多线程操作导致的锁的竞争,从性能角度看是最优的。
最佳实战
时间可控的简单业务直接在 I/O 线程上处理
时间可控的简单业务直接在 I/O 线程上处理,如果业务非常简单,执行时间非常短,不需要与外部网元交互、访问数据库和磁盘,不需要等待其它资源,则建议直接在业务 ChannelHandler 中执行,不需要再启业务的线程或者线程池。避免线程上下文切换,也不存在线程并发问题。
复杂和时间不可控业务建议投递到后端业务线程池统一处理
复杂和时间不可控业务建议投递到后端业务线程池统一处理,对于此类业务,不建议直接在业务ChannelHandler 中启动线程或者线程池处理,建议将不同的 业务统一封装成 Task,统一投递到后端的业务线程池中进行处理。过多的业务 ChannelHandler 会带来开发效率和可维护性问题,不要把 Netty 当作业务容器, 对于大多数复杂的业务产品,仍然需要集成或者开发自己的业务容器,做好和 Netty 的架构分层。
业务线程避免直接操作 ChannelHandler
业务线程避免直接操作 ChannelHandler,对于 ChannelHandler,IO 线程和 业务线程都可能会操作,因为业务通常是多线程模型,这样就会存在多线程操作 ChannelHandler。为了尽量避免多线程并发问题,建议按照 Netty 自身的做法, 通过将操作封装成独立的 Task 由 NioEventLoop 统一执行,而不是业务线程直接操作.
Netty架构解析
逻辑架构
Netty 采用了典型的三层网络架构进行设计和开发
高性能
- 采用异步非阻塞的 I/O 类库,基于 Reactor 模式实现,解决了传统同 步阻塞 I/O 模式下一个服务端无法平滑地处理线性增长的客户端的问题。
- TCP 接收和发送缓冲区使用直接内存代替堆内存,避免了内存复制, 提升了 I/O 读取和写入的性能。
- 支持通过内存池的方式循环利用 ByteBuf,避免了频繁创建和销毁 ByteBuf 带来的性能损耗。
- 可配置的 I/O 线程数、TCP 参数等,为不同的用户场景提供定制化的 调优参数,满足不同的性能场景。
- 采用环形数组缓冲区实现无锁化并发编程,代替传统的线程安全容器 或者锁。
- 合理地使用线程安全容器、原子类等,提升系统的并发处理能力
- 关键资源的处理使用单线程串行化的方式,避免多线程并发访问带来 的锁竞争和额外的 CPU 资源消耗问题。
- 通过引用计数器及时地申请释放不再被引用的对象,细粒度的内存管 理降低了 GC 的频率,减少了频繁 GC 带来的时延增大和 CPU 损耗。
可靠性
- 链路有效性检测
- 内存保护机制
- 优雅停机
可定制性
- 责任链模式:ChannelPipeline基于责任链模式开发,便于业务逻辑的拦截、定制和扩展。
- 基于接口的开发:关键的类库都提供了接口或者抽象类,如果Netty自身的实现无法满足用户的需求,可以由用户自定义实现相关接口。
- 提供了大量工厂类,通过重载这些工厂类可以按需创建出用户实现的对象。
- 提供了大量的系统参数供用户按需设置,增强系统的场景定制性。
可扩展性
基于 Netty 的基础 NIO 框架,可以方便地进行应用层协议定制,例如 HTTP 协议栈、Thrift 协议栈、FTP 协议栈等。这些扩展不需要修改 Netty 的源码,直 接基于 Netty 的二进制类库即可实现协议的扩展和定制。
目前,业界存在大量的基于 Netty 框架开发的协议,例如基于 Netty 的 HTTP 协议、Dubbo 协议、RocketMQ 内部私有协议等。