|
|
|
|
# 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 </br>
|
|
|
|
|
![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<I>
|
|
|
|
|
- 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
|
|
|
|
|
>* <b>lengthAdjustment</b> = <b>-2</b> (= 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 编程
|
|
|
|
|
- ChannelOption<Integer>SO_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 的 <option>ture 进行设置 所以可以直接使用
|
|
|
|
|
- 修改 JDK logger 级别
|
|
|
|
|
- 默认 JDK 的日志没有 "DEBUG" 的日志级别, 默认是使用 "FINE"
|
|
|
|
|
- 使用 slf4j + log4j 示例
|
|
|
|
|
- 加入相关的 日志依赖即可
|
|
|
|
|
- 衡量好 longing handler 的位置和级别
|
|
|
|
|
- 参考代码 com.baiye.case5.server.ServerV1
|
|
|
|
|
- **可视化**
|
|
|
|
|
- 演示如何做 Netty 可视化
|
|
|
|
|
- 实现一个: 统计并展示当前系统连接数
|
|
|
|
|
- Console 日志定时输出
|
|
|
|
|
- JMX实时展示
|
|
|
|
|
- ~~ELKK TIG etc~~ (耗费时间, 但是是可视化图表)
|
|
|
|
|
- 参考 : com.baiye.case5.server.handler.MetricsHandler
|
|
|
|
|
- Netty 值得可视化的数据
|
|
|
|
|
- ![Netty值得可视化的数据](pic/Netty值得可视化的数据.png)
|
|
|
|
|
- ![Netty值得可视化的数据](pic/Netty值得可视化的数据1.png)
|
|
|
|
|
- ![Netty值得可视化的数据](pic/Netty值得可视化的数2.png)
|
|
|
|
|
- **内存不泄露**
|
|
|
|
|
- Netty 内存泄露指的是什么?
|
|
|
|
|
- 原因: "忘记" release
|
|
|
|
|
- ByteBuf buffer = ctx.alloc().buffer();
|
|
|
|
|
- ~~buffer.release() / ReferenceCountUtil.release(buffer);~~
|
|
|
|
|
- 后果: 资源未释放 -> OOM
|
|
|
|
|
- 堆外: 未 free (PlatformDependent.freeDirectBuffer(buffer));
|
|
|
|
|
- 池化: 未归还 (recyclerHandle.recycle(this))
|
|
|
|
|
- Netty 内存泄露检测核心思路
|
|
|
|
|
- 引用计数 (buffer.refCnt()) + 弱引用 (Weak reference)
|
|
|
|
|
- 引用计数
|
|
|
|
|
- 判断历史人物到底功大于过, 还是过大于功?
|
|
|
|
|
- 功 + 1, 过 -1 , =0时: 资源就该释放了
|
|
|
|
|
- 那什么时候判断? "盖棺定论" 时 -> 对象被 GC 后
|
|
|
|
|
- ![强引用和弱引用](pic/强引用和弱引用.png)
|
|
|
|
|
- ![Netty内存检测的核心](pic/Netty内存检测的核心.png)
|
|
|
|
|
- Netty 内存泄露检测的源码解析
|
|
|
|
|
- io.netty.util.ResourceLeakDetector
|
|
|
|
|
- ![Netty内存检测的核心](pic/Netty内存检测的核心1.png)
|
|
|
|
|
- 示例: 用 Netty 内存泄露检测工具做检测
|
|
|
|
|
- ![Netty内存检测的核心](pic/Netty内存检测的核心2.png)
|
|
|
|
|
|
|
|
|
|
### 5.3 优化使用
|
|
|
|
|
|
|
|
|
|
#### A. 用好 Netty 自带注解
|
|
|
|
|
- @Sharable
|
|
|
|
|
- 标识 handler 提醒可共享, 不标记共享的不能重复加入 pipeline, 会造成程序的中断
|
|
|
|
|
- @Skip
|
|
|
|
|
- 跳过 handler 的执行
|
|
|
|
|
- 4.x 不让使用, 因为直接把代码可以进行删除, 没必要通过比偶记得方式进行
|
|
|
|
|
- 但是他自己还是私人调用
|
|
|
|
|
- @UnstableApi
|
|
|
|
|
- 提醒不稳定, 慎用, 一般对未进行正式测试的类进行标记,不要进行使用
|
|
|
|
|
- @SuppressJava6Requirement
|
|
|
|
|
- ![去除Java6的报警注解](pic/去除Java6的报警注解.png)
|
|
|
|
|
- ![去除Java6的报警注解1](pic/去除Java6的报警注解1.png)
|
|
|
|
|
- ![去除Java6的报警注解2](pic/去除Java6的报警注解2.png)
|
|
|
|
|
- @SuppressForbidden
|
|
|
|
|
- ![去除报警注解](pic/去除报警注解.png)
|
|
|
|
|
- ![去除报警注解1](pic/去除报警注解1.png)
|
|
|
|
|
- ![去除报警注解1](pic/去除报警注解2.png)
|
|
|
|
|
|
|
|
|
|
#### B. 整改线程模型加快响应
|
|
|
|
|
- 业务常见的场景
|
|
|
|
|
- CPU密集型: 运算型
|
|
|
|
|
- IO密集型: 等待型
|
|
|
|
|
- CPU密集型
|
|
|
|
|
- ![CPU密集型](pic/CPU密集型.png)
|
|
|
|
|
- IO密集型
|
|
|
|
|
- ![IO密集型](pic/IO密集型.png)
|
|
|
|
|
|
|
|
|
|
#### C. 增强写, 延迟和吞吐量的抉择
|
|
|
|
|
- 写的 "问题"
|
|
|
|
|
- ![写的问题](pic/写的问题.png)
|
|
|
|
|
- 增加一点点延迟, 减少 flush 次数
|
|
|
|
|
- 改进方式1 : channelReadComplete
|
|
|
|
|
- ![channelReadComplete改进写](pic/channelReadComplete改进写.png)
|
|
|
|
|
- ![channelReadComplete改进写问题](pic/channelReadComplete改进写问题.png)
|
|
|
|
|
- 改进方式2 : flushConsulidationHandler
|
|
|
|
|
- ![flushConsulidationHandler源码分析](pic/flushConsulidationHandler源码分析.png)
|
|
|
|
|
|
|
|
|
|
#### D. 流量整形, 让其运行平稳
|
|
|
|
|
- 流量整形的用途
|
|
|
|
|
- 网盘限速(有意)
|
|
|
|
|
- 景点限流(无奈)
|
|
|
|
|
- Netty 内置的三种流量整形
|
|
|
|
|
- Channel 级别 io.netty.handler.traffic.ChannelTrafficShapingHandler
|
|
|
|
|
- Global 级别 io.netty.handler.traffic.GlobalTrafficShapingHandler
|
|
|
|
|
- Netty 流量整形的源码分析和总结
|
|
|
|
|
- ![流量整形的源码总结](pic/流量整形的源码总结.png)
|
|
|
|
|
- 示例: 流量整形的使用
|
|
|
|
|
- ![流量整形的使用](pic/流量整形的使用.png)
|
|
|
|
|
- com.baiye.case5.server.ServerV4
|
|
|
|
|
|
|
|
|
|
#### E. 不同平台开启 native
|
|
|
|
|
- 如何开启 native
|
|
|
|
|
- ![如何开启native](pic/如何开启native.png)
|
|
|
|
|
- com.baiye.case5.server.ServerV5
|
|
|
|
|
- 源码分析 Native 库的加载逻辑
|
|
|
|
|
- ![native库的加载逻辑](pic/native库的加载逻辑.png)
|
|
|
|
|
- 常见问题
|
|
|
|
|
- ![native库的加载逻辑](pic/native库的加载逻辑1.png)
|
|
|
|
|
- ![native库的加载逻辑](pic/native库的加载逻辑2.png)
|
|
|
|
|
|
|
|
|
|
### 5.4 安全增强
|
|
|
|
|
|
|
|
|
|
#### A. 设置 "水位线" 保护自己安全
|
|
|
|
|
- Netty OOM 的根本原因
|
|
|
|
|
- ![OOM的根本原因](pic/OOM的根本原因.png)
|
|
|
|
|
- Netty OOM - ChannelOutboundBuffer
|
|
|
|
|
- ![ChannelOutboundBuffer-OOM](pic/ChannelOutboundBuffer-OOM.png)
|
|
|
|
|
- Netty OOM - TrafficShapingHandler
|
|
|
|
|
- ![TrafficShapingHandler-OOM](pic/TrafficShapingHandler-OOM.png)
|
|
|
|
|
- ![unwritable-OOM](pic/unwritable-OOM.png)
|
|
|
|
|
- Netty OOM 的对策
|
|
|
|
|
- ![OOM-对策](pic/OOM-对策.png)
|
|
|
|
|
- com.baiye.case5.server.handler.OrderServerProcessHandler
|
|
|
|
|
|
|
|
|
|
#### B. 启动空闲检测
|
|
|
|
|
- ![启动空闲检测](pic/启动空闲检测.png)
|
|
|
|
|
- Server : com.baiye.case5.server.handler.ServerIdleCheckHandler
|
|
|
|
|
- Cliet : com.baiye.case5.client.handler.ClientIdleCheckHandler [编码前]| com.baiye.case5.client.handler.KeepaliveHandler [编码后]
|
|
|
|
|
- PS: 记得把整个 Handler 放到 Client 中
|
|
|
|
|
|
|
|
|
|
#### C. 黑白名单
|
|
|
|
|
- Netty 中的 "cidrPrefix" 是什么?
|
|
|
|
|
- ![cidrPrefix](pic/cidrPrefix.png)
|
|
|
|
|
- Netty 地址过滤功能源码分析
|
|
|
|
|
- 同一个 Ip 只能有一个连接
|
|
|
|
|
- Ip 地址过滤: 黑名单, 白名单
|
|
|
|
|
- 示例: 使用黑名单增强安全
|
|
|
|
|
- com.baiye.case5.server.ServerV7
|
|
|
|
|
|
|
|
|
|
#### D. 自定义授权
|
|
|
|
|
- com.baiye.case5.server.handler.AuthHandler [编码后]
|
|
|
|
|
- 对应Client端必须把整个消息进行第一个发送出去
|
|
|
|
|
|
|
|
|
|
#### E. SSL 设置
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 成就点
|
|
|
|
|
- 负责公司千万级别实时在线用户长连消息推送系统
|
|
|
|
|
- 企业社交 Sass 完美解决方案
|
|
|
|
|
|
|
|
|
|
## 主要方案
|
|
|
|
|
- 完美应用示例(十万级): 线程池异步业务 + (Linux 句柄) + 单机Netty + 日志
|
|
|
|
|
- 异步化完美方案(百万级): 线程池异步业务 + (Linux 句柄) + HA高可用 + 日志
|
|
|
|
|
- 多服务端解决方案(千万级): Disruptor异步业务 + (Linux 句柄) + HA高可用 + 日志
|
|
|
|
|
|
|
|
|
|
## 参考资料
|
|
|
|
|
- https://www.imooc.com/t/6224286 慕课网牛人教程
|
|
|
|
|
-
|
|
|
|
|
|
|
|
|
|
## Netty基本组件
|
|
|
|
|
- Netty对传统 Socket 进行封装
|
|
|
|
|
- NioEventLoop,Channel,ByteBuf,Pipeline,ChannelHandler
|
|
|
|
|
![Netty组件对应](pic/Netty组件对应关系.png)
|
|
|
|
|
- NioEventLoop => Thread(监听客户端的连接 + 处理每个连接的读写线程)
|
|
|
|
|
- Channel => Socket 对一条连接的封装
|
|
|
|
|
- ByteBuf => IO Bytes 每一个Channel上数据流的处理
|
|
|
|
|
- Pipeline => Logic Chain 逻辑处理链
|
|
|
|
|
- Channel Handler => Logic 每一个逻辑
|
|
|
|
|
|
|
|
|
|
## Netty服务端启动
|
|
|
|
|
- 分析服务端启动流程,包括服务端Channel的创建,初始化,以及注册到selector
|
|
|
|
|
- 服务端的启动在哪里初始化?
|
|
|
|
|
- 在哪里accept连接?
|
|
|
|
|
- 启动过程
|
|
|
|
|
- 创建服务端 Channel -> newChannel()
|
|
|
|
|
- 初始化服务端 Channel -> init()
|
|
|
|
|
- 注册 selector -> register()
|
|
|
|
|
- 端口绑定 -> doBind()
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
- 源码分析1 - 创建服务端Channel
|
|
|
|
|
- bind()[用户代码入口] -> initAndRegister()[初始化并注册] -> newChannel()[创建服务端channel]
|
|
|
|
|
- 反射创建服务端Channel : NioServerSocketChannel()[构造函数] -> newSocket()[通过jdk来创建底层jdk channel] -> NioServerSocketChannelConfig()[tcp参数配置类] -> AbstractNioChannel() ->
|
|
|
|
|
- configureBlocking(false)[阻塞模式] -> AbstractChannel()[创建id, unsafe, pipeline]
|
|
|
|
|
- 源码分析2 - 初始化服务端Channel
|
|
|
|
|
- init()[初始化入口] -> set ChannelOptions, ChannelAttrs [这个配置用的不多] -> set ChildOptions,ChildAttrs [每次创建的新连接都会把这两个属性配置上去] -> config handler[配置服务端pipeline] ->
|
|
|
|
|
- add ServerBootstrapAcceptor[添加连接器,默认添加特殊的Handler]
|
|
|
|
|
- 源码分析3 - 注册 selector
|
|
|
|
|
- AbstractChannel.register(channel)[入口] -> this.eventLoop = eventLoop[绑定线程] -> register0()[实际注册] -> doRegister()[调用jdk底层注册] -> invokeHandlerAddedIfNeeded()[做一些事件的回调] ->
|
|
|
|
|
- fireChannelRegistered()[传播事件]
|
|
|
|
|
- 源码分析4 - 端口绑定
|
|
|
|
|
- AbstractUnsafe.bind()[入口] -> doBind() -> javaChannel().bind()[jdk底层绑定] -> pipeline.fireChannelActive()[传播事件] -> HeadContext.readIfIsAutoRead()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## NioEventLoop
|
|
|
|
|
- 分析Netty reactor 线程处理过程,包括事件监听,事件处理,常规任务处理和定时任务处理
|
|
|
|
|
- 问题:
|
|
|
|
|
- 默认情况下, Netty 服务端启多少线程? 何时启动?
|
|
|
|
|
- Netty是如何解决jdk空轮询Bug的?会导致CPU飙升到100%
|
|
|
|
|
- Netty是如何保证异步串行无锁化?
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
- NioEventLoop 创建
|
|
|
|
|
- new NioEventLoopGroup()[线程组, 默认2*CPU] -> new ThreadPerTaskExecutor()[线程创建器] -> for(){new Child()}[循环构造NioEventLoop对象数组] ->
|
|
|
|
|
- chooserFactory.newChooser()[线程选择器:给每个新连接分配NioEventLoop线程]
|
|
|
|
|
- 深入剖析 ThreadPerTaskExecutor()
|
|
|
|
|
- 每次执行任务都会创建一个线程实体 -> FastThreadLocalThread
|
|
|
|
|
- NioEventLoopGroup 线程命名规则 nioEventLoop-1-xx
|
|
|
|
|
- 深入剖析 newchild()
|
|
|
|
|
- 保存线程执行器 ThreadPerTaskExecutor
|
|
|
|
|
- 创建一个 MpscQueue
|
|
|
|
|
- 创建一个 selector
|
|
|
|
|
- 深入剖析 chooserFactory.newChooser() 创建线程选择器
|
|
|
|
|
- chooserFactory.newChooser() -> isPowerOfTwo()[判断是否是2的幂, 如2,4,8,16] ->[TRUE] PowerOfTwoEventExecutorChooser[优化] -> index++ & (length-1) ->
|
|
|
|
|
- [FALSE] GenericEventExecutorChooser[普通] -> abs(index++ % length)
|
|
|
|
|
- ps: 计算机底层 & 操作比 取模操作高效的多, &操作是二进制的操作
|
|
|
|
|
---
|
|
|
|
|
- NioEventLoop 启动触发器
|
|
|
|
|
- 服务端启动绑定端口
|
|
|
|
|
- 新连接接入通过 chooser 绑定一个 NioEventLoop
|
|
|
|
|
- 启动流程
|
|
|
|
|
- bind() -> execute(task)[入口] -> startThread() -> doStartThread()[创建线程] -> ThreadPerTaskExecutor.execute()[线程执行器进行创建] ->
|
|
|
|
|
- thread = Thread.currentThread()[保存创建的线程] -> NioEventLoop.run()[启动]
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
- NioEventLoop 执行逻辑
|
|
|
|
|
- NioEventLoop.run() -> for(;;) -> select()[检查是否有io事件] -> processSelectedKeys()[处理io事件] -> runAllTasks()[处理异步任务队列]
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
- select() 方法执行逻辑 - 检测IO事件
|
|
|
|
|
- deadline 以及任务穿插逻辑处理
|
|
|
|
|
- 阻塞式的 select
|
|
|
|
|
- 解决 jdk 空轮训的 bug
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 新连接接入
|
|
|
|
|
- 分析新连接接入以及绑定reactor线程,绑定到selector的过程
|
|
|
|
|
|
|
|
|
|
## pipeline
|
|
|
|
|
- 分析pipeline的创建,初始化,添加和删除ChannelHandler,事件传播机制,异常传播机制
|
|
|
|
|
|
|
|
|
|
## ByteBuf
|
|
|
|
|
- 详细分析ByteBuf种类,如何减少多线程内存分配竞争,不同大小内存是如何分配的
|
|
|
|
|
|
|
|
|
|
## Netty解码
|
|
|
|
|
- 详细分析Netty解码原理,解码器抽象,以及几种常见的解码器
|
|
|
|
|
|
|
|
|
|
## Netty编码及writeAndFlush()
|
|
|
|
|
- writeAndFlush传播流程,编码器抽象,writeAndFlush详细流程
|
|
|
|
|
|
|
|
|
|
## Netty性能优化工具类解析
|
|
|
|
|
- 详细分析Netty里面最高频使用的两个性能优化类FastThreadLocal以及轻量级对象池Recycler
|
|
|
|
|
|
|
|
|
|
## Netty设计模式应用
|
|
|
|
|
- 分析各类常见设计模式以及在Netty中的应用
|
|
|
|
|
|
|
|
|
|
## Netty高性能并发调优
|
|
|
|
|
- 系统层面单机如何支持百万连接,如何提升应用层面性能
|
|
|
|
|
- 单机通过修改Linux句柄数目限制提升
|
|
|
|
|
- 应用层主要是通过线程池和异步方式进行调优
|
|
|
|
|
|
|
|
|
|
## Netty调优
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 实战及应用
|
|
|
|
|
- 开源软件中的应用
|
|
|
|
|
- 实现 RPC
|
|
|
|
|
|
|
|
|
|
## 数据可靠性通信场景分析与架构设计 - 实际场景
|
|
|
|
|
- 数据通信要求实时性高, 且高性能, 异构系统
|
|
|
|
|
- 需要保障不同的业务请求对应不同的实现
|
|
|
|
|
- 支持负载均衡策略(TCP级别的), 故障切换
|
|
|
|
|
- 需要可靠性保障的支持, 数据不允许丢失
|
|
|
|
|
![高可靠架构](pic/高可靠性架构设计分析.png)
|
|
|
|
|
- 定时任务是对中间状态进行定时的扫描, 进行消息推送补偿, 改写为最终状态的
|
|
|
|
|
|
|
|
|
|
### 1. 思考的问题
|
|
|
|
|
- 怎么定义数据结构?
|
|
|
|
|
- 怎么做整合, 比如和 Spring 进行整合使用?
|
|
|
|
|
- 怎么让 Netty 对不同的业务有不同的实现?
|
|
|
|
|
![Netty对不同的请求进行不同的Server处理](pic/Netty对不同的请求进行不同的Server处理.png)
|
|
|
|
|
|
|
|
|
|
### 2 自定义数据格式 - 定义使用的数据结构
|
|
|
|
|
- Message 的格式是通用的
|
|
|
|
|
- 只需要把业务相关的数据格式进行定义即可,就是对应的什么模型类
|
|
|
|
|
|
|
|
|
|
### 3 整合 SpringBoot - 思路
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 高可用 Netty 架构分析
|
|
|
|
|
![Netty高可用架构](pic/Netty架构设计.png)
|
|
|
|
|
- 常见的负载均衡策略 : LBS / Haproxy + Keepalived / Nginx
|
|
|
|
|
- 选择最小连接数的策略
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Netty调优
|
|
|
|
|
|
|
|
|
|
### Netty调优 - 内存调优
|
|
|
|
|
#### 1. netty客户端连接池泄漏问题复现及原因解析
|
|
|
|
|
- 问题复现: 生产环境使用 netty 作为客户端通信框架, 在客户端创建一个 tcp 连接池, 随着也无压力的上升, 在高峰期出现OOM问题, 需要重启才能恢复
|
|
|
|
|
|
|
|
|
|
- 参考: com.baiye.case2
|
|
|
|
|
|
|
|
|
|
- Bootstrap 不是线程安全的
|
|
|
|
|
- 真正的逻辑是 bossGroup 来进行处理的
|
|
|
|
|
|
|
|
|
|
- **总结**:编码规范问题, 注意客户端连接建立的编写, 关闭chanel的问题
|
|
|
|
|
|
|
|
|
|
### 2. netty内存池泄漏问题复现及排查解析
|
|
|
|
|
- 问题复现: 服务端使用 netty 作为通信框架, 负责消息接入和路由转发, 在功能测试时没有发现问题, 转性能测试后, 运行一段时间发现内存分配异常, 服务端无法接收请求消息, 系统吞吐量为0
|
|
|
|
|
- 当消息进来的时候, Netty给他分配的内存没有释放,最终导致了内存泄漏
|
|
|
|
|
- ByteBuf申请和释放场景分析
|
|
|
|
|
- 基于内存池的请求 ByteBuf
|
|
|
|
|
- 我们建议在使用 ServerHandler 时候实际使用 SimpleChannelInboundHandler , 不会存在内存泄露问题
|
|
|
|
|
- 基于非内存池请求的 ByteBuf (不推荐), 还是要自己释放
|
|
|
|
|
- 基于内存池的响应 ByteBuf
|
|
|
|
|
- 基于非内存池的响应 ByteBuf (不推荐) 还是要自己释放
|
|
|
|
|
|
|
|
|
|
- **总结**: ServerHandler 选用 SimpleChannelInboundHandler 来规避内存泄漏问题,但是仅限于你的Handler里面是同步处理的逻辑, 如果是异步的处理, 只能用 ChannelInboundHandlerAdapter
|
|
|
|
|
|
|
|
|
|
### 3. netty 内存池 的性能对比
|
|
|
|
|
- 4.x 引入 内存池机制, 对netty的提升很大
|
|
|
|
|
- com.baiye.case3.ByteBufPerformance 池化代码测试
|
|
|
|
|
- netty 内存池很大程度上提升了系统性能, 但是无用则会代理内存泄漏问题, 由于内存的申请和释放可能由Netty框架隐形完成, 所以增加了内存管理复杂度,所以必须深入理解ByteBuf的申请和释放机制,以免误用
|
|
|
|
|
|
|
|
|
|
- **总结**: 使用 4.x 提供的池化技术 PooledByteBufAllocator , 提升性能
|
|
|
|
|
|
|
|
|
|
### 4. ByteBuf 故障排查及优化 - HTTP 响应 Body 获取异常
|
|
|
|
|
- HTTP协议栈 ByteBuf 使用问题
|
|
|
|
|
- netty 解决好 HTTP 协议的问题, 就可以当 Tomcat 进行使用
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Netty调优 - 并发调优
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|