新增 glance 进程监控命令:采集 cpu/内存/句柄数并生成报告
TDevOpsCICD / build-kit (push) Successful in 5m54s

This commit is contained in:
cheney
2026-09-18 10:00:15 +08:00
parent 593ac96c8c
commit 072c9bfa50
7 changed files with 713 additions and 4 deletions
+148
View File
@@ -0,0 +1,148 @@
/**
* 进程监控指标采集(仅支持 Linux)
* - CPU 占用率:两次采样 /proc/<pid>/stat 与 /proc/stat 差值计算,内部休眠约 1 秒
* - 内存占用:/proc/<pid>/status 的 VmRSS,单位 MB
* - 句柄数:/proc/<pid>/fd 目录条目数
* - 端口 -> pid:解析 /proc/net/tcp 与 /proc/net/tcp6,结合 /proc/<pid>/fd 的 socket inode
*/
const fs = require("fs");
const path = require("path");
const os = require("os");
function isLinux() {
return os.type() === "Linux";
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function readPidTicks(pid) {
const txt = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
// 字段 14=utime 15=stime,comm 可能含空格,按最后一个 ) 截断
const rest = txt.slice(txt.lastIndexOf(")") + 1).trim().split(/\s+/);
return Number(rest[11]) + Number(rest[12]);
}
function readCpuTotal() {
const line = fs.readFileSync("/proc/stat", "utf8").split("\n")[0];
// user nice system idle iowait irq softirq steal(guest 已计入 user)
const parts = line.trim().split(/\s+/).slice(1, 9).map(Number);
let total = 0;
for (const v of parts) {
total += Number.isFinite(v) ? v : 0;
}
return total;
}
function readMemMB(pid) {
const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
const m = status.match(/^VmRSS:\s+(\d+)\s+kB/m);
if (!m) {
return null;
}
return Math.round(Number(m[1]) / 1024 * 100) / 100;
}
function readHandleCount(pid) {
return fs.readdirSync(`/proc/${pid}/fd`, { withFileTypes: true }).length;
}
/**
* 采集单个进程的监控数据
* @param {number} pid
* @returns {Promise<{pid:number,cpu:number,mem:number,handle:number}|null>} 进程不存在/不可读返回 null
*/
async function collectPid(pid) {
try {
const beforePid = readPidTicks(pid);
const beforeTotal = readCpuTotal();
await sleep(1000);
const afterPid = readPidTicks(pid);
const afterTotal = readCpuTotal();
const totalDelta = afterTotal - beforeTotal;
const pidDelta = afterPid - beforePid;
const cpu = totalDelta > 0 ? Math.round(pidDelta / totalDelta * 10000) / 100 : 0;
const mem = readMemMB(pid);
const handle = readHandleCount(pid);
return { pid, cpu, mem, handle };
} catch (e) {
return null;
}
}
/**
* 根据端口查找监听进程 pid
* @param {number} port
* @returns {number|null}
*/
function findPortPid(port) {
const targetInodes = new Set();
for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
let text;
try {
text = fs.readFileSync(file, "utf8");
} catch (e) {
continue;
}
const lines = text.split("\n").slice(1);
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length < 10) {
continue;
}
// st == 0A 表示 LISTEN
if (parts[3] !== "0A") {
continue;
}
const local = parts[1];
const idx = local.lastIndexOf(":");
if (idx < 0) {
continue;
}
const realPort = parseInt(local.slice(idx + 1), 16);
if (Number.isFinite(realPort) && realPort === Number(port)) {
targetInodes.add(parts[9]);
}
}
}
if (targetInodes.size === 0) {
return null;
}
const entries = fs.readdirSync("/proc");
for (const entry of entries) {
if (!/^\d+$/.test(entry)) {
continue;
}
const fdDir = `/proc/${entry}/fd`;
let names;
try {
names = fs.readdirSync(fdDir);
} catch (e) {
continue;
}
for (const fd of names) {
try {
const target = fs.readlinkSync(path.join(fdDir, fd));
const m = target.match(/^socket:\[(\d+)\]$/);
if (m && targetInodes.has(m[1])) {
return Number(entry);
}
} catch (e) {
// 忽略无法读取的 fd
}
}
}
return null;
}
module.exports = {
isLinux,
collectPid,
findPortPid
};