From 00aaf282bb52ceac6c018f6870c82213a7a3528e Mon Sep 17 00:00:00 2001 From: cheney Date: Fri, 18 Sep 2026 12:00:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(glance):=20=E7=AB=AF=E5=8F=A3=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E9=80=8F=E8=BF=87=20docker-proxy=20=E5=AE=9A=E4=BD=8D?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E5=86=85=E7=9C=9F=E5=AE=9E=E8=BF=9B=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kit/src/glance/monitor.js | 278 ++++++++++++++++++++++++++++++++------ 1 file changed, 237 insertions(+), 41 deletions(-) diff --git a/kit/src/glance/monitor.js b/kit/src/glance/monitor.js index 708e89b..51ee941 100644 --- a/kit/src/glance/monitor.js +++ b/kit/src/glance/monitor.js @@ -3,7 +3,13 @@ * - CPU 占用率:两次采样 /proc//stat 与 /proc/stat 差值计算,内部休眠约 1 秒 * - 内存占用:/proc//status 的 VmRSS,单位 MB * - 句柄数:/proc//fd 目录条目数 - * - 端口 -> pid:解析 /proc/net/tcp 与 /proc/net/tcp6,结合 /proc//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} + */ +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} 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//net/fib_trie) + * @param {string} netNs + * @returns {Set} + */ +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 + 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,