服务器控制台

一台自有服务器的网页面板。打开一个网页,就知道机器还剩多少内存、哪个服务掉了、配置改坏没有 —— 不用 ssh 上去,也不用记住那七八条命令分别叫什么。

下面第 2 节是可操作的演示:服务列表、日志、曲线、磁盘全是合成数据,随便点 —— 能启停重启、能看日志、能按「模拟故障」把一个服务打红再修回来。 本页不连任何服务器,所有状态只活在你这个标签页里,刷新即还原。 真实系统在访问闸后自用,服务的是一台真实机器;它比这个演示更保守(见第 4 节:真面板是只读的)。

1 · 为什么要这个东西

一台自己的服务器上跑着若干个站、一个反向代理、一堆 systemd 单元。想知道"现在怎么样",原本的动作是 ssh 上去,然后凭记忆敲一串命令。真正的麻烦不在敲命令,在下面这几条:

要记的东西太多uptime / free -m / df -h / systemctl list-units / docker ps / nginx -t / ss -tln —— 七条命令、七种输出格式。隔两周不碰就要现查参数。→ 取舍:把这组命令固化成 5 个只读端点,一次刷新全部出来,顺序就是排查顺序。
手机上 ssh 很难用人在外面收到"站打不开了",掏手机开终端、找密钥、在小屏幕上敲长命令,基本等于放弃。→ 取舍:做成网页,任何设备打开就是。
分不清是站挂了还是机器挂了同一个"打不开",可能是磁盘满、可能是某个 unit 掉了、可能是容器退了、也可能只是配置写坏没重载。要横着看好几层才判得出来。→ 取舍:面板按层分块——主机资源 / 服务 / 日志 / 磁盘,从上往下扫一遍就定位到层。
就一个人用,上不起监控栈Prometheus + exporter + Grafana 三件套要装、要配、要占内存,还得再维护一套;而需求只是"现在怎么样"。→ 取舍:一个约 190 行的 Python 文件。代价是没有长期历史、没有告警,只有此刻与最近几分钟的窗口 —— 这一点是想清楚之后接受的,不是没做完。

2 · 面板:直接点

下面这块是真的在跑:一个前端状态机 + 每 2 秒推进一格的合成采样。启停有 2 秒过渡态、停掉的服务会让内存曲线真的往下走、模拟故障会同时改状态、写日志、顶高 CPU 曲线。

主机面板 host demo-node-1 · 内网地址 · 模式 local

资源曲线 最近 2 分钟 · 每 2 秒一格

CPU 使用率 内存使用率

服务 点一行看它的日志

磁盘占用 df

故障期间日志卷会持续涨 —— 磁盘被日志撑满,是这类小机器最常见的死法。

日志

演示与真机的差别,写在这里而不是藏起来:这个演示里的启动 / 停止 / 重启按钮是模拟的状态流转, 为了让你看清「一个服务掉了、面板上会长什么样」。真实那套面板没有这三个按钮 —— 理由见第 4 节, 它是一处刻意的能力缺失,不是还没做。

3 · 关键设计决策

