编写网络应用程序基本步骤:
- 需求分析 -> 定义业务数据结构 -> 实现业务逻辑
-> 选择传输协议 -> 定义传输信息结构 ->选择编解码 -> 实现所有的编解码 -> 编写应用程序 -> 测试与改进 - 定义传输信息结构,选择编解码:
数据本身编解码
压缩等编解码
粘包/半包处理编解码 - 编写应用程序:
编写服务器
编写客户端 - 编写之后:复查,检索最佳实践,检索坑,对比经典项目实现,同行评审->检查是否可诊断,检查是否可度量->上线-> 反馈:收集错误数据,收集性能数据
案例介绍及数据结构设计:
- Netty Client ->AuthOperation Netty Server
<-AuthOperationResult
->OrderOperation
<-OrderOperationResult
->KeepaliveOperation
<-KeepaliveOperationResult - 定义传输信息结构:
Frame
Message
MessageHeader | MessageBody(JSON)
length |version | opCode | streamId | operation/operation result
Netty编程易错点:
- LengthFieldBasedFrameDecoder 中 initialBytesToStrip 未考虑设置
- ChannelHandler 顺序不正确
- ChannelHandler 该共享不共享 不该共享却共享
- 分配ByteBuf: 分配器直接用 ByteBufAllocator.DEFAULT 等,而不是采用 ChannelHandlerContext.alloc()
- 未考虑ByteBuf的释放
- 错以为ChannelHandlerContext.write(msg) 就写出数据了
- 乱用ChannelHandlerContext.channel().writeAndFlush(msg)
实战进阶:
- 调优参数:
- 调整System参数:
- Linux系统参数:
/proc/sys/net/ipv4/tcp_keepalive_time - Netty支持的系统参数:
serverBoostrap.option(ChannelOption.SO_BACKLOG, 1024);
SocketChannel -> .childOption
ServerSocketChannel -> .option - Linux 系统参数:
- 进行TCP连接时,系统为每个TCP连接创建一个socket句柄,也就是一个文件句柄,但是Linux对每个进程打开的文件句柄数量做了限制,如果超出:报错:too many open file
ulimit -n[xxx] ulimit命令修改的数值只对当前登录用户的目前适用环境有效,系统重启或用户推出后就会失效,所以可以作为程序启动脚本一部分,让它再程序启动前执行 - Netty支持的系统参数(ChannelOption.[xxx]):
不考虑UDP:
IP_MULTICAST_TTL
不考虑OIO编程:
ChannelOptionSO_TIMEOUT=(“SO_TIMEOUT”);
SocketChannel(7个: childOption):
Netty系统相关参数 功能 默认值
SO_SNDBUF TCP数据发送缓冲区大小 /proc/sys/net/ipv4/tcp_wmem: 4K[min,default,max]动态调整
SO_REVBUF TCP数据接受缓冲区大小 /proc/sys/net/ipv4/tcp)rmem: 4K
SO_KEEPALIVE TCP层keepalive 默认关闭
SO_REUSEADDR 地址重用,解决Address already in use 默认关闭
常用开启场景:多网卡(IP)绑定相同端口
SO_LINGER 关闭Socket的延迟时间,默认禁用 默认不开启
IP_TOS 设置IP头部的Type-of-Service字段 1000 minimize delay
用于描述IP包的优先级和Qos选型 0100 maximize throughput
0010 maximize reliability
0001 minimize monetary cost
0000 normal service
TCP_NODELAY 设置是否启用Nagle算法:用将小的碎片数据 False 如果需要发送一些较小的报文,则需要禁用该算法
连接成更大的报文来提高发送效率
ServerSocketChannel (3个: option):
SO_RCVBUF 为Accept创建的socket channel设置SO_RCVBUF
SO_REUSEADDR 是否可以重用端口 默认false
SO_BACKLOG 最大的等待连接数量 Netty再linux下值的获取:io.netty.util.NetUtil:
先尝试: /proc/sys/net/core/somaxcon
然后尝试:sysctl
最终没有取到:用默认 128
使用方式:javaChannel().bind(localAddress, config.getBacklog())
- Linux系统参数:
- 调整System参数:
- 权衡Netty核心参数:
- 参数调整要点:
option/childOption分不清:不会报错,但是不会生效
不懂不要动,避免过早优化
可配置(动态配置更好) - 需要调整的参数:
最大打开文件数:
TCP_NODELAY SO_BACKLOG SO_REUSEADDR(酌情处理) - ChannelOption
childOption(ChannelOption.[XXX], [YYY])
option(ChannelOption.[XXX],[YYY]) - System property
-Dio.netty.[XXX] = [YYY] - ChannelOption (非系统相关 11个)
Netty参数 功能 默认值
WRITE_BUFFER_WATER_MARK 高低水位先,间接放置写数据OOM 32K -> 64K
CONNECT_TIMEOUT_MILLIS 客户端连接服务器的最大允许时间 30秒
MAX_MESSAGE_PER_READ 最大允许“连续”读次数 16次
WRITE_SPIN_COUNT 最大允许“连续”写次数 16次
ALLOCATOR ByteBuf分配器 ByteBufAllocator.DEFAULT: 大多池化,堆外
RCVBUF_ALLOCATOR 数据接收ByteBuf分配大小计算器+读次数控制器 AdaptiveRecvByteBufAllocator
AUTO_READ 是否监听“读事件” 默认监听读事件
AUTO_CLOSE "写数据"失败,是否关闭连接 默认打开
MESSAGE_SIZE_ESTIMATOR 数据(ByteBuf FileRegion)大小计算器 DefaultMessageSizeEsimatro.DEFAULT
SINGLE_EVENTEXECUTOR_PER_GROUP 当增加一个handler且指定EventExecutorGroup 默认true
ALLOW_HALF_CLOSURE 关闭连接时,允许半关 默认:不允许半关 - 3个费脑参数:
SO_REUSEADDR
SO_LINGER
ALLOW_HALF_CLOSURE
- 参数调整要点:
跟踪诊断:
- 如何让应用易诊断:
完善”线程名“
完善“Handler”名称
使用好Netty的日志
Netty日志的原理及使用:
Netty日志框架原理
修改JDK logger级别
使用slf4j + log4j示例
衡量好logging handler 的位置和级别 - 应用可视:
如何做Netty的可视化
Console 日志定时输出
JMX实时展示
Netty值得可视化的数据
外在:
可视化信息 来源 备注
连接信息统计 channelActive/channelInactive
收数据统计 channelRead
发数据统计 write ctx.write(msg).addListener() 更准确
异常统计 exceptionCaught/ChannelFuture ReadTimeoutException.INSTANCE
内在:
可视化信息 来源 备注
线程数 根据不同实现计算 nioEventLoopGroup.executorCount()
待处理任务 executor.pendingTasks() 例如:Nio Event Loop 的带处理任务
积累的数据 channelOutboundBuffer.totalPendingSize Channel级别
可写状态切换 channelWriteabilityChanged
触发事件统计 userEventTriggered IdleStateEvent
ByteBuf分配细节 Pooled/UnpooledByteBufAllocator.DEFAULT.metric() - 让应用内存不“泄露”
本节的Netty内存泄露:
原因:”忘记release“
ByteBuf buffer = ctx.alloc().buffer()
后果:资源未释放 -> OOM
堆外:未free (PlatformDependent.freeDirectBuffer(buffer0));
池化:未归还 (recyclerHandler.recycle(this)
Netty内存泄露检测核心思路
引用计数(buffer.refCnt()) + 弱引用(Weak reference)
引用计数:
强引用与弱引用:
Netty内存泄露检测核心思路:
ByteBuf buffer = ctx.alloc().buffer -> 引用计数 + 1 -> 定义弱引用对象DefaultResourceLeak加到Set(#allLeaks)里
buffer.release: -> 引用计数 - 1 -> 减到0时,自动执行释放资源操作,并将弱引用对象从Set里移除
判断依据:弱引用对象在不在Set里?如果在,说明引用计数还没到0 -> 没有到0,说明没有执行释放
判断时机:弱引用执行对象被回收时,可以把弱引用放进指定ReferenceQueue里面去,所以遍历queue拿出所有弱引用用来判断
Netty内存泄露检测的源码解析
全样本?抽样? PaltformDependent.threadLocalRandom().nextInt(samplingInterval)
记录访问信息: new Record(): record extends Throwable
级别/开关: io.netty.util.ResourceLeakDetector.Level
信息呈现: logger.error
触发汇报时机: AbstractByteBufAllocator#buffer(): io.netty.util.ResourceLeakDetector#track()
示例:用Netty内存泄露检测工具做检测
方法: -Dio.netty.leakDetaction.level=PARANOID
注意:
默认级别:SIMPLE 不是每次都检测
GC后,才有可能检测到
注意日志级别:
上线前用最高级级别,上线后用默认
优化使用:
- 用好注解:
- @Sharable
标识handler提醒可共享,不标记共享的不能重复加入pipeline - @Skip
跳过handler的执行 - @UnstableApi
提醒不稳定,慎用 - @SuppressJava6Requirement
去除 Java6 需求的报警 - @SuppressForbidden
取出禁用报警
- @Sharable
- 整改线程模型,让响应健步如飞
- 业务的两种场景:
- CPU密集型:运算型
保持当前线程模型:
Runtime.getRuntime().availableProcessors() * 2
io.netty.availableProcessors * 2
io.netty.eventLoopThreads - IO密集型: 等待型
整改线程模型:独立出 ”线程池“ 来处理业务
在handler内部使用JDK Executors
添加handler时,指定1个:
EventExecutorGroup eventExecutorGroup = new UnorderedThreadPoolEventExecutor(10);
pipeline.addLast(eventExecutorGroup, serverHandler)
- CPU密集型:运算型
- 业务的两种场景:
- 增强写,延迟与吞吐量的抉择:
- “写”的问题:
- 改进方式1:channelReadComplete
- 改进方式2:flushConsolidationHandler
- “写”的问题:
- 如何让应用丝般”平滑“:
- 流量整形的用途:
- 网盘限速(主动)
- 景点限流(被动)
- Netty内置的三种流量整形:
- Channel级别
- ChannelTrafficShapingHandler
- GlobalTrafficShapingHandler
- Netty流量整形的源码分析与总结:
- 读写流空判断:按一定时间段checkInterval(1s)来统计。writeLimit/readLimit设置的值为0时,表示关闭写整形/读整形
- 等待事件范围控制:10ms(MINIMAL_WAIT) -> 15s (maxTime)
- 读流控:取消读事件监听,让都缓存区满,然后对端写缓存区满,然后对端写不进去,对端对数据进行丢弃或减缓发送
- 写流控:待发数据入Queue。
- 流量整形的使用:
- ChannelTrafficShapingHandler
- GlobalTrafficShapingHandler: share
- GlobalChannelTrafficShapingHandler: share
- 流量整形的用途:
- 为不同平台开启Native
如何开启Native:
修改代码:
NioServerSocketChannel -> [Prefix]ServerSocketChannel
NioEventLoopGroup -> [Prefix]EventLoopGroup
准备好native库:
java.library.path: /usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
META-INF/native
Native相关的参数:
io.netty.transport.noNative
io.netty.native.workdir
io.netty.native.deleteLibAfterLoading
源码分析Native库的加载逻辑:
平台:
执行权限
安全增强:
- 设置”高低水位线“保护
Netty OOM的根本原因
根源: 进(读速度) 大于出(写速度)
表象:
上游发送太快:任务重
自己:处理慢/不发或发的慢:处理能力有限,流量控制等原因
网速:卡
下游处理速度慢:导致不及时读取接受Buffer数据,然后反馈到这边,发送速度降速
Netty OOM ChannelOutboundBuffer
存的对象:Linked list 存 ChannelOutboundBuffer.Entry
解决方式:判断totalPendingSize > writeBufferWaterMark.high()设置unwritable
ChannelOutboundBuffer
Netty OOM TrafficShapingHandler
存的对象:messageQueue 存 ChannelTrafficShpingHandler.ToSend
解决方式:判断queueSize > maxWriteSize 或 delay > maxWriteDelay 设置 unwritable
AbstractTrafficShapingHandler
Netty OOM 对策
设置好参数:判断channel.isWritable()
高低水位线(默认32k-64k)
启用流量整形时才需要考虑
maxwrite(默认4M)
maxGlobalWriteSize(默认400M)
maxWriteDelay(默认4s) - 启用空闲监测:
服务器加上 read idle check -服务器10s接收不到channel的请求就断掉连接
保护自己(及时清理空闲的连接)
客户端加上write idle check + keepalive - 客户端5s不发送数据就发一个keepalive
避免连接被断
启用不频繁的keepalive - 简单有效的黑白名单
Netty中的 ”cidrPrefix“
网络位 主机位
Netty地址过滤功能源码分析:
同一个IP只能有一个连接
IP地址过滤:黑名单 白名单
使用黑名单增强安全 - 少不了的自定义授权:
使用自定义授权: - 拿来即用的SSL-对话呈现表象:
SSL
SSL/TLS协议在传输层之上封装了应用层数据,不需要修改应用层协议的前提下提供安全保障
TLS(传输层安全) 是更为安全的升级版SSL
SSL的功能与设计:
基于”单向验证+交换密钥方式为RSA方式“
角色:
内容的加密: 对称加密方式 (效率高)
对称加密密钥的传递:非对称加密方式 公钥:邮箱 私钥:邮箱密码
Netty中使用SSL: io.netty.handler.ssl.SslHandler
单向认证
服务器端准备证书:自签或购买
服务器端加上SSL功能
导入证书到客户端
客户端加入SSL功能