需求

无论是在spring boot 还是spring cloud 项目中,随着应用的不断增多,JVM参数的统一管理的重要性就会凸显出来,否则你可能会遇到几个问题:

  • Java进程出现性能问题,无GC日志支撑提供重要信息;
  • OOM异常频发,无法通过dump文件进行分析定位;
  • JVM堆内存设置规格不一致,被动等待出问题时发现;

作为运维,虽然没有超强的能力去最终的定位、分析、排查问题,但并不意味着我们就可以袖手旁观,那么我们能做什么呢?

  • 首先,我们要知道Java进程默认参数启动并不会打印某些我们需要的日志,而是需要我们按需去设置的。
  • 其次,即使开启了相应的日志参数,其统一输出位置就成了我们需要面对的问题,毕竟我们不希望满盘搜文件,浪费不必要的时间。
  • 最后,各种个性化的JVM参数,无益于运维对数量为百、千级别进程的有效管理。

此时统一的Java进程管理规范就可以发挥作用,通过标准化部署,Java使用统一的JVM参数运行,一旦某个应用出现异常,我们可以快速收集各种异常日志提供给研发进一步定位问题。

最终目标是,运维能提供给研发的有效信息,肯定是排除了因环境差异、参数配置差异等这些低级别干扰因素的,这样才能保证运维和研发的快速定位。

进程规范

1.GC日志

GC日志是用来描述JAVA虚拟机垃圾回收情况,主要用来快速定位潜在的内存故障和性能瓶颈。默认情况下是关闭的,我们需要通过参数设置启用。

参数格式如下:

-XX:+PrintGC 输出简要GC日志 
-XX:+PrintGCDetails 输出详细GC日志 
# gc.log输出到统一的日志目录
-Xloggc:/data/logs/gc.log  输出GC日志到文件
-XX:+PrintGCTimeStamps 输出GC的时间戳(以JVM启动到当期的总时长的时间戳形式) 
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800) 
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-XX:+PrintReferenceGC 打印年轻代各个引用的数量以及时长

2.dump文件

dump文件是Java进程出现OOM时自动生成的文件,通过此文件可以定位进程发生OOM的位置。可配置参数自动生成 dump 文件。

-XX:+HeapDumpOnOutOfMemoryError 
# dump文件输出到统一的日志目录
-XX:HeapDumpPath=/data/logs/HeapDumpOnOutOfMemoryError.dump

3.堆内存

堆内存设置是我们最容易设置混乱的参数,但对于我们标准化交付的服务器,虽不至于所有的应用使用完全一致的堆内存参数,但是大多数情况下是可以统一的。

-Xms2048m
-Xmx2048m
-XX:MaxPermSize:256m

至此,常见的JVM参数设置完毕,由于这些参数多和开发定位问题有关,因此我们在此只是将其作为进程管理规范的内容进行讲解,而不是研究其具体作用。但你以为我们的工作就到此为止了吗?

我们还可以通过设置JVM环境变量来实现部分扩展功能,因此也需要将环境变量作为进程管理规范的一部分。

4.JVM环境变量

环境变量便于运维能够灵活控制java进程运行的参数,这样可以和自动化相结合,实现应用的统一部署,有效避免更改配置文件的动作。

关于环境变量的定义,需要结合各自的生产环境特性来自行定义,我这面的定义的变量如下:

# 应用名
-Dapp.name=test
# 环境区分
-Denv=prod或uat或stg
# 临时文件目录
-Djava.io.tmpdir=/data/tmpdir

其中比较重要的是-Denv=xxx,通过在各个环境预设此变量,可实现不同环境配置文件的绑定。例如:在spring cloud项目中可以和各环境的配置中心调用进行关联。

其他环境变量大家可以和研发协商按需灵活添加。

按规范管理

1.统一管理

通过以上各参数的简单讲解,我们有了一套比较固定且完整的JVM参数,稳定性和灵活性兼顾,也便于我们后续的管理。

-server
-Xms2048m
-Xmx2048m
-XX:MaxPermSize:256m
-Dapp.name=test
-Denv=prod
-Djava.io.tmpdir=/data/tmpdir
-Xloggc:/data/logs/gc.log
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps 
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
-XX:+PrintReferenceGC
-Dsun.jnu.encoding=UTF-8

2.supervisor守护管理

随着JVM启动参数的尘埃落定,如何启动Java进程就是我们接下来要面对的问题。
我们经常使用的后台启动方式有以下几种:

  • nohup
  • screen
  • supervisor

其中nohup、screen都需要配合脚本,才能更友好的管理,因此我还是选择supervisor作为java应用的守护进程。

# 1.安装
yum install supervisor
systemctl enable supervisord
systemctl start supervisord

# 2.配置
vim /etc/supervisord.d/test.ini
[program:test]
;启动用户
user=work
;程序启动命令
command=java -server -Xms2048m -Xmx2048m -XX:MaxPermSize:256m -Dapp.name=test -Denv=prod -Djava.io.tmpdir=/data/tmpdir -Xloggc:/data/logs/gc.log -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps  -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintReferenceGC -Dsun.jnu.encoding=UTF-8 -jar test.jar
numprocs=1
;程序启动目录
directory=/App/java_app/test
;在supervisord启动时自启动
autostart=true
;程序异常退出后自动重启,可选值:[unexpected,true,false],默认为unexpected
autorestart=true
;启动10秒后没有异常退出,就表示进程正常启动了
startsecs=10
;启动失败自动重试次数
startretries=3

# 3.更新配置文件,更新配置文件并重启
supervisorctl update

# 4.重载配置文件 ,注意reload会导致supervisor重启,所管理的进程会重启
supervisorctl reload

# 5.查看状态
supervisorctl status

# 6.启动
supervisorctl start test

在此需要注意的是supervisor并不能托管任何进程,而只适合管理运行于前台的进程(如java 直接启动),对于运行后台daemon的进程(如tomcat),supervisorctl status会报错"BACKOFF Exited too quickly (process log may have details"。

总结

总结出一份用于运维过程中各个环境的JVM参数其实很简单,关键在于我们是否意识到了一份进程管理规范的重要性,怎样和当前的自动化水平来结合,实现其最终的价值。而对于只负责躺平的规范来说,我们做的这些工作意义不大。