跟踪诊断
让应用易诊断
完善线程名
nioEvemtLoopGroup-2-1:代表bossGroup
nioEventLoopGroup-3-1:带表workerGroup
如果netty改变了实现,这个命名可能也会发送变化
在声明NioEventLoopGroup时添加DefaultThreadFactory,给线程命名
完善handler名称
pipeline中有多个handler,handler经常会加上#0
的标记,甚至出现一个$1
,$1
是一个匿名内部类,#0
是为了防止一个piple中加入了多个handler。
在pipeline添加handler时声明名称即可
Netty日志的原理及使用
Netty日志框架原理
进入LoggingHandler类,查看构造器
InternalLoggerFactory.getInstance(getClass());生成了一个logger实例,查看getInstance()方法
可以看到是从一个工厂创建了实例
可以看到,获取LoggerFactory的顺序是slf4j–>log4j2–>log4j–>jdk
修改JDK logger级别
在jre的lib目录下,有一个logging.properties文件,里面设置日志级别,jdk的日志级别没有DEBUG关键字,可以用FINE
使用slf4j+log4j
只需要在项目中引入相关的jar包即可
比如引入slf4j的依赖
衡量logging handler的位置及级别
这里LoggingHandler拿到的数据已经是被解析成对象的数据了,如果想debug出原始的数据,可以将LoggingHandler移到最上面
此时日志中就能打印出原始的数据了
应用可视化
Netty可视化案例演示:
- 实现一个小目标:统计并展示当前系统连接数
- Console日志定时输出
- JMX实时展示
ELKK、TIG、etc
代码示例
创建handler
创建一个用于监控的Handler:MetricHandler,因为是统计当前系统的连接数,所以需要对开启连接和关闭连接都进行处理,因此MetricHandler继承ChannelDuplexHandler,ChannelDuplexHandler既支持输入也支持输出,同时重写channelActive和channelInactive方法
此时连接数的统计就写好了,接着需要将统计的数据展示出来
引入metrics依赖
此处使用dropwizard的metrics
引入相关依赖:
将我们统计的连接数进行注册和展示
添加pipeline
将handler加入到pipeline中
启动Server,就能在控制台看到相关输出,此时如果启动client,就能看到数据的变化
打开jconsole,连接到server之后,进入mBean就能看到相关的数据
Netty值得可视化的数据
外在数据
可视化信息 | 来源 | 备注 |
---|---|---|
连接信息统计 | channelActive/channelInactive | |
收数据统计 | channelRead | |
发数据统计 | write | 这个wirte只是写到buffer中,ctx.write(msg).addListener()更准确 |
异常统计 | exceptionCaught/ChannelFuture | ReadTimeoutException.INSTANCE |
内在数据
可视化信息 | 来源 | 备注 |
---|---|---|
线程数 | 根据不同实现计算 | 例如:nioEventLoopGroup.executorCount(); |
待处理任务 | executor.pendingTask() | 例如:NioEventLoop的待处理任务 |
积累的数据 | channelOutboundBuffer.totalPendingSize() | Channel(Connection)级别,不是整个系统的 |
可写状态切换 | channelWritabilityChanged | |
触发事件统计 | userEventTriggered | IdleStateEvent |
ByteBuf分配细节 | Pooled/UnpooledByteBufAllocator.DEFAULT.metric() |
让应用内存不泄露
Netty内存泄露指什么
原因:忘记release
后果:资源未释放 -->OOM
- 堆外:未free (PlatformDependent.freeDirectBuffer(buffer))
- 池化:未归还 (recyclerHandler.recycle(this))
Netty内存泄露检测核心思路
引用计数(buffer.refCnt())+弱引用(Week reference)
引用计数:
-
判断历史人物到底功大于过还是过大于功
功 +1,过 -1,=0时,尘归尘土归土,资源就该释放了
- 什么时候判断?“盖棺定论”时–>对象被GC后
强引用与弱引用
-
String 我是战斗英雄型强保镖 = new String(“我是主人”);这就是强引用
-
WeakReference<String> 我是爱写作的弱保镖 = new WeakRefrence<String>(new String(“我是主人”));
只有一个爱写作的保镖(弱引用)守护(引用)时:刺客(GC)来袭,主人(referent)必挂(GC掉)
不过主人挂掉(被GC掉)的时候,我还是可以发挥写作特长:把我自己记到“小本本(ReferenceQueue)”上去。
-
有了ReferenceQueue,netty就可以知道对象是否被GC了
Netty内存检测的核心思路:
ByteBuf buffer = ctx.alloc().buffer() —>引用计数+1—>定义弱引用对象DefaultResourceLeak加到list(#allLeaks)里
buffer.release()—>引用计数-1—>减到0时,自动执行释放资源操作,并将弱引用对象从list里移除
判断依据:若引入对象在不在list里面?如果在,说明引用计数还没有到0—>没有到0,说明没有执行释放
判断实际:弱引用指向对象被回收时,可以把弱引用放进指定ReferenceQueue里面去,所以遍历queue拿出所有弱引用来判断
Netty内存泄露检测源码分析
ResourceLeakDetector
总结:
- 全样本还是抽样:PlatformDependent.threadLoadkRandom().nextInt(samplingInterval)
- 记录访问信息:new TraceRecord() :TraceRecord extends Throwable
- 级别/开关:io.netty.util.ResourceLeakDetector.Level
- 信息呈现:logger.error
- 触发汇报时机:AbstractByteBufAllocator#buffer():io.netty.ResourceLeakDetector#track
示例:用Netty内存泄露检测工具做检测
用Netty内存泄露检测工具做检测
方法:-Dio.netty.leakDetection.level=PARANOID
注意:
- 默认级别是SIMPLE,不是每次都检查
- GC后才有可能检测到
- 注意日志级别
- 上线前用最高级别,上线后用默认
在OrderServerProcessHandler中,创建ByteBuf,但是不回收,就会导致内存泄露
同时将logback.xml的日志级别调整为ERROR,避免其他日志造成干扰
让client发送10000次数据
在server的启动参数中增加 -Dio.netty.leakDetection.level=PARANOID
然后启动server,再启动client,然后就可以server的控制台中看到内存泄露日志
Netty自带注解
@Sharable
标识handler,提醒可共享,不标记共享的不能重复加入pipeline
不带该注解的handler如果共享使用,第一次请求的客户端会正常访问,第二次请求的客户端则会被断开连接,同时server端也会抛出异常
例如:将之前的MetricsHandler上的@ChannelHandler.Sharable注解去掉,然后启动server,此时在分别启动client和clien1
client:
client1:
server:
查看堆栈信息的最后一个io.netty.channel.DefaultChannelPipeline.checkMultiplicity(DefaultChannelPipeline.java:600)
io.netty.channel.ChannelInitializer.initChannel(ChannelInitializer.java:129)
initChannel(© ctx.channel());方法抛出了异常,被捕获后执行了exceptionCaught()方法
可以看到这里channel就被关闭了
所以这个注解不仅仅是一个标识,也是一种保护,防止不需要被共享的handler被共享
@Skip
跳过handler的执行
查看pipeline.addLast()方法,在最底层的addLast方法中,通过newContext()方法创建了一个AbstractChannelHandlerContext
查看newContext()方法,executionMask就是执行资格的判断
查看mask方法
判断是否跳过
在netty4中,@Skip并未开放给用户使用
仅仅是包可见的
Netty5开放了@Skip注解
@UnstableApi
提醒被标记的对象不稳定,慎用
@SuppressJava6Requirement
去除“Java6需求”的报警
比如Netty编译出的jar想在java6的版本上运行,就需要找出使用jdk6以上的功能,这些功能就不能使用了,所以需要用插件扫描出使用了java6以上特性的api。
比如代码中判断了jdk的版本,比如jdk版本大于等于7的时候调用DnsResolveContextException(String message, boolean shared)方法,但是插件在扫描时还是会将该调用报警出来,所以要用注解标识这个方法不用报警
插件使用
插件地址:https://github.com/mojohaus/animal-sniffer
@SuppresForbidden
去掉”禁用“报警
io.netty.util.NettyRuntime.AvailableProcessorsHolder#availableProcessors
计算CPU核数时,正常是调用Runtime.getRuntime().availableProcessors(),对于JDK10以下,使用docker,并且对docker进行了CPU限制的时候,这时候取出的数值就不准了,是宿主机的CPU核数,这时用户就可以通过设置io.netty.availableProcessors
参数来指定正确的CPU核数
如果用户不知道有availableProcessors这个方法,他去调用Runtime.getRuntime().availableProcessors(),这个时候就可能出现问题,这个时候就需要用插件去扫描避免这个错误,插件可以扫描到这个错误,但是也会将availableProcessors()方法报警出来,所以需要@SuppressForbidden这个注解,让availableProcessors()方法不被报警。
插件使用
插件地址:https://github.com/policeman-tools/forbidden-apis
signatures.txt