# Netty ## 目录 - Netty 的背景现状趋势 - Netty 源码 从 领域知识分析 - Netty 源码 从 请求处理分析 - Netty 基本原理编码 - Netty 生产级实战 - Netty 精通 --- ## 1. 概述 - 官网 https://netty.io/ - 用户指导文档 User guide for 4.x https://netty.io/wiki/user-guide-for-4.x.html - API文档 4.1.79.Final https://netty.io/4.1/api/index.html
![Netty官方介绍图](pic/Netty官方介绍图.png) - Netty 是 Trustin Lee (韩国, Line 公司) 2004 研发 - 本质: 网络应用程序框架 - 非单机版程序 - 实现: 异步, 事件驱动 - 高性能 - 特性: 高性能, 可维护, 快速开发 - 能快速开发 - 用途: 开发服务器和客户端 --- - 思考: 为什么不直接使用 JDK NIO - Netty做的优化: - 支持常用应用层协议 - 解决传输问题: 粘包, 半包现象 - 支持流量整形 - 完善的断连, ldle等异常处理 - 规避 JDK NIO bug - 经典的 epoll bug : 异常唤醒空转导致 CPU 100% - Netty 解决方式: 检测问题发生, 然后处理 - io.netty.channel.socket.nio.NioChannelOption#setOption - IP_TOS 参数(IP包的优先级和QoS选项)使用时抛出异常 - java.lang.AssertionError:Option not found - Netty 解决方式: 遇到问题绕路走 - Netty API 更友好和强大 - 比如 ByteBuffer 和 Netty 的 ByteBuf - Threadlocal 和 Netty 的 FastThreadLocal - Netty 隔离变化 屏蔽细节 - 隔离 JDK NIO 的实现变化: nio -> nio2(aio) -> ... - 屏蔽 JDK NIO 的实现细节 - 思考: 自己去开发一个 Netty 的类似框架需要什么 - 需要你自己去维护基础 Nio 的 bug - 且连续的维护很久 --- - 类似的网络通信框架 - **Apache Mina** - Netty 是 Mina 的升级版本 - **Sun Grizzly** - 用得少,文档少,更新慢 - **Apple Swift NIO ,ACE** 等 - 语言不统配 - **Cindy** 等 - 生命周期短 - **Tomcat, Jetty**等 - 没有独立出来 - 版本发展 - 2004年6月 Netty2 发布 - 声称 Java 社区中第一个基于事件驱动的应用网络框架 - 2008年10月 Netty3 发布 - 2013年7月 Netty4 发布 - 2013年12月 发布 5.0.0.Alpha1 - 2015年11月11日 废弃 5.0.0 - 废弃原因1: 复杂,没有证明明显性能优势, 维护不过来 - 废弃原因2: 与 Apache Mina 关系 --- - 现状与趋势 - 维护者 core: Trustin Lee & Norman Maurer - 分支 - 4.1 master 支持 Android - 4.0 线程模型优化, 包结构, 命名 - Netty 无处不在 - 典型项目使用 - 数据库: Cassandra - 大数据处理: Spark Hadoop - Message Queue: RocketMQ - 检索: ElasticSearch - 框架: gRPC, Apache Dubbo, Spring5(Spring WebFlux) - 分布式协调器: Zookeeper - 工具类: async-http-client - 等... - 趋势 - 更多流行协议的支持 - 紧跟 JDK 新功能的步伐 - 更多易用, 人性化的功能 - IP地址黑名单, 流量整形 - 应用越来越多 --- ## 2. 原理剖析 ### 2.1 I/O 模式 - 经典的 I/O 模式 - BIO - NIO - AIO - 概念 - 阻塞和非阻塞 - 同步和异步 - 为什么 Netty 仅支持 NIO? - 为什么不建议 阻塞 I/O (BIO/OIO) - 连接数高的情况下: 阻塞 -> 耗资源,效率低 - 为什么删掉已经做好的AIO支持? - Windows 实现成熟,但是恩少用来做服务器 - Linux 常用来做服务器, 但是AIO实现不够成熟 - Linux 下 AIO 相比较 NIO 的性能提升不明显 - ![Netty的多种NIO实现](pic/Netty的多种NIO实现.png) - 为什么 Netty 有多种 NIO 实现 - 通用的 NIO 实现在 Linux 下也是使用 epoll, 为什么自己单独实现? - 实现的更好 - Netty 暴露了更多的可控制参数 - JDK 的 NIO 默认实现是水平触发 - Netty 是边缘触发(默认)和水平触发可切换 - Netty 实现的垃圾回收更好, 性能更好 - NIO 一定优于 BIO 么 - BIO 代码简单 - 特定场景: 连接数少, 并发度低, BIO 性能不输 NIO - Netty 切换 I/O 模式支持 - 在 new EventLoopGroup 的实现的时候和在指定 channel 的使用同时指定对应的 I/O 模式支持即可 - EventLoopGroup 采用的是 死循环监听 + 处理事件 - Netty channel 采用的是 泛型 + 反射 + 工厂模式 实现的 I/O 模式的切换 ### 2.2 Netty 支持三种 Reactor - Reactor 是一种开发模式,模式的核心流程 - 注册感兴趣的事件 -> 扫描是否有感兴趣的事件发生 -> 事件发生后做出相应的处理 - ![Netty中使用Reactor模式](pic/Netty中使用Reactor模式.png) - 主从 Reactor 模式是最常用的 ### 2.3 Netty 支持 TCP粘包,半包 - 粘包的主要原因 - 发送方每次写入数据 < 嵌套字缓冲区大小 - 接收方读取套接字缓冲区数据不够及时 - 半包的主要原因 - 发送方写入数据 > 套接字缓冲区大小 - 发送的数据大于协议的MTU(Maximum Transmission Unit, 最大传输单元), 必须拆包 - 换个角度来看 - 收发 - 一个发送可能被多次接收, 多个发送可能被一次接收 - 传输 - 一个发送可能占用多个传输包, 多个发送可能公用一个传输包 - 根本原因 - TCP 是流式协议, 消息无边界 - PS: UDP虽然一次运输多个,但是每个传输单元都有界限, 一个一个接收,所以无粘包, 半包问题 - ![找出消息边界的方式](pic/找出消息边界的方式.png) - ![Netty对三种常用封帧方式的支持](pic/Netty对三种常用封帧方式的支持.png) ### 2.4 Netty 对 "二次" 编解码方式的支持 - TCP 包法人编解码是一次编解码, 但是我们要对一次解码的字节进行更好的使用, 所以要对所使用的对象进行转换 - 对应的编解码器就是为了将Java对象转换成字节流方便存储或传输 - 一次解码器: ByteToMessageDecoder - io.netty.buffer.ByteBuf (原始数据流) -> io.netty.buffer.ByteBuf (用户数据) - 二次解码器: MessageToMessageDecoder - io.netty.buffer.ByteBuf (用户数据) -> Java Obj - 是否要把两次编解码合二为一? - 不建议 - 没有分层, 不够清晰 - 耦合性高, 不容易置换方案 - 常用的"二次"编解码方式 - Java 序列化 - Marshaling - XML - JSON - MessagePack - Protobuf - 其他 - 选择依据 - 需要比较不同的数据大小情况 - 空间: 编码后占用空间 - 时间: 编解码速度 - 是否追求可读性 - 多语言的支持 - Google Protobuf - Protobuf 是一个灵活的, 高效的用于序列化数据的协议 - 相对比XML和JSON, Protobuf 更小, 更快, 更便捷 - Protobuf 是跨语言的, 并且自带了一个编译器(protoc), 只需要用它进行编译,可以自动生成 Java,Python 等代码, 不需要自己进行编写 - 使用 ```shell # 定义 .proto 文件 # 安装工具 # 执行生成文件 protoc --java_out=[生成文件的目录] ``` - ![Proto的使用](pic/Proto的使用.png) ### 2.5 keepalive 与 idle 监测 - 为什么还需要应用层的 keepalive? - 协议不同, 各层的关注点不同: - 传输层关注是否 "通", 应用层关注是否可服务? - TCP 层的 keepalive 默认关闭, 且经过路由中转设备 keepalive 包可能会被丢弃 - TCP层的 keepalive 时间太长 - 默认 > 2 小时 , 但属于系统参数, 改动影响所有应用 - Tips: HTTP 属于应用层协议, 但是常常听到的名词 "HTTP Keep-Alive" 指的是对长连接和短连接的选择 - Connection: Keep-Alive 长连接 (HTTP/1.1 默认长连接, 不需要带这个 header) - Connection: Close 短连接 - idle 监测是什么? - 发送 keepalive: 一般用来配合 keepalive, 减少 keepalive 消息 - KeepAlive 设计演进 : V1 定时 keepalive 消息 -> V2 空闲监测 + 判定为 idle 时才发 keepalive - 实际应用: 结合起来使用, 按需 keepalive, 保证不会空闲, 如果空闲, 关闭连接 - 在 Netty 中开启 TCP keepalive 和 idle 检测 - 开启 keepalive (Server 端开启 TCP keepalive) - bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true) - bootstrap.childOption(NioChannelOption.of(StandardSocketOptions.SO_KEEPALIVE), true) - 提示: .option(ChannelOption.SO_KEEPALIVE, true) 存在但是无效 - 开启不同的 idle Check - ch.pipeline().addLast("idleCheckHandler", new IdleStateHandler(0,20,TimeUnit.SECONDS)); ### 2.6 Netty 锁 相关的支持 - 同步问题的核心三要素 - 原子性 - 可见性 - 有序性 - 锁的分类 - 对竞争的态度: 乐观锁(java.util.concurrent 包中的原子类), 悲观锁(Synchronized) - 等待锁的人是否公平而言: 公平锁 new ReentrantLock(true) 与 非公平锁 new ReentrantLock() - 是否可以共享: 共享锁与独享锁: ReadWriteLock, 其读锁是共享锁, 其写锁是独享锁 - 在意锁的对象和范围 -> 减少粒度 - 例: 初始化 channel (io.neety.bootstrap.ServerBootstrap#init) # 该代码已经被 Netty 重构 - Synchronized method -> Synchronized block - 在意锁的对象本身大小 -> 减少空间占用 - 例: 统计待发送的字节数(io.neety.channel.ChannelOutboundBuffer) - AtomicLong -> Volatile long + AtomicLongFieldUpdate - Atomic long VS long: - volatile long = 8 bytes - AtomicLong = 8 bytes(volatile long) + 16bytes(对象头) + 8 bytes(引用) = 32 bytes [不考虑优化] - 至少节省了 24 个 字节 - 结论: Atomic* objects -> Volatile primary type + Static Atomic* FieldUpdater - 注意锁的速度 -> 提高并发性 - 记录内存分配字节数等功能用到的 LongCounter - (io.netty.util.internal.PlatformDependent#newLongCounter) - 高并发时候: AtomicLong -> LOngAdder - 结论: 及时衡量, 使用JDK最新的功能 - ![Netty锁的并发性提升](pic/Netty锁的并发性提升.png) - 根据不同的场景选择不同的并发包 -> 因需而变 - 关闭和等待关闭事件执行器(Event Executor): - Object.await/notify -> CountDownLatch - ![Netty并发包因需而变](pic/Netty并发包因需而变.png) - ![Netty并发包的因需而变2](pic/Netty并发包的因需而变2.png) - 衡量好锁的价值, 能不用就不用 - 锁管理一定要和生活中的实际场景息息相关, 服务员服务包厢的想法 - 局部串行: Channel的 I/O 请求处理是串行的 - 整体并行: 多个串行化的线程(NioEventLoop) - ![Netty衡量锁的价值](pic/Netty衡量锁的价值.png) - Netty 的应用场景: 局部串行 + 整体并行 -> 一个队列 + 多个线程模式: - 降低用户开发难度, 逻辑简单, 提升处理性能 - 避免锁带来的上下文切换和并发保护等额外开销 - 避免用锁: 用 ThreadLocal 来避免资源争用,例如 Netty 轻量级的线程池实现 - io.netty.util.Recycler#threadLocal ### 2.7 Netty 内存的管理和使用 - 目标: - 内存占用少(空间) - 应用速度快(时间) - 对 Java 而言, 减少 Full GC 的 STW(Stop the world)时间 - Netty 内存技巧 - 减少对象本身大小 - 能用基本类型就不要使用包装类型 - 应该定义成类变量的不要定义为实例变量: - 一个类 -> 一个变量 - 一个实例 -> 一个实例变量 - 一个类 -> 多个实例 - 实例越多, 浪费就越多 - Netty对前两者的实际使用 - ![Netty内存技巧减少内存本身大小](pic/Netty内存技巧减少内存本身大小.png) - Netty 内存技巧 - 对分配内存进行预估 - ![Netty内存技巧对分配内存预估](pic/Netty内存技巧对分配内存预估.png) - Netty 内存技巧 - 预测分配大小 - ![Netty内存技巧预测分配大小](pic/Netty内存技巧预测分配大小.png) - Netty 内存技巧 - Zero-Copy - 使用逻辑组合 - ![Netty内存技巧ZeroCopy](pic/Netty内存技巧ZeroCopy.png) - 使用包装 - ![Netty内存技巧ZeroCopy1](pic/Netty内存技巧ZeroCopy1.png) - 使用 JDK 的 Zero-Copy 接口 - ![Netty内存技巧ZeroCopy2](pic/Netty内存技巧ZeroCopy2.png) - Netty 内存技巧 - 堆外内存 - 生活中的场景 - 门口烧烤摊坐不下, 在门口放点桌子 - 店内 -> JVM 内存 -> 堆(heap) + 非堆 (non heap) - 店外 -> JVM 外部 -> 堆外(off heap) - Netty 使用堆外内存 - ![Netty使用堆外内存](pic/Netty使用堆外内存.png) - Netty 内存技巧 - 内存池 - 类似: 平板点菜 -> 替代纸点菜 - 为什么要引入对象池: - 创建开销大 - 对象高频率创建且可复用 - 支持高并发又能保护系统 - 维护, 共享有限的资源 - 如何实现内存池? - 开源实现: Apache Common Pool - Netty 轻量级对象池实现 io.netty.util.Recycler [他自己的场景来建立的] ## 4. 基本原理编码 ### 4.1 编写网络应用程序的基本步骤 - ![编写网络应用程序的基本步骤](pic/编写网络应用程序的基本步骤.png) - ![编写网络应用程序的基本步骤1](pic/编写网络应用程序的基本步骤1.png) ### 4.2 数据结构设计 - ![Netty案例数据结构设计](pic/Netty案例数据结构设计.png) - opration/ opration result - 封装成 Body [这里采用的是Json编码] - version - 头信息, 版本号 - 处理兼容性 - opCode - 头信息, opration 类型 - Json解析的时候用到 - streamId - 头信息, 标识信息唯一的Id - length - 处理粘包和半包问题 ### 4.3 代码 - case 查看 : com.baiye.case5 ### 4.4 Netty 编程中的易错点 - LengthFieldBasedFrameDecoder 中 initialBytesToStrip 未考虑设置 - 见 com.baiye.case5.server.codec.OrderFrameDecoder - 见 io.netty.handler.codec.LengthFieldBasedFrameDecoder 源码 - >* lengthFieldOffset = 0 >* lengthFieldLength = 2 >* lengthAdjustment = -2 (= the length of the Length field) >* initialBytesToStrip = 0 >* >* BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes) >* +--------+----------------+ +--------+----------------+ >* | Length | Actual Content |----->| Length | Actual Content | >* | 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" | >* +--------+----------------+ +--------+----------------+ > - 一定要进行设置,这样才会跳过 Length field 的长度 - 避免使用 LengthFieldBasedFrameDecoder(...) 最简单的构造器, 因为最简单的构造器 initialBytesToStrip 是 0 - ChannelHandler 顺序不正确 - 如何保证顺序正确: - 先请求后响应, 包 -> 协议 -> 协议 -> 包 - 业务的Handler 顺序都可以 , 可以放在最后写 - ChannelHandler 该共享不共享, 不该共享却共享,触发多线程问题 - LoggingHandler 就是要共享的, 因为Channel 都有自己的 pipeline , 不共享就是浪费空间, 问题也不大 - PS: 总结有哪些需要共享的 Handler, 哪些不需要共享的 Handler - 分配 ByteBuf: 分配器直接用 ByteBufAllocator.DEFAULT 等, 而不是采用 ChannelHandlerContext.alloc() - 见 com.baiye.case5.server.codec.OrderProtocolEncoder - ctx 中可以获取 alloc, 不要自己去创建, 因为当源码重新创建了一个 alloc 的实现,会有可能造成实现的不一致 - 未考虑 ByteBuf 的释放 - 继承 SimpleChannelInboundHandler 可以帮我们进行释放资源, 如果使用 ChannelInboundHandlerAdapter 就要自己进行释放 ReferenceCountUtil.release(msg); , 不接收消息也要进行释放 fireChannelRead() - 错以为 ChannelHandlerContext.writer(msg) 就写出数据了 - writer 仅仅是将消息加到队列中, 不是进行真正的发送 - 乱用 ChannelHandlerContext.channel().writerAndFlush(msg) - ctx.writeAndFlush(responseMessage); : 是在当前 pipelineHandler 的位置寻找下一个符合条件的 Handler,所以并不是把 pipeline 重新走了一遍 - ctx.channel().writerAndFlush(msg); : 是调用的 pipeline.writeAndFlush(msg); , 表示 pipeline 重新走了一遍, 如果是中间的 Handler 就会造成死循环 - ctx.channel().writerAndFlush(msg); 常用在客户端, ctx.writeAndFlush(responseMessage); 用在服务端 ## 5. 实战及调优 ### 5.1 调优参数: 调整 System 参数及 Netty 核心参数 - Linux 系统参数 - ![调整System参数](pic/调整System参数.png) - 1: 查看 云服务器 支持不支持这种帮助你进行一键调优的工具或者配置 ```shell /proc/sys/net/ipv4/tcp_keepalive_time ``` --- - Netty 支持的系统参数 - Netty 支持的系统参数 ChannelOption.XXX 讨论 - 不考虑 UDP - IP_MULTICAST_TTL - 不考虑 OIO 编程 - ChannelOptionSO_TIMEOUT=("SO_TIMEOUT"); - 控制阻塞时间 - 参数列表 - ![Netty支持的调优参数表](pic/Netty支持的调优参数表.png) - SO_SNDBUF 和 SO_RCVBUF 参数现在因为Linux是动态的, 所以不用调整 - SO_KEEPALIVE 我们用应用层控制, 所以这里保持默认关闭 - SO_REUSEADDR - SO_LINGER - IP_TOS - TCP_NODELAY 这个要设置为 true - 小报文比较多的情况下 - ![Netty支持的调优参数表1](pic/Netty支持的调优参数表1.png) - SO_RCVBUF - SO_REUSEADDR - SO_BACKLOG - ~~IP_TOS~~ - 参数调优要点 - 权衡 Netty 核心参数 - option/childOption 要分清, 不会报错, 也不会生效 - 不懂不要动, 避免过早优化 - 可配置(动态配置更好) - 需要调整的参数 - 最大打开文件数 - TCP_NODELAY SO_BACKLOG SO_REUSEADDR (酌情处理) - ![Netty支持的调优参数表2](pic/Netty支持的调优参数表2.png) - ![Netty支持的调优参数表3](pic/Netty支持的调优参数表3.png) - 第一个参数值, 不大是因为这是每个连接的范围值, 其实很大了 - 两个兄弟的关系 - ![Netty支持的调优参数表4](pic/Netty支持的调优参数表4.png) - 功能上可以细分为3类 - ![Netty支持的调优参数表5](pic/Netty支持的调优参数表5.png) - ![Netty支持的调优参数表6](pic/Netty支持的调优参数表6.png) - ![Netty支持的调优参数表7](pic/Netty支持的调优参数表7.png) - 服务端调优 com.baiye.case5.server.ServerV1 - 客户端调优 com.baiye.case5.client.OrderClientV4 --- - SO_REUSEADDR - 一般不会开启这个参数 - 地址重用参数 - ![地址重用参数](pic/地址重用参数.png) - SO_LINGER - 一般不会开启这个参数 - ![SO_LINGER参数](pic/SO_LINGER参数.png) - ALLOW_HALF_CLOSURE - 半关参数 - 一般不会开启这个参数 - ![ALLOW_HALF_CLOSURE参数](pic/ALLOW_HALF_CLOSURE参数.png) ```shell serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024); ``` - SocketChannel -> .childOption - ServerSocketChannel -> .option ### 5.2 跟踪诊断 应用易诊断 | 可视化 | 内存不泄露 - **"易" 诊断** - 完善 "线程名" - 完善 "Handler" 名称 - 使用好 Netty 日志 - ![完善 "线程名"](pic/完善线程名.png) - 一般实现 2-1 表示 boss group - 一般实现 3-1 表示 worker group - 以后可能改变 - ![完善Handler名称](pic/完善Handler名称.png) - "$1" 表示一个匿名内部类 - "#0" 防止一个pipeline中加入多个 handler - Netty 日志原理及使用 - Netty 日志框架原理 - Netty 会自动去依赖主流的日志框架的实现, 把 slf4j log4j 的