Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ server.js(入口,10 行)
- CPU/RSS 每 2s 从 `/proc/<pid>/stat|status` 采样,两档保留:秒级 150 点(≈5 分钟,实时曲线)+
分钟级 1440 点(24 小时)。分钟档同时存均值**和峰值** —— 只存均值会把瞬时尖峰抹平,
而排查 OOM 时要看的恰恰是尖峰。都在内存,面板重启即丢
- **口径**:采到的是 `VmRSS`,而 `metrics.ramMax` 是 `-Xmx`(堆)。两者不同口径,
`ram > ramMax` 是**常态** —— 差额是 Metaspace / Code Cache / 线程栈 / GC 结构 /
Netty direct buffer。仪表盘的内存条原先 `Math.min(100, …)` 一夹就永久钉在 100%,
等于把"健康的堆外开销"和"真要 OOM 了"渲染成同一个样子;现在超出部分换色并标注增量。
配额侧同理走 `utils.memFootprintMB()` 折算,见下面「内存配额的口径」
- **隧道进程**(独立于服务端,重启实例不断线):ngrok / frpc / playit / bore /
Pinggy / Serveo 六种驱动,统一输出解析(`\r`/`\n` 双分隔 + ANSI 清洗),
公网地址就绪后 4s 自动做一次 mcPing 连通性验证,失败原因写入 `tunnelError`
Expand Down Expand Up @@ -112,6 +117,28 @@ Fabric/Forge 的模组配置数量不定,额外扫一层 `config/`。
协作者能操作实例,但**不能删实例、不能改协作者名单**(`isOwnerOrAdmin`)。
删除用户时会把他从所有实例的名单里摘掉,否则残留一个已不存在的用户名。

## 内存配额的口径(src/utils.js)

`utils.memFootprintMB(xmx)` = `xmx + max(memOverheadMinMB, xmx × memOverheadPct%)`,
是内存配额**唯一**的折算入口(`quotaError`、`usageOf`、`/api/host` 的 `committedMem` 都走它)。

为什么不能直接用 `-Xmx` 累加:`-Xmx` 只管堆,JVM 还要 Metaspace、Code Cache、线程栈、
GC 自身结构和 Netty 的 direct buffer —— MC 服务端网络层就是 Netty,这块不小。
实测 `-Xmx4G` 的 Paper 稳定运行 RSS 常在 4.5G 以上,重模组服更多。按 Σ-Xmx 把宿主机排满,
实际 RSS 之和必超,**而超出的部分不会报错**:是 OOM killer 半夜随机挑一个服务端杀掉。
沉默的超卖比当场拒绝难查得多。

余量取百分比与固定下限的**较大值**:小堆按比例算不够(1G 的 13% 只有 133M,盖不住
Metaspace + CodeCache + 线程栈),大堆按固定值算又不够(模组服 class 多、direct buffer 大)。
两个参数在 `settings.thresholds` 里,**同时设 0 即退回纯 Σ-Xmx**——
这是逃生阀,给"我自己知道我在超卖"的部署留的。

`settings` 用惰性 `require`:`settings.js` 自己 require 了 `utils.js`,顶层 require 会成环。

配额只在**内存往上加**时校验(`PATCH` 里 `if (mb > inst.xmx)`)。持平和缩小一律放行 ——
前端保存实例设置时总会带上 `xmx`,不这么写的话,管理员一旦调低某人配额、或者口径变严,
已超额的用户会连改个实例名都 403,而"把内存调小自救"这条唯一出路恰好被同一条拦住。

## 磁盘用量(src/disk.js)

