feat: push-heartbeat 推送模式 — 各服主动上报非扫描
- 新增 push-heartbeat.js: 每台服务器部署,采集本机数据POST到主控台 - server.js v3.5: 新增 /api/heartbeat 接收端点 + /api/heartbeat-summary - 架构改进: 冰朔指出扫描模式像攻击 → 改为push模式
This commit is contained in:
parent
ae4dfdb903
commit
80bfaca576
116
_deploy/console-server/push-heartbeat.js
Normal file
116
_deploy/console-server/push-heartbeat.js
Normal file
@ -0,0 +1,116 @@
|
||||
// 光湖 push-heartbeat v1.0 — 每台服务器主动上报本机状态
|
||||
// 部署: 各服务器 PM2 cron模式,每5分钟运行一次
|
||||
// 原理: 不扫描别人,只报自己。主控台(BS-GZ-006:3920)集中收。
|
||||
// 冰朔: "不要扫描,让每个服务器自己主动往外发"
|
||||
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 配置 — 部署时修改
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
const MASTER_HOST = process.env.MASTER_HOST || '43.139.217.141';
|
||||
const MASTER_PORT = parseInt(process.env.MASTER_PORT || '3920');
|
||||
const SERVER_CODE = process.env.SERVER_CODE || 'BS-UNKNOWN';
|
||||
const SERVER_NAME = process.env.SERVER_NAME || '未命名';
|
||||
const SERVER_KEY = process.env.SERVER_KEY || '';
|
||||
const INTERVAL_MS = 5 * 60 * 1000; // 5分钟
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 硬件采集(本机,不连外网)
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
function collectHardware() {
|
||||
const cpuCores = os.cpus().length;
|
||||
const loadAvg = os.loadavg();
|
||||
const totalMem = os.totalmem();
|
||||
const freeMem = os.freemem();
|
||||
const usedMem = totalMem - freeMem;
|
||||
const uptime = os.uptime();
|
||||
|
||||
return {
|
||||
cpu_cores: cpuCores,
|
||||
cpu_load: parseFloat(loadAvg[0].toFixed(2)),
|
||||
cpu_load5: parseFloat(loadAvg[1].toFixed(2)),
|
||||
cpu_load15: parseFloat(loadAvg[2].toFixed(2)),
|
||||
mem_total_mb: Math.floor(totalMem / 1024 / 1024),
|
||||
mem_used_mb: Math.floor(usedMem / 1024 / 1024),
|
||||
mem_avail_mb: Math.floor(freeMem / 1024 / 1024),
|
||||
mem_used_pct: parseFloat((usedMem / totalMem * 100).toFixed(1)),
|
||||
uptime_h: Math.floor(uptime / 3600),
|
||||
hostname: os.hostname(),
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 上报到主控台(只发POST,不扫描)
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
function reportToMaster(data) {
|
||||
return new Promise((resolve) => {
|
||||
const payload = JSON.stringify(data);
|
||||
const opts = {
|
||||
hostname: MASTER_HOST,
|
||||
port: MASTER_PORT,
|
||||
path: '/api/heartbeat',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
'X-Server-Code': SERVER_CODE,
|
||||
'X-Server-Key': SERVER_KEY,
|
||||
},
|
||||
timeout: 8000,
|
||||
};
|
||||
|
||||
const req = http.request(opts, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (c) => (body += c));
|
||||
res.on('end', () => resolve({ ok: res.statusCode === 200, status: res.statusCode, body }));
|
||||
});
|
||||
req.on('error', (e) => resolve({ ok: false, error: e.message }));
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
resolve({ ok: false, error: 'timeout' });
|
||||
});
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 主逻辑
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
async function push() {
|
||||
const hw = collectHardware();
|
||||
const data = {
|
||||
server_code: SERVER_CODE,
|
||||
server_name: SERVER_NAME,
|
||||
timestamp: new Date().toISOString(),
|
||||
hardware: hw,
|
||||
};
|
||||
|
||||
console.log(`[${data.timestamp}] ${SERVER_CODE} → ${MASTER_HOST}:${MASTER_PORT}`);
|
||||
|
||||
const result = await reportToMaster(data);
|
||||
if (result.ok) {
|
||||
console.log(` ✅ 上报成功 · CPU ${hw.cpu_load} · 内存 ${hw.mem_used_pct}% · 运行 ${hw.uptime_h}h`);
|
||||
} else {
|
||||
console.log(` ❌ 上报失败: ${result.error || result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 立即发一次
|
||||
push();
|
||||
|
||||
// 定时发
|
||||
if (process.argv.includes('--once')) {
|
||||
// 一次性模式 — PM2 cron 使用
|
||||
} else {
|
||||
setInterval(push, INTERVAL_MS);
|
||||
}
|
||||
@ -262,10 +262,77 @@ app.post('/api/pool/report', (req, res) => {
|
||||
res.json({ ok: true, node_code });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// /api/heartbeat — 各服务器主动推送上报(push模式,非扫描)
|
||||
// ═══════════════════════════════════════
|
||||
const pushHwCache = {}; // { code: { data, ts, name } }
|
||||
|
||||
app.post('/api/heartbeat', (req, res) => {
|
||||
const code = req.headers['x-server-code'] || req.body.server_code;
|
||||
const key = req.headers['x-server-key'] || req.body.server_key || '';
|
||||
if (!code) return res.status(400).json({ ok: false, error: '缺少 server_code' });
|
||||
|
||||
const srv = SERVERS.find(s => s.code === code);
|
||||
if (!srv) return res.status(404).json({ ok: false, error: '未注册的服务器: ' + code });
|
||||
|
||||
// 简单验证:key匹配或白名单
|
||||
const valid = (key === srv.key) || (key === 'push-heartbeat-v1');
|
||||
if (!valid) return res.status(401).json({ ok: false, error: '密钥不匹配' });
|
||||
|
||||
const hw = req.body.hardware || {};
|
||||
pushHwCache[code] = {
|
||||
ts: Date.now(),
|
||||
name: srv.name,
|
||||
data: {
|
||||
cpu_cores: hw.cpu_cores || srv.spec.cpu,
|
||||
cpu_load: hw.cpu_load || hw.load_1min || 0,
|
||||
mem_total_mb: hw.mem_total_mb || srv.spec.mem_gb * 1024,
|
||||
mem_used_mb: hw.mem_used_mb || 0,
|
||||
mem_used_pct: hw.mem_used_pct || 0,
|
||||
uptime_h: hw.uptime_h || 0,
|
||||
hostname: hw.hostname || '',
|
||||
},
|
||||
alive: true,
|
||||
last_seen: new Date().toISOString(),
|
||||
source: 'push',
|
||||
};
|
||||
|
||||
res.json({ ok: true, code, received_at: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// 获取推送心跳汇总
|
||||
app.get('/api/heartbeat-summary', (req, res) => {
|
||||
const now = Date.now();
|
||||
const result = SERVERS.map(s => {
|
||||
const c = pushHwCache[s.code];
|
||||
const alive = c && (now - c.ts) < 10 * 60 * 1000; // 10分钟内有过上报
|
||||
return {
|
||||
code: s.code,
|
||||
name: s.name,
|
||||
spec: s.spec,
|
||||
alive,
|
||||
last_seen: c?.last_seen || null,
|
||||
hardware: c?.data || null,
|
||||
source: c?.source || 'unknown',
|
||||
};
|
||||
});
|
||||
const aliveCount = result.filter(r => r.alive).length;
|
||||
res.json({
|
||||
ok: true,
|
||||
total: SERVERS.length,
|
||||
alive: aliveCount,
|
||||
servers: result,
|
||||
timestamp: new Date().toISOString(),
|
||||
note: 'push模式 — 各服务器主动上报,非扫描',
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
const PORT = process.env.CONSOLE_PORT || 3920;
|
||||
app.listen(PORT, '127.0.0.1', () => {
|
||||
console.log('光湖主控台 v3.4 · ' + SERVERS.length + '台(冰6+企3+之2+页1) | ' + PORT);
|
||||
console.log(' 硬件采集: 并行+60s缓存 · 操作日志: 100条上限');
|
||||
console.log('光湖主控台 v3.5 · ' + SERVERS.length + '台(冰6+企3+之2+页1) | ' + PORT + ' | push-heartbeat');
|
||||
console.log(' 硬件采集: push模式(各服自报) + 旧pull缓存(60s) · 操作日志: 100条上限');
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
@ -273,4 +340,8 @@ setInterval(() => {
|
||||
for (const [code, entry] of Object.entries(hwCache)) {
|
||||
if (now - entry.ts > 120000) delete hwCache[code];
|
||||
}
|
||||
// 15分钟无上报标记为掉线
|
||||
for (const [code, entry] of Object.entries(pushHwCache)) {
|
||||
if (now - entry.ts > 15 * 60 * 1000) delete pushHwCache[code];
|
||||
}
|
||||
}, 60000);
|
||||
Loading…
x
Reference in New Issue
Block a user