178 lines
5.7 KiB
JavaScript
178 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)
|