一、前言

近期我们要对项目线上的Java应用进行JVM监控,由于我们的Java程序都部署在Kubernetes集群中,难点就是不能像常规传统的方式对其监控,这里我们采用Grafana+Prometheus的方式去实时监控Java应用的JVM数据,那么中间就缺少不了jmx_exporter。接下来就开始实战,全程干货!

二、实践流程

2.1、封装jvm-exporter以及JXM监控指标

我们采集java监 Jvm指标一般都是通过jmx_javaagent程序去实现,另外为了便于监控和采集,需要将采集到的JMX指标转换成Prometheus可以监控指标,那就需要具体的规则。同样在这里将其封装到基础镜像中

下载指标规则文件以及jmx-prometheus_javaagent.jar

wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.16.0/jmx_prometheus_javaagent-0.16.0.jar

官网标配:https://github.com/prometheus/jmx_exporter/blob/main/example_configs/tomcat.yml

#vim jmx-exporter.yaml
---   
lowercaseOutputLabelNames: true
lowercaseOutputName: true
whitelistObjectNames: ["java.lang:type=OperatingSystem", "Catalina:*"]
blacklistObjectNames: []
rules:
  - pattern: 'Catalina<type=Server><>serverInfo: (.+)'
    name: tomcat_serverinfo
    value: 1
    labels:
      serverInfo: "$1"
    type: COUNTER
  - pattern: 'Catalina<type=GlobalRequestProcessor, name=\"(\w+-\w+)-(\d+)\"><>(\w+):'
    name: tomcat_$3_total
    labels:
      port: "$2"
      protocol: "$1"
    help: Tomcat global $3
    type: COUNTER
  - pattern: 'Catalina<j2eeType=Servlet, WebModule=//([-a-zA-Z0-9+&@#/%?=~_|!:.,;]*[-a-zA-Z0-9+&@#/%=~_|]), name=([-a-zA-Z0-9+/$%~_-|!.]*), J2EEApplication=none, J2EEServer=none><>(requestCount|processingTime|errorCount):'
    name: tomcat_servlet_$3_total
    labels:
      module: "$1"
      servlet: "$2"
    help: Tomcat servlet $3 total
    type: COUNTER
  - pattern: 'Catalina<type=ThreadPool, name="(\w+-\w+)-(\d+)"><>(currentThreadCount|currentThreadsBusy|keepAliveCount|connectionCount|acceptCount|acceptorThreadCount|pollerThreadCount|maxThreads|minSpareThreads):'
    name: tomcat_threadpool_$3
    labels:
      port: "$2"
      protocol: "$1"
    help: Tomcat threadpool $3
    type: GAUGE
  - pattern: 'Catalina<type=Manager, host=([-a-zA-Z0-9+&@#/%?=~_|!:.,;]*[-a-zA-Z0-9+&@#/%=~_|]), context=([-a-zA-Z0-9+/$%~_-|!.]*)><>(processingTime|sessionCounter|rejectedSessions|expiredSessions):'
    name: tomcat_session_$3_total
    labels:
      context: "$2"
      host: "$1"
    help: Tomcat session $3 total
    type: COUNTER

构建基础镜像

#vim Dockerfile
FROM registry.cn-beijing.aliyuncs.com/devops-op/openjdk:1.8.0_212 
RUN mkdir /opt/apps/prometheus/ -p
ADD jmx_prometheus_javaagent-0.16.0.jar /opt/apps/prometheus
ADD jmx-exporter.yaml /opt/apps/prometheus 
#dockewr build registry.cn-beijing.aliyuncs.com/devops-op/openjdk:1.8.0

编写java启动脚本

编写shell启动脚本,脚本中我指定了jmx_prometheus_javaagent并暴露出对应的Metrics端口,同时指定了指标规则配置文件。因为前面做了封装,所以不需要考虑java agent以及规则文件挂载映射等关联性问题

#!/bin/sh
echo -------------------------------------------
echo start server
echo -------------------------------------------
# JAVA_HOME
#export JAVA_HOME=/data/jdk
# 设置项目代码路径
export _EXECJAVA="$JAVA_HOME/bin/java"

JAVA_OPTS="-XX:+UseContainerSupport -XX:InitialRAMPercentage=40.0 -XX:MinRAMPercentage=20.0 -XX:MaxRAMPercentage=80.0  -XX:-UseAdaptiveSizePolicy"
#jar包 名称
JAR_NAME=`ls /data/webserver/*jar`

# 启动类
$_EXECJAVA -javaagent:/opt/apps/prometheus/jmx_prometheus_javaagent-0.16.0.jar=9901:/opt/apps/prometheus/jmx-exporter.yaml $JAVA_OPTS -jar $Xms $Xmx -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC -XX:+PrintReferenceGC -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintCommandLineFlags -XX:+PrintFlagsFinal -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45 -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs   $JAR_NAME

