feat(overlay): 覆盖网络线序①–⑮ 代码与测试产物入库
覆盖网络线累积产物(此前只在工作区、未入版本库):
- 新增 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
This commit is contained in:
1 parent
640813e84e
commit
146c3d25ef
53 files changed
+16187
-63
No files matched your search
@@ -0,0 +1,178 @@
|
||||
#!/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)
|
||||
Reference in new issue
Block a user