有一个自己的服务器监控面板是每个开发者的浪漫。不需要 Grafana + Prometheus 那一套重型方案,用 FastAPI + WebSocket + 纯前端就能在半天内搭出一个风格酷炫的实时仪表盘。这篇文章带你从零到部署,完整走一遍。
整体架构
后端使用 FastAPI 的 WebSocket 端点,通过 psutil 库采集 CPU、内存、磁盘和网络数据,按照固定频率推送给所有连接的客户端。前端使用纯 HTML/CSS/JavaScript,用 SVG 绘制仪表盘组件(CPU 环形表盘、内存柱状条、网络流量折线图),无需任何第三方前端框架。
WebSocket 连接的生命周期管理
WebSocket 连接有四个关键阶段:连接建立 → 数据推送 → 心跳维持 → 连接断开。管理好这些阶段是稳定性的基础。
from fastapi import WebSocket, WebSocketDisconnect
class ConnectionManager:
def __init__(self):
self.active: list[WebSocket] = []
async def connect(self, ws: WebSocket):
await ws.accept()
self.active.append(ws)
def disconnect(self, ws: WebSocket):
self.active.remove(ws)
async def broadcast(self, data: dict):
dead = []
for ws in self.active:
try:
await ws.send_json(data)
except Exception:
dead.append(ws)
for ws in dead: # 清理断开的连接
self.active.remove(ws)
manager = ConnectionManager()
这里有个细节:broadcast 中不要直接 remove 正在迭代的列表,而是先收集「死亡连接」再统一清理。
psutil 数据采集与采样频率
psutil 是采集系统指标的瑞士军刀,但有些 API 调用方式需要特别注意。比如 psutil.cpu_percent() 如果带 interval 参数会阻塞当前线程:
# ❌ 阻塞事件循环
cpu = psutil.cpu_percent(interval=1) # 阻塞 1 秒!
# ✅ 使用非阻塞版本
cpu = psutil.cpu_percent(interval=None) # 返回上次调用以来的值
正确做法是在后台用 asyncio.create_task 运行一个采集循环,定期(比如每 2 秒)采集一次,将结果缓存起来;WebSocket 广播时直接读缓存,不再触发新的采集。采样频率的选择取决于你需要的实时性——监控面板一般 1-2 秒已经足够,太频繁反而增加 CPU 开销。
import asyncio
import psutil
latest_stats = {}
async def collect_stats():
global latest_stats
# 第一次调用需要 interval 来校准
psutil.cpu_percent(interval=None)
while True:
await asyncio.sleep(2)
latest_stats = {
"cpu": psutil.cpu_percent(interval=None),
"memory": psutil.virtual_memory().percent,
"disk": psutil.disk_usage("/").percent,
"net_sent": psutil.net_io_counters().bytes_sent,
"net_recv": psutil.net_io_counters().bytes_recv,
}
@app.on_event("startup")
async def startup():
asyncio.create_task(collect_stats())
前端 Gauge 组件的 SVG 实现
环形仪表盘是监控面板的灵魂。用 SVG 实现一个 CPU 用量环形图只需要一个 <circle> 配合 stroke-dasharray 和 stroke-dashoffset:
function updateGauge(svgId, percent) {
const circle = document.querySelector(`#${svgId} .gauge-fill`);
const radius = 54;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (percent / 100) * circumference;
circle.style.strokeDasharray = `${circumference}`;
circle.style.strokeDashoffset = offset;
}
SVG 的 stroke-dashoffset 从完整圆周长递减到 0,配合 transition: stroke-dashoffset 0.6s ease 就能得到丝滑的动画效果。颜色可以按百分比阈值动态切换——低于 60% 用绿色,60-85% 用黄色,85% 以上用红色。
自动重连机制
WebSocket 连接断开是常态,不是异常。前端需要实现指数退避的自动重连:
let retryDelay = 1000;
function connectWS() {
const ws = new WebSocket("ws://localhost:8000/ws");
ws.onclose = () => {
setTimeout(connectWS, retryDelay);
retryDelay = Math.min(retryDelay * 2, 30000); // 最大 30 秒
};
ws.onopen = () => { retryDelay = 1000; }; // 连上后重置
}
生产环境:Uvicorn + Gunicorn
开发时用 uvicorn main:app --reload 没问题,但生产环境需要 Gunicorn 管理多个 worker 进程。不过 WebSocket 场景下有特殊注意事项——不能使用多个 worker,因为 WebSocket 连接是粘性会话,必须确保同一个连接始终路由到同一个 worker。
# 使用单个 worker + Uvicorn worker class
gunicorn main:app -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
如果需要横向扩展,应该用 Nginx 做反向代理,配合 ip_hash 或基于 cookie 的会话保持来确保 WebSocket 路由的一致性。
总结
从零搭一个监控仪表盘,核心代码不到 300 行。FastAPI 的异步能力 + WebSocket 的实时推送 + 纯 SVG 的前端渲染,构成了一个轻量但完整的全栈方案。部署时注意单 worker 的 WebSocket 限制,以及 psutil API 的非阻塞调用方式——这两个坑踩一次就记住了。