背景
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脚本:
具体执行步骤:
- 在Kubernetes部署文件中,为目标Pod添加preStop钩子。
- 编写preStop脚本,实现检查其他Pod状态和dump heap的逻辑。
- 将脚本添加到容器镜像中,并在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 中,控制平面的控制器实现控制循环,反复将集群的期望状态与其实际状态进行比较。如果集群的实际状态与所需状态不匹配,控制器将采取措施来解决问题。
实现步骤(以下内容未经过验证,只是理论可行性):
- 创建 CRD:定义 JvmMonitor 资源,包含监控参数如内存阈值、GC 频率等。
- 编写控制器:实现监控逻辑,定期检查 JVM 状态,触发 heap dump。
- 实现协调循环:比较实际状态和期望状态,执行必要的操作。
- 集成监控系统:与 Prometheus 等监控工具集成,获取实时 JVM 指标。
- 实现 heap dump 逻辑:在需要时安全地执行 heap dump,并存储到持久化存储。
- 添加集群级别协调:确保同一时间只有一个 Pod 在执行 heap dump。
- 部署 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重启情况。
以下是实现这一策略的步骤:
- 在Kubernetes中设置事件监听器,专门监听Pod重启事件。
- 当检测到Pod重启事件时,立即触发JVM状态检查。
- 如果重启是由于内存问题或JVM相关问题引起的,执行heap dump操作。
- 将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/