241 lines
7.1 KiB
Bash
241 lines
7.1 KiB
Bash
#!/bin/bash
|
|
# eBPF 自动持续火焰图生成脚本
|
|
# 以 agent_runner 为入口,监控其所有子进程,定期生成火焰图
|
|
|
|
set -e
|
|
|
|
DIAG_OUTPUT_DIR="${DIAG_OUTPUT_DIR:-/app/container-logs/diag}"
|
|
MONITOR_LOG="$DIAG_OUTPUT_DIR/auto-flamegraph.log"
|
|
SAMPLE_DURATION=${SAMPLE_DURATION:-30} # 每次采样时长(秒),默认 30 秒
|
|
GENERATE_INTERVAL=${GENERATE_INTERVAL:-60} # 生成火焰图间隔(秒),默认 60 秒
|
|
MAX_FLAMEFILES=${MAX_FLAMEFILES:-50} # 最多保留火焰图文件数量
|
|
|
|
mkdir -p "$DIAG_OUTPUT_DIR"
|
|
|
|
log() {
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$MONITOR_LOG"
|
|
}
|
|
|
|
# 获取 agent_runner 的 PID
|
|
get_agent_runner_pid() {
|
|
pgrep -f "agent_runner" | head -1
|
|
}
|
|
|
|
# 获取 agent_runner 及其所有子进程的 PID 列表
|
|
get_process_tree_pids() {
|
|
local parent_pid=$1
|
|
if [ -z "$parent_pid" ]; then
|
|
return
|
|
fi
|
|
|
|
# 输出父进程 PID
|
|
echo "$parent_pid"
|
|
|
|
# 递归获取所有子进程
|
|
local child_pids=$(pgrep -P "$parent_pid" 2>/dev/null || true)
|
|
for child_pid in $child_pids; do
|
|
get_process_tree_pids "$child_pid"
|
|
done
|
|
}
|
|
|
|
# 生成火焰图
|
|
generate_flamegraph() {
|
|
local timestamp=$(date '+%Y%m%d_%H%M%S')
|
|
local bpftrace_data="$DIAG_OUTPUT_DIR/profile-${timestamp}.bt"
|
|
local folded_output="$DIAG_OUTPUT_DIR/profile-${timestamp}.folded"
|
|
local flamegraph_svg="$DIAG_OUTPUT_DIR/flamegraph-${timestamp}.svg"
|
|
local agent_pid=$1
|
|
|
|
log "🔥 开始生成火焰图 (agent_runner PID: $agent_pid)..."
|
|
|
|
# 获取进程树 PID 列表
|
|
local pids=$(get_process_tree_pids "$agent_pid" | tr '\n' ' ')
|
|
log "📋 监控进程树: $pids"
|
|
|
|
# 构建 bpftrace 脚本,监控所有相关进程
|
|
local pid_filter=""
|
|
for pid in $pids; do
|
|
if [ -n "$pid_filter" ]; then
|
|
pid_filter="$pid_filter || "
|
|
fi
|
|
pid_filter="${pid_filter}pid == $pid"
|
|
done
|
|
|
|
log "📊 采样 ${SAMPLE_DURATION} 秒..."
|
|
|
|
# 使用 bpftrace 采样(后台运行)
|
|
timeout ${SAMPLE_DURATION}s bpftrace -e "
|
|
profile:hz:99 /($pid_filter) && comm != \"bpftrace\"/ {
|
|
@[ustack] = count();
|
|
}
|
|
" 2>/dev/null | sort -rn -k2 > "$bpftrace_data" || true
|
|
|
|
if [ ! -s "$bpftrace_data" ]; then
|
|
log "⚠️ 未采集到性能数据(进程可能已结束)"
|
|
rm -f "$bpftrace_data"
|
|
return 1
|
|
fi
|
|
|
|
log "📊 采样完成,共 $(wc -l < "$bpftrace_data") 个堆栈样本"
|
|
|
|
# 调试:保存原始数据样本(前 10 行)
|
|
head -10 "$bpftrace_data" > "$DIAG_OUTPUT_DIR/debug-bpftrace-sample.txt"
|
|
log "📋 原始数据样本已保存到: $DIAG_OUTPUT_DIR/debug-bpftrace-sample.txt"
|
|
|
|
# 转换为 FlameGraph 格式
|
|
log "🔄 转换为火焰图格式..."
|
|
|
|
# bpftrace 实际输出格式(多行):
|
|
# ]: 10
|
|
# Attaching 1 probe...
|
|
# @[
|
|
# 0x634c5c
|
|
# 0x634758
|
|
# ...
|
|
#
|
|
# 使用 awk 解析多行格式
|
|
awk '
|
|
BEGIN {
|
|
count = 0
|
|
in_stack = 0
|
|
stack = ""
|
|
}
|
|
/^]:/ {
|
|
# 提取计数值: ]: 10
|
|
count = $2
|
|
next
|
|
}
|
|
/^\@\[/ {
|
|
# 开始新的堆栈
|
|
in_stack = 1
|
|
stack = ""
|
|
next
|
|
}
|
|
/^$/ {
|
|
# 空行结束当前堆栈
|
|
if (in_stack && stack != "" && count > 0) {
|
|
# 移除末尾分号并输出
|
|
gsub(/;$/, "", stack)
|
|
print stack " " count
|
|
}
|
|
in_stack = 0
|
|
stack = ""
|
|
count = 0
|
|
next
|
|
}
|
|
{
|
|
# 跳过 Attaching 消息等非堆栈行
|
|
if (in_stack && $0 !~ /^Attaching/) {
|
|
# 提取地址(缩进或未缩进的十六进制地址)
|
|
if (match($0, /0x[0-9a-f]+/)) {
|
|
addr = substr($0, RSTART, RLENGTH)
|
|
if (stack != "") {
|
|
stack = stack ";"
|
|
}
|
|
stack = stack addr
|
|
}
|
|
}
|
|
}
|
|
END {
|
|
# 处理最后一个堆栈
|
|
if (in_stack && stack != "" && count > 0) {
|
|
gsub(/;$/, "", stack)
|
|
print stack " " count
|
|
}
|
|
}
|
|
' "$bpftrace_data" > "$folded_output"
|
|
|
|
# 检查转换结果
|
|
if [ ! -s "$folded_output" ]; then
|
|
log "⚠️ 火焰图格式转换失败,保存原始数据用于调试"
|
|
cp "$bpftrace_data" "$DIAG_OUTPUT_DIR/debug-${timestamp}.bt"
|
|
log "📋 请检查以下文件以诊断问题:"
|
|
log " - 原始数据: $DIAG_OUTPUT_DIR/debug-${timestamp}.bt"
|
|
log " - 数据样本: $DIAG_OUTPUT_DIR/debug-bpftrace-sample.txt"
|
|
return 1
|
|
fi
|
|
|
|
log "📊 转换完成,共 $(wc -l < "$folded_output") 个有效堆栈"
|
|
|
|
# 调试:保存转换后的样本(前 5 行)
|
|
head -5 "$folded_output" > "$DIAG_OUTPUT_DIR/debug-folded-sample.txt"
|
|
log "📋 转换后样本已保存到: $DIAG_OUTPUT_DIR/debug-folded-sample.txt"
|
|
|
|
# 生成火焰图
|
|
log "🎨 生成 SVG 火焰图..."
|
|
if command -v flamegraph.pl &> /dev/null; then
|
|
flamegraph.pl \
|
|
--title="agent_runner 进程树火焰图 (${timestamp})" \
|
|
--width=1600 \
|
|
--height=800 \
|
|
"$folded_output" > "$flamegraph_svg"
|
|
|
|
if [ -s "$flamegraph_svg" ]; then
|
|
log "✅ 火焰图已生成: $flamegraph_svg"
|
|
log "💡 复制到宿主机查看: docker cp <container>:$flamegraph_svg ./"
|
|
|
|
# 清理临时文件
|
|
rm -f "$bpftrace_data" "$folded_output"
|
|
|
|
# 清理旧火焰图文件(保留最新的 MAX_FLAMEFILES 个)
|
|
cleanup_old_flamegraphs
|
|
return 0
|
|
else
|
|
log "❌ 火焰图生成失败"
|
|
return 1
|
|
fi
|
|
else
|
|
log "❌ flamegraph.pl 未安装,无法生成火焰图"
|
|
log "📋 原始数据保存在: $bpftrace_data"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# 清理旧火焰图文件
|
|
cleanup_old_flamegraphs() {
|
|
local flame_count=$(ls -1 "$DIAG_OUTPUT_DIR"/flamegraph-*.svg 2>/dev/null | wc -l)
|
|
|
|
if [ "$flame_count" -gt "$MAX_FLAMEFILES" ]; then
|
|
local delete_count=$((flame_count - MAX_FLAMEFILES))
|
|
log "🗑️ 清理 ${delete_count} 个旧火焰图文件..."
|
|
|
|
ls -1t "$DIAG_OUTPUT_DIR"/flamegraph-*.svg | tail -n "$delete_count" | while read -r file; do
|
|
log " 删除: $(basename "$file")"
|
|
rm -f "$file"
|
|
done
|
|
fi
|
|
}
|
|
|
|
# 主监控循环
|
|
monitor_loop() {
|
|
log "🚀 eBPF 自动火焰图生成已启动"
|
|
log "📋 配置: 采样时长=${SAMPLE_DURATION}s, 生成间隔=${GENERATE_INTERVAL}s"
|
|
log "💡 火焰图将保存到: $DIAG_OUTPUT_DIR/flamegraph-*.svg"
|
|
|
|
while true; do
|
|
# 获取 agent_runner PID
|
|
local agent_pid=$(get_agent_runner_pid)
|
|
|
|
if [ -n "$agent_pid" ]; then
|
|
log "✅ 检测到 agent_runner 进程 (PID: $agent_pid)"
|
|
generate_flamegraph "$agent_pid"
|
|
else
|
|
log "⚠️ 未检测到 agent_runner 进程,等待中..."
|
|
fi
|
|
|
|
log "⏰ ${GENERATE_INTERVAL} 秒后生成下一张火焰图..."
|
|
sleep "$GENERATE_INTERVAL"
|
|
done
|
|
}
|
|
|
|
# 启动监控
|
|
case "$1" in
|
|
start)
|
|
monitor_loop
|
|
;;
|
|
*)
|
|
echo "用法: $0 start"
|
|
exit 1
|
|
;;
|
|
esac
|