背景
日常开发,JDK提供的工具类,想必各位同学已经很熟练了。本篇介绍一个相对比较冷门的工具类——toos.jar。
写这篇前铺垫了《如何快速分析Java OOM文件》和《SpringBoot 集成Prometheus》两篇,客官不妨去看看先。按计划轮到整理这块逻辑了,目的就是用代码替代:jmap -histo:live pid 把采集到的数据写到Promethues,再通过Grafana看板展示,节约troubleshooting时间,提高工作效率。
起步
要把数据集成到Prometheus,分两步:
a. 拿到类似上述jmap 指令执行返回的数据
b. 自定义Prometheus指标,把数据放到对应的指标下
对于a点我们可以利用JDK中提供了HotSpotVirtualMachine类来获取JVM中的信息(据悉只针对sun公司提供的可以,IBM的貌似不行),上述jmap 指令执行返回大致如下:
这里先来个类似hello world的代码,看看如何拿到上面的数据:
public static void main(String[] args) throws IOException, AttachNotSupportedException {
RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean();
String name = bean.getName();
int index = name.indexOf('@');
String pid = name.substring(0, index);
// TODO !important 这里我用的平台是windows
HotSpotVirtualMachine machine = (HotSpotVirtualMachine) new sun.tools.attach.WindowsAttachProvider().attachVirtualMachine(pid);
InputStream is = machine.heapHisto("-all");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int read;
byte[] buff = new byte[1024];
while((read = is.read(buff)) > 0){
baos.write(buff, 0, read);
}
is.close();
machine.detach();
System.out.println(baos);
}
这里有个问题需要注意:项目是maven方式管理jar包,而JDK提供的tools.jar在安装目录的lib下:
上面这个问题在实际处理过程中踩了不少坑,针对maven依赖不是外部的,情况可大致有以下几种办法:
直接固定法:缺点很明显,不同的同学、不同的环境这个路径完全有可能不一样。这个只适合玩玩而已。
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.0</version>
<optional>false</optional>
<scope>system</scope>
<!-- 直接固定法-->
<systemPath>D:/ProgramFiles/DevOps/jdk/jdk1.8.0_171/lib/tools.jar</systemPath>
</dependency>
相对固定法:用内置的环境变量来替代固定盘符,如下这样只前进了一小步:
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.0</version>
<optional>false</optional>
<scope>system</scope>
<systemPath>${env.JAVA_HOME}../lib/tools.jar</systemPath>
</dependency>
就在我以为这个可以的时候去Jenkins deploy时发现问题依然存在。
相对路径法:在项目的Resource目录下(创建目录)放置tools.jar,结合相对固定法,应该能解决掉问题。
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.0</version>
<optional>false</optional>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/tools.jar</systemPath>
</dependency>
看似上面的问题解决了,但只解决了同学们安装目录不统一的问题,但引入了新的问题:(貌似)当pom.xml文件存在systemPath标签的时候,其他maven坐标会失效(无论把这个移到最前还是最后),导致项目编译报错。 “偷梁换柱”法: 绞尽脑汁、想去想来还是亮出我的杀手锏:找运维同学把tools.jar上传到maven私服,怎么样?这主意一出来有那么几秒我都佩服自己的脑袋瓜。重新部署后用2牛顿的力点击了鼠标,期待的效果并没有出现,除了这个: 大意了,我没有闪!这把飞刀很明显是提醒着我还年轻,忽略了Java的跨平台特性,我传给运维的jar文件是我本机的,本机的意思就是windows平台,所以当代码发上去的时候结果就早已注定。 值得肯定的一点是,解决jar的问题这个办法是有效的。至于跨平台的问题,我还有办法:让运维把安装在liunx平台下JDK对应目录中的tools.jar上传到私服,问题得以解决 <dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<!--1.0 for windows-->
<!--<version>1.0</version>-->
<!--1.0.1 for linux-->
<version>1.0.1</version>
<optional>false</optional>
</dependency>
为避免有同学模仿我,还有一个小坑要给同学们说下,Java代码中要做相应的调整: //todo 这里要随着平台改变
//tools.jar maven 1.0
HotSpotVirtualMachine machine = (HotSpotVirtualMachine) new sun.tools.attach.WindowsAttachProvider().attachVirtualMachine(pid);
//tools.jar maven 1.0.1
HotSpotVirtualMachine machine = (HotSpotVirtualMachine) new sun.tools.attach.LinuxAttachProvider().attachVirtualMachine(pid);
整理
上面只是一个伪代码,主要解决tools.jar的依赖和跨平台问题。但一个完整的功能还需要继续优化,比如需要考虑各业务怎么接入、怎么灵活控制、采集频率等。
如何接入:
不扯多的犊子了,做成基础工具类,业务系统升级maven依赖是最简单的。
灵活控制:
代码处理能模仿jmap拿到JVM中的实例信息这个只是基本要求,还要考虑到:
升级后若该功能影响性能得话能不能实时关闭?
每次取多少条合适?(在《如何快速分析Java OOM文件中》只取20条就能判断问题)
能不能自定义,只取包含自己公司项目包名关键字的信息?
能不能过滤掉一些类信息?
采集频率:
采集太过频繁影响原有业务,频率太低出现问题时参考的意义不大。
结合上述问题,我们需要先增加一些配置项,用来控制业务代码:
//配置信息,也可以单独写到某个类中
//控制每次提取多少行 默认20(这是由上一次排查问题的经验主义得出)
@Value("${common.tools.show.jvm_line:20}")
private Integer JVM_LINE;
//控制是否只显示包含公司项目关键字的类名 如你的包名都是com.xxxx 那就当该值为true时,判断类名是否包含xxxx
@Value("${common.tools.show.JVM_KEY_WORD:false}")
private Boolean JVM_KEY_WORD;
//总开关,控制是否打开采集JVM信息的开关,为减少大家接入的操作,默认为true
@Value("${common.tools.show.jvm_info:true}")
private Boolean JVM_INFO;
//控制是否要过滤掉某个不想看的类,如[I,[B
@Value("${common.tools.show.jvm_skip:}")
private String JVM_SKIP;
在相关的Class上增加@RefreshScope注解,可以让这些配置项的变更更灵活,应用收到对应的配置变更事件通知后能热加载,无需重启即可按新的配置执行相关逻辑。 //1.用@Scheduled控制频率,刚启动时30秒后开始采集,后续每15秒采集一次(因为promethes 15采集一次)
15000,initialDelay = 30000)//cron="*/5 * * * * ?" (fixedDelay =
public void showJvmBytes() throws IOException, AttachNotSupportedException{
if(!JVM_INFO){
log.debug("showJvmBytes show.jvm_info is:false,stop.");
return;
}
//2.节约时间如果是windows就可以不用往下了,(windows)本地调试时放开
String os = System.getProperty("os.name");
os = os.toLowerCase(Locale.ROOT);
if (os.indexOf("windows")>=0) {
log.debug("showJvmBytes the system is not allow,stop.");
return;
}
RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean();
String name = bean.getName();
int index = name.indexOf('@');
String pid = name.substring(0, index);
HotSpotVirtualMachine machine = (HotSpotVirtualMachine) new sun.tools.attach.LinuxAttachProvider().attachVirtualMachine(pid);
try (InputStream inputStream = machine.heapHisto("-all");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();){
int read;
byte[] buff = new byte[1<<12];
while((read = inputStream.read(buff)) > 0){
outputStream.write(buff, 0, read);
}
machine.detach();
// DEMO
// num #instances #bytes class name
// ----------------------------------------------
// 1: 21309 11975720 [B
// 2: 56059 7190248 [C
// 3: 3297 1638632 [I
//3.把拿到的数据按换行符拆开
String[] arr = outputStream.toString().split("\\n");
int row = 1;
for (String line : arr) {
//4.前几行不要或num大于配置的不要
if(StringUtils.isBlank(line) || line.indexOf("num")>=0 || line.indexOf("---")>=0){
log.debug("showJvmBytes line num is:{},value:{} invalid ,skip.",row,line);
continue;
}
//5.num大于配置的不要
if(row > JVM_LINE){
log.debug("showJvmBytes line num is:{},config:{},skip...",row,JVM_LINE);
break;
}
//6.当需要过滤只展示包含关键字的类信息时,不满足条的不要
boolean FILTER_XXXX = line.indexOf("xxxx")>=0;
if(JVM_KEY_WORD && !FILTER_XXXX){
log.debug("showJvmBytes num:{} line:{} not contain key word:{},skip...",row,line,"xpilot");
continue;
}
log.debug("showJvmBytes line:{}",line);
//7: 21309 11975720 [B
//按空格拆,拿到instance和bytes
String[] line_arr = line.split(("\\s+"));
//过滤掉不希望展示的类
String className = line_arr[line_arr.length-1];
if(!StringUtils.isBlank(JVM_SKIP) && JVM_SKIP.indexOf(className)>=0){
log.debug("showJvmBytes config JVM_SKIP:{} contains:{},skip...",JVM_SKIP,className);
continue;
}
//数据写入Prometheus
microMeterTool.jvmUsageByteGauge(className,Long.parseLong(line_arr[line_arr.length-2]));
microMeterTool.jvmUsageInstanceGauge(className,Long.parseLong(line_arr[line_arr.length-3]));
row++;
}
//清除某个指标的历史数据
microMeterTool.clearHistoryKey(keys,"app_jvm_instances");
microMeterTool.clearHistoryKey(keys,"app_jvm_bytes");
}catch (Exception e){
log.error("showJvmBytes:",e);
}
}
代码中machine.heapHisto("-all")是用来获取堆信息的工具。为尽量避免影响业务、浪费资源,可以进一步优化一下,参数改为传:live(eg:machine.heapHisto("-live")),用来指定只包括存活的对象。但有一个点需要提前告诉大家,使用live参数为导致full gc,所以酌情使用:
还有一个需要注意的点就是对平台的判断:
String os = System.getProperty("os.name");
os = os.toLowerCase(Locale.ROOT);
if (os.indexOf("windows")>=0) {
log.debug("showJvmBytes the system is not allow,stop.");
return;
}
有的同学还是有钱的(也可能是习惯问题),自带电脑上班。收到线报,当用mac时我上面判断环境的地方就绕过了,运行后续代码就会报错:
解决方案方案其实可以类似上面那样根据平台放三个对应的tools.jar到maven私服。这里暂时逆向思维处理,不用根据平台加载jar,代码调整为os.name包含:linux时放过就好,经测试,不影响原有逻辑。
String os = System.getProperty("os.name");
os = os.toLowerCase(Locale.ROOT);
log.debug("showJvmBytes show.jvm_info os_name is:{}",os);
if (os.indexOf("linux") < 0) {
log.debug("showJvmBytes the system is not allow,stop.");
return;
}
效果展示
以上代码运行会在Prometheus产生对应的指标,运维同学配置后可通过Grafana展示:
这些数据只是15秒内JVM的情况,垃圾回收可能会影响这些数据,所以注入Prometheus后需要同步删除历史数据,如何删除已在《SpringBoot集成Prometheus》这篇中有解释。
扩展
@Scheduled注解有2种配置执行频率的方式:
@Scheduled(fixedDelay = 15000,initialDelay = 30000)
@Scheduled(cron = "${common.tools.jvm.monitor.cron:0/15 * * * * ?}")
一开始,我的逻辑写好就能跑,因为其他同学也用这个注解跑了一个定时任务,他的类上加了@EnableScheduling。后来因为逻辑优化:他的定时任务不需要主动触发,所以对方加了通过配置文件的key来管控。奇怪的是我的Scheduled也不跑了,查询资料后得知:默认内部是一个单线程Scheduer来处理@Scheduled注解实现的定时任务:org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#setScheduler
译:如果两者都无法解析,会注册器内部创建一个本地单线程用来默认调度程序。
解决方案:
既然我的这个没有线程处理,那我们就主动造一个:
"showJvmBytesExecutor") (
public ThreadPoolExecutor showJvmBytesExecutor() {
return new ThreadPoolExecutor(2, 2, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), new BasicThreadFactory("JVM_BYTE_MONITOR_EXECUTOR"),
new ThreadPoolExecutor.CallerRunsPolicy());
}
然后在对应的@Scheduled上指定用声明的这个线程异步来跑就可以了:
public void showJvmBytes() throws IOException, AttachNotSupportedException{
//ignore code
}
当然,这种问题也有其他方式可以解决:https://www.jb51.net/article/264297.htm
总结
本篇主要讲toos.jar的使用,功能已实现,大体代码如下。对于上述效果图中出现的[I、[J、[B...等类的解释,一并给大家整理如下:
[ 代表的是数组,[[ 则代表二维数组,字母是Java中的数据类型,对开发同学来说,要记住的话只留意以下3个特殊的就行了(oracle官网有解释):
至此,用JDK下的tools.jar完成了在《如何快速分析Java OOM文件》中提到的:jmap -heap pid 命令类似的信息输出 。
关于指令Java字节码可以看这篇,比较详细,好奇的可以一步步跟着做做看。
P.S:关于Promethes如何搭建、如何监控服务、如何自定义监控指标请查看《SpringBoot集成Prometheus》一文最后的自定义指标部分,祝你好运气。
划水专用:
https://codeleading.com/article/41242615114/#google_vignette
https://qa.1r1g.com/sf/ask/3302421/
https://blog.csdn.net/HO1_K/article/details/127221511
https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getName--
有问题欢迎指出,听说吐槽与关注更配哦
两岸猿声啼不住,轻舟已过万重山。
——李白《早发白帝城》