From d19cb896bfcd06b945eaf8786c7d12d2cc861d43 Mon Sep 17 00:00:00 2001 From: bingshuo Date: Mon, 25 May 2026 23:31:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BF=83=E8=B7=B3Agent=20+=20pool/status=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - heartbeat-agent.js: 30s间隔采集9台服务器硬件+健康数据 - server.js v3.3.2: 新增 /api/pool/status 从心跳数据文件读取 - 心跳Agent独立运行,不依赖pool token注册流程 --- _deploy/console-server/heartbeat-agent.js | 199 ++++++++++++++++++++++ _deploy/console-server/server.js | 18 ++ 2 files changed, 217 insertions(+) create mode 100644 _deploy/console-server/heartbeat-agent.js diff --git a/_deploy/console-server/heartbeat-agent.js b/_deploy/console-server/heartbeat-agent.js new file mode 100644 index 0000000..a0a8d74 --- /dev/null +++ b/_deploy/console-server/heartbeat-agent.js @@ -0,0 +1,199 @@ +#!/bin/bash +# 光湖心跳Agent v1.0 — 采集各节点硬件数据并上报至Console Server +# 部署: GZ-006 PM2 cron模式 +# 用法: node heartbeat-agent.js 或 pm2 start heartbeat-agent.js + +const http = require('http'); +const https = require('https'); +const fs = require('fs'); + +const CONSOLE_HOST = '127.0.0.1'; +const CONSOLE_PORT = 3920; +const REPORT_INTERVAL = 30000; // 30秒 + +// 全服务器配置 — 和 console-server 保持一致 +const SERVERS = [ + { code: "BS-GZ-006", name: "广州 · 代码仓库", ip: "43.139.217.141", key: "zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 60 } }, + { code: "BS-SG-001", name: "新加坡 · 铸渊大脑", ip: "43.156.237.110", key: "zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9", port: 3911, spec: { cpu: 4, mem_gb: 8, disk_gb: 80 } }, + { code: "BS-SG-002", name: "新加坡 · 面孔", ip: "43.134.16.246", key: "zy_gtw_d1f6d2b8cb4ea44292bd036e4dd03a70745ea502d4a3ae40", port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 30 } }, + { code: "BS-SG-003", name: "新加坡 · 中继", ip: "43.153.193.169", key: "zy_gtw_d7c033d8ae992ffc9dd3e0dd5fe332dace0b644c0ae7c847", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 80 } }, + { code: "ZY-SG-006", name: "新加坡 · 语料", ip: "43.153.203.105", key: "zy_gtw_fff861a2fe40cbdc366ee21f218023be8b1c182f66c852d3", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 50 } }, + { code: "BS-SH-005", name: "上海 · 国内节点", ip: "124.223.10.33", key: "zy_gtw_b1045de5ddfd7832e3c53349d9c896f054b85a4a9ece22f9", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 30 } }, + { code: "AW-GZ-001", name: "企业主服务器 · 广州", ip: "43.139.251.175", key: "zy_gtw_cab20e8c1b957aaad7507bf542231521bba30665461aa540", port: 3910, spec: { cpu: 4, mem_gb: 16, disk_gb: 80 } }, + { code: "AW-SH-002", name: "企业灾备 · 上海", ip: "124.222.54.198", key: "zy_gtw_242f1dd3b14c1260fb01196083abde3dcb85e53532d27666", port: 3910, spec: { cpu: 4, mem_gb: 4, disk_gb: 40 } }, + { code: "AW-GZ-003", name: "企业灾备 · 广州", ip: "119.29.181.132", key: "zy_gtw_8c9a8b439af823690ae6ad57f52734771ff39493ab986032", port: 3910, spec: { cpu: 2, mem_gb: 8, disk_gb: 50 } }, +]; + +// 心跳数据存储 +const HB_FILE = '/opt/zhuyuan/data/heartbeat-data.json'; + +function loadHB() { + try { if (fs.existsSync(HB_FILE)) return JSON.parse(fs.readFileSync(HB_FILE, 'utf-8')); } catch(e) {} + return { nodes: {}, pool: {}, updated_at: null }; +} +function saveHB(data) { + try { + fs.mkdirSync('/opt/zhuyuan/data', { recursive: true }); + fs.writeFileSync(HB_FILE, JSON.stringify(data, null, 2)); + } catch(e) { console.error('保存心跳数据失败:', e.message); } +} + +// 通过Gatekeeper执行命令 +function gatekeeperExec(srv, cmd, timeoutMs) { + timeoutMs = timeoutMs || 8000; + const d = JSON.stringify({ cmd }); + const opts = { + hostname: srv.ip, port: srv.port, path: '/exec', method: 'POST', + headers: { 'Authorization': 'Bearer ' + srv.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(d) }, + timeout: timeoutMs + }; + return new Promise((resolve) => { + const req = http.request(opts, (res) => { + let b = ''; res.on('data', c => b += c); + res.on('end', () => { try { resolve({ ok: res.statusCode === 200, body: JSON.parse(b) }); } catch(e) { resolve({ ok: false }); } }); + }); + req.on('error', () => resolve({ ok: false })); + req.on('timeout', () => { req.destroy(); resolve({ ok: false }); }); + req.write(d); req.end(); + }); +} + +// 健康检查 +function healthCheck(srv) { + const data = JSON.stringify({}); + const opts = { + hostname: srv.ip, port: srv.port, path: '/health', method: 'POST', + headers: { 'Authorization': 'Bearer ' + srv.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }, + timeout: 5000 + }; + return new Promise((resolve) => { + const start = Date.now(); + const req = http.request(opts, (res) => { let b = ''; res.on('data', c => b += c); res.on('end', () => resolve({ ok: res.statusCode === 200, latency: Date.now() - start })); }); + req.on('error', () => resolve({ ok: false, latency: 0 })); + req.on('timeout', () => { req.destroy(); resolve({ ok: false, latency: 0 }); }); + req.write(data); req.end(); + }); +} + +// 采集硬件数据 +async function collectHardware(srv) { + const cmd = "echo 'CPU_CORES:'$(nproc);echo 'UPTIME_S:'$(awk '{print int($1)}' /proc/uptime 2>/dev/null||echo 0);free -m|awk '/^Mem:/{printf \"MEM_TOTAL:%d\\nMEM_USED:%d\\nMEM_AVAIL:%d\\n\",$2,$3,$7}';cat /proc/loadavg 2>/dev/null|awk '{printf \"LOAD1:%.2f\\nLOAD5:%.2f\\nLOAD15:%.2f\\n\",$1,$2,$3}';df -h / 2>/dev/null|awk 'NR==2{printf \"DISK_TOTAL:%s\\nDISK_USED:%s\\nDISK_AVAIL:%s\\nDISK_PCT:%s\\n\",$2,$3,$4,$5}'"; + const r = await gatekeeperExec(srv, cmd, 8000); + if (!r.ok || !r.body || !r.body.stdout) return null; + + const stdout = r.body.stdout; + const m = (re) => { const match = stdout.match(re); return match ? match[1] : null; }; + + const cpu_cores = parseInt(m(/CPU_CORES:(\d+)/)) || srv.spec.cpu; + const uptime_h = Math.floor((parseInt(m(/UPTIME_S:(\d+)/)) || 0) / 3600); + const mem_total_mb = parseInt(m(/MEM_TOTAL:(\d+)/)) || srv.spec.mem_gb * 1024; + const mem_used_mb = parseInt(m(/MEM_USED:(\d+)/)) || 0; + const mem_avail_mb = parseInt(m(/MEM_AVAIL:(\d+)/)) || mem_total_mb; + const cpu_load = parseFloat(m(/LOAD1:([\d.]+)/)) || 0; + const disk_used_pct = parseInt(m(/DISK_PCT:(\d+)/)) || 0; + + return { + cpu_cores, + uptime_h, + mem_total_mb, + mem_used_mb, + mem_avail_mb, + cpu_load, + disk_used_pct, + spec: srv.spec + }; +} + +// 主循环 +async function heartbeat() { + const hb = loadHB(); + console.log('[' + new Date().toISOString() + '] 心跳采集开始...'); + + // 并行健康检查 + const healthResults = await Promise.all(SERVERS.map(async (srv) => { + const health = await healthCheck(srv); + return { ...srv, alive: health.ok, latency: health.latency }; + })); + + // 对在线节点采集硬件 + const hwResults = await Promise.all(healthResults.map(async (srv) => { + let hw = null; + if (srv.alive) { + hw = await collectHardware(srv); + } + return { ...srv, hardware: hw }; + })); + + // 汇总数据 + let totalCpu = 0, usedCpu = 0, totalMem = 0, usedMem = 0, onlineNodes = 0; + + for (const srv of hwResults) { + const nodeData = { + name: srv.name, + online: srv.alive, + latency: srv.latency, + last_seen: new Date().toISOString(), + spec: srv.spec + }; + + if (srv.alive && srv.hardware) { + nodeData.hardware = srv.hardware; + nodeData.cpu_load = srv.hardware.cpu_load; + nodeData.mem_total_mb = srv.hardware.mem_total_mb; + nodeData.mem_avail_mb = srv.hardware.mem_avail_mb; + nodeData.disk_used_pct = srv.hardware.disk_used_pct; + nodeData.uptime_hours = srv.hardware.uptime_h; + nodeData.tasks = 0; + + totalCpu += srv.spec.cpu; + usedCpu += srv.hardware.cpu_load; + totalMem += srv.hardware.mem_total_mb; + usedMem += srv.hardware.mem_used_mb; + onlineNodes++; + } else if (srv.alive) { + totalCpu += srv.spec.cpu; + totalMem += srv.spec.mem_gb * 1024; + onlineNodes++; + } + + hb.nodes[srv.code] = nodeData; + } + + // 池数据 + hb.pool = { + total_nodes: SERVERS.length, + online_nodes: onlineNodes, + cpu_total: totalCpu, + cpu_used: parseFloat(usedCpu.toFixed(1)), + cpu_available: parseFloat((totalCpu - usedCpu).toFixed(1)), + mem_total_gb: parseFloat((totalMem / 1024).toFixed(1)), + mem_used_gb: parseFloat((usedMem / 1024).toFixed(1)), + mem_available_gb: parseFloat(((totalMem - usedMem) / 1024).toFixed(1)), + active_tasks: 0 + }; + + hb.total_spec = { + cpu: SERVERS.reduce((s, n) => s + n.spec.cpu, 0), + mem_gb: SERVERS.reduce((s, n) => s + n.spec.mem_gb, 0), + disk_gb: SERVERS.reduce((s, n) => s + n.spec.disk_gb, 0) + }; + + hb.updated_at = new Date().toISOString(); + saveHB(hb); + + console.log(' 在线: ' + onlineNodes + '/' + SERVERS.length + + ' | CPU: ' + hb.pool.cpu_used + '/' + hb.pool.cpu_total + '核' + + ' | 内存: ' + hb.pool.mem_used_gb + '/' + hb.pool.mem_total_gb + 'GB'); +} + +// 启动 +console.log('光湖心跳Agent v1.0 启动'); +console.log(' 节点数: ' + SERVERS.length); +console.log(' 采集间隔: ' + (REPORT_INTERVAL / 1000) + '秒'); +console.log(' 数据文件: ' + HB_FILE); + +// 立即执行一次 +heartbeat(); + +// 定时执行 +setInterval(heartbeat, REPORT_INTERVAL); diff --git a/_deploy/console-server/server.js b/_deploy/console-server/server.js index 82c72c4..580c831 100644 --- a/_deploy/console-server/server.js +++ b/_deploy/console-server/server.js @@ -261,12 +261,30 @@ app.post('/api/exec', async (req, res) => { // 节点池 API const POOL_REGISTRY_FILE = DATA_DIR + '/pool-registry.json'; const JOIN_TOKENS_FILE = DATA_DIR + '/join-tokens.json'; +const HEARTBEAT_FILE = DATA_DIR + '/heartbeat-data.json'; function loadPoolRegistry() { try { if (fs.existsSync(POOL_REGISTRY_FILE)) return JSON.parse(fs.readFileSync(POOL_REGISTRY_FILE, 'utf-8')); } catch(e) {} return { active: {}, pending: {}, history: [] }; } function savePoolRegistry(r) { r.updated_at = new Date().toISOString(); try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {} fs.writeFileSync(POOL_REGISTRY_FILE, JSON.stringify(r, null, 2)); } function loadJoinTokens() { try { if (fs.existsSync(JOIN_TOKENS_FILE)) return JSON.parse(fs.readFileSync(JOIN_TOKENS_FILE, 'utf-8')); } catch(e) {} return { tokens: [] }; } function saveJoinTokens(d) { try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {} fs.writeFileSync(JOIN_TOKENS_FILE, JSON.stringify(d, null, 2)); } function sha256(s) { return crypto.createHash('sha256').update(s).digest('hex'); } +// 算力池状态 — 从心跳数据文件读取 +app.get('/api/pool/status', (req, res) => { + try { + if (fs.existsSync(HEARTBEAT_FILE)) { + const hb = JSON.parse(fs.readFileSync(HEARTBEAT_FILE, 'utf-8')); + res.json({ ok: true, pool: hb.pool || {}, nodes: hb.nodes || {}, total_spec: hb.total_spec || {}, updated_at: hb.updated_at }); + } else { + // 无心跳数据,返回静态池数据 + const totalCpu = SERVERS.reduce((s, n) => s + n.spec.cpu, 0); + const totalMem = SERVERS.reduce((s, n) => s + n.spec.mem_gb, 0); + res.json({ ok: true, pool: { total_nodes: SERVERS.length, online_nodes: 0, cpu_total: totalCpu, cpu_used: 0, cpu_available: totalCpu, mem_total_gb: totalMem, mem_used_gb: 0, mem_available_gb: totalMem, active_tasks: 0 }, nodes: {}, total_spec: { cpu: totalCpu, mem_gb: totalMem }, updated_at: null }); + } + } catch(e) { + res.json({ ok: true, pool: {}, nodes: {}, updated_at: null }); + } +}); + app.post('/api/pool/join', (req, res) => { const { join_token, node_code, node_name, side, team_id, operator_name, hardware, network } = req.body; if (!join_token || !node_code || !side) return res.status(400).json({ ok: false, error: '缺少参数' });