一个端点 = 一条命令主机那一块并不是分别去跑 uptimefreedfnproc,而是把四条串成一条命令,中间插 @@MEM / @@DISK / @@CPU 标记,回来再按标记切段解析。
为什么:远程模式下每执行一条命令就是一次 ssh 往返,握手成本远大于命令本身;4 条变 1 条,等待时间不是省 25%,是省掉 3 次往返。
代价:得自己写一个分段状态机,比调四次 API 难读一点。这笔换得值。
同一份代码,两个地方都能跑启动时判一次:显式标了本机变量、或者"是 Linux 且存在 /var/www" → 在服务器上,直接起子进程读本机;否则 → 在开发机上,走 ssh 远程读。
为什么:否则每改一行前端都要先传上去才能看效果。
远程执行不自己写ssh 那一段调的是共享工具库里已有的封装,这个项目里没有一行拼 ssh 命令行的代码。
为什么:ssh 参数、超时、密钥路径这类东西一旦在第二个地方复制一份,两份就会开始漂移,而漂移只在出事那天才被发现。
失败不抛异常,降级成一块命令执行失败时不抛 500,而是回一个带 __ERR__ 前缀的字符串;每个端点识别到它就返回 {"error": …},前端每块各自渲染自己的错误。
为什么:一块探针挂掉不该让整页白掉 —— 恰恰是出事的时候,你最需要看见其他四块还是好的。
df -hP 而不是 df -h-P 强制 POSIX 单行输出。不加它,设备名一长,一行会被折成两行,按列取第 2、3 个字段就整体错位。
踩坑点:这类 bug 只在某些机器上出现,在你自己那台上永远测不出来。
配置自检要两个串同时命中判定配置有没有问题,要求输出里同时出现 syntax is oksuccessful,缺一个就判红。
为什么:只匹配其中一个,容易被告警行、被"failed"那一行里的字样蒙混过去。守卫要么严到能真拦住,要么就是个会让人放心的摆设。
静态目录必须最后挂前端静态文件挂在根路径上,是个 catch-all;/api/* 这些显式路由必须注册,否则会被它整个吞掉。源码里这一行带着注释,因为顺序写反的结果是所有 API 一起 404,而页面本身还好好的 —— 症状离原因很远。
前端零构建一个 index.html,原生 JS,无框架无打包无依赖;五个端点用 Promise.all 并发拉,15 秒自动刷新一次。
为什么:使用者只有一个人、页面只有一屏,引入构建链的收益是负的 —— 多出来的是一条会腐烂的依赖链。
每条命令 12 秒超时网络卡住时,页面要么出数、要么出错,不能一直转圈。转圈是最坏的状态:它既没告诉你好,也没告诉你坏。

4 · 安全边界:为什么真面板没有上面那三个按钮

一个能看见服务器内部状态的网页,它的价值全部建立在"它只能看"这件事上。所以边界不是靠"我不点那个按钮",而是靠结构上没有那个按钮。

只读,且不可参数化执行函数收到的命令字符串,全部是源码里的字面量;五个端点没有一个接受任何参数。也就是说:不存在一条外部输入能到达 shell 的路径 —— 想让它多做一件事,只能改代码重新发布,不能靠 URL 上加个参数。这比"对输入做转义"强的地方在于,它根本没有输入。
不开公网端口应用进程只绑本地回环地址,外面直接打这个端口是不通的;唯一的入口是同机的反向代理。端口号还不写死在服务定义里,从独立的环境变量文件读。
闸内子域挂访问闸,没过闸拿不到页面。关键是页面和 API 是同一个进程、同一个源——所以闸挡住的不只是那一屏 HTML,/api/* 一并在闸后,不存在"页面进不去但接口裸奔"的缝。
不暴露源站地址域名解析走 CDN 代理进来,公开记录里只有域名,没有服务器地址;回源端口由边缘规则指定。控制台自己也不回传任何地址信息 —— 本演示里主机那行写的是"内网地址"四个字,就是这个原因。
返回的东西本身就很薄接口吐出来的只有:单元名与状态、容器名与状态、vhost 文件名、端口号、容量百分比。不读配置文件正文、不回传日志内容、不带任何凭证。即便某天闸破了,泄露的也只是一组名字和几个百分比。
一处让步:以 root 运行读 systemd、容器、以及跑配置自检需要权限,服务是以 root 起的。这是这套设计里最大的一处妥协,如实写在这里而不是藏起来。之所以敢接受:攻击面被前面几条收在"进程本身"上——没有外部输入能改变它执行什么,能打的只剩"先突破闸、再突破一个不接受参数的只读进程"。要真加上第 2 节演示的那三个按钮,这条让步必须先还掉(降权 + 命令白名单 + 审计留痕),而不是顺手加个按钮。

5 · 技术上怎么做的

后端Python + FastAPI,单文件约 190 行,uvicorn 单进程同源同时服务前端 HTML 与 /api/*。依赖只有两行:fastapi、uvicorn[standard]。
采集本机模式走子进程执行 bash -lc;远程模式调共享库的 ssh 封装。统一 12 秒超时,失败以哨兵字符串回传。没有 agent、没有守护进程、没有数据库——每次刷新现场读一次,读完即弃。
解析一条命令的输出按 @@ 标记切段,各段用对空格不敏感的字段切分取值;内存占比在服务端算好,磁盘占比直接用系统自己给的百分比字符串,不二次换算。
前端单个 HTML,原生 JS,无依赖无构建。接口用相对路径调用(同源,因此不涉及跨域)。阈值判定在前端:内存 70% 转黄、85% 转红;磁盘 75% 转黄、90% 转红 —— 第 2 节演示用的就是这套阈值。
进程管理systemd 常驻单元,Restart=always、失败 3 秒后重起;端口与域名从环境变量注入,不硬编码在单元文件里。
接入反向代理到本地回环端口,vhost 手写并在其中 include 访问闸;子域、端口、访问方式登记在站群的注册表里,由机器对账(注册表说它该有闸,vhost 里必须真有闸,双向校验)。

6 · 现状,以及没做的部分

在线闸内自用,服务的是自己那一台机器。
已实现主机资源(uptime / 负载 / 内存 / 磁盘 / CPU 核数)· systemd 单元矩阵 · 容器 · vhost 列表与配置自检 · 代理容器与监听端口。就这五块。
没实现:证书到期与备份状态站群注册表里给它写的一句话描述包含这两项,但代码里没有对应的采集 —— 这是一处"文档跑在实现前面"的漂移。写在这里而不是把它讲成功能:一份介绍如果把没写的东西说成写了的,那它其余部分的可信度也就没了。
没有长期历史,没有告警只有此刻快照,不落时间序列,也不会主动通知你。这是第 1 节那笔取舍的直接后果,不是待办。真需要趋势与告警时,正确动作是上专门的监控栈,而不是给这个文件继续加功能。
部署还是手工四步同步文件 → 装依赖 → 写端口配置 → 启用服务。站群的部署注册表里,这一项的部署命令字段是空的——如实记"没有一键脚本",而不是把四步压成一条从没跑过、但看着很合理的命令。编出来的部署命令是所有自动检查都抓不到的那类错:它长得和真的一模一样,只在真正要发的那一刻炸。
刻意降为低频件按访问日志,人类访问是个位数/两天 —— 于是它在项目台账里被主动降档为"季节性"。这不是失败:控制台的价值是你需要的时候它在,不是你天天开着它。
本页所有主机名、服务名、端口、日志、曲线、磁盘数字均为合成示例,与任何真实机器无关;不出现任何真实地址(主机一行固定写"内网地址")。页面纯静态、无外部请求,状态只存在于当前标签页,刷新即还原。真实控制台在访问闸后自用,且是只读的。