需求背景:
对内存泄漏进行了修复,通过引流模拟压测,在测试机上收集两个进程的性能数据:cpu、mem、mes_used来对比判断内存泄漏修复的效果
一、收集进程数据
收集数据通过进程名来获取pid
bash
get_pid() {
# 1. 定义局部变量target,接收函数的第一个参数(传入的PID/进程名)
local target=$1
# 2. 判断:参数是否是纯数字(即是否是PID)
if [[ ${target} =~ ^[0-9]+$ ]]; then
# 2.1 如果是数字,验证该PID对应的进程是否存活
if ps -p ${target} >/dev/null 2>&1; then
# 进程存活 → 返回该PID
echo ${target}
else
# 进程不存在 → 返回空字符串
echo ""
fi
# 3. 如果参数不是纯数字(即是进程名/关键词)
else
# 3.1 查找包含该关键词的进程,提取第一个PID
local pid=$(ps -ef | grep -v grep | grep -v $0 | grep ${target} | awk '{print $2}' | head -1)
# 3.2 返回找到的PID(没找到则返回空)
echo ${pid}
fi
}
命令段 作用
ps -ef 列出系统所有进程的完整信息(用户、PID、父 PID、命令等)
grep -v grep 过滤掉 grep 自身进程(避免把 "grep navi-api" 这个临时进程当成目标进程)
grep -v 0 过滤掉当前脚本进程(0是脚本名,避免匹配到脚本自身)
grep ${target} 筛选出包含目标关键词(如 navi-api)的进程
awk '{print $2}' 提取进程信息的第 2 列(即 PID 列)
head -1 只取第一个 PID(避免多个匹配结果时返回多个值)
读取机器的总内存值,用户内存占用率的计算
bash
# 读取系统总内存(从/proc/meminfo读取)
get_total_memory_kb() {
# 1. 核心:读取/proc/meminfo中的MemTotal字段,提取数值(单位KB)
local total_mem=$(cat /proc/meminfo | grep ^MemTotal: | awk '{print $2}')
# 2. 容错:如果读取失败/数值为0,用你机器的固定值兜底
if [[ -z ${total_mem} || ${total_mem} -eq 0 ]]; then
total_mem=527756124 # 你的机器总内存(KB)
fi
# 3. 返回最终的总内存数值
echo ${total_mem}
}
逐行深度解释
- local total_mem=(cat /proc/meminfo \| grep \^MemTotal: \| awk '{print 2}')
local total_mem:声明局部变量,仅在函数内有效,避免污染全局变量;
cat /proc/meminfo:读取 Linux 系统的内存信息文件(/proc是内核虚拟文件系统,实时反映系统状态);
grep ^MemTotal::筛选以MemTotal:开头的行(这一行就是系统总内存,格式如MemTotal: 527756124 kB);
awk '{print $2}':提取该行的第 2 列(即总内存的数值部分,去掉kB单位);
最终效果:拿到你服务器的总内存数值(527756124 KB,约 515GB)。 - if [[ -z {total_mem} \|\| {total_mem} -eq 0 ]]; then
\[ -z ${total_mem} \]\]:判断total_mem是否为空(比如/proc/meminfo读取失败、没有找到 MemTotal 行); \[\[ ${total_mem} -eq 0 \]\]:判断数值是否为 0(异常情况); total_mem=527756124:兜底方案 ------ 直接用你机器的固定总内存值,避免后续计算内存使用率时出错(比如除以 0)。
返回最终的总内存数值,供脚本中计算内存使用率时调用(内存使用率 = 进程已用内存 / 总内存 * 100%)。
collect_process_metrics () 解析(核心:采集单个进程的 CPU / 内存指标)
bash
collect_process_metrics() {
# 1. 接收参数:要采集的进程PID
local pid=$1
# 2. 容错:PID为空/进程不存在(/proc/PID文件缺失),直接返回默认值
if [[ -z ${pid} || ! -f /proc/${pid}/stat || ! -f /proc/${pid}/status ]]; then
echo "0.00,0.0000,0.00" # CPU%、内存%、内存MB,统一带小数点
return # 退出函数,不再执行后续逻辑
fi
# ========== 1. CPU使用率计算(核心:通过/proc/stat和/proc/PID/stat计算) ==========
# 2.1 读取采集前的系统总CPU时间 + 进程已用CPU时间
# /proc/stat的cpu行:包含系统启动以来所有CPU的总耗时(单位:jiffies,约1/100秒)
local cpu_total_prev=$(cat /proc/stat | grep ^cpu | awk '{sum=$2+$3+$4+$5+$6+$7+$8} END {print sum}')
# /proc/PID/stat的14+15列:进程的用户态+内核态CPU耗时(jiffies)
local pid_stat_prev=$(cat /proc/${pid}/stat | awk '{print $14+$15}')
# 2.2 等待0.1秒(必须!否则前后两次读取的数值几乎无差异,计算出的CPU使用率为0)
sleep 0.1
# 2.3 读取采集后的系统总CPU时间 + 进程已用CPU时间
local cpu_total_curr=$(cat /proc/stat | grep ^cpu | awk '{sum=$2+$3+$4+$5+$6+$7+$8} END {print sum}')
local pid_stat_curr=$(cat /proc/${pid}/stat | awk '{print $14+$15}')
# 2.4 计算差值:0.1秒内系统总CPU耗时变化量 + 进程CPU耗时变化量
local cpu_delta=$((cpu_total_curr - cpu_total_prev)) # 系统总耗时增量
local pid_delta=$((pid_stat_curr - pid_stat_prev)) # 进程耗时增量
local cpu_usage=0.00 # 初始化CPU使用率
# 2.5 计算CPU使用率(避免除以0)
if [[ ${cpu_delta} -gt 0 ]]; then
# scale=2:保留2位小数;公式:(进程耗时增量/系统总耗时增量)*100%
cpu_usage=$(echo "scale=2; ${pid_delta}/${cpu_delta}*100" | bc)
fi
# ========== 2. 内存指标计算(核心:读取/proc/PID/status的VmRSS) ==========
# 2.1 读取进程已用物理内存(VmRSS,单位:KB)
local mem_rss=$(cat /proc/${pid}/status | grep ^VmRSS: | awk '{print $2}')
if [[ -z ${mem_rss} ]]; then # 容错:读取失败则设为0
mem_rss=0
fi
# 2.2 获取系统总内存(KB)(调用之前的get_total_memory_kb函数)
local total_mem_kb=$(get_total_memory_kb)
# 2.3 计算内存使用量(MB):KB转MB,保留2位小数
local mem_mb=$(echo "scale=2; ${mem_rss}/1024" | bc)
# 2.4 计算内存使用率(%):保留4位小数,避免显示为0
local mem_usage=0.0000
if [[ ${total_mem_kb} -gt 0 && ${mem_rss} -gt 0 ]]; then
mem_usage=$(echo "scale=4; (${mem_rss}/${total_mem_kb})*100" | bc)
fi
# ========== 3. 格式化数值(适配gnuplot,补前导0/小数点) ==========
cpu_usage=$(format_number "${cpu_usage}")
mem_usage=$(format_number "${mem_usage}")
mem_mb=$(format_number "${mem_mb}")
# ========== 4. 返回结果(CSV格式:CPU%,内存%,内存MB) ==========
echo "${cpu_usage},${mem_usage},${mem_mb}"
}
关键知识点(CPU / 内存计算原理)
- CPU 使用率为什么要算 "差值"?
/proc/stat 和 /proc/PID/stat 存储的是从系统启动到现在的累计耗时(不是实时使用率);
只有计算 "0.1 秒内的增量比值",才能得到这 0.1 秒内的实时 CPU 使用率;
sleep 0.1 是核心:如果不等待,前后两次读取的数值几乎一样,差值为 0,CPU 使用率就会是 0(这也是你之前部分行 CPU 为 0 的原因之一)。 - 内存指标的核心字段:VmRSS
VmRSS(Resident Set Size):进程实际占用的物理内存(KB),是最能反映进程内存使用的指标;
内存使用率公式:(进程VmRSS / 系统总内存) * 100%(你的服务器总内存大,所以使用率低,约 0.1%);
内存 MB 公式:VmRSS / 1024(KB 转 MB,更易理解)。 - 容错设计
PID 为空 / 进程不存在 → 返回默认值 0.00,0.0000,0.00,避免脚本崩溃;
内存读取失败 → 设为 0,避免后续计算出错;
CPU 差值为 0 → 使用率设为 0,避免除以 0 报错。
collect_data () 解析(核心:循环采集 + 写入 CSV)
bash
collect_data() {
# 1. 计算监控结束时间:开始时间(秒级时间戳) + 监控时长(秒)
local start_time=$(date +%s) # 当前时间戳(如1742200000)
local end_time=$((start_time + MONITOR_DURATION)) # 结束时间戳
echo "开始监控(开始时间:$(date +%Y-%m-%d\ %H:%M:%S))..."
# 2. 循环采集:只要当前时间 < 结束时间,就一直采集
while [[ $(date +%s) -lt ${end_time} ]]; do
# 2.1 获取当前时间戳(格式化,写入CSV)
local timestamp=$(date +%Y-%m-%d\ %H:%M:%S) # 如2026-03-17 12:53:42
# 2.2 获取两个进程的PID(调用之前的get_pid函数)
local pid1=$(get_pid ${PROCESS_NAME_1}) # 8078的PID
local pid2=$(get_pid ${PROCESS_NAME_2}) # 8079的PID
# 2.3 采集两个进程的指标(调用collect_process_metrics)
local metrics1=$(collect_process_metrics ${pid1}) # 8078的CPU%,内存%,内存MB
local metrics2=$(collect_process_metrics ${pid2}) # 8079的CPU%,内存%,内存MB
# 2.4 拼接数据并写入CSV
# 格式:时间戳,进程1CPU%,进程1内存%,进程1内存MB,进程2CPU%,进程2内存%,进程2内存MB
echo "${timestamp},${metrics1},${metrics2}" >> ${DATA_FILE}
# 终端打印日志(方便查看实时采集情况)
echo "[$timestamp] P1:${metrics1} | P2:${metrics2}"
# 2.5 等待采集间隔(如60秒),避免高频采集
sleep ${COLLECT_INTERVAL}
done
echo "监控结束(结束时间:$(date +%Y-%m-%d\ %H:%M:%S))"
}
容错:
物理机的内存会比较大,内存利用率会小于0,会导致图表展示失败,gnuplot 对数值格式要求严格:
纯整数(如0)、缺少前导 0 的小数(如.1100)会被识别为 "非有效数值",导致图表为空 / 只显示部分曲线;
这个函数的唯一目的:把所有数值统一成 gnuplot 能识别的格式。
bash
# 格式化数值(补前导0,确保gnuplot能识别)
format_number() {
local num=$1 # 接收传入的数值参数(如0、.1100、9.00)
# 情况1:纯整数(如0、9、50)→ 补小数点后两位(0→0.00,9→9.00)
if [[ ${num} =~ ^[0-9]+$ ]]; then
echo "${num}.00"
# 情况2:以小数点开头(如.1100、.0800)→ 补前导0(.1100→0.1100)
elif [[ ${num} =~ ^\.[0-9]+$ ]]; then
echo "0${num}"
# 情况3:正常格式(如9.00、0.1100)→ 保持不变
else
echo "${num}"
fi
}
二、将收集的数据,按照图表的形式展示
工具:gnuplot
安装:root权限
python
yum install-y gnuplot
校验安装是否成功
bash
gnuplot --version
gnuplog介绍
Gnuplot 是一款跨平台、开源免费的命令行绘图工具,核心用途是将数值数据(如你的进程监控 CSV)转换为可视化图表(PNG/JPG/PDF 等),尤其适合 Linux 服务器环境下的自动化图表生成(比如你用的进程 CPU / 内存监控)。
例如生成图片的命令如下
bash
gnuplot -e "
# 1. 定义输出终端(图表格式/尺寸/增强模式)
set terminal png size 1200,800 enhanced;
# 2. 指定图表保存路径
set output '${CHART_DIR}/cpu_comparison.png';
# 3. 设置图表标题(明确对比的两个进程)
set title 'Process CPU Usage Comparison (navi-api-luciazhaolu-8078 vs 8079)';
# 4. 设置X轴标签
set xlabel 'Time';
# 5. 设置Y轴标签(标注单位)
set ylabel 'CPU Usage (%)';
# 6. 显示网格(方便读取数值)
set grid;
# 7. 定义时间解析格式(匹配CSV中的时间戳)
set timefmt '%Y-%m-%d %H:%M:%S';
# 8. 声明X轴为时间类型(而非普通数值)
set xdata time;
# 9. 设置X轴时间显示格式(简化显示,避免拥挤)
set format x '%m-%d %H:%M';
# 10. X轴标签旋转45度(防止时间戳重叠)
set xtics rotate by 45;
# 11. 指定数据分隔符(CSV用逗号分隔)
set datafile separator ',';
# 12. 核心绘图命令(画两条曲线)
plot '${CSV_FILE}' using 1:2 title 'navi-api-luciazhaolu-8078' with lines lw 2 lc rgb '#1E90FF', \
'${CSV_FILE}' using 1:5 title 'navi-api-luciazhaolu-8079' with lines lw 2 lc rgb '#FF6347';
"
关键要点:
set terminal/set output 决定图表 "长什么样、存在哪";
set xdata time/set timefmt 解决时间轴解析问题;
set datafile separator ',' 确保 CSV 列解析正确;
plot using 1:2/1:5 选择要可视化的核心数据列(进程 1/2 的 CPU 使用率)
三、整体代码
bash
#!/bin/bash
set -e
# ====================== 配置项(根据需求修改)======================
PROCESS_NAME_1="navi-api-luciazhaolu-8078" # navi-api-luciazhaolu-8078
PROCESS_NAME_2="navi-api-luciazhaolu-8079" # navi-api-luciazhaolu-8079
MONITOR_DURATION=600 # 监控时长(秒)
COLLECT_INTERVAL=60 # 采集间隔(秒)
DATA_DIR="/home/xiaoju/qa/zhaolu/process_monitor_data"
CHART_DIR="/home/xiaoju/qa/zhaolu/process_monitor_charts"
# ==================================================================
# 初始化目录
init_dir() {
mkdir -p ${DATA_DIR}
mkdir -p ${CHART_DIR}
DATA_FILE="${DATA_DIR}/process_metrics_$(date +%Y%m%d_%H%M%S).csv"
# 写入CSV表头
echo "timestamp,process1_cpu%,process1_mem%,process1_mem_mb,process2_cpu%,process2_mem%,process2_mem_mb" > ${DATA_FILE}
echo "初始化完成,数据文件:${DATA_FILE}"
echo "监控时长:10分钟(${MONITOR_DURATION}秒),采集间隔:${COLLECT_INTERVAL}秒"
}
# 获取进程PID
get_pid() {
local target=$1
if [[ ${target} =~ ^[0-9]+$ ]]; then
if ps -p ${target} >/dev/null 2>&1; then
echo ${target}
else
echo ""
fi
else
local pid=$(ps -ef | grep -v grep | grep -v $0 | grep ${target} | awk '{print $2}' | head -1)
echo ${pid}
fi
}
# 读取系统总内存(从/proc/meminfo读取)
get_total_memory_kb() {
local total_mem=$(cat /proc/meminfo | grep ^MemTotal: | awk '{print $2}')
if [[ -z ${total_mem} || ${total_mem} -eq 0 ]]; then
total_mem=527756124 # 你的机器总内存
fi
echo ${total_mem}
}
# 格式化数值(补前导0,确保gnuplot能识别)
format_number() {
local num=$1
# 如果是纯整数,补小数点后两位
if [[ ${num} =~ ^[0-9]+$ ]]; then
echo "${num}.00"
# 如果是.xxx格式,补前导0
elif [[ ${num} =~ ^\.[0-9]+$ ]]; then
echo "0${num}"
# 其他情况保持不变
else
echo "${num}"
fi
}
# 采集单个进程的CPU/内存指标
collect_process_metrics() {
local pid=$1
if [[ -z ${pid} || ! -f /proc/${pid}/stat || ! -f /proc/${pid}/status ]]; then
echo "0.00,0.0000,0.00" # 统一格式为带小数点的数值
return
fi
# 1. CPU使用率计算
local cpu_total_prev=$(cat /proc/stat | grep ^cpu | awk '{sum=$2+$3+$4+$5+$6+$7+$8} END {print sum}')
local pid_stat_prev=$(cat /proc/${pid}/stat | awk '{print $14+$15}')
sleep 0.1
local cpu_total_curr=$(cat /proc/stat | grep ^cpu | awk '{sum=$2+$3+$4+$5+$6+$7+$8} END {print sum}')
local pid_stat_curr=$(cat /proc/${pid}/stat | awk '{print $14+$15}')
local cpu_delta=$((cpu_total_curr - cpu_total_prev))
local pid_delta=$((pid_stat_curr - pid_stat_prev))
local cpu_usage=0.00
if [[ ${cpu_delta} -gt 0 ]]; then
cpu_usage=$(echo "scale=2; ${pid_delta}/${cpu_delta}*100" | bc)
fi
# 2. 内存指标计算
local mem_rss=$(cat /proc/${pid}/status | grep ^VmRSS: | awk '{print $2}')
if [[ -z ${mem_rss} ]]; then
mem_rss=0
fi
local total_mem_kb=$(get_total_memory_kb)
local mem_mb=$(echo "scale=2; ${mem_rss}/1024" | bc)
local mem_usage=0.0000
if [[ ${total_mem_kb} -gt 0 && ${mem_rss} -gt 0 ]]; then
mem_usage=$(echo "scale=4; (${mem_rss}/${total_mem_kb})*100" | bc)
fi
# 格式化数值(确保带小数点)
cpu_usage=$(format_number "${cpu_usage}")
mem_usage=$(format_number "${mem_usage}")
mem_mb=$(format_number "${mem_mb}")
echo "${cpu_usage},${mem_usage},${mem_mb}"
}
# 主采集逻辑
collect_data() {
local start_time=$(date +%s)
local end_time=$((start_time + MONITOR_DURATION))
echo "开始监控(开始时间:$(date +%Y-%m-%d\ %H:%M:%S))..."
while [[ $(date +%s) -lt ${end_time} ]]; do
local timestamp=$(date +%Y-%m-%d\ %H:%M:%S)
local pid1=$(get_pid ${PROCESS_NAME_1})
local pid2=$(get_pid ${PROCESS_NAME_2})
local metrics1=$(collect_process_metrics ${pid1})
local metrics2=$(collect_process_metrics ${pid2})
# 拼接并写入CSV
echo "${timestamp},${metrics1},${metrics2}" >> ${DATA_FILE}
echo "[$timestamp] P1:${metrics1} | P2:${metrics2}"
sleep ${COLLECT_INTERVAL}
done
echo "监控结束(结束时间:$(date +%Y-%m-%d\ %H:%M:%S))"
}
# 生成可视化图表(增加容错,确保所有图表都能生成)
generate_charts() {
echo "开始生成可视化图表..."
local cpu_chart="${CHART_DIR}/cpu_comparison.png"
local mem_chart="${CHART_DIR}/mem_comparison.png"
local mem_mb_chart="${CHART_DIR}/mem_mb_comparison.png"
# 生成CPU对比图(修复数据解析问题)
gnuplot -e "
set terminal png size 1200,800 enhanced;
set output '${cpu_chart}';
set title 'Process CPU Usage Comparison (navi-api-luciazhaolu-8078 vs 8079)';
set xlabel 'Time';
set ylabel 'CPU Usage (%)';
set grid;
set timefmt '%Y-%m-%d %H:%M:%S';
set xdata time;
set format x '%m-%d %H:%M';
set xtics rotate by 45;
set datafile separator ','; # 明确指定分隔符
plot '${DATA_FILE}' using 1:2 title 'navi-api-luciazhaolu-8078' with lines lw 2 lc rgb '#1E90FF', \
'${DATA_FILE}' using 1:5 title 'navi-api-luciazhaolu-8079' with lines lw 2 lc rgb '#FF6347';
" || echo "CPU图表生成警告(继续生成其他图表)"
# 生成内存使用率对比图(修复空图问题)
gnuplot -e "
set terminal png size 1200,800 enhanced;
set output '${mem_chart}';
set title 'Process Memory Usage (%) Comparison (navi-api-luciazhaolu-8078 vs 8079)';
set xlabel 'Time';
set ylabel 'Memory Usage (%)';
set grid;
set timefmt '%Y-%m-%d %H:%M:%S';
set xdata time;
set format x '%m-%d %H:%M';
set xtics rotate by 45;
set datafile separator ','; # 明确指定分隔符
plot '${DATA_FILE}' using 1:3 title 'navi-api-luciazhaolu-8078' with lines lw 2 lc rgb '#1E90FF', \
'${DATA_FILE}' using 1:6 title 'navi-api-luciazhaolu-8079' with lines lw 2 lc rgb '#FF6347';
" || echo "内存使用率图表生成警告(继续生成其他图表)"
# 生成内存使用量(MB)对比图(确保生成)
gnuplot -e "
set terminal png size 1200,800 enhanced;
set output '${mem_mb_chart}';
set title 'Process Memory Usage (MB) Comparison (navi-api-luciazhaolu-8078 vs 8079)';
set xlabel 'Time';
set ylabel 'Memory Usage (MB)';
set grid;
set timefmt '%Y-%m-%d %H:%M:%S';
set xdata time;
set format x '%m-%d %H:%M';
set xtics rotate by 45;
set datafile separator ','; # 明确指定分隔符
plot '${DATA_FILE}' using 1:4 title 'navi-api-luciazhaolu-8078' with lines lw 2 lc rgb '#1E90FF', \
'${DATA_FILE}' using 1:7 title 'navi-api-luciazhaolu-8079' with lines lw 2 lc rgb '#FF6347';
" || echo "内存使用量图表生成警告"
echo "图表生成完成:"
echo " CPU对比图:${cpu_chart}"
echo " 内存使用率对比图:${mem_chart}"
echo " 内存使用量对比图:${mem_mb_chart}"
}
# 检查依赖
check_dependencies() {
local dependencies=("bc" "gnuplot" "ps" "cat")
for dep in ${dependencies[@]}; do
if ! command -v ${dep} &> /dev/null; then
echo "错误:缺少依赖 ${dep},请先安装!"
exit 1
fi
done
}
# 兼容已有CSV文件(修复格式问题,无需重跑监控)
fix_existing_csv() {
local csv_file=$1
if [[ ! -f ${csv_file} ]]; then
echo "CSV文件不存在:${csv_file}"
return 1
fi
# 创建临时文件
local tmp_file=$(mktemp)
# 保留表头,修复数据行格式
head -1 ${csv_file} > ${tmp_file}
tail -n +2 ${csv_file} | while read line; do
# 拆分列
IFS=',' read -r ts cpu1 mem1 mb1 cpu2 mem2 mb2 <<< "${line}"
# 格式化每一列数值
cpu1=$(format_number "${cpu1}")
mem1=$(format_number "${mem1}")
mb1=$(format_number "${mb1}")
cpu2=$(format_number "${cpu2}")
mem2=$(format_number "${mem2}")
mb2=$(format_number "${mb2}")
# 写入临时文件
echo "${ts},${cpu1},${mem1},${mb1},${cpu2},${mem2},${mb2}" >> ${tmp_file}
done
# 替换原文件
mv ${tmp_file} ${csv_file}
echo "已修复CSV文件格式:${csv_file}"
}
# 主流程
main() {
check_dependencies
init_dir
collect_data
generate_charts
echo "==================== 监控完成 ===================="
echo "数据文件路径:${DATA_FILE}"
echo "图表目录:${CHART_DIR}"
}
# 如果你想修复已有CSV文件并重新生成图表,执行:
# fix_existing_csv "/home/xiaoju/qa/zhaolu/process_monitor_data/process_metrics_20260317_125342.csv"
# 然后手动执行generate_charts函数(替换DATA_FILE为你的文件路径)
# 启动主流程
main