Netty笔记

选择Netty而不使用原生NIO编程原因

  1. NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、 ServerSocketChannel、SocketChannel、ByteBuffer等。
  2. 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为 NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能 编写出高质量的NIO程序。
  3. 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网 络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都 非常大。
  4. JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询, 最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但 是直到JDK1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而 已,它并没有被根本解决。该BUG以及与该BUG相关的问题单可以参见以下 链接内容。

Netty优点

  1. API使用简单,开发门槛低;
  2. 功能强大,预置了多种编解码功能,支持多种主流协议;
  3. 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
  4. 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;
  5. 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不
    需要再为NIO的BUG而烦恼;
  6. 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的
    新功能会加入;
  7. 经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络
    游戏、企业应用、电信软件等众多行业得到成功商用,证明了它已经完全
    能够满足不同行业的商业应用了。

Netty架构图

Netty线程模型

Reactor单线程模型

Reactor 单线程模型,是指所有的 I/O 操作都在同一个 NIO 线程上面完成。 NIO 线程的职责如下。

  1. 作为NIO服务端,接收客户端的TCP连接;
  2. 作为NIO客户端,向服务端发起TCP连接;
  3. 读取通信对端的请求或者应答消息;
  4. 向通信对端发送消息请求或者应答消息。

在一些小容量应用场景下,可以使用单线程模型。但是这对于高负载、大并
发的应用场景却不合适,主要原因如下:

  1. 一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的 CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送。
  2. 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超
    时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致
    大量消息积压和处理超时,成为系统的性能瓶颈。
  3. 可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统
    通信模块不可用,不能接收和处理外部消息,造成节点故障。

Reactor多线程模型

Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程来处理 I/O操作

Reactor 多线程模型的特点如下:

  1. 有专门一个NIO线程——Acceptor线程用于监听服务端,接收客户端的TCP连接请求
  2. 网络I/O操作——读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送。
  3. 一个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 采用了典型的三层网络架构进行设计和开发

高性能

  1. 采用异步非阻塞的 I/O 类库,基于 Reactor 模式实现,解决了传统同 步阻塞 I/O 模式下一个服务端无法平滑地处理线性增长的客户端的问题。
  2. TCP 接收和发送缓冲区使用直接内存代替堆内存,避免了内存复制, 提升了 I/O 读取和写入的性能。
  3. 支持通过内存池的方式循环利用 ByteBuf,避免了频繁创建和销毁 ByteBuf 带来的性能损耗。
  4. 可配置的 I/O 线程数、TCP 参数等,为不同的用户场景提供定制化的 调优参数,满足不同的性能场景。
  5. 采用环形数组缓冲区实现无锁化并发编程,代替传统的线程安全容器 或者锁。
  6. 合理地使用线程安全容器、原子类等,提升系统的并发处理能力
  7. 关键资源的处理使用单线程串行化的方式,避免多线程并发访问带来 的锁竞争和额外的 CPU 资源消耗问题。
  8. 通过引用计数器及时地申请释放不再被引用的对象,细粒度的内存管 理降低了 GC 的频率,减少了频繁 GC 带来的时延增大和 CPU 损耗。

可靠性

  1. 链路有效性检测
  2. 内存保护机制
  3. 优雅停机

可定制性

  1. 责任链模式:ChannelPipeline基于责任链模式开发,便于业务逻辑的拦截、定制和扩展。
  2. 基于接口的开发:关键的类库都提供了接口或者抽象类,如果Netty自身的实现无法满足用户的需求,可以由用户自定义实现相关接口。
  3. 提供了大量工厂类,通过重载这些工厂类可以按需创建出用户实现的对象。
  4. 提供了大量的系统参数供用户按需设置,增强系统的场景定制性。

可扩展性

基于 Netty 的基础 NIO 框架,可以方便地进行应用层协议定制,例如 HTTP 协议栈、Thrift 协议栈、FTP 协议栈等。这些扩展不需要修改 Netty 的源码,直 接基于 Netty 的二进制类库即可实现协议的扩展和定制。
目前,业界存在大量的基于 Netty 框架开发的协议,例如基于 Netty 的 HTTP 协议、Dubbo 协议、RocketMQ 内部私有协议等。

你可能感兴趣的:(netty)