2.3、集成业务镜像

最后模拟生产环境持续集成手动构建一个业务镜像。首先编写一个Dockerfile,将前面写的shell启动脚本作为java程序的启动命令

FROM  registry.cn-beijing.aliyuncs.com/devops-op/openjdk:1.8.0
RUN mkdir /data/webserver -p
ENV LANG en_US.UTF-8
ADD start.sh /data/webserver/
RUN chmod +x /data/webserver/start.sh

ADD demo-0.0.1-SNAPSHOT.jar /data/webserver/

EXPOSE 8081

ENTRYPOINT ["/data/webserver/start.sh"]

所有准备工作就绪之后,就可以开始build了

#[root@k8s-master01 apps]# ls
buobi-demo.yaml  demo-0.0.1-SNAPSHOT.jar  Dockerfile  prometheus  start.sh
[root@k8s-master01 apps]# docker build -t registry.cn-beijing.aliyuncs.com/devops-op/huobi-demo:1.0 .

如何实现Kube-Prometheus 监控Kubernets 集群java应用JVM性能_java

2.4、创建DeployMent无状态应用

这里编写YAML文件,尝试运行一个无状态java应用Pod,作为我们Promehtues所要监控的对象

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: huobi-demo #定义deployment标签
    metrics: jmx-metrics
  name: huobi-demo  #Deployment 名称
  namespace: default  #命名空间
spec:
  replicas: 1   #Pod副本
  selector:
    matchLabels:
      app: huobi-demo
      metrics: jmx-metrics
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate
  template:
    metadata: #定义资源的元数据信息,比如资源的名称,namespace,标签等信息
      creationTimestamp: null # 表示该资源的创建时间戳尚未被创建,当创建一个资源时,kubernetes会自动分配一个创建时间戳
      labels:
        app: huobi-demo #pod的标签
        metrics: jmx-metrics
    spec:
      affinity: #定义亲和力配置,用于指定Pod的调度规则
        podAntiAffinity: #pod反亲和力
          preferredDuringSchedulingIgnoredDuringExecution: #定义一个优先级较高的反亲和力规则
          - podAffinityTerm: #定义了一个Pod亲亲和性条件,用于选择与之匹配的pod
              labelSelector:  #定义了标签选择器,用于选择具体特定标签的Pod
                matchExpressions: #定义了匹配表达式,通过Key和operator指定匹配条件
                - key: app #表示匹配的标签键为"app"的Pod
                  operator: In #表示匹配操作为“In”,即匹配标签值在指定的列表中
                  values:
                  - auth-be #表示要匹配的标签值是"auth-be"
              topologyKey: kubernetes.io/hostname #表示使用节点主机名作为拓扑键,用于根据节点进行反亲和力调度
            weight: 100 #表示该反亲和性规则的权重为100,即比其他规则更具有优先级
#上述反亲和性规则:这样的配置可以确保在调度该Pod时,尽量避免与具有标签 app=auth-be 的其他Pod调度在同一节点上。
      containers: 
      - env:
        - name: TZ
          value: Asia/Shanghai
        - name: LANG 
          value: C.UTF-8
        #image: registry.cn-beijing.aliyuncs.com/devops-op/huobi-demo:latest  #此处使用nginx镜像作为原始镜像,通过 Jenkins 构建并发版后,就会被替换成Java应用的镜像
        image: registry.cn-beijing.aliyuncs.com/devops-op/huobi-demo:1.0  #此处使用nginx镜像作为原始镜像,通过 Jenkins 构建并发版后,就会被替换成Java应用的镜像
        imagePullPolicy: IfNotPresent #镜像拉取规则, 表示本地存在则使用本地镜像,不拉取
        lifecycle: {} #生命周期
        livenessProbe: #存活探针(通过检测容器内部程序响应是否正常来决定是否重启)
          failureThreshold: 2 #最大失败次数(检测失败两次就表示该容器不健康,就会尝试重启)
          initialDelaySeconds: 120 #延迟的时间s(表示延迟30秒后开始进行存活探测。这个参数用于等待容器启动并初始化完成。)
          periodSeconds: 10  #探测间隔时间 (表示每隔10s就会探测一次)
          successThreshold: 1 #表示连续探测成功一次,容器就会被认为是健康状态
          tcpSocket:    #字段定义了一个TCP Socket探测容器内部程序响应是否正常
            port: 8081 #探测的目标端口是8081
          timeoutSeconds: 2 #探测超时时间2秒,如果超过这个时间,仍然还没有收到响应,那么探测将被认为是失败的
        name: huobi-demo
        ports:
        - containerPort: 8081
          name: web
          protocol: TCP
        readinessProbe: #就绪探针(通过确定容器是否就绪,决定是否接受请求)
          failureThreshold: 2 #最大失败次数(探测失败两次就表示未就绪,此时就会尝试切断流量)
          initialDelaySeconds: 30 #延迟时间(表示延迟30秒后开始进行就绪探测。这个参数用��等待容器启动并初始化完成。)
          periodSeconds: 10 #探测间隔时间(表示每隔10s就会开始探测一次)
          successThreshold: 1  #表示连续探测成功一次,容器就会被认为是就绪状态,开始接受流量请求,
          tcpSocket: #字段定义了一个TCP Socket探测方式,用于检测容器内部程序是否就绪
            port: 8081 
          timeoutSeconds: 2 #探测超时2秒,如果超过该时间,仍然没有响应,那认为是失败的
        resources: #资源限制
          limits: #最大限制
            cpu: 1024m
            memory: 1024Mi
          requests: #最小请求
            cpu: 512m
            memory: 512Mi 
      dnsPolicy: ClusterFirst 
      imagePullSecrets:
      - name: Harbor-auth #指定harbor仓库的认证密钥文件(secret)
      restartPolicy: Always 
      securityContext: {} 
      serviceAccountName: default

