服务器监控
HeteroFlow 是本站的异构算力与大模型调度产品。服务器监控。服务器监控 调度平台自己也是一台需要被监控的服务器:控制面进程所在主机的 CPU、内存、磁盘、负载一旦失控,上层服务全部跟着遭殃。HeteroFlow 为此建了一条完整的可观测性链路:gopsutil 周期采集整机指标沉淀为历史曲线( 表),仪表盘内嵌仅平台管理员可见的监控面板,…
服务器监控
调度平台自己也是一台需要被监控的服务器:控制面进程所在主机的 CPU、内存、磁盘、负载一旦失控,上层服务全部跟着遭殃。HeteroFlow 为此建了一条完整的可观测性链路:gopsutil 周期采集整机指标沉淀为历史曲线(server_metrics 表),仪表盘内嵌仅平台管理员可见的监控面板,六项健康自检一键体检,/health 存活探针供负载均衡摘流量,Prometheus 格式的 /metrics 把 HTTP 延迟、调度延迟、goroutine 等指标交给外部监控系统。
这条链路与节点监控是两回事:节点监控(node_metrics 表)由各 GPU 节点上的 Agent 随心跳上报,管"算力节点健康不健康"——GPU 利用率、显存、温度、功耗(详见《节点管理》);本篇管控制面自身——跑 Server 进程的那台服务器的整机负载与进程运行时状态。两类监控在代码、存储表、API、页面区块上完全分离。
功能总览
| 能力 | 一句话说明 | 状态 |
|---|---|---|
| 整机实时指标 | CPU/内存/磁盘/负载/网络/运行时间七卡实时展示,gopsutil 现场采集 | 已实现 |
| 历史指标曲线 | CPU/内存/磁盘、网络流量、温度三张历史图,1h/6h/24h/7d 四档 | 已实现 |
| 周期采集入库 | 每 30 秒采一次写入 server_metrics 表,仅主实例采集 | 已实现 |
| 保留期自动清理 | 保留 7 天,约每小时触发一次清理,防表无限增长 | 已实现 |
| 六项健康自检 | 数据库/存储/调度器/GPU 检测/网络/配置一键体检,三档总评 | 已实现 |
| 存活探针 | /health 数据库不可达回 503,供 LB/systemd 摘流量 | 已实现 |
| 租户收窄健康明细 | /api/v1/health/detail 登录可读,按调用者可见范围收窄 | 已实现 |
| Prometheus 指标暴露 | /metrics 输出 21 个 heteroflow_* 指标,来源 IP 白名单收口 | 已实现 |
| 独立指标端口 | METRICS_LISTEN_ADDR 可选隔离指标与业务端口,附带 /healthz /readyz | 已实现 |
| 运行时诊断端点 | /debug/vars 暴露 expvar 运行时变量,仅平台管理员 | 已实现 |
| CPU 温度采集 | 接口保留但当前恒上报 0,面板如实显示"未检测到传感器" | 占位-不可宣传 |
采集机制
采集内容
采集器 GetServerMetrics(core/pkg/api/server_metrics.go:113)基于 gopsutil 在 Server 进程内直接读取宿主机,载荷为 server_metrics.go:25 的 ServerMetrics 结构体(17 字段):
| 指标 | 采集来源 | 说明 |
|---|---|---|
| CPU 使用率 | gopsutil cpu.Percent,1 秒窗口 | 采样阻塞 1 秒才返回 |
| 内存使用率/已用/总量 | gopsutil mem.VirtualMemory | — |
| 磁盘使用率/已用/总量 | gopsutil disk.Usage("/") | 根分区,非数据盘全集 |
| 系统负载 1/5/15 分钟 | gopsutil load.Avg | — |
| 网络接收/发送量 | gopsutil net.IOCounters | 开机以来累计值,非速率 |
| 系统运行时间 | gopsutil host.Info | 前端格式化为天/时/分 |
| 操作系统/架构 | runtime.GOOS / GOARCH | 面板头部展示 |
| CPU 温度 | 保留字段,硬编码 0(server_metrics.go:160) | 当前不采集 |
采集周期与写入节奏
采集循环 collectServerMetrics(v2/cmd/main.go:961)以 30 秒 Ticker 周期采集写库。多实例部署下该循环只在主实例(leader)运行(main.go:912-917):表没有主机维度列,多实例各自写入会把多台机器的数据混进同一条曲线——面板反映的是主实例所在主机的状况,其余实例所在机器不在采集范围。
每轮写入后调用 MaybeCleanupServerMetrics(server_metrics.go:642):每累计 120 次写入(约 1 小时)触发一次清理,原子 CAS 防重入,清理跑在独立 goroutine、独立 context 上(60 秒超时),不拖慢采集循环、不被进程退出信号半途取消。
存储与保留
表为自增主键 + timestamp(TIMESTAMPTZ)+ 上表 14 个指标列(core/pkg/storage/postgres/schema.go:625),timestamp DESC 索引(schema.go:1239)支撑时间范围查询。保留期硬编码 7 天(postgres/server_metrics.go:154)——METRICS_RETENTION_HOURS 只作用于 node_metrics,对本表不生效,控制面指标保留期当前不可配置。
历史查询采样
GET /api/v1/server/metrics/history?hours=N(合法区间 1~168,越界回落 1 小时)由 ServerMetricsStore.GetHistory(postgres/server_metrics.go:69)执行:按 date_trunc 分桶、每桶取最新一行、升序返回。实际粒度两档:24 小时以内按分钟分桶,以上按小时分桶。一个已知偏差:代码注释声称 6h 档每 5 分钟、24h 档每 15 分钟一点,但分桶单位变量最终都取 minute(postgres/server_metrics.go:101-114),6h/24h 档实际也是每分钟一点(24h 档约 1440 点),功能正确、粒度与注释不符。
健康探针体系
健康检查分四层,各有各的消费者与鉴权口径(core/pkg/api/server.go:223-262):
| 层 | 端点 | 鉴权 | 消费者 |
|---|---|---|---|
| 存活探针 | GET /health | 免鉴权 | LB / systemd,按状态码摘流量 |
| 存活明细 | GET /health/detail | 免鉴权 | status + database + scheduler 三项存活语义 |
| 健康明细 | GET /api/v1/health/detail | 登录(dashboard:read) | 前端全局状态栏 |
| 深度自检 | GET /api/v1/server/health-check | 仅平台管理员 | 仪表盘"系统健康检测"区块 |
/health 的状态码语义经过实战打磨(server_metrics.go:205-235):数据库探活失败(pgxpool 真实 Ping,2 秒超时)回 503——这个实例处理不了任何请求,必须让流量绕开;调度器没在本实例运行只回 200 + degraded——多实例下只有 leader 跑调度器,follower 不跑属正常,按 503 摘掉 follower 会把 API 流量全压到 leader,反而制造故障。/health/detail 是它的免鉴权探针版,刻意不下发数据库错误文本。
/api/v1/health/detail(health_detail.go:39)是按调用者收窄的健康明细:GPU 资源只数调用者可见节点(运营者全平台、租户按可见集合)且只数 online/busy 节点(health_detail.go:129);任务统计与 /api/v1/tasks/stats 同口径;分片只数归属本租户的;插件清单只发给平台运营者。这些字段 2026-09-01 前挂在免鉴权的 /health/detail 上(匿名可读全平台明细),收紧后才迁到此处。
深度自检 handleServerHealthCheck(server_metrics.go:364)逐项检查六个维度,三档总评(0 项异常 healthy / 1~2 项 degraded / ≥3 项 unhealthy):
| 维度 | 检查方式 | 异常判定 |
|---|---|---|
| 数据库连接 | 真实执行 NodeStore().List 探活 | 报错即 error |
| 存储服务 | 读根分区磁盘使用率 | >90% 为 warning |
| 调度器 | 是否运行 + 待处理任务数 | 未运行 error;待处理 >10 warning |
| GPU 检测 | 数 online/busy 且带卡的节点 | 0 个为 warning |
| 网络状态 | 服务端口监听断言 | 恒 ok |
| 配置文件 | 能执行到此处即 config.yaml 已加载 | 恒 ok |
调度器维度由纯函数 evaluateSchedulerHealth(server_metrics.go:339)判定——这是可观测性审计修复的产物:原实现 warning 赋值被同分支 ok 覆盖、正常路径无赋值,会产生"overall=degraded 但六项全 ok"的自相矛盾输出;抽成互斥三路径后每条分支可单测。
页面功能介绍
仪表盘·服务器监控面板
面板由 ServerMetrics 组件(web/src/components/dashboard/ServerMetrics.tsx,542 行)渲染,内嵌在仪表盘首页(端点健康摘要之后、功能导航之前)。挂载条件在 DashboardPage.tsx:368:user?.role === 'admin' 才渲染——后端 2026-09-01 起三条 server API 仅平台管理员可读,非 admin 渲染只会拿到三个 403;判据刻意用平台角色而非租户内角色(租户的 admin 不是平台运维)。
头部:面板标题、时间范围选择器(1小时/6小时/24小时/7天,对应 hours=1/6/24/168,ServerMetrics.tsx:22-27)、操作系统/架构。
实时指标七卡(数据源 GET /api/v1/server/metrics,每 5 秒轮询):
| 卡片 | 展示内容 | 阈值着色 |
|---|---|---|
| CPU 使用率 | 百分比 + 进度条 | >80% 红,其余蓝 |
| 内存使用率 | 百分比 + 已用/总量 GB + 进度条 | >80% 红,其余绿 |
| 磁盘使用率 | 百分比 + 已用/总量 GB + 进度条 | >90% 红,其余紫 |
| CPU 温度 | 温度值;无传感器显示 N/A | >80°C 红、>60°C 黄 |
| 系统负载 | load_1 主显,load_5/15 副显 | 不着色 |
| 网络流量 | ↓接收 ↑发送累计 MB | 不着色 |
| 运行时间 | X天 X时 X分 | 不着色 |
历史曲线三图(数据源 GET /api/v1/server/metrics/history?hours=,随时间范围切换重取,ECharts 渲染):
| 图表 | 曲线 | 形态 |
|---|---|---|
| CPU/内存/磁盘使用率 | 三线同图,Y 轴 0-100% | CPU 蓝面积、内存绿面积、磁盘紫虚线 |
| 网络流量 | 接收/发送两线,MB | 折线,纵轴自适应 |
| CPU 温度 | 单线,Y 轴 0-100°C | 橙色面积,附 60/80°C 参考线;温度恒 0 时为贴地直线 |
历史数据为空时显示"暂无历史数据"空态;底部脚注展示最后更新时间与历史点数。
系统健康检测区块(数据源 GET /api/v1/server/health-check,同频 5 秒轮询):六项逐条展示图标(ok ✅ / warning ⚠️ / error ❌)、组件名、消息与建议,顶部总评三色着色。
容错行为:实时指标与健康检查各自 catch(() => null) 静默降级(ServerMetrics.tsx:42-45)——单接口失败不弹错误面板,保留上一次数据。
全局状态栏(所有登录用户)
页面底部状态栏(web/src/App.tsx:59)展示"N GPU 在线 / N 任务运行中 / N 告警",前两项来自 GET /api/v1/health/detail 的 checks.gpu_resources.total 与 checks.tasks.running——按当前用户可见范围收窄,租户用户看到的是"我看得见的 GPU";告警数来自 /api/v1/alerts/stats 的 ACTIVE 计数。未登录不发起请求。
Prometheus 指标暴露
GET /metrics 输出 Prometheus 文本格式,由 core/pkg/metrics/prometheus.go 统一注册 21 个 heteroflow_* 指标,分四类:
| 类别 | 代表指标 | 说明 |
|---|---|---|
| HTTP 服务 | heteroflow_http_requests_total、heteroflow_http_request_duration_seconds | 中间件逐请求累计;path 标签归一化防基数爆炸 |
| 调度与任务 | heteroflow_tasks_scheduled_total、heteroflow_schedule_latency_seconds、heteroflow_tasks_total | 调度延迟为创建→调度成功的直方图 |
| 集群快照 | heteroflow_nodes_online{gpu_type}、heteroflow_scheduler_running、heteroflow_oldest_pending_task_age_seconds | 抓取时从存储层现算刷新(RefreshMetricsSnapshot,server_metrics.go:486),任一步失败只跳过对应指标、绝不整体 500 |
| 进程自身 | heteroflow_goroutines、heteroflow_memory_bytes{type} | 每 15 秒读 runtime.MemStats(prometheus.go:251) |
访问控制是"免鉴权 + 来源 IP 白名单"(metrics_guard.go:50):Prometheus 抓取器普遍不带应用层凭据,加鉴权会打断标准抓取链路;但免鉴权不等于公网裸奔——loopback、RFC1918 私网、CGNAT(100.64/10)放行,公网源一律 403,判 r.RemoteAddr 且不信任可伪造的转发头。需要公网抓取时设 HETEROFLOW_METRICS_ALLOW_PUBLIC=true 全开。
需要把指标端口与业务端口隔离时,设 METRICS_LISTEN_ADDR(如 127.0.0.1:9090)额外起独立指标服务器(v2/cmd/main.go:470-480),同时提供 /healthz(存活)与 /readyz(带数据库探活的就绪探针,不可达回 503 并写明原因)。
另有 GET /debug/vars 暴露 Go expvar 运行时内部变量,仅平台管理员可读——它暴露的是进程内部变量而非聚合指标,鉴权口径与 /metrics 刻意不同。
后端逻辑概念表
| 概念 | 定义 | 代码出处 |
|---|---|---|
| GetServerMetrics | gopsutil 现场采集函数,CPU 采样阻塞 1 秒 | core/pkg/api/server_metrics.go:113 |
| collectServerMetrics | 30 秒 Ticker 采集写库循环 | v2/cmd/main.go:961 |
| leader-only 收敛 | 多实例仅主实例采集,防多主机数据混写一张表 | v2/cmd/main.go:912 |
| ServerMetricsStore | server_metrics 表的存取器(Save/GetHistory/Cleanup) | core/pkg/storage/postgres/server_metrics.go:31 |
| GetHistory 分桶采样 | date_trunc 分桶取桶内最新一行,分钟/小时两档 | core/pkg/storage/postgres/server_metrics.go:69 |
| CleanupOldMetrics | 保留期硬编码 7 天的过期删除 | core/pkg/storage/postgres/server_metrics.go:154 |
| MaybeCleanupServerMetrics | 每 120 次写入触发清理,CAS 防重入、独立 context | core/pkg/api/server_metrics.go:642 |
| handleHealth | DB 挂 503 / 调度器没跑 degraded 200 的状态码语义 | core/pkg/api/server_metrics.go:205 |
| handleScopedHealthDetail | 鉴权且按调用者租户收窄的健康明细层 | core/pkg/api/health_detail.go:39 |
| evaluateSchedulerHealth | 调度器维度三路互斥判定的纯函数 | core/pkg/api/server_metrics.go:339 |
| RefreshMetricsSnapshot | /metrics 抓取时现算刷新 gauge 快照,失败不 500 | core/pkg/api/server_metrics.go:486 |
| metricsSourceGuard | /metrics 来源 IP 白名单(loopback/私网/CGNAT) | core/pkg/api/metrics_guard.go:50 |
| StartMetricsServer | 独立指标端口,附带 /healthz 与 /readyz | core/pkg/metrics/prometheus.go:399 |
API 参考
| 接口 | 说明 | 权限 |
|---|---|---|
GET /api/v1/server/metrics | 整机实时指标(现场采集,约 1 秒) | 仅平台管理员 |
GET /api/v1/server/metrics/history?hours=1..168 | 历史采样数据,越界回落 1 小时 | 仅平台管理员 |
GET /api/v1/server/health-check | 六维深度自检 + 三档总评 | 仅平台管理员 |
GET /api/v1/health/detail | 按调用者收窄的健康明细 | 登录用户 |
GET /health | 存活探针:数据库不可达 503,非 leader degraded 200 | 免鉴权 |
GET /health/detail | 存活探针层:status + database + scheduler | 免鉴权 |
GET /metrics | Prometheus 指标(21 个 heteroflow_* 系列) | 免鉴权 + 来源 IP 白名单 |
GET /debug/vars | expvar 运行时内部变量 | 仅平台管理员 |
METRICS_LISTEN_ADDR(环境变量) | 设置后额外起 /metrics /healthz /readyz | 运维配置 |
已知边界
1. CPU 温度未真实采集:采集器硬编码 Temperature=0(读温度需特殊权限),面板温度卡如实显示 N/A,但历史温度曲线仍会画出 0 值贴地直线。该字段属预留接口,不可宣传为温度监控能力。
2. 网络流量是累计计数器:卡片与曲线都是开机以来累计 MB,不是带宽速率;曲线斜率才是速率,纵轴随运行天数单调抬升。
3. 保留期不可配置:server_metrics 硬编码 7 天;METRICS_RETENTION_HOURS 只作用于节点指标表,对本表无效。
4. 历史采样粒度与注释不符:6h/24h 档注释宣称 5/15 分钟分桶,实际同为分钟分桶(24h 档约 1440 点);功能正确,本篇已按代码实际行为描述。
5. 实时接口有约 1 秒固有延迟:CPU 使用率按 1 秒窗口计算,每次调用现场阻塞采样。
6. 单机视角:多实例部署下只有主实例所在主机被采集,表无主机维度列;主实例跨主机迁移后历史曲线会拼接两台机器的数据。
7. /metrics 免鉴权:按 Prometheus 社区惯例有意为之,靠来源 IP 白名单收口;判 RemoteAddr 不信转发头,若前置代理,放行面只会变宽不会误拦。
8. 健康自检两项是静态断言:"网络状态"与"配置文件"在 handler 能执行到的前提下恒 ok;"存储服务"项查的是根分区磁盘占用,非数据库存储健康。
9. 进程指标仅两个自造系列:进程自身可观测只有 heteroflow_goroutines 与 heteroflow_memory_bytes(alloc/total_alloc/sys),注册用自定义 registry,标准 go_* 系列不在输出中。
<!-- 状态标注说明: 真机验证=真实GPU端到端 / 已实现=代码+测试钉住 / 已适配=检测逻辑对齐官方文档但无真机 / 实验性=机制可用但效果未验证 / 占位-不可宣传=接口预留、不可对外宣传 -->