This commit is contained in:
+237
-41
@@ -3,7 +3,13 @@
|
||||
* - 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
|
||||
* - 端口 -> pid:
|
||||
* 1. 宿主网络命名空间中监听该端口的 fd 属主(普通端口服务,如 sshd)
|
||||
* 2. 若属主是 docker-proxy,则解析其 -container-ip/-container-port,
|
||||
* 在容器网络命名空间内按容器内端口查找持有监听 fd 的真实应用进程
|
||||
* (处理 0.0.0.0:6380->6379 这类端口映射不一致,并用 fib_trie 的 /32
|
||||
* 本地地址匹配容器 IP 以区分布局在网段相同、内网端口相同的多个容器)
|
||||
* 3. 宿主无监听时(userland-proxy 关闭),直接扫描非宿主 netns 的监听进程
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
@@ -74,51 +80,73 @@ async function collectPid(pid) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据端口查找监听进程 pid
|
||||
* @param {number} port
|
||||
* @returns {number|null}
|
||||
* 读取进程所在网络命名空间标识
|
||||
* @param {number} pid
|
||||
* @returns {string|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) {
|
||||
function readNetNs(pid) {
|
||||
try {
|
||||
return fs.readlinkSync(`/proc/${pid}/ns/net`);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync("/proc");
|
||||
for (const entry of entries) {
|
||||
/**
|
||||
* 读取一个网络命名空间内监听指定端口的 socket inode 集合
|
||||
* @param {string} netNs
|
||||
* @param {number} port
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
function listenInodesInNetNs(netNs, port) {
|
||||
const out = new Set();
|
||||
for (const entry of fs.readdirSync("/proc")) {
|
||||
if (!/^\d+$/.test(entry)) {
|
||||
continue;
|
||||
}
|
||||
if (readNetNs(Number(entry)) !== netNs) {
|
||||
continue;
|
||||
}
|
||||
for (const file of ["tcp", "tcp6"]) {
|
||||
let text;
|
||||
try {
|
||||
text = fs.readFileSync(`/proc/${entry}/net/${file}`, "utf8");
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
for (const line of text.split("\n").slice(1)) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length < 10 || parts[3] !== "0A") {
|
||||
continue;
|
||||
}
|
||||
const local = parts[1];
|
||||
const idx = local.lastIndexOf(":");
|
||||
if (idx < 0) {
|
||||
continue;
|
||||
}
|
||||
if (parseInt(local.slice(idx + 1), 16) === Number(port)) {
|
||||
out.add(parts[9]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定网络命名空间内查找持有这些 socket inode 的进程 pid(归一化为 Tgid)
|
||||
* @param {string} netNs
|
||||
* @param {Set<string>} inodes
|
||||
* @returns {number|null}
|
||||
*/
|
||||
function ownerInNetNs(netNs, inodes) {
|
||||
for (const entry of fs.readdirSync("/proc")) {
|
||||
if (!/^\d+$/.test(entry)) {
|
||||
continue;
|
||||
}
|
||||
if (readNetNs(Number(entry)) !== netNs) {
|
||||
continue;
|
||||
}
|
||||
const fdDir = `/proc/${entry}/fd`;
|
||||
let names;
|
||||
try {
|
||||
@@ -130,8 +158,8 @@ function findPortPid(port) {
|
||||
try {
|
||||
const target = fs.readlinkSync(path.join(fdDir, fd));
|
||||
const m = target.match(/^socket:\[(\d+)\]$/);
|
||||
if (m && targetInodes.has(m[1])) {
|
||||
return Number(entry);
|
||||
if (m && inodes.has(m[1])) {
|
||||
return tgidOf(Number(entry));
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略无法读取的 fd
|
||||
@@ -141,6 +169,174 @@ function findPortPid(port) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化线程 pid 为进程 Tgid
|
||||
* @param {number} pid
|
||||
* @returns {number}
|
||||
*/
|
||||
function tgidOf(pid) {
|
||||
try {
|
||||
const m = fs.readFileSync(`/proc/${pid}/status`, "utf8").match(/^Tgid:\s+(\d+)/m);
|
||||
return m ? Number(m[1]) : pid;
|
||||
} catch (e) {
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取进程启动参数
|
||||
* @param {number} pid
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function cmdlineOf(pid) {
|
||||
try {
|
||||
return fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0");
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取网络命名空间内声明的本地 IPv4 地址集合(/proc/<pid>/net/fib_trie)
|
||||
* @param {string} netNs
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
function netNsIpv4s(netNs) {
|
||||
const ips = new Set();
|
||||
for (const entry of fs.readdirSync("/proc")) {
|
||||
if (!/^\d+$/.test(entry)) {
|
||||
continue;
|
||||
}
|
||||
if (readNetNs(Number(entry)) !== netNs) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const text = fs.readFileSync(`/proc/${entry}/net/fib_trie`, "utf8");
|
||||
const re = /\b(\d+\.\d+\.\d+\.\d+)\b/g;
|
||||
let m;
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
ips.add(m[1]);
|
||||
}
|
||||
break;
|
||||
} catch (e) {
|
||||
// 继续尝试下一个候选
|
||||
}
|
||||
}
|
||||
return ips;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据端口查找监听进程 pid
|
||||
*
|
||||
* 优先命中容器内真实应用进程而非 docker-proxy;端口映射不一致
|
||||
* (如 0.0.0.0:6380->6379)时按 docker-proxy 解析出的容器 IP/端口定位。
|
||||
* @param {number} port
|
||||
* @returns {number|null}
|
||||
*/
|
||||
function findPortPid(port) {
|
||||
const hostNs = readNetNs(1);
|
||||
if (hostNs === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hostInodes = listenInodesInNetNs(hostNs, port);
|
||||
if (hostInodes.size > 0) {
|
||||
const owner = ownerInNetNs(hostNs, hostInodes);
|
||||
if (owner !== null) {
|
||||
const cmd = cmdlineOf(owner);
|
||||
if (cmd.length > 0 && cmd[0].includes("docker-proxy")) {
|
||||
const args = {};
|
||||
for (let i = 1; i < cmd.length - 1; i++) {
|
||||
if (cmd[i].startsWith("-")) {
|
||||
args[cmd[i]] = cmd[i + 1];
|
||||
}
|
||||
}
|
||||
const cip = args["-container-ip"];
|
||||
const cport = Number(args["-container-port"]);
|
||||
if (Number.isFinite(cport) && cport > 0) {
|
||||
// 所有可能持有该容器内端口的网络命名空间
|
||||
const groups = new Map(); // netNs -> Set<inode>
|
||||
for (const entry of fs.readdirSync("/proc")) {
|
||||
if (!/^\d+$/.test(entry)) {
|
||||
continue;
|
||||
}
|
||||
const ns = readNetNs(Number(entry));
|
||||
if (ns === null || ns === hostNs) {
|
||||
continue;
|
||||
}
|
||||
const inodes = new Set();
|
||||
for (const file of ["tcp", "tcp6"]) {
|
||||
let text;
|
||||
try {
|
||||
text = fs.readFileSync(`/proc/${entry}/net/${file}`, "utf8");
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
for (const line of text.split("\n").slice(1)) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length < 10 || parts[3] !== "0A") {
|
||||
continue;
|
||||
}
|
||||
const idx = parts[1].lastIndexOf(":");
|
||||
if (idx >= 0 && parseInt(parts[1].slice(idx + 1), 16) === cport) {
|
||||
inodes.add(parts[9]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inodes.size > 0) {
|
||||
groups.set(ns, inodes);
|
||||
}
|
||||
}
|
||||
// 优先选 fib_trie 本地地址与容器 IP 一致的命名空间
|
||||
if (cip) {
|
||||
for (const [ns, inodes] of groups) {
|
||||
if (netNsIpv4s(ns).has(cip)) {
|
||||
const pid = ownerInNetNs(ns, inodes);
|
||||
if (pid !== null) {
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 退而求其次:任意持有该容器内端口监听的命名空间的属主
|
||||
for (const [ns, inodes] of groups) {
|
||||
const pid = ownerInNetNs(ns, inodes);
|
||||
if (pid !== null) {
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 容器应用未就绪:退回 docker-proxy,保留可见性
|
||||
return owner;
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
}
|
||||
|
||||
// 宿主无监听(userland-proxy 关闭):扫描非宿主命名空间内监听该端口的进程
|
||||
const groups = new Map();
|
||||
for (const entry of fs.readdirSync("/proc")) {
|
||||
if (!/^\d+$/.test(entry)) {
|
||||
continue;
|
||||
}
|
||||
const ns = readNetNs(Number(entry));
|
||||
if (ns === null || ns === hostNs) {
|
||||
continue;
|
||||
}
|
||||
const inodes = listenInodesInNetNs(ns, port);
|
||||
if (inodes.size > 0) {
|
||||
groups.set(ns, inodes);
|
||||
}
|
||||
}
|
||||
for (const [ns, inodes] of groups) {
|
||||
const pid = ownerInNetNs(ns, inodes);
|
||||
if (pid !== null) {
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isLinux,
|
||||
collectPid,
|
||||
|
||||
Reference in New Issue
Block a user