验证是否正常启动

可以直接进入到容器里面测试下metrics接口,检测是否有监控指标数据返回

#kubectl get pod  -l app=huobi-demo
NAME                          READY   STATUS    RESTARTS   AGE
huobi-demo-7d678fcb69-ssd7t   1/1     Running   0          11m

如何实现Kube-Prometheus 监控Kubernets 集群java应用JVM性能_java_02

创建Sevice服务路由

pod服务正常启动,那么要想ServiceMonitor采集到对应Pod的Metrics指标,中间肯定需要一个访问入口,这时候就用到了Service,注意这里Service需要关联到Pod的Lables标签,要保持一致才行

#vim huobi-demo-servcie.yaml
apiVersion: v1
kind: Service
metadata:
  namespace: default
  labels:
    metrics: jmx-metrics #定义Service 本身的Labels标签
  name: huobi-demo
spec:
  sessionAffinity: None
  selector:
    metrics: jmx-metrics  #该标签要与Pod Lables标签一致,否则Service不知道自己要监控的目标是谁
  ports:
    - name: http-metrics
      protocol: TCP
      targetPort: 9901
      port: 9901
    - name: http-8081
      protocol: TCP
      targetPort: 8081
      port: 8081
#kubectl apply -f huobi-demo-servcie.yaml

如何实现Kube-Prometheus 监控Kubernets 集群java应用JVM性能_jvm_03

验证Service ClusterIP是否正常

直接curl 访问ClusterIP及对应的metrics接口

如何实现Kube-Prometheus 监控Kubernets 集群java应用JVM性能_jvm_04

2.4、创建ServiceMonitor自动发现机制

概念剖析

什么是ServiceMonitor? 简单一句话就是它是prometheus Opertor中的一个自定义资源,,用于定义Prometheus如何自动发现和监控服务的指标资源,它为Kubernetes提供了更加只能和自动化监控目标配置方式。

在整个监控体系中,如何动态发现的ServiceMetrics指标数据呢?此时就用到了ServiceMonitor, 它的出现给我们带来很一下优势:

  • 自动发现监控目标:使用ServiceMonitor,Prometheus不需要手动配置要监控的服务和目标,而是通过Selector(标签选择器)自动发现符合条件的Service,并开始监控它们
  • 动态配置:在kubernetes中,Pod服务的增删改查是常见的操作,ServiceMonitor允许管理员在运行的时候动态的增删改查,无需手动干预和修改Prometheus配置文件
  • 集中化管理:Promehtues Operator负责管理ServiceMonitor和Prometheus的配置,将监控配置集中话管理,这样运维人员可以更加方便的管理整个监控系统,同时减少配置冗余和错误

编写ServiceMonitor Yaml文件

#vim jmx-ServiceMonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor # prometheus-operator 定义的CRD
metadata:
  name: jmx-metrics
  namespace: monitoring
  labels:
    k8s-apps: jmx-metrics
spec:
  jobLabel: metrics #监控数据的job标签指定为metrics label的值,即加上数据标签job=jmx-metrics
  selector:
    matchLabels:
      metrics: jmx-metrics # 自动发现 label中有metrics: jmx-metrics 的service
  namespaceSelector:
    matchNames:  # 配置需要自动发现的命名空间,可以配置多个
    - default
  endpoints:
  - port: http-metrics # 拉去metric的端口,这个写的是 service的端口名称,即 service yaml的spec.ports.name
    interval: 15s # 拉取metric的时间间隔
