背景
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, timedeltadef scan_gc_log(log_file, time_threshold, frequency_threshold):
full_gc_count = 0
young_gc_count = 0
last_gc_time = Nonewith 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 += 1if 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正在dumpreturn 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
elseecho "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>() {@Overridepublic void onAdd(Event event) {if (isPodRestartEvent(event)) {handlePodRestart(event);}}@Overridepublic void onUpdate(Event oldEvent, Event newEvent) {if (isPodRestartEvent(newEvent)) {handlePodRestart(newEvent);}}@Overridepublic 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/