心跳Agent + pool/status API
- heartbeat-agent.js: 30s间隔采集9台服务器硬件+健康数据 - server.js v3.3.2: 新增 /api/pool/status 从心跳数据文件读取 - 心跳Agent独立运行,不依赖pool token注册流程
This commit is contained in:
parent
4fa5deccc6
commit
d19cb896bf
199
_deploy/console-server/heartbeat-agent.js
Normal file
199
_deploy/console-server/heartbeat-agent.js
Normal file
@ -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);
|
||||
@ -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: '缺少参数' });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user