前端时间把公司的一个分布式定时调度的系统弄上了容器云,部署在kubernetes,在容器运行的动不动就出现问题,特别容易jvm溢出,导致程序不可用,终端无法进入,日志一直在刷错误,kubernetes也没有将该容器自动重启。业务方基本每天都在反馈task不稳定,后续就协助接手看了下,先主要讲下该程序的架构吧。
该程序task主要分为三个模块:
console进行一些cron的配置(表达式、任务名称、任务组等);
schedule主要从数据库中读取配置然后装载到quartz再然后进行命令下发;
client接收任务执行,然后向schedule返回运行的信息(成功、失败原因等)。
整体架构跟github上开源的xxl-job类似,也可以参考一下。
容器的网络使用了BGP,打通了公司的内网,所以可以直接通过ip来进行程序的调试,主要是在启动的jvm参数中添加:
JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=0.0.0.0:8000,server=y,suspend=n " JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false "其中,调试模式的address最好加上0.0.0.0,有时候通过netstat查看端口的时候,该位置显示为127.0.0.1,导致无法正常debug,开启了jmx之后,可以初步观察堆内存的情况。
堆内存(特别是cms的old gen),初步看代码觉得是由于用了大量的map,本地缓存了大量数据,怀疑是每次定时调度的信息都进行了保存。
2. memory analyzer、jprofiler进行堆内存分析先从容器中dump出堆内存
jmap -dump:live,format=b,file=heap.hprof 58由图片可以看出,这些大对象不过也就10M,并没有想象中的那么大,所以并不是大对象的问题,后续继续看了下代码,虽然每次请求都会把信息放进map里,如果能正常调通的话,就会移除map中保存的记录,由于是测试环境,执行端很多时候都没有正常运行,甚至说业务方关闭了程序,导致调度一直出现问题,所以map的只会保留大量的错误请求。不过相对于该程序的堆内存来说,不是主要问题。
3. netty的方面的考虑另一个小伙伴一直怀疑的是netty这一块有错误,着重看了下。该程序用netty自己实现了一套rpc,调度端每次进行命令下发的时候都会通过netty的rpc来进行通信,整个过程逻辑写的很混乱,下面开始排查。
首先是查看堆内存的中占比:
可以看出,io.netty.channel.nio.NioEventLoop的占比达到了40%左右,再然后是io.netty.buffer.PoolThreadCache,占比大概达到33%左右。猜想可能是传输的channel没有关闭,还是NioEventLoop没有关闭。再跑去看一下jmx的线程数:
达到了惊人的1000个左右,而且一直在增长,没有过下降的趋势,再次猜想到可能是NioEventLoop没有关闭,在代码中全局搜索NioEventLoop,找到一处比较可疑的地方。
声明了一个NioEventLoopGroup的成员变量,通过构造方法进行了初始化,但是在执行syncRequest完之后并没有进行对group进行shutdownGracefully操作,外面对其的操作并没有对该类的group对象进行关闭,导致线程数一直在增长。
最终解决办法:
在调用完syncRequest方法时,对ChannelBootStrap的group对象进行行shutdownGracefully
提交代码,容器中继续测试,可以基本看出,线程基本处于稳定状态,并不会出现一直增长的情况了
还原本以为基本解决了,到最后还是发现,堆内存还算稳定,但是,直接内存依旧打到了100%,虽然程序没有挂掉,所以,上面做的,可能仅仅是为这个程序续命了而已,感觉并没有彻底解决掉问题。
4. 直接内存排查第一个想到的就是netty的直接内存,关掉,命令如下:
-Dio.netty.noPreferDirect=true -Dio.netty.leakDetectionLevel=advanced