分区容量走 `fs.statfs`(用 `bavail` 而非 `bfree` —— 后者含 root 保留块,普通用户拿不到)。
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ from that process. No Java pre-install, no public IP required.
| 🔍 **恢复前预览**:恢复是覆盖式且不可撤销,而备份名只有个时间戳 —— 点「恢复」先列出包内文件数、世界、插件目录,以及**哪些现有内容会被覆盖**;顺带验证归档完整性,损坏的包在覆盖任何东西之前就被拦下 | 🔍 **Pre-restore preview**: restores overwrite irreversibly and backup names are just timestamps — so the confirm dialog lists the file count, worlds, plugin dirs and **exactly which existing paths will be overwritten**. It also verifies archive integrity, so a corrupt archive is caught before a single byte is overwritten |
| 🕵 **操作审计**:谁在什么时候动了什么,**含失败尝试**(403/404/登录失败);口令类字段自动脱敏,管理员在系统设置页可筛选查看 | 🕵 **Audit log**: who did what and when, **including failed attempts** (403/404/bad logins); credential fields auto-redacted, filterable by admins |
| 🤝 **实例共享 + 三档权限**:把实例分享给其他面板用户,每人独立设 **只读**(看状态·日志·玩家)/ **运维**(+ 启停·命令·建备份·踢人封禁)/ **管理**(除下面两条外同主人)。任何一档都**不能删实例、不能改名单**;配额始终算在主人头上 | 🤝 **Share an instance** with a **per-collaborator permission tier**: **viewer** (status, logs, players), **operator** (+ start/stop, console commands, backups, kick/ban) or **manager** (everything but the two below). No tier can **delete the instance or edit the collaborator list**; quota always counts against the owner |
| ◉ 多租户:普通用户实例**隔离**,配额真实生效——实例数 / 内存(-Xmx 之和)/ CPU 核(taskset 绑核)/ **磁盘**(实例目录 + 备份,上传·解压·打包·备份五处校验) | Multi-tenant: isolated user instances with enforced quotas — instance count / memory (Σ-Xmx) / CPU cores (taskset pinning) / **disk** (instance dir + backups, enforced on upload, extract, pack and backup) |
| ◉ 多租户:普通用户实例**隔离**,配额真实生效——实例数 / 内存(**-Xmx 之和 + 每实例一份堆外余量**)/ CPU 核(taskset 绑核)/ **磁盘**(实例目录 + 备份,上传·解压·打包·备份五处校验) | Multi-tenant: isolated user instances with enforced quotas — instance count / memory (**Σ-Xmx plus a per-instance off-heap reserve**) / CPU cores (taskset pinning) / **disk** (instance dir + backups, enforced on upload, extract, pack and backup) |
| 🧮 **内存配额算的是 RSS 不是堆**:`-Xmx` 只管堆,而 Metaspace、Code Cache、线程栈、GC 自身结构和 Netty 的 direct buffer 都在堆外 —— MC 服务端网络层就是 Netty,实测 `-Xmx4G` 的 Paper 稳定运行 RSS 常在 4.5G 以上,重模组服更多。**按 Σ-Xmx 排满宿主机,实际内存一定会超**,而超出的部分不会报错,是 OOM killer 半夜随机挑一个服务端杀掉。所以配额按堆 + 堆外计,余量 `max(下限 MB, -Xmx×%)` 可调,**两项设 0 退回旧口径**。总览页把「实际用量」和「已承诺」并排显示,因为配额还有余、机器已经满了这种局面得看得出来 | 🧮 **Memory quota counts RSS, not heap**: `-Xmx` bounds only the heap — Metaspace, code cache, thread stacks, GC structures and Netty's direct buffers all live outside it, and a Minecraft server's network layer *is* Netty. A `-Xmx4G` Paper server steadily sits above 4.5G RSS; heavy modpacks more. **Packing a host by Σ-Xmx therefore always overcommits**, and the overshoot doesn't raise an error — it's the OOM killer picking a server at 3am. Quota counts heap + off-heap with a tunable `max(floor MB, -Xmx×%)` reserve; **set both to 0 for the old behaviour**. The overview shows *actual usage* and *committed* side by side, because "quota says room, machine says full" has to be visible |
| 🎚 **阈值可配置**:磁盘告警线、崩溃计数窗口 / 次数 / 重启延迟,原先是源码常量,改一次要重启面板。重度模组服启动慢又崩得勤,固定「10 分钟 3 次」会被误判成重启风暴而停手 —— 这本来就该按部署调。存完下一次判断即生效 | 🎚 **Tunable thresholds**: disk-warning level and the crash window / count / restart delay used to be source constants. A heavy modded server is slow to boot and crashes more often, so a fixed "3 in 10 min" gets misread as a restart storm — this belongs in config. Changes apply on the next check, no restart |
| 💾 **面板配置导出 / 导入**:打包 `settings` / `users` / `instances` / `tasks` / `oauth` 五个 JSON,换机器或误删时用。导入前强制干跑校验(格式、版本、**包里至少有一个管理员**),写入前把现有文件另存为 `.bak-<时间戳>`。不含会话与审计日志 —— 那是「这台机器发生过什么」,搬家没有意义 | 💾 **Panel config export / import**: bundles `settings` / `users` / `instances` / `tasks` / `oauth` for migration or recovery. Import is dry-run validated first (format, version, **at least one admin in the bundle**) and backs up existing files as `.bak-<ts>` before writing. Sessions and the audit log are excluded — those describe *this* machine and don't travel |
| ⬆ 侧栏显示**面板版本**并可点击**检查更新**(比对 GitHub 最新 tag);查不到时如实显示「更新状态未知」而不是假装最新 | ⬆ The sidebar shows the **panel version** and checks for updates against the latest GitHub tag — and says "unknown" when it can't reach it rather than pretending you're current |
Expand Down
34 changes: 29 additions & 5 deletions public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,19 @@ function applyDashboardStats(inst) {
$('#stat-players').innerHTML = `${inst.playersOnline} <span class="dim small">/ ${inst.maxPlayers}</span>`;
$('#stat-cpu').innerHTML = `${inst.metrics.cpu}<span class="dim small">%</span>`;
$('#bar-cpu').style.width = Math.min(100, inst.metrics.cpu) + '%';
$('#stat-ram').innerHTML = `${inst.metrics.ram} <span class="dim small">/ ${inst.metrics.ramMax} MB</span>`;
$('#bar-ram').style.width = Math.min(100, (inst.metrics.ram / inst.metrics.ramMax) * 100) + '%';
/* RSS 超过 -Xmx 是**常态**,不是异常:-Xmx 只管堆,RSS 还含 Metaspace、Code Cache、
线程栈、GC 自身结构和 Netty 的 direct buffer。原先这里 Math.min(100, …) 一夹,条子
就永久钉在 100% —— "健康的堆外开销"和"真的要 OOM 了"长得一模一样,等于把唯一的
证据藏起来了。照样满条,但换色并把超出量写出来,让人看得见超了多少。 */
const overMB = inst.metrics.ram - inst.metrics.ramMax;
const ramPct = inst.metrics.ramMax ? (inst.metrics.ram / inst.metrics.ramMax) * 100 : 0;
$('#stat-ram').innerHTML = `${inst.metrics.ram} <span class="dim small">/ ${inst.metrics.ramMax} MB 堆`
+ (overMB > 0 ? ` · 堆外 +${overMB}` : '') + '</span>';
$('#bar-ram').style.width = Math.min(100, ramPct) + '%';
$('#bar-ram').classList.toggle('over', overMB > 0);
$('#bar-ram').title = overMB > 0
? `RSS ${inst.metrics.ram} MB = 堆上限 ${inst.metrics.ramMax} MB + 堆外 ${overMB} MB(Metaspace / Code Cache / 线程栈 / Netty direct buffer)`
: `RSS ${inst.metrics.ram} MB / 堆上限 ${inst.metrics.ramMax} MB`;
renderTps(inst);
}

