k8s 优雅监控jvm及dump heap的方案探讨

背景

k8s cluster 的健康检测失败会主动重启pod,而大部份情况下健康检测失败都是由full gc引起的。往往发生重启时已经没有条件dump heap排查full gc的原因。

如何监控

为了避免因健康检测失败而导致的pod重启,我们需要实施有效的监控策略,这包括监控JVM的内存使用情况、GC活动以及应用程序的响应时间。通过设置适当的告警阈值,可以在问题变得严重之前及时发现并采取行动。

监控有两种方式:基于脚本扫描gc log、基于JMX(GitHub - prometheus/jmx_exporter: A process for exposing JMX Beans via HTTP for Prometheus consumption

这两种监控方式各有优缺点。基于脚本扫描gc log的方法简单直接,但可能会有一定的延迟。而开启JMX则能提供实时的监控数据,但需要额外的配置和资源。

1、基于脚本扫描的实现方式:

我们可以编写一个简单的脚本来定期扫描gc日志文件,检查是否存在长时间的Full GC或者频繁的Young GC。这个脚本可以设置为一个cron job,定期运行并在发现异常时发送告警。

以下是一个基本的伪代码示例:

复制代码
import re`
`from datetime import datetime, timedelta`

`def` `scan_gc_log(log_file, time_threshold, frequency_threshold):`
`    full_gc_count =` `0`
`    young_gc_count =` `0`
`    last_gc_time =` `None`

    `with` `open(log_file,` `'r')` `as f:`
        `for line in f:`
            `if` `'Full GC'` `in line:`
`                full_gc_count +=` `1`
`                last_gc_time = parse_time(line)`
            `elif` `'Young GC'` `in line:`
`                young_gc_count +=` `1`

    `if full_gc_count >` `0` `and` `(datetime.now()` `- last_gc_time)` `< timedelta(minutes=time_threshold):`
`        send_alert(f"Full GC detected in last {time_threshold} minutes")`
    
    `if young_gc_count > frequency_threshold:`
`        send_alert(f"High frequency of Young GC: {young_gc_count} in last hour")`

`# 实现parse_time和send_alert函数

2、基于JMX(GitHub - prometheus/jmx_exporter: A process for exposing JMX Beans via HTTP for Prometheus consumption)的实现方式:

创建一个配置文件,指定要收集的JMX指标。

复制代码
-javaagent:/path/to/jmx_prometheus_javaagent.jar=8080:/path/to/config.yaml`
`

这将在端口8080上启动一个HTTP服务器,暴露Prometheus格式的指标。配置Prometheus来抓取这些指标,并在Grafana中创建仪表板来可视化这些数据。

如何实现dump相关操作

当收到监控的告警时,通过以下方式获取当前pod实例的heap文件。

一、人工执行命令:

收到告警,及时用以下步骤获取heap文件的步骤:

1.首先,确定目标 Pod的名称和所在的命名空间。

2.使用kubectl exec命令连接到Pod:

复制代码
kubectl exec -it [pod-name] -n [namespace] -- /bin/bash

3.在Pod内部,使用jcmd命令生成heap dump:

复制代码
jcmd [pid] GC.heap_dump /tmp/heapdump.hprof

这将在Pod的/tmp目录下创建一个名为heapdump.hprof的heap dump文件。

二、基于preStop机制自动脚本:

人工执行命令可能会发生健康检测不通过的情况,而导致pod重启,错过了dump heap的机会。那么需要以k8s preStop机制来做到自动dump,但是需要注意不能引起Pod同时都在dump的情况。

例如:某个jvm服务,有3个Pod实例,当其中一个发生full gc,并导致健康检测不通过从而触发了k8s的主动重启。此时Pod实例进入preStop,执行preStop脚本,脚本先判断是否存在有正在dump的其他pod,否则将开始dump heap操作。

以下是执行步骤及preStop脚本:

具体执行步骤:

  1. 在Kubernetes部署文件中,为目标Pod添加preStop钩子。
  2. 编写preStop脚本,实现检查其他Pod状态和dump heap的逻辑。
  3. 将脚本添加到容器镜像中,并在preStop钩子中调用该脚本。

preStop伪脚本:

复制代码
#!/bin/bash`

`# 检查是否有其他Pod正在dump`
`function` `check_other_pods()` `{`
    `# 实现检查逻辑,例如通过API或共享存储检查其他Pod状态`
    `# 返回0表示可以进行dump,返回1表示其他Pod正在dump`
    `return` `0`
`}`

`# 执行heap dump`
`function` `do_heap_dump()` `{`
    `PID=$(jps -l)`
    `DUMP_FILE="/tmp/heapdump_$(date +%Y%m%d_%H%M%S).hprof"`
`    jcmd $PID GC.heap_dump $DUMP_FILE`
    `# 可以添加将dump文件传输到持久存储的逻辑`
`}`

`# 主逻辑`
`if check_other_pods;` `then`
`    do_heap_dump`
`else`
    `echo` `"Another pod is currently dumping, skipping..."`
`fi

4、基于k8s operator的实现(Operator 模式 | Kubernetes):

使用Kubernetes Operator是一种更高级和自动化的方法来管理heap dump。这种方法可以通过自定义资源定义(CRD)和控制器来自动监控和响应JVM的状态。当检测到潜在的内存问题时,Operator可以自动触发heap dump过程,并确保在集群级别协调这些操作,避免多个Pod同时进行dump。这种方法不仅可以提高自动化程度,还能更好地与Kubernetes生态系统集成。

基本概念:

  • Kubernetes Operator是一种打包、部署和管理 Kubernetes 应用程序的方法。 Kubernetes 应用程序既部署在Kubernetes上,又使用 Kubernetes API(应用程序编程接口)和 kubectl 工具进行管理。
  • Kubernetes Operator 是一个特定于应用程序的控制器,它扩展了 Kubernetes API 的功能,以代表 Kubernetes 用户创建、配置和管理复杂应用程序的实例。
  • 它建立在基本的 Kubernetes 资源和控制器概念之上,但包含特定于领域或应用程序的知识,以自动化其管理软件的整个生命周期。
  • 在 Kubernetes 中,控制平面的控制器实现控制循环,反复将集群的期望状态与其实际状态进行比较。如果集群的实际状态与所需状态不匹配,控制器将采取措施来解决问题。

实现步骤(以下内容未经过验证,只是理论可行性):

  1. 创建 CRD:定义 JvmMonitor 资源,包含监控参数如内存阈值、GC 频率等。
  2. 编写控制器:实现监控逻辑,定期检查 JVM 状态,触发 heap dump。
  3. 实现协调循环:比较实际状态和期望状态,执行必要的操作。
  4. 集成监控系统:与 Prometheus 等监控工具集成,获取实时 JVM 指标。
  5. 实现 heap dump 逻辑:在需要时安全地执行 heap dump,并存储到持久化存储。
  6. 添加集群级别协调:确保同一时间只有一个 Pod 在执行 heap dump。
  7. 部署 Operator:将 Operator 部署到 Kubernetes 集群中。

通过这种方式,我们可以实现一个全面的、自动化的 JVM 监控和 heap dump 解决方案,大大提高问题诊断和解决的效率。

基于java-operator-sdk实现的Operator controller伪代码:

复制代码
import io.javaoperatorsdk.operator.api.*;`
`import io.javaoperatorsdk.operator.api.reconciler.*;`

`@ControllerConfiguration`
`public class JvmMonitorController implements Reconciler<JvmMonitor> {`
    
`    @Override`
`    public UpdateControl<JvmMonitor> reconcile(JvmMonitor jvmMonitor, Context context) {`
`        // 检查JVM状态`
`        if (needsHeapDump(jvmMonitor)) {`
`            // 确保集群中只有一个Pod在执行heap dump`
`            if (acquireLock()) {`
`                try {`
`                    performHeapDump(jvmMonitor);`
`                } finally {`
`                    releaseLock();`
`                }`
`            }`
`        }`
        
`        return UpdateControl.noUpdate();`
`    }`
    
`    private boolean needsHeapDump(JvmMonitor jvmMonitor) {`
`        // 实现检查逻辑`
`    }`
    
`    private boolean acquireLock() {`
`        // 实现分布式锁逻辑`
`    }`
    
`    private void performHeapDump(JvmMonitor jvmMonitor) {`
`        // 实现heap dump逻辑`
`    }`
    
`    private void releaseLock() {`
`        // 释放分布式锁`
`    }`
`}`
`

实现基于事件的触发机制:

除了定期检查和基于指标的触发机制外,我们还可以利用Pod重启事件来触发JVM状态检查和潜在的heap dump。这种方法特别有助于捕获因内存问题导致的Pod重启情况。

以下是实现这一策略的步骤:

  1. 在Kubernetes中设置事件监听器,专门监听Pod重启事件。
  2. 当检测到Pod重启事件时,立即触发JVM状态检查。
  3. 如果重启是由于内存问题或JVM相关问题引起的,执行heap dump操作。
  4. 将heap dump结果保存到持久存储中,以便后续分析。

伪代码:

复制代码
import` `io.fabric8.kubernetes.api.model.Event;`
`import` `io.fabric8.kubernetes.client.KubernetesClient;`
`import` `io.fabric8.kubernetes.client.informers.ResourceEventHandler;`

`public` `class` `PodRestartMonitor` `{`
    `private` `final` `KubernetesClient client;`
    `private` `final` `JvmMonitorController jvmMonitorController;`

    `public` `PodRestartMonitor(KubernetesClient client,` `JvmMonitorController jvmMonitorController)` `{`
        `this.client = client;`
        `this.jvmMonitorController = jvmMonitorController;`
        `setupPodRestartWatcher();`
    `}`

    `private` `void` `setupPodRestartWatcher()` `{`
`        client.v1().events().inAnyNamespace().watch(new` `ResourceEventHandler<Event>()` `{`
            `@Override`
            `public` `void` `onAdd(Event event)` `{`
                `if` `(isPodRestartEvent(event))` `{`
                    `handlePodRestart(event);`
                `}`
            `}`

            `@Override`
            `public` `void` `onUpdate(Event oldEvent,` `Event newEvent)` `{`
                `if` `(isPodRestartEvent(newEvent))` `{`
                    `handlePodRestart(newEvent);`
                `}`
            `}`

            `@Override`
            `public` `void` `onDelete(Event event,` `boolean deletedFinalStateUnknown)` `{`
                `// 通常不需要处理删除事件`
            `}`
        `});`
    `}`

    `private` `boolean` `isPodRestartEvent(Event event)` `{`
        `return` `"Pod".equals(event.getInvolvedObject().getKind())` 
               `&&` `"Restarted".equals(event.getReason());`
    `}`

    `private` `void` `handlePodRestart(Event event)` `{`
        `String podName = event.getInvolvedObject().getName();`
        `String namespace = event.getInvolvedObject().getNamespace();`
        
        `// 触发JVM状态检查`
`        jvmMonitorController.checkJvmState(podName, namespace);`
    `}`
`}`
`

点点关注,下期精彩继续!

道一云七巧-与你在技术领域共同成长

更多技术知识分享: https://bbs.qiqiao668.com/

相关推荐
fen_fen20 分钟前
Docker如何运行一个python脚本Hello World
运维·docker·容器
檀越剑指大厂35 分钟前
【Docker系列】Docker 构建多平台镜像:arm64 架构的实践
docker·容器·架构
阿moments2 小时前
Docker - 速成
运维·docker·云原生·容器
LeonNo118 小时前
k8s,operator
云原生·容器·kubernetes
云川之下8 小时前
【k8s源码】kubernetes-1.22.3\staging 目录作用
云原生·容器·kubernetes
怡雪~8 小时前
k8s的Pod亲和性
linux·容器·kubernetes
A5rZ9 小时前
CTF: 在本地虚拟机内部署CTF题目docker
运维·网络安全·docker·容器
fragrans10 小时前
设置docker镜像加速器
运维·docker·容器
Karoku06611 小时前
【自动化部署】Ansible 基础命令行模块
运维·服务器·数据库·docker·容器·自动化·ansible
sky丶Mamba11 小时前
Java虚拟机启动时默认携带参数(jdk8)
java·jvm