覆盖网络线累积产物(此前只在工作区、未入版本库):
- 新增 relay 子系统 src/net/relay/**(wire/duplex/server/client/dialer/switcher/directory/identity/keys/placement/network/addr-override/main/index)
- 新增 src/worker/relay-tunnel.ts、src/web/routes/overlay.ts
- 新增观测/演练脚本 overlay-probe、overlay-failover-drill、overlay-keyring、overlay-holepunch、overlay-jitter、overlay-wan、overlay-relaykey-add、relay-mem-calibrate
- 新增测试 12 个(relay / relay-failover / remote-spawner / instance-port / overlay-{network,auth,bootstrap,identity} / remote-user-fs 等)
验收基线:npm test = 162 pass / 0 fail / 1 skip;--scene all = 12 PASS / 0 SKIP / 0 FAIL;overlay-probe = 12/12
179 lines
5.7 KiB
JavaScript
179 lines
5.7 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 覆盖网络 序⑥ · S6 `MEM_PER_HOST_MB` 校准(⛔ 一次性脚本、不进产品路径)。
|
||
*
|
||
* ## 为什么必须放大测
|
||
* 现网 `used = 2` ⇒ relay 的 RSS(72888 KB)**只反映 Node 基座**,参数表 §5.1 的
|
||
* "2 MB/台" 是**推导值不是实测**。本脚本把 N 拉到 2/10/25/50/100,量 RSS 斜率。
|
||
*
|
||
* ## 干净方案(⛔ 不污染生产)
|
||
* - **relay 起在本机回环**(独立进程、独立高层口号,与生产的 20080 无关);
|
||
* - **N 个合成 client 跑在"本脚本进程内"** —— 这是刻意的:只要被测量的对象是 **relay 子进程**
|
||
* 的 RSS,client 的内存长在本进程里 ⇒ **不进被测量**。(若让 client 也各自起进程,
|
||
* 机器上会多出 100 个 Node 基座 ≈ 4 GB,纯浪费。)
|
||
* - 全程 **127.0.0.1**,零公网面 ⇒ ⛔ 不动 47 的任何配置(S6 权限附注)。
|
||
*
|
||
* ## 判据
|
||
* 线性回归 `RSS(N) = a + b·N`,`R² ≥ 0.9` 方为有效;否则**如实报告跳点**,⛔ 不许硬套斜率。
|
||
*
|
||
* 用法:node scripts/relay-mem-calibrate.mjs [--base 19000] [--port 23456]
|
||
*
|
||
* @module scripts/relay-mem-calibrate
|
||
*/
|
||
|
||
import { spawn } from 'node:child_process'
|
||
import { randomBytes } from 'node:crypto'
|
||
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
||
import { tmpdir } from 'node:os'
|
||
import { join, dirname } from 'node:path'
|
||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||
const REPO = join(HERE, '..')
|
||
|
||
function arg(name, dflt) {
|
||
const i = process.argv.indexOf(`--${name}`)
|
||
return i >= 0 ? process.argv[i + 1] : dflt
|
||
}
|
||
|
||
const PORT = Number(arg('port', 23456))
|
||
const BASE = Number(arg('base', 19000))
|
||
const SPAN = Number(arg('span', 3000))
|
||
const POINTS = (arg('points', '2,10,25,50,100')).split(',').map(Number)
|
||
const logs = []
|
||
|
||
const { RelayClient } = await import(pathToFileURL(join(REPO, 'lib/net/relay/client.js')).href)
|
||
|
||
const tmp = mkdtempSync(join(tmpdir(), 'relay-mem-'))
|
||
const keysFile = join(tmp, 'keys.json')
|
||
const keys = {}
|
||
const secrets = {}
|
||
for (let i = 1; i <= Math.max(...POINTS); i++) {
|
||
const sec = randomBytes(32).toString('hex')
|
||
secrets[i] = sec
|
||
keys[`ops/w-${i}`] = sec
|
||
}
|
||
writeFileSync(keysFile, JSON.stringify(keys, null, 2))
|
||
|
||
const relay = spawn(
|
||
process.execPath,
|
||
[
|
||
join(REPO, 'lib/net/relay/main.js'),
|
||
'--port',
|
||
String(PORT),
|
||
'--keys-file',
|
||
keysFile,
|
||
'--base',
|
||
String(BASE),
|
||
'--span',
|
||
String(SPAN),
|
||
'--max-hosts',
|
||
'0',
|
||
],
|
||
{ cwd: REPO, stdio: ['ignore', 'pipe', 'pipe'] },
|
||
)
|
||
relay.stdout.on('data', (d) => logs.push(String(d)))
|
||
relay.stderr.on('data', (d) => logs.push(String(d)))
|
||
|
||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||
|
||
async function status() {
|
||
const r = await fetch(`http://127.0.0.1:${PORT}/status`)
|
||
return r.json()
|
||
}
|
||
|
||
async function used() {
|
||
try {
|
||
return (await status()).capacity.used
|
||
} catch {
|
||
return -1
|
||
}
|
||
}
|
||
|
||
async function rssKbMedian(pid, times = 7, gapMs = 500) {
|
||
const arr = []
|
||
for (let i = 0; i < times; i++) {
|
||
arr.push(await rssKbOnce(pid))
|
||
await sleep(gapMs)
|
||
}
|
||
const s = arr.filter((x) => Number.isFinite(x)).sort((a, b) => a - b)
|
||
return { median: s[Math.floor(s.length / 2)], min: s[0], max: s[s.length - 1], n: s.length }
|
||
}
|
||
|
||
async function rssKbOnce(pid) {
|
||
if (process.platform === 'win32') {
|
||
const r = await new Promise((res) => {
|
||
const p = spawn('powershell', ['-NoProfile', '-Command', `(Get-Process -Id ${pid}).WorkingSet64`])
|
||
let o = ''
|
||
p.stdout.on('data', (d) => (o += d))
|
||
p.on('close', () => res(o.trim()))
|
||
})
|
||
return Math.round(Number(r) / 1024)
|
||
}
|
||
const r = await new Promise((res) => {
|
||
const p = spawn('sh', ['-c', `ps -o rss= -p ${pid}`])
|
||
let o = ''
|
||
p.stdout.on('data', (d) => (o += d))
|
||
p.on('close', () => res(o.trim()))
|
||
})
|
||
return Number(r)
|
||
}
|
||
|
||
const clients = []
|
||
const samples = []
|
||
const results = { platform: process.platform, node: process.version, points: [], raw: [] }
|
||
|
||
for (const target of POINTS) {
|
||
while (clients.length < target) {
|
||
const i = clients.length + 1
|
||
const c = new RelayClient({
|
||
url: `ws://127.0.0.1:${PORT}/dshs-relay`,
|
||
hostId: `w-${i}`,
|
||
networkId: 'ops',
|
||
secret: secrets[i],
|
||
ports: [BASE + i],
|
||
})
|
||
c.start()
|
||
clients.push(c)
|
||
}
|
||
let u = -1
|
||
for (let i = 0; i < 60; i++) {
|
||
u = await used()
|
||
if (u >= target) break
|
||
await sleep(500)
|
||
}
|
||
await sleep(1000)
|
||
const rss = await rssKbMedian(relay.pid)
|
||
samples.push({ n: clients.length, used: u, rssKb: rss.median, rssMin: rss.min, rssMax: rss.max, nSamples: rss.n })
|
||
process.stderr.write(`[mem] N=${clients.length} used=${u} RSS=${rss.median} KB (min ${rss.min} / max ${rss.max})\n`)
|
||
}
|
||
|
||
// 线性回归
|
||
const n = samples.length
|
||
const sx = samples.reduce((a, s) => a + s.n, 0)
|
||
const sy = samples.reduce((a, s) => a + s.rssKb, 0)
|
||
const sxx = samples.reduce((a, s) => a + s.n * s.n, 0)
|
||
const sxy = samples.reduce((a, s) => a + s.n * s.rssKb, 0)
|
||
const b = (n * sxy - sx * sy) / (n * sxx - sx * sx)
|
||
const a = (sy - b * sx) / n
|
||
const my = sy / n
|
||
const ssTot = samples.reduce((acc, s) => acc + (s.rssKb - my) ** 2, 0)
|
||
const ssRes = samples.reduce((acc, s) => acc + (s.rssKb - (a + b * s.n)) ** 2, 0)
|
||
results.points = samples
|
||
results.fit = {
|
||
interceptKb: Math.round(a),
|
||
slopeKbPerHost: Math.round(b * 10) / 10,
|
||
r2: Math.round((1 - ssRes / ssTot) * 10000) / 10000,
|
||
}
|
||
results.projected = {
|
||
memPerHostMbCeil20: Math.ceil((b / 1024) * 1.2 * 10) / 10,
|
||
at225HostsMb: Math.round((a + b * 225) / 1024),
|
||
}
|
||
const last = samples[samples.length - 1]
|
||
process.stdout.write(`### RESULT ### ${JSON.stringify({ ...results, lastUsed: last.used })}\n`)
|
||
for (const c of clients) c.stop()
|
||
relay.kill()
|
||
await sleep(500)
|
||
rmSync(tmp, { recursive: true, force: true })
|
||
process.exit(0)
|