Expand Down Expand Up @@ -377,11 +388,20 @@ async function loadOverview() {

function renderHostCard(host) {
$('#host-name').textContent = `${host.hostname} · ${host.platform}`;
const memPct = Math.round(((host.totalMem - host.freeMem) / host.totalMem) * 100);
/* 用 availMem(后端显式读 MemAvailable)而不是 freeMem。os.freemem() 的口径随 libuv
版本变过 —— 老版本是 MemFree,会把可回收的 page cache 算作已用,而 MC 刷世界文件
时 page cache 很大,这张卡会常年虚高。老面板的响应里没有 availMem,退回 freeMem。 */
const avail = host.availMem !== undefined ? host.availMem : host.freeMem;
const memPct = Math.round(((host.totalMem - avail) / host.totalMem) * 100);
/* 已承诺 = Σ(堆 + 堆外余量),也就是配额拦人用的那个数。跟真实用量并排放,是因为
两者不是一回事:配额还有余、机器已经满了,这种局面得在这里看得出来 */
const commit = host.committedMem || 0;
const commitPct = Math.round((commit / host.totalMem) * 100);
$('#host-grid').innerHTML = [
['CPU', `${escapeHtml(host.cpuModel.split(' ').slice(0, 3).join(' '))} <span class="dim small">× ${host.cores}</span>`],
['负载', `${host.loadavg.join(' / ')}`],
['内存', `${memPct}% <span class="dim small">${Math.round((host.totalMem - host.freeMem) / 1024)} / ${Math.round(host.totalMem / 1024)} GB</span>`],
['内存', `${memPct}% <span class="dim small">${Math.round((host.totalMem - avail) / 1024)} / ${Math.round(host.totalMem / 1024)} GB 实际用量</span>`],
['已承诺', `<span class="${commitPct >= 100 ? 'disk-warn' : ''}">${commitPct}%</span> <span class="dim small">${(commit / 1024).toFixed(1)} GB · 全部实例 -Xmx + 堆外余量</span>`],
['磁盘', host.disk
? `<span class="${host.disk.usedPct >= 90 ? 'disk-warn' : ''}">${host.disk.usedPct}%</span> <span class="dim small">${(host.disk.usedMB / 1024).toFixed(1)} / ${(host.disk.totalMB / 1024).toFixed(1)} GB</span>`
: '<span class="dim small">不可用</span>'],
Expand Down Expand Up @@ -2719,7 +2739,7 @@ async function loadUsers() {
${u.defaultPassword ? '<span class="task-badge off" style="color:#ffe479;border-color:rgba(255,214,10,0.4)">默认密码未修改</span>' : ''}
</div>
<div class="backup-meta">${u.limits
? `实例 ${u.usage.instances}/${u.limits.maxInstances} · 内存 ${u.usage.memMB}/${u.limits.maxMemMB} MB · CPU ${u.limits.maxCpuCores} 核 · 磁盘 ${u.usage.diskMB}/${u.limits.maxDiskMB || '∞'} MB · `
? `实例 ${u.usage.instances}/${u.limits.maxInstances} · 内存 <span title="${u.usage.memMB} MB 堆(-Xmx 之和)+ ${(u.usage.memReservedMB || u.usage.memMB) - u.usage.memMB} MB 堆外余量。配额按含堆外的实际预留计">${u.usage.memReservedMB || u.usage.memMB}/${u.limits.maxMemMB} MB</span> · CPU ${u.limits.maxCpuCores} 核 · 磁盘 ${u.usage.diskMB}/${u.limits.maxDiskMB || '∞'} MB · `
: `实例 ${u.usage.instances} · 不受配额限制 · `}创建于 ${fmtAgo(u.createdAt)}</div>
</div>
<div class="spacer"></div>
Expand Down Expand Up @@ -2904,6 +2924,8 @@ async function loadSystem() {
$('#sys-crash-win').value = t.crashWindowMin ?? 10;
$('#sys-crash-max').value = t.crashMaxRestarts ?? 3;
$('#sys-crash-delay').value = t.crashRestartDelaySec ?? 5;
$('#sys-mem-pct').value = t.memOverheadPct ?? 13;
$('#sys-mem-min').value = t.memOverheadMinMB ?? 512;
renderRemoteBackup(s.backupRemote);
renderNotify(s.notify);
loadAudit();
Expand Down Expand Up @@ -3007,6 +3029,8 @@ $('#sys-save').addEventListener('click', async () => {
crashWindowMin: parseInt($('#sys-crash-win').value, 10),
crashMaxRestarts: parseInt($('#sys-crash-max').value, 10),
crashRestartDelaySec: parseInt($('#sys-crash-delay').value, 10),
memOverheadPct: parseInt($('#sys-mem-pct').value, 10),
memOverheadMinMB: parseInt($('#sys-mem-min').value, 10),
},
},
});
Expand Down
19 changes: 15 additions & 4 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,11 @@ <h2 class="card-title">新建用户</h2>
<input id="user-lim-cpu" type="number" min="1" placeholder="CPU 核数(默认 2)" />
<input id="user-lim-disk" type="number" min="0" step="1024" placeholder="磁盘配额 MB(默认 20480,0=不限)" />
</div>
<div class="dim small" style="margin-top:10px">普通用户只能看到自己的实例,管理员不受配额限制。内存按全部实例 -Xmx 之和计,CPU 在启动时绑核,磁盘含实例目录与备份。</div>
<div class="dim small" style="margin-top:10px">普通用户只能看到自己的实例,管理员不受配额限制。CPU 在启动时绑核,磁盘含实例目录与备份。<br>
内存按<b>全部实例 -Xmx 之和 + 每个实例一份堆外余量</b>计,不是裸 -Xmx 之和 —— <code>-Xmx</code> 只管堆,
JVM 还要 Metaspace、Code Cache、线程栈和 Netty direct buffer,实测 <code>-Xmx4G</code> 的 Paper
RSS 常在 4.5G 以上。所以<b>实例卡片上的内存读数高于它的 -Xmx 是正常的</b>,不是内存泄漏。
余量大小在「系统设置 → 阈值」里调。</div>
</div>
<div class="card glass" style="margin-top:18px">
<h2 class="card-title">邀请链接 <span class="dim small">一次性,自带配额,不受「开放注册」影响</span></h2>
Expand Down Expand Up @@ -804,6 +808,8 @@ <h2 class="card-title">用户列表 <span class="dim small" id="user-count"></sp
<div class="prop-row"><label>崩溃计数窗口(分钟)<span class="dim small">1–1440</span></label><input id="sys-crash-win" type="number" min="1" max="1440" /></div>
<div class="prop-row"><label>窗口内最多自动重启<span class="dim small">0 = 关闭自动重启</span></label><input id="sys-crash-max" type="number" min="0" max="100" /></div>
<div class="prop-row"><label>崩溃后重启延迟(秒)<span class="dim small">1–3600</span></label><input id="sys-crash-delay" type="number" min="1" max="3600" /></div>
<div class="prop-row"><label>内存配额堆外余量 %<span class="dim small">占 -Xmx 的比例,0–100</span></label><input id="sys-mem-pct" type="number" min="0" max="100" /></div>
<div class="prop-row"><label>内存配额堆外余量下限 MB<span class="dim small">与上一项取较大值,0–4096</span></label><input id="sys-mem-min" type="number" min="0" max="4096" step="64" /></div>
</div>
<div class="prop-row" style="margin-top:14px">
<label>公告<span class="dim small">留空不显示</span></label>
Expand All @@ -821,7 +827,12 @@ <h2 class="card-title">用户列表 <span class="dim small" id="user-count"></sp
要关请先关策略。<br>
API Token 不受此开关影响,否则开启的瞬间所有脚本都会断;签发新 Token 需要网页会话,
所以没配 2FA 的人也绕不过去。<br>
崩溃阈值按实例类型调:重度模组服启动慢、崩得勤,3 次/10 分钟容易误判成"重启风暴"而停手。
崩溃阈值按实例类型调:重度模组服启动慢、崩得勤,3 次/10 分钟容易误判成"重启风暴"而停手。<br>
<b>内存配额堆外余量</b>:<code>-Xmx</code> 只管堆,而 JVM 还要 Metaspace、Code Cache、线程栈、
GC 自身结构和 Netty 的 direct buffer(MC 服务端的网络层就是 Netty)—— 实测 <code>-Xmx4G</code>
的 Paper 稳定运行 RSS 常在 4.5G 以上,重模组服更多。配额按<b>堆 + 堆外</b>计,否则按 -Xmx 之和
把宿主机排满,实际内存一定会超,而超出的部分不会报错,是 OOM killer 半夜随机挑一个服务端杀掉。
余量取「百分比」与「下限 MB」的较大值。<b>两项同时设 0 就退回纯 -Xmx 之和</b>(旧行为)。
</div>
</div>

Expand Down Expand Up @@ -958,7 +969,7 @@ <h2 class="card-title" style="margin-top:16px;font-size:13px">推送哪些事件
<label class="switch prop-switch"><input type="checkbox" id="og-autocreate"><span class="slider"></span></label>
</div>
<div class="prop-row"><label>建号配额:实例数上限</label><input id="og-lim-inst" type="number" min="1" max="12" /></div>
<div class="prop-row"><label>建号配额:内存 MB(全部实例 -Xmx 之和)</label><input id="og-lim-mem" type="number" min="512" max="65536" step="256" /></div>
<div class="prop-row"><label>建号配额:内存 MB(全部实例 -Xmx 之和 + 堆外余量)</label><input id="og-lim-mem" type="number" min="512" max="65536" step="256" /></div>
<div class="prop-row"><label>建号配额:CPU 核数</label><input id="og-lim-cpu" type="number" min="1" /></div>
<div class="prop-row"><label>建号配额:磁盘 MB(0 = 不限)</label><input id="og-lim-disk" type="number" min="0" step="1024" /></div>
</div>
Expand All @@ -979,7 +990,7 @@ <h2 class="card-title" id="limits-title">资源配额 <span class="dim small" id
<div class="field-col">
<label class="dim small">实例数上限</label>
<input id="lim-inst" type="number" min="1" max="12" />
<label class="dim small">内存配额 MB</label>
<label class="dim small">内存配额 MB <span class="dim">按 -Xmx 之和 + 堆外余量计</span></label>
<input id="lim-mem" type="number" min="512" max="65536" step="256" />
<label class="dim small">CPU 核数</label>
<input id="lim-cpu" type="number" min="1" />
Expand Down
Loading
Loading