如何快速排查生产问题

如何快速排查生产问题
大家都遇到过哪些生产问题呢

  1. 磁盘满了?
  2. CPU 飙高?
  3. 内存溢出?

内存溢出又分好几种:
堆内存溢出、
元空间溢出、
线程栈溢出、
直接内存溢出。

在 Netty 中,最常见的当属直接内存溢出了,而内存溢出,往往又是内存泄漏导致的,所以,我们一般是排查内存泄漏。
那么,对于直接内存泄漏,我们该如何快速排查呢?这里,我总结了几个步骤以供参考:
1 查看监控,直接内存使用情况是否稳定;
2 查看日志,是否有直接内存相关的报错;
3 本地调试,关闭池化内存、添加内存泄漏检测、模拟大量请求、步步为营、耐心寻找泄漏点;
所以,我们这里多次使用的时候需要显式地给 msg 加一次引用次数,但是,一不小心,我手抖了一下,多加了一次引用次数就发到生产上了,会出现什么样的情况呢?
首先,程序是肯定可以正常运行的。
其次,内存溢出是一个非常缓慢的过程,可能十天半个月才会出现一次。
最后,导致的结果就是难以排查,无奈之下,只能重启,甚至,有的公司会写个定时任务,每天晚上系统没人用的时候偷偷地自动重启一次。
针对这种情况,我们来看看怎么排查。

1查看监控
查看监控,会发现,直接内存的使用情况,呈现一条缓慢上升的曲线,类似于这样:
在这里插入图片描述这个时间的长度可能是几个小时、一天、几天、十几天,都有可能,取决于系统的流量,随着时间的推移,最终达到了监控告警值,半夜被喊起来排查问题。

2查看日志
遇到这种问题先不要慌,先去日志中查看是否有直接内存相关的报错,如果运气好是有相关日志的,拿出来分析一波,最坏的情况是完全没有,在完全没有日志可以参考的情况下,只能启用本地调试大法了。

3本地调试
本地调试不是说拿到代码胡乱调试一通,也要讲究一些策略。

第一步,关闭池化内存,为什么要关闭池化内存?
因为,使用池化内存的时候,创建第一个 ByteBuf 的时候会分配 16M 的内存块,等这 16M 的内存块不够使用的时候才会创建下一个 16M 的内存块,而且,这 16M 使用完了之后并不会立即释放,而是交给 Netty 的内存来进行管理,方便后续复用,有内存池的干扰,很难看出内存泄漏导致的直接内存缓慢增长的过程。
那么,要怎么关闭池化内存呢?
这就要使用到前面章节讲过的一个参数了:-Dio.netty.allocator.type=unpooled,将这个参数的值设置为 unpooled 即可。
第二步,添加内存泄漏检测,如何添加内存泄漏检测?
这就要使用一个我们没讲过的参数了:-Dio.netty.leakDetection.level=PARANOID,这个参数有四个值:
DISABLED,表示不检测内存泄漏;
SIMPLE,表示会追踪小部分对象的内存使用情况,同时会消耗少量的资源;
ADVANCED,表示会追踪大部分对象的内存使用情况,同时会消息大量的资源;
PARANOID,表示会追踪所有对象的内存使用情况。
其中,默认值是 SIMPLE,四个值的强度为依次增加。
上面在查看日志的时候说的运气好,就是指这里使用 SIMPLE 的情况下有可能会有相关的日志。
在本地调试,直接开启最高级别 ——PARANOID。

第三步,模拟大量请求,为什么要模拟大量请求呢?
因为,上面的内存泄漏检测,只有在 gc 的时候才会打印相关的日志,平时,这些信息会保存在 Set 中,有兴趣的同学可以看看 ResourceLeakDetector 这个类,怎么才能快速 gc 呢?模拟大量的请求不失为一种好方法。
那么,怎么模拟大量的请求呢?
其实,也很简单,只需要在 MockClient 中加一个循环即可:

public class MockClient {
    public static void start(Channel channel) {
        // 发送hello消息
        for (int i = 0; i < 1000; i++) {
            HelloRequest helloRequest = HelloRequest.newBuilder()
                    .setName("哥")
                    .build();
            MessageUtils.sendRequest(helloRequest);
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

第四步,步步为营,耐心寻找泄漏点。
这一步没有什么取巧的办法,只能慢慢分析上面的日志,或者,使用调试大法跟踪 ByteBuf 走过的路径,再慢慢缩小范围一步一步排查。
在上面日志的最后,已经明确告诉了你这个 ByteBuf 是在哪里创建的了,所以,使用调试大法的时候直接把断点打在那里一步一步往后面跟就可以了。
调试大法,前面我们已经用过太多次了,这里就不细说了,主要还是看日志,找到上面日志的 “#”3 处:

好吧,原来是这里多加了一次引用计数,导致这个 msg 回收不掉,最终导致内存泄漏。
把 retain () 去掉一行,重启服务端和客户端,观察服务端日志:

好了,整个排查过程就介绍完了,那么,我们在平时的开发中有没有什么好的方法可以规避这种问题呢?
要想预防这种直接内存泄漏的问题,方法还是有的,最简单有效的方法就是在测试环境设置这两个参数:
-Dio.netty.allocator.type=unpooled
-Dio.netty.leakDetection.level=PARANOID
在测试同学压测的时候,观察一下监控的情况,如果监控出现了直接内存缓慢增长的情况,那说明就有内存泄漏了,提前排查问题提前规避,当然,排查的过程还是一样要分析日志,或者耐心调试,这是少不了的步骤。
最后,在生产环境,记得把这两个参数去掉,或者恢复到默认值。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值