内网算力管理平台。管 4 台昇腾 910B(32 卡)、2 台 A100(15-16 卡)、1 台 RTX 5090(2 卡)。
功能设计与分期见 DESIGN.md。
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
cd server && ../.venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8710打开 http://<主机>:8710/ 。
节点清单读 inventory.yaml(含明文密码,已 chmod 600 + gitignore)。
| 环境变量 | 默认 | 说明 |
|---|---|---|
LEXFORGE_INVENTORY |
./inventory.yaml |
节点清单路径 |
LEXFORGE_DB |
./lexforge.db |
SQLite 文件 |
LEXFORGE_POLL |
15 |
采集间隔(秒) |
LEXFORGE_RETENTION |
30 |
采样保留天数 |
AUTH_MODE |
none |
none 直接开放;logto 走 lexmount SSO |
LEXFORGE_ACCESS_DOC_URL |
空 | 登录信息文档链接,只发给已登录用户 |
inventory.yaml 是给采集器读的,另有一份给人读的飞书文档记录同样的登录信息,
两者必须同步 —— 只改一处,面板全绿而同事照着文档登录会失败,且没有任何告警会发现。
文档链接不写进本仓库(仓库是 public 的)。它等同于凭据:租户内任何人拿到链接
就能读到全部明文口令。URL 存在两处 —— inventory.yaml 顶部的 access_doc: 字段,
以及部署环境的 LEXFORGE_ACCESS_DOC_URL。
平台侧只由 /api/auth/me 下发该链接,无会话直接 401;公开的 /api/auth/config
不带它。测试 test_access_doc_link_never_reaches_an_unauthenticated_caller 锁住这点。
改文档的命令、权限设置和内容约定见 .claude/skills/node-access/SKILL.md。
./deploy.sh # rsync 到 deploy@10.0.0.5,装依赖,用 crontab @reboot 拉起管控机 sudo 需要密码,所以走 crontab @reboot 而不是 systemd。要换成 systemd
服务,在机器上手动执行 deploy.sh 末尾打印的那段。
inventory.yaml 节点清单 + 凭据 + 归属(唯一的事实来源)
probe.sh 一次性巡检脚本,不依赖平台
DESIGN.md 功能设计与分期
server/
config.py 读清单
parsers.py npu-smi / nvidia-smi 解析
test_parsers.py 真机输出做 fixture 的解析测试
collector.py 每节点一条常驻 SSH,定时轮询
db.py SQLite
main.py FastAPI + 静态页
static/ 前端(零构建、零 CDN)
Ascend 网关会限流。 那台跳板对密集连接和认证失败很敏感,触发后四个
端口一起变成 Connection refused / Connection reset by peer,和宕机长得一模一样。
采集器据此做了三件事:每节点只维持一条连接;失败指数退避(30s→10min);
认证失败不密集重试,因为重复错密码正是最初被封的原因。
「认证失败」的处理按凭据是否曾经成功过分两种,而这个事实存在库里而不是内存里: 曾经成功 → 判定为网关在说话,长退避后重试;从未成功 → 放弃该节点等人改 inventory。 早期版本把它放在内存,结果每次部署都会遗忘,重连风暴中的一次瞬时拒绝就能把一台 健康节点打成永久黑名单直到下次发版 —— bms-ZKJH-0014 就是这么被打掉过一次。
npu-smi 的列是右对齐的。 空闲卡打印 3447 / 65536(斜杠前有空格),满载卡打印
59198/ 65536(没空格)。正则两边都要容忍空白,否则空闲卡被静默丢掉、节点看起来
永远满载。test_parsers.py 专门锁了这个用例。
node3 的第 8 张卡在掉线和恢复之间反复。 2026-08-04 上午看到 7 张,下午看到 8 张。
它的 nvidia-smi 因此要 5-12 秒(驱动在重试),所以 NVIDIA 探针合并成两次调用、
超时放宽到 60 秒。卡数变化会记进事件表。
管控机根分区 97% 满(1.8T 剩 67G)。采样表 30 天不到 1GB,但别忽视这个水位。
管控机上已经有另一个 uvicorn main:app。 那是别人的 CUDA 服务,root 身份、占
8000 端口、已连续跑六周。pkill -f 'uvicorn main:app' 会精确命中它 —— 第一版
deploy.sh 就是这么写的,只因 wf 无权 kill root 进程才没出事。而且同一条 pkill
还会打中部署脚本自己(远程 shell 的命令行里就含这个字符串),导致脚本自杀、服务
根本没起来。现在 deploy.sh 通过 /proc/PID/exe 反查是不是本 venv 的解释器来锁定
自己的进程,既不误伤别人也不自杀。改这段时别退回按名字匹配。
光靠 pidfile 不够。 启动失败(比如撞端口)时 pidfile 已经被新 pid 覆盖,而真正 占着端口的老实例从此失联,之后每次部署都只会再撞一次。所以停旧实例走 exe 反查, pidfile 只作辅助,且启动失败时会被删掉。
平台监控它自己所在的那台机器。 ctrl-5090 的 host 就是管控机自己。采集器用一个
UDP socket 的源地址选择来判断"这个 host 是不是我自己",是的话直接走本地子进程,
不走 SSH —— 否则就得往别人机器的 authorized_keys 里塞一把自己的钥匙。
流程与 kong-platform 一致,环境变量同名,
所以两个服务可以共用同一套 Logto 配置习惯:授权码流 → 服务端换 token → 读 /oidc/me
→ 签一个自己的 HS256 session cookie(8 小时)。
AUTH_MODE=logto
LOGTO_ENDPOINT=https://<logto 地址>
LOGTO_APP_ID=<App ID>
LOGTO_APP_SECRET=<App Secret>
LOGTO_REDIRECT_URI=http://<本服务地址>/api/auth/callback
SESSION_SECRET=$(openssl rand -hex 32)在 Logto 控制台建一个 Traditional Web 应用,把上面的 LOGTO_REDIRECT_URI
一字不差地登记为回调地址。
路由:/api/auth/login、/api/auth/callback、/api/auth/me、/api/auth/logout、
/api/auth/config。
几个刻意的选择:
- 默认
none。 升级现有部署不会把人锁在外面。但none模式下如果带了有效 session cookie 依然会被识别,占用登记因此能记到真名而不是 anonymous。 - 配置不全就启动失败。
AUTH_MODE=logto缺任何一项直接抛错退出,而不是 静默降级成无鉴权 —— 后者会让人以为受保护了。 SESSION_SECRET强制 ≥32 位。 logto 模式下这个 cookie 就是鉴权边界本身。- 开了 SSO 之后,占用登记的使用人取自会话,忽略表单值。 否则「谁占着这张卡」 这条记录可以随便伪造,登记也就失去意义了。前端会把该输入框置灰。
Secure跟随X-Forwarded-Proto。 目前 Rainbond 走的是 http,写死Secure会让 cookie 被静默丢弃。
采集器会校验主机密钥。首次连接某节点时把它的公钥固定到 known_hosts(TOFU),
之后每次连接都对着这份指纹校验 —— 因为这些节点用的是密码认证,不校验主机密钥
意味着能劫持内网流量的人可以冒充节点、直接骗走明文 root 口令。
想连首次连接那个窗口也堵上,先预置指纹再启动:
ssh-keyscan -p 48042 203.0.113.10 >> known_hosts # 每个节点一条
LEXFORGE_STRICT_HOSTKEY=1 ... # 未预置的节点直接拒连known_hosts 不入库(各环境不同),所以新机器上首次启动会走一轮 TOFU 并打 WARNING。
密码认证的节点,口令写在 inventory.yaml。密钥认证的节点写 key_file 指到私钥;
路径不存在时会退回 agent 和 ~/.ssh,所以同一份 inventory 在笔记本和容器里都能用。
RTX 5090 那台用的是一把专用收集器密钥,不是谁的个人密钥。它在目标机
authorized_keys 里带了限制项,因为这把钥匙只需要跑只读探测:
no-pty,no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-user-rc ssh-ed25519 AAAA...
要吊销就删掉 authorized_keys 里那一行(注释是 lexforge-collector@rainbond),
不影响任何人的个人密钥。私钥不入库,在 secrets/(已 gitignore),
容器里以 config-file 挂到 /app/ssh/。
按分支名构建会拿到缓存的旧代码。 构建源填 main、点构建,Rainbond 会报
「构建成功」,但产出的镜像可能还是上一次的 —— 容器里连新增的文件都没有。
两次踩到:一次是分支已被合并删除仍报成功,一次是 main 有新 commit 但没重新拉取。
构建成功不等于产物是新的,这种失败最隐蔽。部署后请实际验证,例如:
curl -s http://<面板地址>/api/auth/config # 新接口存在?可靠做法是把构建源的 code_version 钉成 commit SHA 而不是分支名,
每次发布更新成新的 SHA:
rainbond_update_component_build_source
service_source=source_code # 少这个参数会静默不生效
code_version=<40 位 commit SHA>
对齐 kong-platform,方便运维和排障时不用切换心智:
- 探针:
/health(公司发布平台约定的硬路径)、/healthz(k8s 拼法兼容)、/readyz(校验数据库真的能应答)。三者都不需要登录。/readyz刻意不把节点可达性算进去 —— 昇腾网关被限流时不应该把面板摘出 负载均衡,那正是你要看面板的时候。 - 统一错误信封
{"error":{"code","message","details"}},调用方按code分支, 不用解析中文提示。FastAPI 默认的{"detail":...}会让这个服务在内部平台里显得另类。 - 审计日志:
node_events记的是机器干了什么,audit_log记的是人干了什么 —— 谁登记了占用、谁释放了谁的登记。接了 SSO 之后这里才有真名。查询GET /api/audit。 - actor 解析顺序:会话 → 反向代理头(
x-forwarded-user/x-user等, kong-platform 也认这几个)→anonymous。会话优先于请求头,因为头是调用方可伪造的。 LOG_LEVEL可调日志级别。
两档轮询。 表头指标(利用率/显存/温度/功耗/进程)每 15 秒一次;昇腾的细粒度
指标每 60 秒一次,因为它们每张卡要单独调一次 npu-smi —— 实测 8 张卡的
-t usages 要 5.8 秒,全塞进 15 秒轮询会把网关压垮。
| 来源 | 指标 |
|---|---|
| 昇腾 每 15s | 利用率、HBM 占用、温度、功耗、健康、进程归属 |
| 昇腾 每 60s | AICore / AIVector / AICube / AICPU / CtrlCPU、HBM 与 DDR 带宽、HBM 使用率、NPU 总体、SoC / NDIE / HBM 温度 |
| NVIDIA 每 15s | 上述 + 显存带宽利用率、SM 与显存频率、风扇、功耗上限、显存温度、PCIe 代数与宽度、ECC 可纠错数、降频原因(过热 / 功耗墙) |
| 主机 每 15s | 负载、CPU%、内存%、磁盘%、网络收发速率 |
CPU 和网络在 /proc 里是单调计数器,采集器差分成速率后再入库 —— 计数器画成曲线
没有意义。跨重启计数器会归零,差分为负时丢弃该点而不是画出一个巨大的负毛刺。
新增指标只需在 db.METRICS 里加一行,前端会自动多出一个面板(面板列表由
/api/metrics 驱动),并按 vendor 过滤 —— 昇腾不会显示"风扇"这种它报不出的项。
曲线 页签。多序列时间曲线,支持 15m / 1h / 6h / 24h / 7d、节点切换、自动刷新、
悬停十字线 + 全序列数值浮层(按数值降序,和眼睛看到的上下顺序一致)、点击图例
隐藏/恢复单条序列。链接带 hash(#metrics)可直接分享。
图表是手写 SVG,不引任何图表库(内网无外网)。配色是按实体固定分配的分类色板
(0 号卡永远是同一个蓝),不随序列增减重排,这样筛掉一条不会把其余全部换色。
色板针对本面板底色 #14191e 跑过校验:亮度带、彩度下限、色觉障碍相邻区分度
(最差 ΔE 8.4)、正常视觉区分度(19.3)、对比度均 ≥3:1。改任何一个色值都会让
这份校验失效,改前请重跑校验器。
空数据渲染成断线而不是掉到 0 —— 采集中断和"卡是空闲的"是两件事。
运维细节(当前配置、改发到群、轮换密钥、几个会静默失败的坑)见
.claude/skills/lark-alerting/SKILL.md。 改server/alerts.py或排查「告警发不出去」之前先读它。
发到飞书。两种投递方式,二选一,都配则优先用应用机器人。
A. 企业自建应用机器人 —— 公司要求走应用审批流时用这个。
FEISHU_APP_ID=cli_xxxx
FEISHU_APP_SECRET=xxxx
FEISHU_RECEIVE_ID=someone@lexmount.com # 个人:邮箱即可
# FEISHU_RECEIVE_ID=oc_xxxx # 群:chat_idFEISHU_RECEIVE_ID 的类型是自动识别的:带 @ 当邮箱、oc_ 当群、
ou_ 当 open_id、on_ 当 union_id。要发给原始 user_id 才需要用
FEISHU_RECEIVE_ID_TYPE 覆盖。
需要的权限:
| 发给谁 | 权限 | 额外准备 |
|---|---|---|
| 个人(邮箱) | 只要 im:message |
无。不用建群、不用拉机器人 |
| 群 | im:message |
机器人必须已在群里;im:chat:readonly 可让 /api/alerts/lark/chats 列出 chat_id |
不需要配置事件与回调 —— 我们只发不收,事件订阅是给接收用的。
实现细节里有两个坑:content 字段必须是 JSON 字符串而不是对象(传对象会被判
参数错误);tenant_access_token 有效期 7200 秒,代码里缓存并提前 5 分钟刷新,
过期错误码还会自动重试一次(只一次,不会打转)。
B. 群自定义机器人 —— 最简单,不需要任何权限、不用审批。
FEISHU_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/xxxx
FEISHU_WEBHOOK_SECRET= # 仅当机器人开了「签名校验」才需要
LEXFORGE_BASE_URL=https://lexforge.dev.lexmount.net # 消息里附面板链接
LEXFORGE_ALERT_TEMP=85 # 高温阈值 °C
LEXFORGE_ALERT_DISK=90 # 磁盘阈值 %
LEXFORGE_ALERT_INTERVAL=60 # 评估间隔(秒)
LEXFORGE_ALERTS=0 # 整体关闭不配 webhook 也能用:规则照常评估、告警 页签照常显示,只是不往外发。
同一个问题不会按固定间隔重复。每发一次就把下一次推远一格:
首次 → 10 分钟 → 1 小时 → 4 小时 → 24 小时
- 点「我已确认」 → 这次不再提醒。条件恢复后自动清除,同样的故障再犯还会重新告警
- 点「稍后处理」 → 立刻跳到阶梯下一格
- 不理它 → 视同「稍后处理」,自动走下一格。所以既不会一分钟推一次,也不会消失
卡片按钮是普通链接而不是回调按钮:飞书的服务器访问不了我们的私网地址, 链接是在你自己浏览器里打开的(已经在内网),点开落到面板上完成确认。
同样是掉一张卡,代价并不一样,所以 inventory.yaml 里每个集群标了归属:
| 归属 | 谁 | 处理 |
|---|---|---|
rented |
A100 | 按小时付费,掉卡就在烧钱。故障类告警自动升为严重,静默期缩短到 40% |
borrowed |
昇腾 910B | 硬件动不了,通常只能联系出借方 |
owned |
RTX 5090 | 自有裸机在办公室,可以等工作时间 |
升级只作用于故障类规则,不会因为「没人登记占用」半夜把人叫起来。
卡片发出前先让 LLM 做一次归因,这比及时性更重要 —— 一张列了十个症状的卡片, 读的人还得自己找因果。给模型的上下文包括:涉及节点的完整现状、其余节点的状态 (用来区分「这台坏了」和「我们整条链路断了」)、最近的节点事件与告警历史、 以及各批资源的归属。
产出三段:发现了什么(原始现象)、可能原因(含判断理由)、建议排查 (2-4 条动词开头的具体动作),末尾链接回面板。
模型不可用时照常发卡片,只是没有后两段 —— 降级成「少了洞察」可以, 降级成「没有告警」不行。
选型不是拍脑袋:用一个有陷阱的场景实测过 —— 四台昇腾同时掉线,正确答案是
「我方被跳板限流」而不是「四台机器同时宕机」,区分依据是其余归属的节点都正常。
deepseek-v4-flash / glm-5.2 / kimi-k2.6 / gpt-5.4-mini / doubao-seed-2-1-turbo
五个都答对了,默认选 deepseek-v4-flash(10s,简洁,且会给出鉴别诊断)。
换模型只改 LEXFORGE_LLM_MODEL 一个变量。
难的不是发现问题,是不把群变成没人看的噪音源。 三条规矩:
- 必须持续。 条件要连续满足超过静默期才通知。这堆机器什么都会抖 —— 昇腾网关拒连十几秒、node3 的第 8 张卡时有时无、跑批时温度瞬间冲高。 首次观测就报警,一天之内就没人看了。
- 只发一次。 触发后不再重复,除非超过「重复间隔」。恢复时单独发一条 —— 一个从不说「已恢复」的告警会训练所有人忽略它。
- 相关故障合并。 昇腾网关一限流,四台节点同时不可达,此时诚实的消息是 「我们被限流了」,而不是四条「节点宕机」。规则里对「半数以上节点同时掉」 做了合并,只发一条。
内置规则:节点不可达、认证失败、数据陈旧、掉卡、高温、卡异常、降频、 磁盘将满、登记超期、未登记占用。
机器人的三种安全设置都支持,不用改代码:
- 自定义关键词 —— 每条消息都以
[LexForge]开头,用它当关键词即可(有测试锁住这个前缀) - IP 白名单 —— 集群出网 IP 是
45.41.10.243(实测,非查表) - 签名校验 —— 填
FEISHU_WEBHOOK_SECRET。注意飞书这个 HMAC 是反的: key 是timestamp\n密钥、消息体为空,写反了会生成一个格式正确但服务端 静默拒绝的签名,很难查。有测试专门锁这个方向。
速率限制 100 条/分钟、5 条/秒。本平台最多每个评估周期(默认 60 秒)发一条合并消息, 差着两个数量级。
GET /api/alerts/preview 可以看「此刻哪些规则命中」而不触发通知也不改状态,
调阈值时很有用。POST /api/alerts/test 发一条测试消息验证 webhook。
- 卡在用 = 卡上有进程;没有进程时,显存 > 5GB 或利用率 > 5% 也算。 910B 空闲卡本身就常驻约 3.4GB 驱动内存,所以不能只看显存。
- 卡时 直接由采样推导,不需要额外埋点。