文章目录
1 前言
2 基于heapAnalyzer和arthas的线上性能分析
2.1 需求背景
之前在客户现场,公司的大数据平台出现了页面访问卡顿的现象,经过诊断发现有内存泄漏问题,针对这次现象,对系统做一次全面的诊断。
2.2 现场保存
首先需要对线上平台的现场保存,依次执行以下几步。
第一步:ps -ef| grep datacompute 获取进程号${pid}
第二步:执行以下脚本(将内存、cpu以及网络环境生成镜像)
jmap -histo:live ${pid} > histo.snap
jmap -heap ${pid} > heap.snap
jmap -dump:live,format=b,file=dump.snap ${pid}
或者arthas的heapdump --live /home/admin/dump.hprof
jstack ${pid}> jstack.snap
lsof -p ${pid} -nP | grep TCP > alltcp.snap
netstat -an | grep 8181 > netconn.snap
netstat -an | grep -i close > closewait.snap
第三步:将应用全部日志文件拷贝出来。
第四步:重启应用
2.3 heapAnalyzer分析堆栈文件
-
下载
wget https://public.dhe.ibm.com/software/websphere/appserv/support/tools/HeapAnalyzer/ha457.jar -
运行
java -Xmx8g -jar ha*.jar
打开dump文件后点击Leak Suspect查看内存泄露
其中泄露最大的为如上图所示的地方,可以看到hdfs中的FileSystem的Cache对象,里面96.3的内存消耗全来自与它,大约数百兆,定位问题后就马上开干!
2.4 源码分析
首先看FileSystem出问题的get方法,可以发现,默认不开disable.cache,逻辑走红框。
然后查看CACHE的get方法,继续定位到getInternal方法
查看代码,可以看到cache从map中取,根据Key类作为key,那就再看Key类的equals方法,如下图所示:
其中最可疑的是ugi,继续查找红框所示代码,成功就在眼前。
最后如图红框所示,如果指定了用户,每次都会构造一个新的Subject,因此计算出来的UserGroupInformation的hashcode也都不一样。这样也最终导致FileSystem的Cache不生效
知道问题后,把平台上所有使用filesystem的地方都加上close,然后客户端加上
fs.hdfs.impl.disable.cache 为 true
参数即可。
2.5 Arthas检测
修改完代码重新运行起来后,轮到Arthas闪亮登场了!我们将使用Arthas检测一下子变量的值,以印证修改是否成功。
这里使用watch
来检测函数中变量的值,可以 参考之前的帖子《arthas助力线上问题分析-基本使用篇》中关于watch的使用。
在代码检测中,源码中需要检测静态内部类FileSystem.Cache中的map属性,如下图所示:
使用命令:
watch org.apache.hadoop.fs.FileSystem$Cache getInternal 'target.map.size()' -b -s
查看执行函数前后值,然后页面上点击触发。
其中target.map.size()为groovy表达式,查看map大小,也可以不加size,查看map中元素的值。
最后对比一下线上换包前后该变量大小区。
换包前
换包后
可以发现,map大小不增长了,bug修复。
3 问题代码热更新
尽管在生产环境热更新代码,并不是很好的行为,很可能导致:热更不规范,同事两行泪。
但很多时候我们的确希望能热更新代码,比如:
线上排查问题,找到修复思路了,但应用重启之后,环境现场就变了,难以复现。怎么验证修复方案?
又比如:
本地开发时,发现某个开源组件有bug,希望修改验证。如果是自己编译开源组件再发布,流程非常的长,还不一定能编译成功。有没有办法快速测试?
还记得开篇介绍线程cpu占用的那篇吗,如果需要线上动态修改可以继续往下操作。
3.1 查看类的classloaderhash
用sc命令查看需要热更新的类的hashcode
[arthas@3363085]$ sc -d com.hy.arthas.Problems | grep Hash
classLoaderHash 238e0d81
3.2 修复代码
3.2.1 已有源码的修复
使用《arthas助力线上问题分析-基本使用篇》用的cpu问题的代码,将代码修改一下,添加一个睡眠时间,解决cpu消耗过高问题。加上Thread.sleep(10000)
/**
* 模拟cpu高损耗
*/
private static void cpu() {
for (int i = 0; i < CPU_THREADS; i++) {
executorService.submit(new Thread(() -> {
while (true) {
cpurun();
}
}));
}
}
/**
* 只有完整退出的方法才可以被热更新
*/
private static void cpurun() {
log.info("cpu start");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
先执行mc命令将上传至服务器上的java文件编译成class文件。注:如果使用注解三方库等可能会造成失败。这种情况可以再本地编译完class文件再上传至服务器。
mc -c 238e0d81 /home/admin/zhy/arthastest/Problems.java -d /home/admin/zhy/arthastest
3.2.2 没有源码的修复
如果手上没有源代码,就先执行jad命令将class文件还原回java文件,修改后再编译成class文件上传服务器后再执行redefine。
jad --source-only com.hy.arthas.Problems > /home/admin/zhy/arthastest/Problems.java
3.3 热更新覆盖
执行redefine
将class文件热更新至正在运行的程序中。
redefine -c 238e0d81 /home/admin/zhy/arthastest/Problems.class
查看本地的日志可以看到代码已被更新。
3.4 热更新注意点
注意:
- 函数参数格式不能修改,只能修改函数内部的逻辑
- 不允许新增加类的函数或变量
- 函数必须能够退出,如果有函数在死循环中,无法执行更新类。在例子中,只能修改cpurun方法中的内容,cpu方法因为是个死循环,无法修改。
若进程重启,修改的代码会回到之前的版本
3.5 热更新总结
Arthas里 jad/mc/redefine 一条龙来线上热更新代码,非常强大,但也很危险,需要做好权限管理。
比如,线上应用启动帐号是 admin,当用户可以切换到admin,那么
- 用户可以修改,获取到应用的任意内存值(不管是否java应用)
- 用户可以attach jvm
- attach jvm之后,利用jvm本身的api可以redefine class
所以: - 应用的安全主要靠用户权限本身的管理
- Arthas主要是让jvm redefine更容易了。用户也可以利用其它工具达到同样的效果
4 springboot程序监控
虽然之前介绍了很多命令,比如trace/watch等,但是监控非静态的成员比较麻烦(ognl只能监控静态变量,如果理解有误麻烦留言指正)。
继续使用之前的例子,这里我们监控并操作成员变量fieldCache
public class ArthasService {
private static List<String> cache = new ArrayList<>();
//监控
private List<String> fieldCache = new ArrayList<>();
- 首先启动一个timetunnel,并发送curl请求
curl --location --request GET 'http://cdh219:3890/put' --form 'uid=3'
可以看到tt已经捕获到了,index为1000
[arthas@3625353]$ tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod
Press Q or Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 65 ms.
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1000 2021-01-19 16:54:48 5.819855 true false 0x13a01d RequestMappingHandlerAdapter invokeHandlerMethod
- 下一步拿到这个index,获取spring的ApplicationContext
[arthas@3625353]$ tt -i 1000 -w 'target.getApplicationContext()'
@AnnotationConfigServletWebServerApplicationContext[
reader=@AnnotatedBeanDefinitionReader[org.springframework.context.annotation.AnnotatedBeanDefinitionReader@12f4f8e3],
scanner=@ClassPathBeanDefinitionScanner[org.springframework.context.annotation.ClassPathBeanDefinitionScanner@37f5247f],
annotatedClasses=@LinkedHashSet[isEmpty=true;size=0],
basePackages=null,
logger=@Slf4jLocationAwareLog[org.apache.commons.logging.LogAdapter$Slf4jLocationAwareLog@3f98390c],
DISPATCHER_SERVLET_NAME=@String[dispatcherServlet],
webServer=@TomcatWebServer[org.springframework.boot.web.embedded.tomcat.TomcatWebServer@15aa7fed],
...
- 拿到ApplicationContext后基本上可以为所欲为了。。
比如 拿取值
[arthas@3625353]$ tt -i 1000 -w 'target.getApplicationContext().getBean("arthasService").fieldCache'
@ArrayList[
@String[local:2],
@String[local:3]
]
甚至添加/删除值
[arthas@3625353]$ tt -i 1000 -w 'target.getApplicationContext().getBean("arthasService").fieldCache.add("arthas test")'
@Boolean[true]
[arthas@3625353]$ tt -i 1000 -w 'target.getApplicationContext().getBean("arthasService").fieldCache.clear()'
@Boolean[true]
只能说,牛逼。。
5 修改spring的cron调度时间
5.1 需求背景
今天接到了研发帅哥的询问,想把线上的springboot程序内一个cron任务的执行周期改了,有没有可以热更新的方法。
5.2 模拟代码
先写一个测试类,其中cron的时间为10s一次。我们希望把cron间隔改成10s。
@Component
@Slf4j
@EnableScheduling
public class CronTest implements InitializingBean {
@Override
public void afterPropertiesSet() {}
@Scheduled(cron="*/5 * * * * ?")
public void cronTest() {
log.info("cron: "+System.currentTimeMillis());
}
}