[root@k8s-master01 jmx-metrics]# ls
jmx-exporter.yaml  jmx_prometheus_javaagent-0.16.0.jar  jmx-rules.yaml  jmx-ServiceMonitor.yaml  test-service.yaml
您在 /var/spool/mail/root 中有新邮件
[root@k8s-master01 jmx-metrics]# cat  jmx-ServiceMonitor.yaml 
--- # ServiceMonitor 服务自动发现规则
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor # prometheus-operator 定义的CRD
metadata:
  name: jmx-metrics
  namespace: monitoring
  labels:
    k8s-apps: jmx-metrics
spec:
  jobLabel: metrics #监控数据的job标签指定为metrics label的值,即加上数据标签job=jmx-metrics
  selector:
    matchLabels:
      metrics: jmx-metrics # 自动发现 label中有metrics: jmx-metrics 的service
  namespaceSelector:
    matchNames:  # 配置需要自动发现的命名空间,可以配置多个
    - default
  endpoints:
  - port: http-metrics # 拉去metric的端口,这个写的是 service的端口名称,即 service yaml的spec.ports.name
    interval: 15s # 拉取metric的时间间隔
 #kubectl apply -f jmx-ServiceMonitor.yaml

如何实现Kube-Prometheus 监控Kubernets 集群java应用JVM性能_java_05

验证Targets

可以发现,关于jvm监控目标已经正常采集

如何实现Kube-Prometheus 监控Kubernets 集群java应用JVM性能_jvm_06

2.5、创建告警JVM Alerts机制

有了监控,那么还需要结合实际情况对JVM相应的指标进行告警通知,下面定义PrometheusRule资源

定义JVM告警规则

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  labels:
    prometheus: k8s
    role: alert-rules
  name: jvm-metrics-rules
  namespace: monitoring
spec:
  groups:
  - name: jvm-metrics-rules
    rules:
    # 在5分钟里,GC花费时间超过10%
    - alert: GcTimeTooMuch
      expr: increase(jvm_gc_collection_seconds_sum[5m]) > 30
      for: 5m
      labels:
        severity: red
      annotations:
        summary: "{{ $labels.app }} GC时间占比超过10%"
        message: "ns:{{ $labels.namespace }} pod:{{ $labels.pod }} GC时间占比超过10%,当前值({{ $value }}%)"
    # GC次数太多
    - alert: GcCountTooMuch
      expr: increase(jvm_gc_collection_seconds_count[1m]) > 30
      for: 1m
      labels:
        severity: red
      annotations:
        summary: "{{ $labels.app }} 1分钟GC次数>30次"
        message: "ns:{{ $labels.namespace }} pod:{{ $labels.pod }} 1分钟GC次数>30次,当前值({{ $value }})"
    # FGC次数太多
    - alert: FgcCountTooMuch
      expr: increase(jvm_gc_collection_seconds_count{gc="ConcurrentMarkSweep"}[1h]) > 3
      for: 1m
      labels:
        severity: red
      annotations:
        summary: "{{ $labels.app }} 1小时的FGC次数>3次"
        message: "ns:{{ $labels.namespace }} pod:{{ $labels.pod }} 1小时的FGC次数>3次,当前值({{ $value }})"
    # 非堆内存使用超过80%
    - alert: NonheapUsageTooMuch
      expr: jvm_memory_bytes_used{job="jmx-metrics", area="nonheap"} / jvm_memory_bytes_max * 100 > 80
      for: 1m
      labels:
        severity: red
      annotations:
        summary: "{{ $labels.app }} 非堆内存使用>80%"
        message: "ns:{{ $labels.namespace }} pod:{{ $labels.pod }} 非堆内存使用率>80%,当前值({{ $value }}%)"
    # 内存使用预警
    - alert: HeighMemUsage
      expr: process_resident_memory_bytes{job="jmx-metrics"} / os_total_physical_memory_bytes * 100 > 85
      for: 1m
      labels:
        severity: red
      annotations:
        summary: "{{ $labels.app }} rss内存使用率大于85%"
        message: "ns:{{ $labels.namespace }} pod:{{ $labels.pod }} rss内存使用率大于85%,当前值({{ $value }}%)"

如何实现Kube-Prometheus 监控Kubernets 集群java应用JVM性能_java_07

验证JVM规则

如何实现Kube-Prometheus 监控Kubernets 集群java应用JVM性能_java_08

三、Grafana监控展示

导入JVM监控大盘模版,这里需要注意。job对应的value要与Promehtues的job Value保持一致

如何实现Kube-Prometheus 监控Kubernets 集群java应用JVM性能_jvm_09

如何实现Kube-Prometheus 监控Kubernets 集群java应用JVM性能_java_10

好了,到这里整个环节都结束了,后续还会给大家继续更新更多干货,点点关注不迷路!