Prometheus监控JVM实战
# 一、概述
我们知道,一款产品的生命周期,不仅仅是前期的规划设计,再到后续的开发,应用的运维也是重中之重。在运维过程中,监控则更是占据重要位置,它可以提供关键的运行时信息和指标,有助于优化应用程序的性能、稳定性和可伸缩性。
关于java的监控,这里有两种方式,一种是使用JMX Exporter
,一种是服务主动暴露actuator端点
,提供Prometheus进行指标抓取。
# 二、JMX Exporter方式
JMX Exporter 是一个用于将 Java Management Extensions (JMX) 指标导出为 Prometheus 可以抓取的格式的工具。JMX 是 Java 平台提供的一种管理和监控应用程序的标准方式,它允许我们在应用程序中公开各种指标和管理操作。
JMX Exporter 的作用是通过连接到应用程序的 JMX 接口,并将 JMX 指标转换为 Prometheus 可以解析的指标格式。这使得 Prometheus 可以定期抓取这些指标并进行长期存储、查询和可视化。
# 2.1 安装准备
这里我们使用JVM 进程内启动的方式,就是通过 javaagent 的形式运行 JMX-Exporter 的 jar 包,然后读取jvm的数据指标进行相关的指标暴露,基于这种方式,我们需要把JMX-Exporter
的jar包放置基础镜像中,这样我们每个应用都可以在启动的时候,直接配置参数就可以实现指标的暴露。
关于 JMX-Exporter
的下载,参考官方Github地址:点我查看 (opens new window)
# 2.2 jar包下载
[root@localhost sale-toc]# wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.19.0/jmx_prometheus_javaagent-0.19.0.jar
# 2.3 配置文件准备
jmx-config.yaml
lowercaseOutputLabelNames: true
lowercaseOutputName: true
whitelistObjectNames: ["java.lang:type=OperatingSystem"]
blacklistObjectNames: []
rules:
- pattern: 'java.lang<type=OperatingSystem><>(committed_virtual_memory|free_physical_memory|free_swap_space|total_physical_memory|total_swap_space)_size:'
name: os_$1_bytes
type: GAUGE
attrNameSnakeCase: true
- pattern: 'java.lang<type=OperatingSystem><>((?!process_cpu_time)\w+):'
name: os_$1
type: GAUGE
attrNameSnakeCase: true
2
3
4
5
6
7
8
9
10
11
12
13
参数解释
lowercaseOutputLabelNames
:指定输出的标签名称是否转换为小写
lowercaseOutputName
: 指定是否将输出的指标名称转换为小写
whitelistObjectNames
:只有符合指定 ObjectName 的 MBean 才会被暴露
pattern
: 用于匹配要暴露的 MBean 的 ObjectName
rules
:定义了两个规则来转换 JMX 指标为 Prometheus 指标。
- 第一个规则使用正则表达式匹配
java.lang<type=OperatingSystem>
的 MBean,并将匹配的属性转换为以os_
开头的指标名称,后跟_bytes
。attrNameSnakeCase: true
将属性名称转换为蛇形命名法。 - 第二个规则使用正则表达式匹配
java.lang<type=OperatingSystem>
的 MBean 中除了process_cpu_time
以外的所有属性,并将属性转换为以os_
开头的指标名称。同样,attrNameSnakeCase: true
将属性名称转换为蛇形命名法。
# 2.4 镜像构建
Dockerfile文件
FROM openjdk:17-jdk-alpine3.14
LABEL maintainer="tchuaxiaohua@163.com"
ENV appPort 9021
ENV appName sale-toc-1.0-SNAPSHOT.jar
COPY ${appName} /apps/
COPY jmx_prometheus_javaagent-0.19.0.jar /jmx/
COPY jmx-config.yaml /jmx/
COPY entrypoint.sh /usr/bin/entrypoint.sh
RUN chmod +x /usr/bin/entrypoint.sh
RUN apk --update add tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone && \
apk del tzdata && \
rm -rf /var/cache/apk/*
EXPOSE ${appPort}
ENTRYPOINT ["entrypoint.sh","start"]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
docker build -t registry.cn-hangzhou.aliyuncs.com/tfgol-dev/auto-toc:v2 .
docker push registry.cn-hangzhou.aliyuncs.com/tfgol-dev/auto-toc:v2
2
entrypoint.sh 脚本内容
#!/bin/sh
# 应用名
PROG_NAME=$0
ACTION=$1
APP_HOME=/apps # 应用部署家目录
JAR_NAME="sale-toc-1.0-SNAPSHOT.jar" # jar包的名字
JAVA_OPTS="-Xms512M -Xmx512M --spring.profiles.active=dev"
# 判断目录是否存在
if [ ! -d "$APP_HOME" ]; then
mkdir -p ${APP_HOME}
fi
usage() {
echo "Usage: $PROG_NAME {start|stop|restart}"
exit 2
}
start_application() {
echo "starting java process"
java -jar ${APP_HOME}/${JAR_NAME} ${JAVA_OPTS}
echo "started java process"
}
stop_application() {
checkjavapid=`ps -ef | grep java | grep ${APP_NAME} | grep -v grep |grep -v 'run.sh'| awk '{print$2}'`
if [[ ! $checkjavapid ]];then
echo -e "\rno java process"
return
fi
echo "stop java process"
times=60
for e in $(seq 60)
do
sleep 1
COSTTIME=$(($times - $e ))
checkjavapid=`ps -ef | grep java | grep ${APP_NAME} | grep -v grep |grep -v 'run.sh'| awk '{print$2}'`
if [[ $checkjavapid ]];then
kill -9 $checkjavapid
echo -e "\r-- stopping java lasts `expr $COSTTIME` seconds."
else
echo -e "\rjava process has exited"
break;
fi
done
echo ""
}
start() {
start_application
}
stop() {
stop_application
}
case "$ACTION" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
*)
usage
;;
esac
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
关于Dockerfile的更多应用,参考Java应用Dockerfile编辑 (opens new window)
# 2.5 配置启动参数
启动参数我们可以在两个地方配置,一个是在构建镜像的时候,直接把启动参数放到应用的启动脚本entrypoint.sh
之中,另一种就是启动的时候配置环境变量JAVA_TOOL_OPTIONS
,JAVA_TOOL_OPTIONS
是 JVM 工具选项的标准环境变量,启动的时候会读取该变量。
pod
资源清单文件
注意添加
JAVA_TOOL_OPTIONS
变量时,jmx配置文件及jar包路径,8088指的时暴露的指标获取端口
apiVersion: apps/v1
kind: Deployment
metadata:
name: sale-toc-service
namespace: dev
labels:
app: sale-toc
spec:
revisionHistoryLimit: 10
replicas: 1
selector:
matchLabels:
app: sale-toc
template:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8088"
prometheus.io/path: "/"
labels:
team: tfgol
app: sale-toc
spec:
imagePullSecrets:
- name: tfgol-registry
containers:
- name: sale-toc
image: registry.cn-hangzhou.aliyuncs.com/tfgol-dev/auto-toc:v2
imagePullPolicy: Always
env:
- name: JAVA_TOOL_OPTIONS
value: '-javaagent:/jmx/jmx_prometheus_javaagent-0.19.0.jar=8088:/jmx/jmx-config.yaml'
resources:
limits:
memory: 1024Mi
requests:
memory: 512Mi
livenessProbe:
tcpSocket:
port: 9201
initialDelaySeconds: 10
timeoutSeconds: 3
periodSeconds: 10
successThreshold: 1
failureThreshold: 5
startupProbe:
tcpSocket:
port: 9201
initialDelaySeconds: 25
timeoutSeconds: 3
periodSeconds: 10
successThreshold: 1
failureThreshold: 10
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
注意
上面清单文件中,我们在pod模板中定义了annotations
,新增了Prometheus相关的标签,这主要是为了后面注册时,我们会根据自标签过滤:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8088"
prometheus.io/path: "/"
2
3
4
除了上面在启动时,配置系统变量的方式,我们还可以直接修改entrypoint.sh
脚本,把启动参数添加至脚本中,修改启动参数:
# 添加变量
JAVA_TOOL_OPTIONS="-javaagent:/jmx/jmx_prometheus_javaagent-0.19.0.jar=8088:/jmx/jmx-config.yaml"
# 修改启动函数中的启动命令 把JAVA_TOOL_OPTIONS参数添加上
start_application() {
echo "starting java process"
java -jar ${JAVA_TOOL_OPTIONS} ${APP_HOME}/${JAR_NAME} ${JAVA_OPTS}
echo "started java process"
}
2
3
4
5
6
7
8
9
# 2.6 查看指标
上面我们应用资源清单文件之后,待pod启动完成,可以查看下podIP,然后直接访问${podip}:8088端口,即可查看指标数据
注意
JMX Exporter
有两种方式使用:作为 Java 代理(in-process)或作为独立的进程,上面我们是基于Java 代理模式,当然官方也比较推荐使用这种模式。
另一种作为独立进程,等于我们需要为没一个应用都起一个独立的jmx进程,这增加了复杂度,因为多了一个进程对于维护也不方便,因此生产中建议都直接使用Java代理的模式。
# 三、 Spring Actuator方式
Spring Boot Actuator 是一个用于监控和管理 Spring Boot 应用程序的模块,它提供了许多有用的特性,包括指标收集、健康检查、日志查看等,Actuator 提供了一组默认的 HTTP 端点,我们可以通过 HTTP 请求访问这些端点来获取应用程序的信息和指标。
此种方式因涉及到项目pom文件修改建议与开发人员一起配置
# 3.1 修改pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
2
3
4
5
6
7
8
# 3.2 修改配置
编辑 resources 目录下的
application.yml
文件,修改 actuator 相关的配置来暴露 Prometheus 协议的指标数据
management:
endpoints:
web:
exposure:
include: prometheus
metrics:
distribution:
slo:
http:
server:
requests: 1ms,5ms,10ms,50ms,100ms,200ms,500ms,1s,5s
tags:
application: ${spring.application.name}
2
3
4
5
6
7
8
9
10
11
12
13
# 3.3 打包启动
上面配置之后,如果本地有环境可以调试,启动项目之后,直接访问http://localhost:port/context-path/actuator/prometheus
访问到 Prometheus 协议的指标数据,说明相关的依赖配置已经正确,如果想要发生生产,则根据实际情况构建镜像发布即可,这种模式使用的端口直接就是应用的端口。
# 四、接入Prometheus
上面无论哪种方式,应用启动之后,通过访问指定的端点信息都可以访问到应用的jvm指标数据,这里我们以jmx exporter
方式,说明下接入Prometheus的步骤。
# 4.1 自定义应用监控
准备Prometheus文件,把pod中暴露的数据接入至Prometheus,更多方式参考Kube-Prometheus自定义告警 (opens new window)
- job_name: 'bsd-tfgol-jvm'
metrics_path: "/"
scrape_interval: 30s
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
tls_config:
insecure_skip_verify: true
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
target_label: __address__
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: namespace
- source_labels: [__meta_kubernetes_pod_label_app]
action: replace
target_label: app_name
- source_labels: [__meta_kubernetes_pod_label_team]
action: replace
regex: (.*)
target_label: team
- source_labels: [__meta_kubernetes_pod_name]
action: replace
regex: (.*)
target_label: pod
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
配置说明
relabel_configs
: 这里是用来过滤哪些pod可以注册至该job下。
__meta_kubernetes*
: k8s中几乎所有标签都是以此开头,后面跟的是所属资源
# 4.2 配置文件注册至Prometheus
这里操作演示是基于Kube-Prometheus在看k8s集群中部署的监控系统
# 创建secret
kubectl create secret generic tfgol-jvm --from-file=tfgol-jvm.yaml -n monitoring
# 注册至prometheus
vim prometheus-prometheus.yaml # 新增
additionalScrapeConfigs:
name: tfgol-jvm
key: tfgol-jvm.yaml
# 激活配置
kubectl apply -f prometheus-prometheus.yaml
2
3
4
5
6
7
8
9
注意
如果使用的Prometheus版本比较新,可能会出现以下权限错误:
ts=2023-07-07T08:20:32.971Z caller=klog.go:116 level=error component=k8s_client_runtime func=ErrorDepth msg="pkg/mod/k8s.io/client-go@v0.26.1/tools/cache/reflector.go:169: Failed to watch *v1.Endpoints: failed to list *v1.Endpoints: endpoints is forbidden: User \"system:serviceaccount:monitoring:prometheus-k8s\" cannot list resource \"endpoints\" in API group \"\" at the cluster scope"
ts=2023-07-07T08:20:56.421Z caller=klog.go:108 level=warn component=k8s_client_runtime func=Warningf msg="pkg/mod/k8s.io/client-go@v0.26.1/tools/cache/reflector.go:169: failed to list *v1.Pod: pods is forbidden: User \"system:serviceaccount:monitoring:prometheus-k8s\" cannot list resource \"pods\" in API group \"\" at the cluster scope"
ts=2023-07-07T08:20:56.421Z caller=klog.go:116 level=error component=k8s_client_runtime func=ErrorDepth msg="pkg/mod/k8s.io/client-go@v0.26.1/tools/cache/reflector.go:169: Failed to watch *v1.Pod: failed to list *v1.Pod: pods is forbidden: User \"system:serviceaccount:monitoring:prometheus-k8s\" cannot list resource \"pods\" in API group \"\" at the cluster scope"
2
3
这是因为对应的sa没有权限,修改下Prometheus角色权限即可,prometheus-clusterRole.yaml
:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/component: prometheus
app.kubernetes.io/instance: k8s
app.kubernetes.io/name: prometheus
app.kubernetes.io/part-of: kube-prometheus
app.kubernetes.io/version: 2.42.0
name: prometheus-k8s
rules:
- apiGroups:
- ""
resources:
- nodes/metrics
- endpoints
- pods
- services
verbs:
- get
- list
- nonResourceURLs:
- /metrics
verbs:
- get
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 4.3 查看job注册
可以看到pod实例中的jvm指标已经正常采集过来
# 4.4 告警规则
告警规则我们直接基于operator,把资源清单应用即可,需要主要
prometheus: k8s
和role: alert-rules
必须要有
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
labels:
prometheus: k8s
role: alert-rules
name: bsd-tfgol-jvm
namespace: monitoring
spec:
groups:
- name: bsd-tfgol-jvm-rules
rules:
- alert: 堆内存使用率
expr: sum(jvm_memory_used_bytes{area="heap"}) by(instance,app_name,namespace) / sum(jvm_memory_max_bytes{area="heap"}) by(instance,app_name,namespace) * 100 > 95
for: 2m
labels:
severity: warning
annotations:
description: '环境:{{ $labels.namespace }},应用:{{ $labels.app_name }} 堆内存使用率超过95%,当前值:{{ $value | printf "%.2f" }}'
- alert: 应用线程阻塞预警
expr: sum(jvm_threads_states_threads{state="blocked",job="bsd-channel-jvm",namespace="prod"})by (pod,application,namespace) > 0
for: 2m
labels:
severity: warning
annotations:
description: '应用{{ $labels.pod }}当前有{{ $value | printf "%.2f" }}个线程处于blocked状态'
- alert: 应用等待线程预警
expr: sum(jvm_threads_states_threads{state="timed-waiting",job="bsd-channel-jvm",namespace="prod"})by (pod,application,namespace) > 500
for: 10s
labels:
severity: warning
annotations:
description: '应用{{ $labels.pod }}当前有{{ $value | printf "%.2f" }}个线程处于等待状态'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 4.5 grafana模版导入
注: 如果你是基于jmx,则可以使用模版
8563
如果是基于spring actuator
则可以使用官网模板4701