387 lines
17 KiB
Markdown
387 lines
17 KiB
Markdown
# 设计 — 服务器实时监测槽(4 台自选 · 宝塔式)
|
||
|
||
**日期**:2026-06-12
|
||
**状态**:待实施
|
||
**关联讨论**:非全舰队;SSH/Redis 探针;8h 自动下架;独立历史页
|
||
|
||
## 背景与目标
|
||
|
||
运维需要在 **服务器页** 对 **手动选定的少数子机**(最多 **4 台**)做 **近实时** 资源监视(CPU、内存、磁盘、负载、网络/磁盘 IO、进程 Top),体验接近 **宝塔面板监控**:数字与曲线 **约 5 秒刷新**,而非现有 10 分钟历史粒度。
|
||
|
||
**不在范围**:全舰队 5s 探针;改造现有 Schedules(推送/脚本)页;无 SSH 且无 Agent 的机器无法探针(需明确失败态)。
|
||
|
||
## 已确认产品决策
|
||
|
||
| 项 | 决定 |
|
||
|----|------|
|
||
| 同时监测上限 | **4 台**(固定 4 槽) |
|
||
| 选机方式 | **服务器列表** 每行 **「+」** 加入监测;槽位区也可点「+」选机 |
|
||
| 范围 | **仅 Pin 的子机** 高频采样;未 Pin **零额外 SSH/探针** |
|
||
| 生命周期 | 添加后 **8 小时** 自动下架;用户可手动删除;重复添加同一台 **重置 8h** |
|
||
| 实时感 | 目标 **5s** 刷新(探针 + WebSocket 推送) |
|
||
| 数据源 | **有 Agent 且 Redis 新鲜** → 读 Redis;否则 **SSH psutil 脚本** |
|
||
| 历史 | Pin 期间 **每次探针均落库**(成功/失败皆有记录);**独立页面** 查趋势 + **探针记录**;7 天 purge |
|
||
| 隔离 | **按管理员** 4 槽(独立表 + `admin_id`) |
|
||
| **探针记录** | **强制**:每 5s 一轮必有 DB 行;失败写 `probe_status` + `error`,**禁止只打日志不落库** |
|
||
| **指标范围** | **每轮 5s 全量刷新**:CPU、内存、**磁盘使用率**、负载、**磁盘读写**、**网络入口/出口(带宽)**、进程 Top(10s);见下节 |
|
||
|
||
## 指标清单(每轮探针必采 · 与宝塔对齐)
|
||
|
||
> **原则**:Pin 期间 **所有下列指标同一 5s 周期采集、落库、推 WS、刷新 UI**;不得把磁盘/网络放到「二期」或降频。
|
||
|
||
| 类别 | 字段 | 展示(槽位卡) | 说明 |
|
||
|------|------|----------------|------|
|
||
| CPU | `cpu_pct` | 圆环 + 曲线 | % |
|
||
| 内存 | `mem_pct` | 圆环 + 曲线 | % |
|
||
| **磁盘空间** | `disk_pct` | 圆环 + 曲线 | 根分区 `/` 使用率 %(与 Agent/SSH 一致) |
|
||
| 负载 | `load_1/5/15` | 数字 | load average |
|
||
| **磁盘 IO** | `disk_read_bytes`, `disk_write_bytes` | **读/写速率** | 累计字节;API/前端用相邻两点差算 **MB/s** |
|
||
| **网络出入口** | `net_bytes_recv`, `net_bytes_sent` | **下行↓ / 上行↑ 速率** | recv=入口/下载,sent=出口/上传;差算 **Mbps 或 MB/s** |
|
||
| 进程 | `processes_json` | Top5 表 | 每 **10s** 刷新内容,仍每 5s 写探针行 |
|
||
|
||
**速率计算**(中央统一,避免前端各算各的):
|
||
|
||
```
|
||
rate_sent = (sent[t] - sent[t-1]) / (ts[t] - ts[t-1]) → 出口带宽
|
||
rate_recv = (recv[t] - recv[t-1]) / (ts[t] - ts[t-1]) → 入口带宽
|
||
disk_read_rate / disk_write_rate 同理
|
||
```
|
||
|
||
首点无上一采样时速率显示 `—`;WS 推送 payload 含 **已算好的** `net_up_bps`, `net_down_bps`, `disk_read_bps`, `disk_write_bps` 供卡片直接用。
|
||
|
||
**数据源**:
|
||
|
||
| 路径 | CPU/MEM/DISK% | 网络 cumulative | 磁盘 IO cumulative |
|
||
|------|---------------|-----------------|---------------------|
|
||
| Redis(Agent 心跳) | ✅ `system_info` 已有 | ⚠ 心跳 **默认无** → Pin 机走 SSH 补采 **或** Agent 小改带上 `net_io`/`disk_io`(推荐 **M1:SSH 脚本一次拿全**) |
|
||
| SSH 探针 | ✅ psutil 一次 JSON | ✅ `psutil.net_io_counters()` | ✅ `psutil.disk_io_counters()` |
|
||
|
||
Pin 且 Redis 仅含 cpu/mem/disk% 时:**同轮 SSH 补采 IO/网络**,合并为一行 `watch_probe_records`(`source=redis+ssh` 或主来源记 `ssh`)。
|
||
|
||
**UI(槽位卡 · 宝塔式)**:
|
||
|
||
```
|
||
CPU 78% MEM 62% DISK 41%
|
||
↑ 12.3 MB/s ↓ 8.6 MB/s ← 网络出入口(实时)
|
||
读 2.1 MB/s 写 0.4 MB/s ← 磁盘 IO(可选一行,M1 即显示)
|
||
[15min 折线:CPU / MEM / DISK / 下行带宽 可切换图例]
|
||
```
|
||
|
||
## 方案对比
|
||
|
||
| 方案 | 优点 | 缺点 |
|
||
|------|------|------|
|
||
| A. 仅复用 `server_metric_samples`(10min) | 无新表 | 无法宝塔式实时 |
|
||
| B. 全舰队 Agent 5s 心跳 | 真实时 | 2000+ 台不可接受 |
|
||
| C. **自选 4 台 + 5s 探针 + WS(选定)** | 负载可控;SSH 覆盖无 Agent;体验接近宝塔 | 需新表、worker、WS 通道 |
|
||
| D. 前端 5s 轮询 HTTP | 实现简单 | 延迟与连接数差于 WS;仍要后台探针 |
|
||
|
||
**选定 C**:后台 `watch_probe` 仅对 Pin 列表采样;Redis 滑动窗口 + MySQL 持久化;WebSocket 推送到浏览器。
|
||
|
||
## 信息架构
|
||
|
||
### 服务器页布局
|
||
|
||
```
|
||
StatCardsRow(现有)
|
||
WatchSlotRow(新增,4 格)
|
||
空槽:虚线卡片「+ 添加监测」
|
||
有槽:主机名、CPU/MEM/**DISK** 三环、15min 滚动折线(**CPU+内存+磁盘** 可切换)、网络/磁盘 IO 速率、剩余时间、删除
|
||
服务器列表(现有)
|
||
操作列新增:mdi-plus 图标按钮「加入实时监测」
|
||
已在监测:显示已占用槽位或 disabled + tooltip「已在监测 · 槽 2」
|
||
```
|
||
|
||
### 列表「+」交互
|
||
|
||
1. 点击 **+** → 若未满 4 槽:直接占用 **第一个空槽**(或弹出选槽 1~4,**默认最小空槽号**)。
|
||
2. 若已满 4 槽:**对话框**「监测已满,请选择要替换的槽位」→ 选槽 → 替换。
|
||
3. 已在监测的 server:**+ 变为 mdi-check 或隐藏**,tooltip 提示槽位与剩余时间。
|
||
4. 无 SSH 凭据且无 Agent:+ 可点但 POST 返回 422,snackbar 说明原因(不静默失败)。
|
||
|
||
### 独立历史页
|
||
|
||
- 路由:`#/watch-metrics`(侧栏「监测历史」或从 WatchSlotRow「查看历史 →」进入)
|
||
- **Tab A — 资源趋势**:选 1~4 台、时间范围 1h/6h/24h/7d;曲线含 **CPU / 内存 / 磁盘使用率 / 网络↑↓ / 磁盘读写的速率**(由相邻探针差分计算);离线 shading;导出 CSV
|
||
- **Tab B — 探针记录**:按服务器/时间/状态筛选的 **只读表格**(见下文「探针记录」);支持导出 CSV
|
||
- 与 Dashboard 舰队均值页区分:本页 **单机/自选多机** + **Pin 期间细粒度 + 全量探针日志**
|
||
|
||
## 探针记录(强制)
|
||
|
||
> **原则**:监测槽的价值不仅在实时曲线,还在 **可回溯的探针执行记录**——SSH 超时、凭据失败、Redis 过期等必须可查,不得仅写应用日志或静默丢弃。
|
||
|
||
### 记什么
|
||
|
||
每一轮 `watch_probe` 对每台 **Pin 中的 server**(去重后)**必须 INSERT 一行**,无论成功或失败:
|
||
|
||
| 字段语义 | 成功 | 失败示例 |
|
||
|----------|------|----------|
|
||
| `probe_status` | `ok` | `ssh_timeout` / `ssh_auth` / `no_cred` / `redis_stale` / `offline` / `parse_error` |
|
||
| 指标列 | 有值 | **NULL**(禁止用 0 冒充) |
|
||
| `error` | NULL | 人类可读摘要(≤255 字) |
|
||
| `duration_ms` | 探针耗时 | 同上 |
|
||
| `source` | `redis` / `ssh` | 尝试的数据源 |
|
||
|
||
### 与会话关联
|
||
|
||
每次用户点列表 **+** 加入监测,创建一条 **`watch_pin_sessions`**(8h 监测会话);探针记录通过 `session_id` 归属到「哪一段 Pin 期间」。
|
||
|
||
Pin 到期或手动删除 → 更新 `ended_at` + `end_reason`(`expired` / `manual` / `replaced`);**已写入的探针记录不删**,保留至 purge 策略。
|
||
|
||
### UI 在哪看
|
||
|
||
1. **监测历史页 · 探针记录 Tab** — 主入口,可跨会话、跨机器查
|
||
2. **监测槽卡片 · 「记录」链接** — 跳转并预填当前 server + 当前 session 时间范围
|
||
3. **服务器列表展开详情(可选 M2)** — 若该台正在 Pin,展示最近 20 条探针摘要
|
||
|
||
### 运维用途
|
||
|
||
- 排查「曲线断档」:按 `probe_status=ssh_timeout` 过滤
|
||
- 对比 Redis vs SSH:看 `source` 列
|
||
- 审计:谁在何时 Pin 了哪台(`watch_pin_sessions` + audit log)
|
||
|
||
## 数据模型
|
||
|
||
### `watch_pin_sessions`
|
||
|
||
```sql
|
||
watch_pin_sessions (
|
||
id INT PK AUTO_INCREMENT,
|
||
admin_id INT NOT NULL,
|
||
server_id INT NOT NULL,
|
||
slot_index TINYINT NOT NULL,
|
||
started_at DATETIME NOT NULL,
|
||
ended_at DATETIME NULL,
|
||
end_reason VARCHAR(16) NULL, -- expired | manual | replaced
|
||
INDEX (admin_id, started_at),
|
||
INDEX (server_id, started_at)
|
||
)
|
||
```
|
||
|
||
`watch_pins` 当前活跃 Pin 可 FK 指向 `session_id`;到期删 pin 行,**session 保留**。
|
||
|
||
### `watch_pins`
|
||
|
||
```sql
|
||
watch_pins (
|
||
id INT PK AUTO_INCREMENT,
|
||
admin_id INT NOT NULL FK admins,
|
||
session_id INT NOT NULL FK watch_pin_sessions,
|
||
slot_index TINYINT NOT NULL, -- 0..3
|
||
server_id INT NOT NULL FK servers,
|
||
pinned_at DATETIME NOT NULL, -- UTC naive
|
||
expires_at DATETIME NOT NULL, -- pinned_at + 8h
|
||
UNIQUE (admin_id, slot_index),
|
||
UNIQUE (admin_id, server_id),
|
||
INDEX (expires_at)
|
||
)
|
||
```
|
||
|
||
### `watch_probe_records`
|
||
|
||
> 表名实现可用 `watch_probe_records`;与早期草案 `watch_metric_samples` 合并为 **同一张表**(指标 + 探针状态一体),避免双写。
|
||
|
||
```sql
|
||
watch_probe_records (
|
||
id BIGINT PK AUTO_INCREMENT,
|
||
session_id INT NOT NULL FK watch_pin_sessions,
|
||
server_id INT NOT NULL,
|
||
admin_id INT NOT NULL, -- Pin 发起人(查询隔离)
|
||
recorded_at DATETIME NOT NULL,
|
||
source VARCHAR(16) NOT NULL, -- redis | ssh
|
||
probe_status VARCHAR(24) NOT NULL, -- ok | ssh_timeout | ...
|
||
duration_ms INT NOT NULL,
|
||
is_online BOOLEAN NOT NULL,
|
||
cpu_pct SMALLINT NULL,
|
||
mem_pct SMALLINT NULL,
|
||
disk_pct SMALLINT NULL, -- 根分区 / 使用率 %
|
||
disk_mount VARCHAR(255) NULL DEFAULT '/', -- 采样分区(M1 固定 /)
|
||
load_1 FLOAT NULL,
|
||
load_5 FLOAT NULL,
|
||
load_15 FLOAT NULL,
|
||
net_bytes_sent BIGINT NULL, -- 累计:网络出口(上传)
|
||
net_bytes_recv BIGINT NULL, -- 累计:网络入口(下载)
|
||
disk_read_bytes BIGINT NULL,
|
||
disk_write_bytes BIGINT NULL,
|
||
net_up_bps BIGINT NULL, -- 出口带宽 bytes/s(写入时算好)
|
||
net_down_bps BIGINT NULL, -- 入口带宽 bytes/s
|
||
disk_read_bps BIGINT NULL,
|
||
disk_write_bps BIGINT NULL,
|
||
processes_json JSON NULL, -- top 5;10s 更新内容
|
||
error VARCHAR(255) NULL,
|
||
INDEX (session_id, recorded_at),
|
||
INDEX (server_id, recorded_at),
|
||
INDEX (admin_id, recorded_at),
|
||
INDEX (probe_status, recorded_at)
|
||
)
|
||
```
|
||
|
||
**写入纪律**:
|
||
|
||
- `watch_probe` **每 5s 每台 Pin 机至少 1 INSERT**;进程 Top 可每 2 轮更新 `processes_json`(仍写行,仅进程字段可空)。
|
||
- 探针异常:`probe_status != ok` 时指标列全 NULL + `error` 必填。
|
||
- **保留 7 天**;`watch_records_purge` 每日 batch DELETE。
|
||
|
||
**估算**:1 台 Pin 8h ≈ 5760 行(5s);4 台 ≈ 2.3 万行/8h,7 天可接受。
|
||
|
||
### Redis(实时滑动窗口)
|
||
|
||
- Key:`watch:live:{server_id}` — **5s 一点**,保留 **30 分钟**;每点 JSON **含完整指标**:
|
||
|
||
```json
|
||
{
|
||
"ts": "2026-06-12T14:05:05Z",
|
||
"cpu_pct": 12,
|
||
"mem_pct": 45,
|
||
"disk_pct": 61,
|
||
"load_1": 0.8,
|
||
"net_bytes_sent": 102400,
|
||
"net_bytes_recv": 204800,
|
||
"net_up_bps": 1310720,
|
||
"net_down_bps": 917504,
|
||
"disk_read_bytes": 4096,
|
||
"disk_write_bytes": 8192,
|
||
"disk_read_bps": 2202009,
|
||
"disk_write_bps": 419430,
|
||
"probe_status": "ok",
|
||
"source": "ssh"
|
||
}
|
||
```
|
||
|
||
- Key:`watch:proc:{server_id}` — 进程 JSON,TTL 60s,**10s 更新**
|
||
- Pub/Sub:`nexus:watch` — payload 与上同结构 + `server_id`, `admin_ids[]`
|
||
|
||
## 后台任务
|
||
|
||
| 任务 | 间隔 | 职责 |
|
||
|------|------|------|
|
||
| `watch_probe` | **5s** | 读 Pin 并集;Redis 或 SSH;**每轮每台必 INSERT `watch_probe_records`**;写 Redis 窗口;Pub/Sub WS |
|
||
| `watch_pin_expiry` | 60s | 过期 pin → 更新 session.`ended_at`;删 pin 行 |
|
||
| `watch_records_purge` | 每日 | DELETE `watch_probe_records` / 旧 session 元数据 > 7d |
|
||
|
||
**Primary worker only**(与 `heartbeat_flush` 一致)。
|
||
|
||
### SSH 探针脚本(单次 JSON,指标一次带回)
|
||
|
||
在 `remote_probe` 基础上扩展,**一次 exec** 输出:
|
||
|
||
```json
|
||
{
|
||
"cpu_usage": 12.5,
|
||
"mem_usage": 45.2,
|
||
"disk_usage": 61.0,
|
||
"disk_mount": "/",
|
||
"load_1": 0.82,
|
||
"load_5": 0.91,
|
||
"load_15": 1.05,
|
||
"net_bytes_sent": 123456789,
|
||
"net_bytes_recv": 987654321,
|
||
"disk_read_bytes": 1234567890,
|
||
"disk_write_bytes": 9876543210,
|
||
"top_processes": [{"name":"nginx","pid":1234,"cpu_pct":2.1,"mem_pct":1.2}]
|
||
}
|
||
```
|
||
|
||
超时 **8s**;失败仍 INSERT;**禁止**只采 CPU/内存而跳过磁盘与网络出入口。
|
||
|
||
## API
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| GET | `/api/watch/pins` | 当前管理员 4 槽 + 最新实时指标 + 剩余秒数 + `session_id` |
|
||
| POST | `/api/watch/pins` | `{ "server_id": 1, "slot_index"?: 0 }`;创建 session;满则 409 |
|
||
| PUT | `/api/watch/pins/{slot}` | 替换槽内 server_id(旧 session `end_reason=replaced`) |
|
||
| DELETE | `/api/watch/pins/{slot}` | 手动移除(session `end_reason=manual`) |
|
||
| GET | `/api/watch/live?server_ids=1,2` | 读 Redis 窗口(兜底轮询) |
|
||
| GET | `/api/watch/metrics?server_ids=&hours=24` | 从 `watch_probe_records` 聚合曲线(仅 `probe_status=ok`) |
|
||
| GET | `/api/watch/probe-records` | **探针记录分页**;query: `server_id`, `session_id`, `hours`, `probe_status`, `page` |
|
||
| GET | `/api/watch/sessions?hours=168` | Pin 会话列表(起止时间、机器、结束原因、探针成功/失败计数) |
|
||
|
||
**`GET /api/watch/probe-records` 响应示例**:
|
||
|
||
```json
|
||
{
|
||
"items": [
|
||
{
|
||
"id": 9912,
|
||
"recorded_at": "2026-06-12 14:05:05",
|
||
"server_id": 42,
|
||
"server_name": "web-01",
|
||
"source": "ssh",
|
||
"probe_status": "ssh_timeout",
|
||
"duration_ms": 8001,
|
||
"error": "SSH exec timed out after 8s",
|
||
"cpu_pct": null,
|
||
"disk_pct": null,
|
||
"net_up_bps": null,
|
||
"net_down_bps": null
|
||
}
|
||
],
|
||
"total": 5760,
|
||
"page": 1,
|
||
"per_page": 50
|
||
}
|
||
```
|
||
|
||
鉴权:`Depends(get_current_admin)`;仅可查 **本人 `admin_id`** 的 session/记录;CUD Pin 写 audit log(`watch_pin_add/remove/replace`)。
|
||
|
||
## WebSocket
|
||
|
||
- 端点:复用 `/ws/alerts` 扩展 `type: watch_metrics` **或** 新 `/ws/watch`(推荐 **新 channel**,避免告警拥塞)
|
||
- 订阅:连接后带 JWT;服务端维护 `admin_id → socket`;仅推送 **该 admin 当前 Pin 的 server_id** 更新
|
||
- 前端:Servers 页 mount 时 connect;unmount disconnect;收到包更新 WatchSlotRow 与 ECharts append
|
||
|
||
## 前端组件
|
||
|
||
| 组件/页 | 职责 |
|
||
|---------|------|
|
||
| `WatchSlotRow.vue` | StatCardsRow 下 4 卡;gauge + 15min 折线;倒计时 |
|
||
| `WatchSlotCard.vue` | 单槽 UI:三环 + **↑出口 ↓入口** + 磁盘读写速率 |
|
||
| `useWatchPins.ts` | Pin CRUD、WS、倒计时 |
|
||
| `WatchMetricsPage.vue` | 历史趋势 + **探针记录 Tab**(`v-data-table-server`) |
|
||
| `WatchProbeRecordsTable.vue` | 状态、source、CPU/MEM/**DISK**、**网络↑↓**、耗时、error |
|
||
| `ServersPage.vue` | 操作列 `mdi-plus`;接 POST pin |
|
||
|
||
**图表**:ECharts(与 `FleetTrendEChart` 同栈);实时 `appendData` + 窗口上限 360 点。
|
||
|
||
## 安全与性能
|
||
|
||
- **硬上限**:每 admin 4 pin;探针 server 去重(多 admin pin 同一台 → 探针 **1 次**,WS **分 admin 推**)
|
||
- **SSH**:仅 Pin 集合;连接池复用;并发 ≤4(可配置)
|
||
- **无静默吞错**:SSH 失败、无凭据、超时 → 卡槽红态 + **探针记录行** + 最后 error 文案
|
||
- **进程 JSON**:命令行截断 80 字符,不落盘敏感 env
|
||
- **估算负载**:100 admin × 4 pin 极端 = 400 台(理论);实际运维 **个位数 admin × 4** → 探针 QPS ≈ 0.8/s 可接受
|
||
|
||
## 与现有能力关系
|
||
|
||
| 现有 | 关系 |
|
||
|------|------|
|
||
| Agent 60s 心跳 + Redis | Pin 且 Redis 新鲜 → **优先**,减 SSH |
|
||
| `server_metric_samples` 10min | **保留**;与 watch 探针记录 **独立**;长历史兜底 |
|
||
| 列表展开 `ServerMetricSparklines` | **保留**;与监测槽互补 |
|
||
| Schedules | **不扩展** schedule_type |
|
||
|
||
## 验收标准
|
||
|
||
1. 服务器列表每行有 **+**,点击后 **5s 内** CPU/内存/磁盘% 与 **网络出入口速率** 连续更新。
|
||
2. 满 4 槽再点 **+** 出现替换对话框;手动删除后槽变空。
|
||
3. **8h** 到期自动消失(误差 ≤ 2min);再次 **+** 同一台重置 8h。
|
||
4. 未 Pin 的子机:抓包/日志 **无** 额外 SSH 探针。
|
||
5. `#/watch-metrics` **探针记录 Tab** 可见每次 5s 探针(含失败);按 session 筛选与 Pin 时段一致。
|
||
6. SSH 失败:卡槽红态 + **DB 有 `probe_status=ssh_*` 行**;**不**显示 0% 冒充正常。
|
||
7. 模拟 SSH 超时单测:断言 **仍有 INSERT** 且 `cpu_pct IS NULL`。
|
||
8. 探针记录与趋势页均含 **disk_pct、net_up_bps、net_down_bps** 列/序列;相邻 5s 点带宽差分正确。
|
||
9. `local_verify` + 单测(含速率差分、全指标 SSH mock)+ type-check。
|
||
|
||
## 实施分期
|
||
|
||
| 阶段 | 内容 |
|
||
|------|------|
|
||
| **M1** | 表 + 探针 **全指标(含磁盘% + 网络出入口 + 磁盘 IO 速率)** + 5s WS + 4 槽 UI + 列表 **+** |
|
||
| **M2** | 进程 Top5 抽屉 + **探针记录 Tab** + 历史趋势多序列(含带宽曲线) |
|
||
| **M3** | CSV 导出 + Agent 可选补 net_io(减 SSH)+ 告警联动 |
|
||
|
||
## 回滚
|
||
|
||
- 删除路由与 UI;停 `watch_probe`;表可保留;Redis key TTL 自愈。
|