跳转至

SOP — 环境监控扩面(P0:14 台非 K8s 主机主机层接入)

本文件是变更档案:这次改了什么、怎么改、怎么验、怎么退。 日常使用与排障见同目录 RUNBOOK.md。 变更主题:把 Slurm / Lustre / LSF / HPC / FlexLM 五套集群共 14 台主机的主机层 指标接入本环境 monitoring 命名空间的 kube-prometheus-stack。

目录

  1. 背景
  2. 目标
  3. 前置条件
  4. 影响范围
  5. 分步操作
  6. 验证
  7. 回滚
  8. 风险说明
  9. 附录

1. 背景

触发条件:现有监控只有「K8s 集群监控」,不是「环境监控」。

实测基线(2026-09-29,命令见附录 B):

事实 数值
受管主机 18 台 VM + 1 台物理宿主(Dell R620)
有指标的主机 5 台(4 台 K8s 节点 + 宿主 .250)→ 覆盖率 26%
零采集的主机 14 台(.16–.29,Slurm/Lustre/LSF/HPC/FlexLM)
现有大盘 25 个 chart 自带 mixin + 1 个 R620 硬件盘,纯 K8s 视角,无 HPC 语义
告警 Alertmanager 默认 route 为 null(只存不发);仅 R620 硬件走 163 邮件

即:这套环境真正的生产力(作业调度、并行文件系统、许可服务)完全不可见, 且出了问题没有任何通知路径。

2. 目标

变更完成后应达到可验证状态:

  1. 14 台主机全部产生 node_* 指标,Prometheus 中 count(up{job=~"external-node-exporters.*"} == 1) == 14。
  2. 每台主机带 cluster / role 标签,instance 为主机名形式(如 slurm-work-01:9100)。
  3. 新增 16 条主机基础告警规则(磁盘 / inode / 内存 / CPU 饱和度 / 系统单元 / 时间同步 / 网卡错误 / 重启 / 失联)。
  4. 新增「环境总览」大盘(19 台主机统一视图)与社区大盘 Node Exporter Full (1860)。
  5. 主机层告警按 severity 分级投递邮件:critical 立即发、warning 降频发、 info 不发(仅大盘可见);主机整体失联时抑制其衍生告警。
  6. 幂等:deploy 连续执行两次,第二次零变更、全跳过。

3. 前置条件

以下条件全部已落为可执行检查(utils/precheck.py + utils/config_validate.py), 由所有变更类子命令的第一步自动执行;写在这里供人工复核。

# 条件 检查方式 级别
1 操作机 Python ≥ 3.9 且运行在工程 .venv 内 check_local WARN
2 共享库可导入(.pth 已挂载) check_local BLOCK
3 配置文件存在且合法(必填/取值域/唯一性/跨字段) utils/config_validate.py BLOCK
4 kubectl、helm 可用且集群可达 check_k8s BLOCK
5 monitoring 命名空间与 helm release 存在 check_k8s BLOCK
6 抓取源网段可由 Calico IPPool 推导(失败即拒绝,不做全开) check_source_cidr BLOCK
7 14 台目标主机 SSH 免密可达 check_ssh BLOCK
8 目标机为 Rocky/RHEL 系、主版本 9、x86_64 check_os BLOCK
9 包可安装:已装 或 EPEL 可用且 dnf 能解析到包 check_package BLOCK
10 /etc/dnf/dnf.conf 的 gpgcheck=1(包来源可信) check_package BLOCK
11 9100 端口未被其它进程占用 check_port WARN
12 目标机 / 与 /var 可用空间 ≥ 5 GiB check_free_space BLOCK
13 目标机与操作机时钟偏差 ≤ 5s check_clock WARN
14 SELinux / firewalld 现状记录(只预警,不阻断) check_selinux_port、check_firewalld WARN/INFO

权限:主机侧需 root(装包、改 systemd、改防火墙),集群侧用 config/env-monitor.yaml 中 kubernetes.kubeconfig 指定的凭证。 凭据:SMTP 授权码不入库,运行时从 ~/.secrets/dell-r620-monitor.env 的 DELL_R620_SMTP_AUTH_PASSWORD 读取(600 权限,值不落日志、不落工程目录)。

4. 影响范围

维度 内容
变更主机 14 台:.16–.29(Slurm 5 / Lustre 2 / LSF 5 / HPC 1 / FlexLM 1)
是否停服 否。仅新增采集进程与端口放行,不改动任何既有服务
集群侧对象 monitoring 命名空间:Service / Endpoints / ServiceMonitor / PrometheusRule / ConfigMap×2
既有配置变更 仅 Alertmanager route(null → 两级邮件路由),改前导出 live values 备份
窗口期 无特殊要求。dnf install 为单次事务,对 Slurm/LSF 计算节点影响可忽略;建议避开作业高峰
在跑作业 装包不重启任何调度器/计算服务,不影响正在运行的作业;yum 锁冲突会直接失败并跳过该台
目标机新开端口 9100/tcp,仅对 10.244.0.0/16(Calico Pod 网段)放行,不对全网开放
新增文件(目标机) /etc/systemd/system/prometheus-node-exporter.service.d/10-env-monitor.conf、/var/lib/node_exporter/textfile/

5. 分步操作

实现体:main.py + deployer.py + modules/host_agent.py + modules/k8s_ingest.py。 以下每步给出等价 shell 命令,供阅读、复核与手工复现; 幂等、重试、批次门禁、备份编排由实现体保证,不在代码块里展开。 示例只以 .16(slurm-master-01)演示,其余 13 台同理。

步骤 1:准备操作机运行环境

工程用独立 venv,共享库通过 .pth 挂载(不 pip install,避免与工程内同名模块冲突)。

坑:必须用本机正式 python3 建 venv;编辑器内置解释器常缺 venv 模块。

cd /home/rain/Documents/k8s_ins/env-monitor-deploy
python3 -m venv .venv
.venv/bin/python -c "import sysconfig,pathlib; p=pathlib.Path(sysconfig.get_paths()['purelib'])/'py_public_modules.pth'; p.write_text('/home/rain/py_public_modules',encoding='utf-8')"
.venv/bin/python -m pip install -r requirements.txt
# 验证挂载:应输出 True 与共享库内模块真实路径
.venv/bin/python -c "import sys,importlib.util as u; t='/home/rain/py_public_modules'; print(t in sys.path, u.find_spec('config_module').origin)"

预期:True /home/rain/py_public_modules/config_module.py。 失败判断:输出 False 或 None → 挂载未生效,不要在工程内重建公共模块, 先排查 .pth 路径。

步骤 2:拉取社区大盘制品(固定 revision)

1860 大盘是 468 KB 的 JSON,从 grafana.com 拉取后以 ConfigMap 交给 Grafana sidecar 导入 (集群侧不通外网,不能由 Grafana 自行下载)。

坑:必须固定 revision(本工程用 45),用 latest 会让制品随时间漂移, sha256 校验也就失去意义。

cd /home/rain/Documents/k8s_ins/env-monitor-deploy
curl -fsSL -o artifacts/dashboards/node-exporter-full-1860-r45.json \
  https://grafana.com/api/dashboards/1860/revisions/45/download
python3 -c "import json;d=json.load(open('artifacts/dashboards/node-exporter-full-1860-r45.json'));print(d['title'],d['uid'],len(d['panels']))"
sha256sum artifacts/dashboards/node-exporter-full-1860-r45.json | tee artifacts/dashboards/SHA256SUMS

预期:Node Exporter Full rYdddlPWk 31;sha256 为 184c6b7409f306da75525d7772f71945b10cea23ad16b5d78c4698ea0ea51986。 失败判断:HTTP 非 200 或 uid 不是 rYdddlPWk → 制品来源存疑,停止后续步骤。

步骤 3:变更前预检查(只读)

目标机侧逐台确认:SSH、OS 版本、包与包源、gpgcheck、端口占用、SELinux、firewalld、 时钟、磁盘;集群侧确认 kubectl/helm/命名空间/release,并推导防火墙放行源网段。

坑:源网段必须由 Calico IPPool 推导(本环境实测 10.244.0.0/16), 不能用 node.spec.podCIDR(它写着每节点 /24,与真实 Pod 分布不符); 推导失败时实现体拒绝执行,不会退化成全开。

# 逐台基础事实(示例 .16)
timeout 20 ssh -o BatchMode=yes -o ConnectTimeout=4 root@192.168.31.16 \
  "cat /etc/os-release | grep -E '^(ID|VERSION_ID)='; uname -m; getenforce; firewall-cmd --state"
# 包与签名校验
timeout 200 ssh root@192.168.31.16 \
  "rpm -q node-exporter; rpm -q epel-release; grep -i ^gpgcheck /etc/dnf/dnf.conf; dnf -q list --available node-exporter"
# 端口占用
timeout 20 ssh root@192.168.31.16 "ss -lntH | grep -c ':9100' || true"
# 抓取源网段(推导依据)
kubectl --kubeconfig /home/rain/.kube/config get ippools.crd.projectcalico.org \
  -o jsonpath='{.items[*].spec.cidr}'

预期:BLOCK=0,报告落 reports/precheck-<时间戳>.md。 失败判断:出现任何 BLOCK → 先按报告的修复建议处理,再重跑本步;不要带病进入步骤 5。

步骤 4:预演(dry-run,零改动)

cd /home/rain/Documents/k8s_ins/env-monitor-deploy
.venv/bin/python main.py deploy --dry-run

预期:日志逐台打印「将执行」的动作清单,系统状态零变化 (可用步骤 3 的探测命令对照确认)。 失败判断:dry-run 期间发现任何实际写入(远端文件、端口、systemd 状态变化)→ 实现体缺陷,停止并修复。

步骤 5:单台试点(关键:实测 SELinux 行为)

这一步不可跳过。 本环境 8 台为 SELinux=Enforcing,且实测 9100 已被标签为 hplip_port_t(不是 http_port_t)—— 与 16443 事故同源的风险点。 9100 到底能否被 node-exporter 绑定,只有实测能下结论。

在 .16 上执行步骤 5.1–5.5:

5.1 补仓与装包(.21 / .22 / .23 / .24 / .29 需先补 epel-release)

ssh root@192.168.31.16 "dnf install -y node-exporter"

预期:Complete!,退出码 0;已装时报 Nothing to do(幂等跳过)。 失败判断:Error: Unable to find a match → 该机缺 EPEL,先 dnf install -y epel-release。

5.2 写入 systemd drop-in(高危:覆盖既有配置)

用 drop-in 而非改包自带的文件,避免依赖包的文件布局,ExecStart= 先清空再重设。

坑(实测踩到,两处): 1. 单元名必须是 prometheus-node-exporter.service。EPEL 包同时提供 node_exporter.service(只是 alias)与真实单元 prometheus-node-exporter.service, 用错名字会报 Unit file … does not exist; 2. node_exporter 1.12 已无 cmdline collector,写 --no-collector.cmdline 会让进程启动即失败(该开关不在 --help 选项表里)。

ssh root@192.168.31.16 "install -d -m 0755 /var/lib/node_exporter/textfile /etc/systemd/system/prometheus-node-exporter.service.d"
ssh root@192.168.31.16 "cat > /etc/systemd/system/prometheus-node-exporter.service.d/10-env-monitor.conf <<'EOF'
# 由 env-monitor-deploy 生成,请勿手工修改
[Unit]
Description=Prometheus Node Exporter (managed by env-monitor-deploy)

[Service]
ExecStart=
ExecStart=/usr/bin/node_exporter --collector.systemd --collector.processes --collector.textfile --no-collector.wifi --no-collector.nvme --no-collector.rapl --collector.textfile.directory=/var/lib/node_exporter/textfile --web.listen-address=:9100
Restart=on-failure
RestartSec=5s
EOF"

预期:命令退出码 0。 失败判断:目录不可写(SELinux/权限)→ 检查 getenforce 与目标目录属主。

5.3 启用并启动(高危:启停服务)

ssh root@192.168.31.16 "systemctl daemon-reload && systemctl enable --now prometheus-node-exporter"
ssh root@192.168.31.16 "systemctl is-active prometheus-node-exporter; ss -lntH | grep ':9100'"

预期:active 且 LISTEN 0 4096 *:9100。 失败判断(本步是本变更最大的未知):若服务反复重启且端口不监听, 优先查 SELinux 是否拒绝绑定:

ssh root@192.168.31.16 "journalctl -u prometheus-node-exporter -n 30 --no-pager | grep -iE 'denied|bind|permission'; grep 'type=AVC' /var/log/audit/audit.log | tail -5"

若确为端口标签所拒,按实测结论处理(semanage port -a/-m -t http_port_t -p tcp 9100, 需先装 policycoreutils-python-utils),并把该操作回写进本 SOP 第 5.2 步 与本工程 agent 配置,不要只留在临时命令里。

5.4 防火墙放行(高危:改防火墙规则)

只放行抓取来源,不放宽其它来源。

坑(本次变更最大的设计错误,实测教训):放行来源不是 Pod 网段 10.244.0.0/16,而是K8s 各节点的 InternalIP。原因:Calico 的 IPPool 开启了 natOutgoing,Pod 访问集群外地址时会被 SNAT 成所在节点的 IP,目标机防火墙 看到的源是节点 IP。首次按 Pod 网段放行后,11 台启用 firewalld 的主机抓取 全部失败,只有未启用 firewalld 的 3 台成功。 故实现体按 kubectl get nodes 的 InternalIP 逐台生成 rich rule。

# 取的来源(实测:192.168.31.10 / .13 / .14 / .15,逐条放行)
kubectl --kubeconfig /home/rain/.kube/config get nodes \
  -o jsonpath='{range .items[*]}{.status.addresses[?(@.type=="InternalIP")].address}{"\n"}{end}'

# 逐条放行(以 .10 为例;实现体对 4 个来源各生成一条)
ssh root@192.168.31.16 "firewall-cmd --permanent --add-rich-rule='rule family=\"ipv4\" source address=\"192.168.31.10\" port port=\"9100\" protocol=\"tcp\" accept'"
ssh root@192.168.31.16 "firewall-cmd --permanent --add-rich-rule='rule family=\"ipv4\" source address=\"192.168.31.13\" port port=\"9100\" protocol=\"tcp\" accept'"
ssh root@192.168.31.16 "firewall-cmd --permanent --add-rich-rule='rule family=\"ipv4\" source address=\"192.168.31.14\" port port=\"9100\" protocol=\"tcp\" accept'"
ssh root@192.168.31.16 "firewall-cmd --permanent --add-rich-rule='rule family=\"ipv4\" source address=\"192.168.31.15\" port port=\"9100\" protocol=\"tcp\" accept'"
ssh root@192.168.31.16 "firewall-cmd --reload && firewall-cmd --list-rich-rules | grep 9100"

预期:输出 4 条针对 9100/tcp 的 rich rule。 说明:.23/.24 的 firewalld 未运行、.25 为 failed(既有异常), 这三台无包过滤,实现体会跳过本步并留日志。

5.5 试点验证

kubectl --kubeconfig /home/rain/.kube/config apply -f logs/rendered-manifests.yaml   # 先接入抓取
kubectl --kubeconfig /home/rain/.kube/config exec -n monitoring \
  $(kubectl --kubeconfig /home/rain/.kube/config get pods -n monitoring -l app.kubernetes.io/name=prometheus -o jsonpath='{.items[0].metadata.name}') \
  -c prometheus -- wget -qO- --post-data='query=up{instance="slurm-master-01:9100"}' \
  http://localhost:9090/api/v1/query

预期:返回 "value":["<ts>","1"]。 失败判断:value 为 0 → 抓取不通,回到 5.3/5.4 查端口与防火墙。

试点通过后才进入步骤 6。

步骤 6:分批全量主机侧

按 ops.batch_size(默认 5 台)分批推进;批内并发 batch_max_workers; 单批失败台数达到 failure_threshold(默认 1)即停止后续批次并回报失败主机。 每台的动作与步骤 5.1–5.4 完全一致(同一实现,不分叉)。

cd /home/rain/Documents/k8s_ins/env-monitor-deploy
.venv/bin/python main.py deploy

预期:日志按批打印,每台出现「收敛完成」;结束状态 success。 失败判断:出现 FAILED → 按日志定位该台(常见:yum 锁冲突、EPEL 不可达、SELinux 拒绝绑定), 修复后直接重跑(幂等,已达标主机会被跳过)。

步骤 7:集群侧接入

清单由配置渲染(14 台端点 + 每台三条 relabel 规则把 instance 规整为主机名、 并补 cluster / role 标签),随后下发;Alertmanager 用渲染后的 overlay values 更新。

大盘由 dashboards/ 精简源经 main.py build-dashboards 生成(唯一真源是精简源, 详见附录 G.9);manifests/ 里的大盘 YAML 是生成物,不得手改—— pre_check 会校验二者一致性,不一致即 BLOCK。

# 7.1 渲染并下发抓取/告警/大盘清单(渲染产物见 logs/rendered-manifests.yaml)
kubectl --kubeconfig /home/rain/.kube/config apply -f logs/rendered-manifests.yaml

# 7.2 下发社区大盘 1860 的 ConfigMap(label 驱动 sidecar 导入)
kubectl --kubeconfig /home/rain/.kube/config create configmap node-exporter-full-1860 \
  -n monitoring --from-file=node-exporter-full-1860.json=artifacts/dashboards/node-exporter-full-1860-r45.json \
  --dry-run=client -o yaml | kubectl --kubeconfig /home/rain/.kube/config apply -f -

# 7.3 更新 Alertmanager 邮件路由(授权码由实现体注入临时 600 文件,不落明文)
#     先导出 live values 作为回滚基线:
helm --kubeconfig /home/rain/.kube/config get values kube-prometheus-stack -n monitoring -o yaml \
  > backup/cluster/kube-prometheus-stack-values.live.yaml.bak.$(date +%Y%m%d-%H%M%S)
helm --kubeconfig /home/rain/.kube/config upgrade kube-prometheus-stack \
  ../prometheus_deploy/charts/kube-prometheus-stack-91.4.1.tgz \
  -n monitoring -f backup/cluster/kube-prometheus-stack-values.live.yaml.bak.<时间戳> \
  -f <渲染后的 Alertmanager values> --no-hooks --timeout 15m

坑:--no-hooks 不可省。chart 的 pre-upgrade hook 需拉 ghcr.io/jkroepke/kube-webhook-certgen,而集群侧 ghcr.io 不可达,会一直卡到 --timeout 失败(实测)。 预期:STATUS: deployed;Prometheus Operator 随后重建 Alertmanager 配置 (实测 reload 需 1~2 分钟,实现体用轮询等待而非固定 sleep)。

步骤 8:幂等复跑(验收硬条件)

cd /home/rain/Documents/k8s_ins/env-monitor-deploy
.venv/bin/python main.py deploy

预期:第二次执行零变更——14 台全部「已就绪,跳过」,集群侧全部「已存在,跳过」, Alertmanager 判定为「已是最新」。 失败判断:出现任何变更动作 → 幂等判据不完整,先定位再继续。

6. 验证

# 验证项 命令 预期
1 抓取目标数 kubectl ... exec -n monitoring <prom-pod> -c prometheus -- wget -qO- --post-data='query=count(up{job=~"external-node-exporters.*"} == 1)' http://localhost:9090/api/v1/query 14
2 标签规整 同上,query 换成 count(up{job=~"external-node-exporters.*", cluster="slurm"}) 5
3 告警规则已加载 kubectl ... get prometheusrule env-host-baseline-alerts -n monitoring 存在
4 大盘已导入 Grafana → Dashboards → 搜「环境总览」与「Node Exporter Full」 均可打开且变量有值
5 Alertmanager 路由 kubectl ... get secret alertmanager-kube-prometheus-stack-alertmanager-generated -n monitoring -o jsonpath='{.data.alertmanager\.yaml}' \| base64 -d \| grep env-monitor-email 两个 receiver 名均出现
6 邮件可投递 临时构造一条 category="host" 的测试告警,或等待真实告警 收到 163 邮件
7 主机侧五要素 .venv/bin/python main.py verify 全部通过
8 漂移检测 .venv/bin/python main.py drift,退出码应为 0 无漂移
9 幂等 步骤 8 零变更

7. 回滚

备份位置与台账

  • 目录:env-monitor-deploy/backup/
  • 台账:backup/README.md(逐条登记:备份文件、原路径 / 主机、产生时间、因何产生、回滚什么、何时可删)
  • 集群侧:backup/cluster/kube-prometheus-stack-values.live.yaml.bak.<时间戳>(helm live values) 与 alertmanager-config.yaml.bak.<时间戳>(改前 Alertmanager 配置快照)
  • 主机侧:drop-in 若原本已存在,会在目标机原位留 <原文件>.bak.<时间戳>(就地回滚,不跨机搬运), 其位置已在 backup/README.md 中登记

回滚步骤

cd /home/rain/Documents/k8s_ins/env-monitor-deploy
# 1) 逆序回滚:先撤集群侧抓取,再撤主机侧采集(避免抓取一个已停采集的主机)
.venv/bin/python main.py rollback

等价的手工步骤(集群侧):

kubectl --kubeconfig /home/rain/.kube/config delete -f logs/rendered-manifests.yaml --ignore-not-found
kubectl --kubeconfig /home/rain/.kube/config delete configmap node-exporter-full-1860 env-monitor-overview -n monitoring --ignore-not-found
helm --kubeconfig /home/rain/.kube/config upgrade kube-prometheus-stack \
  ../prometheus_deploy/charts/kube-prometheus-stack-91.4.1.tgz -n monitoring \
  -f backup/cluster/kube-prometheus-stack-values.live.yaml.bak.<时间戳> --no-hooks --timeout 15m

等价的手工步骤(主机侧,以 .16 为例;高危,会卸载软件包):

ssh root@192.168.31.16 "systemctl disable --now prometheus-node-exporter"
ssh root@192.168.31.16 "rm -f /etc/systemd/system/prometheus-node-exporter.service.d/10-env-monitor.conf && systemctl daemon-reload"
ssh root@192.168.31.16 "firewall-cmd --permanent --remove-rich-rule='rule family=\"ipv4\" source address=\"10.244.0.0/16\" port port=\"9100\" protocol=\"tcp\" accept' && firewall-cmd --reload"
ssh root@192.168.31.16 "rm -rf /var/lib/node_exporter/textfile"
ssh root@192.168.31.16 "dnf remove -y node-exporter"

回滚后验证

  1. count(up{job=~"external-node-exporters.*"}) == 0(若已删除 ServiceMonitor 则该 job 直接消失)。
  2. kubectl get servicemonitor,configmap -n monitoring | grep -E 'external-node-exporters|env-monitor' 无输出。
  3. 目标机 ss -lntH | grep ':9100' 无监听。
  4. Alertmanager 配置回到改动前内容(与 alertmanager-config.yaml.bak.<时间戳> 比对)。

注意:dnf remove node-exporter 这一步不可逆到「未安装」之前的状态 (重装会重新拉包);如需完全复原请重跑 deploy。

8. 风险说明

# 风险 影响 缓解 残余风险
1 SELinux 拒绝绑定 9100 8 台 Enforcing 主机上服务起不来 单台试点实测(步骤 5.3);备选方案 semanage port -t http_port_t -p tcp 9100 9100 现属 hplip_port_t,改标签会影响 hplip(本环境未使用打印机服务),改动前需确认
2 防火墙误放行 端口暴露面扩大 源网段由 Calico IPPool 推导,只放 9100/tcp,不给 any 推导出的 10.244.0.0/16 若与实际不同会漏抓(预检查已交叉核对 Prometheus Pod IP)
3 5 台缺 epel-release 装包失败 实现体先补 epel-release;失败即停该批 依赖公网可达(实测可)
4 包来源被替换 装上非预期软件 预检查强制 gpgcheck=1;大盘制品固定 revision + sha256 校验 无
5 .25 firewalld failed(既有) 该机无包过滤 判 WARN 并记录,不在本变更修复 安全基线问题遗留,已登记
6 批次中途失败 半程状态 分批 + 失败阈值 + 立即停止;已成功主机重跑幂等跳过 需人工介入失败主机
7 helm upgrade 卡死 变更超时 固定使用 --no-hooks(ghcr.io 不可达);用本地 tgz 免拉取 若日后恢复 ghcr.io,可去掉该参数
8 TSDB 容量增长 写满 PVC 实测当前 head series 76,135 / 采样 2,680 samples/s / TSDB 4.02 GiB(PVC 100 GiB);预计 +15~22%,余量充足 若后续 P1 再叠加多个 exporter,需重做容量预算
9 Alertmanager 路由误配 告警噪声或漏报 改前备份 live values;info 级不发信;主机失联时抑制衍生告警 Alertmanager 原生不支持「工作时段/日报」,P3 以「不发信 + 大盘可见」实现
10 helm upgrade 漏传现场 values 基线 其余配置回落 chart 默认值,触发全栈重建(本次已实际发生) 实现体强制要求基线:-f <现场 values> -f <overlay>,缺基线直接拒绝执行;改前导出基线并脱敏 见 INCIDENT-20260930-monitoring-fullstack-rollout.md
11 Grafana PVC 被删 面板与 Grafana 内部状态丢失 基线里保留 persistence.enabled: true;改前备份 NFS 数据目录 本次已发生并回填恢复
12 k8s-node-02 上新 Pod 挂 NFS 卷卡在 Init:0/1 Pod 长时间不可用 用 manifests/06-grafana-node-pin.yaml 把 Grafana 钉到 k8s-node-03 规避 已调查未复现(复现实验 11 秒通过、无脏状态残留,判为当时临时状态);排除清单与预防建议见 node02-init-stuck-20260930/20260930-k8s-node-02-Pod卡Init故障报告.md
13 v1 Endpoints 在 K8s v1.33+ 已弃用 未来版本移除后抓取失效 当前可用;升级到移除该 API 的版本前,需改用 EndpointSlice kubectl 会打印 deprecation 警告

9. 附录

A. 交付物清单

路径 说明
main.py、deployer.py 入口与编排层
modules/host_agent.py 主机侧收敛模块
modules/k8s_ingest.py 集群侧接入模块
utils/ 命令层、配置校验、预检查、异常、大盘生成器(dashboard_builder.py)
utils/probe/ 变更前的前置只读实测脚本(留档 + 可复跑)
dashboards/ 大盘精简源(5 个环境大盘的唯一真源,容器见附录 G.9)
config/env-monitor.yaml 配置唯一真源(受管主机、包、防火墙、集群侧、告警、批次)
manifests/ 抓取清单模板、告警规则、Alertmanager values 模板;大盘 YAML 为生成物
artifacts/dashboards/ 社区大盘制品(固定 revision)与 SHA256SUMS
reports/ 预检查报告、漂移报告、前置实测原始输出
backup/ 备份与 README.md 台账
docs/SOP.md、docs/RUNBOOK.md 本文件与运行手册

B. 基线数据来源

  • 覆盖率、包与包源、firewalld、SELinux、9100 标签:reports/probe-*.md、reports/probe-ne-service-*.txt、reports/probe-pkg-source-*.txt(2026-09-29)
  • Pod 网段:kubectl get ippools.crd.projectcalico.org -o jsonpath='{.items[*].spec.cidr}' → 10.244.0.0/16
  • 抓取来源:kubectl get nodes -o jsonpath=…InternalIP → 192.168.31.10/.13/.14/.15(Calico SNAT 后的真实源)
  • Prometheus 容量:prometheus_tsdb_head_series、rate(prometheus_tsdb_head_samples_appended_total[5m])、prometheus_tsdb_storage_blocks_bytes

C. 实施期实测结论(均已验证,非推断)

结论 证据
SELinux 不阻止 node-exporter 绑定 9100 8 台 Enforcing 主机上服务均 active、9100 正常监听;EPEL 包以 User=prometheus 运行,未落入受限域。无需补 semanage 端口标签
9100 上原有的 hplip_port_t 标签未造成影响 同上;预检查里的该项预警按「只预警不下结论」设计,实测确认为无害
单台试点耗时约 9 秒(已装包时) .16 试点:success | 耗时: 8.60s
全量 13 台耗时约 100 秒 分 3 批、批内并发 5
幂等复跑零变更 主机侧 14 | 跳过 14;集群侧「渲染清单与实机一致」「Alertmanager 邮件路由已是最新」

D. 实施期事故

本次实施过程中发生一次 P1 级事故(监控栈全栈重建、Grafana PVC 被删), 已恢复并复盘:见 INCIDENT-20260930-monitoring-fullstack-rollout.md。

E. 执行性能记录(2026-09-30 提速改造)

先测量再优化:改造前的基线取自实测(precheck 计时 + 日志里每次运行的时间戳与 SSH 连接计数)。

指标 改造前 改造后 变化
precheck 耗时 136.8 s 10.9 s 12.6×
precheck 的 SSH 连接次数 126(14 台 × 9 次逐项探测) 14(每台 1 次合并采集) 9×
预检结论复用 无(每次 deploy 全量重跑) 命中缓存约 0.3 s 单轮省 ~11 s
幂等复跑 deploy 总耗时 ~26 s(含每次 11 s 预检) 16.2 s(预检复用) —
批次间隔等待 20 s × 2 = 40 s/轮 0(批内完成即继续) -40 s
校验等待上限 180 s + 240 s 45 s(纳管)+ 90 s(全 up)+ 120 s(AM) 最坏 -5 min

改动清单

  1. 预检每台只发一次 SSH:把 hostname/发行版/包/端口/SELinux/firewalld/时钟/磁盘 合并成一段脚本一次取回,判定改为本地纯函数(_collect_facts + _judge_*)。
  2. 预检主机间并发:线程池,度数与 ops.batch_max_workers 同源。
  3. 昂贵探测按需:dnf list --available 只在「包未装且 EPEL 已配」时才查 (原实现无条件查,曾与 dnf makecache 争锁阻塞 9 分钟)。
  4. 预检结论复用(T2):缓存指纹 = 配置 + 全部实现体源码的 sha256, TTL 默认 300 s(ops.precheck_cache_ttl_seconds),--refresh-precheck 强制重跑。
  5. 批次间不再固定 sleep:batch_pause_seconds 默认 0,批内全部完成即进入下一批。
  6. 变更阶段复用探测结果:_converge_host 改为只对缺失项发命令, 不再重查 RPM/SHA/服务状态/防火墙规则;已就绪的机器一条命令都不发。
  7. 校验按状态量收敛即退出:拆成「纳管」与「全部 up」两段,超时按实测收紧。
  8. 判定纠偏:port/9100 被本工程的 node-exporter 占用时降为 INFO (否则变更后每轮都刷 14 条噪声 WARN)。

仍未解决的性能点(下一轮):实测 每次远端命令会产生 2 次 SSH 握手 (一次复跑 14 台探测 = 28 次连接,约 13 s 中绝大部分是握手)。共享库 paramiko_module.connection_pool.get_connection 是「每次新建连接」的实现, 要彻底解决需引入连接复用;它被多个工程共用,改动须单独回归,故本轮未动。

F. 规范取舍说明(豁免登记)

规范项 本工程做法 理由
离线 bundle / 本地仓库 未做 目标环境 14 台均可访问 EPEL 与公网(实测),RPM 由 EPEL 官方仓库 + GPG 签名保证来源可信;离线仓库收益低于维护成本。按规范的「建议项」登记豁免
K8s 侧与主机侧同一工程 合并为一个工程 主机侧是主变更单元(主机型收敛);集群侧是本工程的幂等制品下发,非控制器式声明收敛。拆成两个工程会让门禁、幂等、回滚各自一套
分批执行 已做(默认 5 台/批,失败阈值 1) 本变更动作可重入,属于建议项;仍按建议做了分批与逐台可用性复核
Alertmanager 三级分级 落为两级 + P3 不发信 Alertmanager 原生不支持「工作时段」与「定时日报」,如需日报须另加定时组件,不在本变更范围

G. 增量变更记录

G.1 2026-09-30:补集群侧「工作负载长时间起不来」告警

背景:当天变更期间 Grafana 等 Pod 在 k8s-node-02 卡在 Init:0/1 超过 20 分钟, 当时没有任何告警,是靠人工在另一次变更里偶然发现。这类盲区必须用告警兜住, 否则下次仍要靠运气。故障本身结论为「已调查·未复现」,见 node02-init-stuck-20260930/20260930-k8s-node-02-Pod卡Init故障报告.md。

改了什么

文件 改动
manifests/07-env-cluster-baseline-alerts.yaml 新增 PrometheusRule env-cluster-baseline-alerts,2 条规则:EnvPodStuckPending(phase=Pending 持续 10 分钟)、EnvPodContainerCreateFailed(容器处于 ImagePullBackOff/CreateContainerError 等持续 10 分钟),标签 category=cluster、severity=warning
manifests/05-alertmanager-routing-values.yaml 路由新增 category = "cluster" 的 critical / warning 两条分支——原先只认 category = "host",不加则新告警只会「只存不发」;邮件主题前缀由「主机P1/P2」改为「P1/P2」
config/env-monitor.yaml resources.prometheus_rule_names(列表)取代单数写法;manifests 列表加入 07
modules/k8s_ingest.py validate 遍历全部规则名逐个检查;新增 expected_am_matchers()
utils/config_validate.py kubernetes.resources 的值允许为资源名列表

两个必须留痕的坑

  1. 只查 receiver 名会把「新增路由」误判为「已是最新」——receiver 名没变,但 matcher 变了。故 _alertmanager_needs_update() 增加matcher 文本比对: 从模板文件自动提取全部 matcher(形如 - 'category = "host"'),逐个确认出现在 Alertmanager 运行配置里(实测 operator 写入 Secret 的格式为 category = "host")。
  2. 配置校验会拦住列表写法——kubernetes.resources 原先只接受字符串,改列表后 门禁报 BLOCK 并终止(门禁工作正常)。已放开为「字符串或字符串列表」。

验证

# 两条规则都在
kubectl --kubeconfig /home/rain/.kube/config get prometheusrule -n monitoring -o name | grep env-
# 预期:env-cluster-baseline-alerts、env-host-baseline-alerts

# Prometheus 已加载新规则组
kubectl --kubeconfig /home/rain/.kube/config exec -n monitoring \
  $(kubectl --kubeconfig /home/rain/.kube/config get pods -n monitoring -l app.kubernetes.io/name=prometheus -o jsonpath='{.items[0].metadata.name}') \
  -c prometheus -- wget -qO- 'http://localhost:9090/api/v1/rules?type=alert'
# 预期:出现 env-cluster-pod-stuck 组,含 EnvPodStuckPending / EnvPodContainerCreateFailed

实测结果:两条规则均已下发;Prometheus 已加载 env-cluster-pod-stuck 组; Alertmanager 运行配置出现两条 category = "cluster" 路由;幂等复跑退出码 0 (复用预检结论、渲染清单一致、AM 已是最新、主机侧 14 台全跳过)。

回滚

# 1) 删除新增的规则对象
kubectl --kubeconfig /home/rain/.kube/config delete prometheusrule env-cluster-baseline-alerts -n monitoring
# 2) 用备份覆盖被改的四个文件(见 backup/README.md 的 2026-09-30 条目)
cp backup/manifests/05-alertmanager-routing-values.yaml.bak.<TS> manifests/05-alertmanager-routing-values.yaml
cp backup/config/env-monitor.yaml.bak.<TS> config/env-monitor.yaml
cp backup/modules/k8s_ingest.py.bak.<TS> modules/k8s_ingest.py
# 3) 重跑 deploy 以恢复 Alertmanager 路由(删掉 cluster 分支)
.venv/bin/python main.py deploy --refresh-precheck

G.3 2026-09-30:P1 深化(EDA 运营视角与 FlexLM 数据修复)

变更结论:本轮在既有 P1 基础上深化 Slurm 与 FlexLM,不改变调度器、许可服务或作业状态。

  • Slurm 采集器新增低基数聚合:Pending 原因、用户/Account 作业数;不输出 JobID,避免 Prometheus 高基数。
  • Slurm 大盘新增 Pending 原因、用户/Account 分布面板;新增 Pending 原因持续升高告警。
  • FlexLM 对计数型 feature 输出 license_type="counted" 的 issued/in_use;Uncounted、node-locked 只输出 feature 存在性,不伪造容量。
  • FlexLM 占用率、明细表和耗尽告警统一限定 license_type="counted",避免将不可计量 feature 当作可用许可。
  • 实机核验:.29 的 Synopsys 2700 与 Cadence 5280 均 server/vendor daemon UP;Prometheus 查询 flexlm_up=1;Slurm 两个控制节点 slurm_up=1;外部抓取目标 14/14 UP。
  • 幂等复跑:第二次 deploy 集群侧清单状态一致并跳过,主机侧无非预期变更。

限制与后续:本轮已补充许可拒绝累计计数与 Slurm 当前作业等待时延;统计均依赖当前日志/队列可见窗口,不能替代完整历史作业审计。FairShare/QOS/TRES、License 与作业关联需在取得稳定历史基线后继续补充。

G.4 2026-09-30:P1 深化补充(FlexLM denied 与 Slurm 调度等待)

  • FlexLM 采集器新增 --log VENDOR=PATH,读取 .29 两个 lmgrd.log,按拒绝事件关键字累计输出 flexlm_license_denied_total{vendor};当前实测 Synopsys/Cadence 均为 0。
  • 新增 EnvFlexlmLicenseDenied 告警:15 分钟窗口出现新增拒绝事件并持续 5 分钟时告警。
  • Slurm squeue 增加 %V 提交时间和 %S 启动时间,输出 slurm_pending_wait_seconds(当前 Pending 作业等待)与 slurm_dispatch_wait_seconds(运行作业提交到启动) 的 avg/max。
  • 新增 Slurm 等待时延看板与 30 分钟 Pending 最大等待、10 分钟平均调度等待告警。
  • 变更不触碰作业提交/取消、调度器、license 服务或日志文件;第二次 deploy 集群侧清单状态一致并跳过。

G.2 2026-09-30:P1 四领域指标接入(Slurm / Lustre / LSF / FlexLM)

背景与目标:监控体系蓝图(docs/MONITORING-ROADMAP.md)的 P1 要求「加 Slurm、Lustre、 FlexLM exporter(LSF 生态封闭,用 textfile collector 解析 bhosts/bjobs 兜底);分配角色 看板 + 对应 PrometheusRule」。P0 只交付了主机层,领域指标全缺(缺口 G4)。本次把四个领域的 指标、角色看板与告警一次补齐。

选型决策:走 textfile collector,不部署专用 exporter(依据为实测,非推断)

依据 实测结论
制品可达性 集群侧 ghcr.io 不可达,专用 exporter 需额外离线制品;textfile 路线无需新制品
端口与防火墙 textfile 复用 node-exporter 的 9100,不新增端口、不改防火墙、不改集群侧抓取配置
原生查询命令 四领域均可用(路径见下表);14 台均有 python3
已知限制 Lustre 容量只能在挂载了客户端的节点取:.21/.22 是 MGS/OSS,无客户端视图,lfs df 无输出;.28 已挂载 /mnt/lustre

目标分布与命令

领域 主机(role) 主命令(绝对路径,均不在默认 PATH) 主要指标
slurm .16/.17(controller-primary/backup) /usr/local/slurm/bin/{sinfo,squeue,scontrol} slurm_up、slurm_partition_{nodes,cpus,avail}、slurm_jobs、slurm_ctld_up
lustre .21(mgs-mds)、.22(oss)、.28(submit) /usr/sbin/lctl、/usr/bin/lfs lustre_{mdt,ost}_exports、lustre_ost_ops_samples、lustre_fs_bytes
lsf .23/.24(master-primary/backup) /data/nfs/lsf/lsf/10.1/linux2.6-glibc2.3-x86_64/bin/{bhosts,bqueues,bjobs} lsf_host_{status,jobs}、lsf_queue_jobs、lsf_jobs_unfinished
flexlm .29(license-server) /opt/FlexLM/Synopsys/lmutil(通用工具;Cadence 目录无此文件) flexlm_server_up、flexlm_vendor_daemon_up、flexlm_license_{total,in_use}

变更点(工程内)

  • 新增 modules/domain_collectors.py:主机型变更单元,与 host_agent 并列;生产物为 「采集脚本 + systemd service + timer」,写 agent.textfile_dir 下的 .prom。 幂等判据四要素:脚本 sha256 / service sha256 / timer sha256 / timer active+enabled, 外加「指标文件已生成」。
  • 新增顶层配置段 collectors.<领域>(enabled / clusters / roles / script / remote_path / bin_dir / probe_cmd / interval_seconds,可选 extra_args)。
  • 新增制品 artifacts/collectors/{slurm,lustre,lsf,flexlm}_metrics.py。
  • 预检查新增判定:按 collectors.<领域> 的作用域判定「本节点是否应采该领域」,命令不可执行即 BLOCK(与变更阶段共用同一套作用域逻辑,避免两处漂移)。
  • 集群侧新增 manifests/08-env-slurm-dashboard.yaml、09-env-slurm-alerts.yaml、 10-env-lustre-dashboard.yaml、11-env-lsf-dashboard.yaml、12-env-flexlm-dashboard.yaml、 13-env-domain-alerts.yaml;config 的 manifests 与 resources.prometheus_rule_names 同步。

分步操作(与实现体等价;实现体:modules/domain_collectors.py)

# 1) 门禁:配置校验 + 环境预检查(含四领域命令可用性)
.venv/bin/python main.py precheck
# 预期:BLOCK=0;.16/.17 出现 collector/slurm「采集命令可用」,
#       .21/.22/.28 出现 collector/lustre,.23/.24 出现 collector/lsf,.29 出现 collector/flexlm

# 2) 预演(零改动)
.venv/bin/python main.py deploy --dry-run
# 预期:列出待变更目标与动作(确保 textfile 目录 → 传脚本 → chmod → 写 unit/timer
#       → daemon-reload → enable --now timer → 立即执行一次)

# 3) 真实变更(分批 + 批内并发;failure_threshold=1)
.venv/bin/python main.py deploy

# 4) 目标机核对产物(以 Lustre MDS 为例)
ssh root@192.168.31.21 'systemctl is-active env-lustre-metrics.timer'
ssh root@192.168.31.21 'cat /var/lib/node_exporter/textfile/lustre.prom'
# 预期:active;文件含 lustre_up 1 与 lustre_mdt_exports

# 5) 集群侧核对对象
kubectl --kubeconfig /home/rain/.kube/config -n monitoring get prometheusrule,cm | grep -E 'slurm|lustre|lsf|flexlm'
# 预期:4 个 PrometheusRule(env-slurm-alerts/env-domain-alerts 及既有)+ 4 个看板 ConfigMap

坑与注意事项(实现体已处理,此处留痕,便于复现与排障)

  1. 命令必须写绝对路径:Slurm 与 LSF 的二进制不在默认 PATH(由 /etc/profile.d 注入), 非交互 shell 取不到 —— 实测 command -v sinfo 在 bash -s 下为空。
  2. lctl get_param 不要加 -n:-n 只输出裸值(如 3),丢掉参数名后无法区分多个 target;不带 -n 时输出形如 mdt.edafs-MDT0000.num_exports=3,才能解析出 target 名。
  3. 三个采集脚本的参数接口必须统一(--bin-dir / --output / --cluster,FlexLM 另有 --server):首次上线时 Lustre/FlexLM 脚本因缺 --bin-dir/--cluster 而 systemctl start 失败(unrecognized arguments),修正后通过。
  4. 同一集群多采集点须去重:Slurm 与 LSF 都在主备两台控制器上采同一集群视图,告警表达式 一律用 max by (...) 聚合,否则一次故障会因两个采样点而重复发信。
  5. 零作业时 squeue 无输出:脚本对 PENDING/RUNNING 补零值,避免大盘显示「无数据」而非 0。
  6. closed_Full 不是异常:.28 是提交节点,在 LSF 里正常显示 closed_Full; EnvLsfHostAbnormal 只匹配 unreach|unavail|unusable,避免恒定噪声。

验证(实测结论)

检查 结果
四领域指标经 9100 暴露 22 个指标名齐备(slurm 6 / lustre 4 / lsf 5 / flexlm 5,另含各自 _up)
Prometheus 抓取 up 14/14;slurm_up=1(2 实例)、lustre_up=1(3)、lsf_up=1(2)、flexlm_up=1(1)
告警规则加载 6 组共 20 条:env-host-(ExternalHost)/ env-cluster- / env-slurm- / env-lustre / env-lsf / env-flexlm
角色看板 4 个 ConfigMap:env-slurm / env-lustre / env-lsf / env-flexlm
verify 14 台主机「五要素」+ 8 个采集目标「四要素」全部满足
幂等复跑 14 + 8 + 3 全部「状态一致,无需变更」,零变更、退出码 0

顺带修复的两处 P0 遗留(同一轮变更内完成)

  1. Watchdog 告警此前落到默认 receiver null(等于放弃自监控心跳)——新增 env-monitor-email-p3 接收器与 alertname = "Watchdog" 路由,每日一报(repeat_interval: 24h)。
  2. inhibit_rules 的 source_matchers 写的是并不存在的 EnvHostDown,导致 「主机失联时抑制其衍生告警」从未生效 —— 已更正为实际告警名 ExternalHostNodeExporterDown(见 manifests/02-env-host-baseline-alerts.yaml)。

回滚

# 1) 主机侧:停用并删除四类采集器(timer / service / 脚本 / prom)
.venv/bin/python main.py rollback

# 2) 集群侧:删除本次新增的看板与告警对象
kubectl --kubeconfig /home/rain/.kube/config -n monitoring \
  delete prometheusrule env-slurm-alerts env-domain-alerts
kubectl --kubeconfig /home/rain/.kube/config -n monitoring \
  delete configmap env-slurm-dashboard env-lustre-dashboard env-lsf-dashboard env-flexlm-dashboard

# 3) 工程侧:用备份覆盖(见 backup/README.md 的 2026-09-30 P1 条目)
cp backup/docs/SOP.md.bak.<TS> docs/SOP.md
cp backup/config/env-monitor.yaml.bak.<TS> config/env-monitor.yaml

回滚触发条件(动手前量化):① verify 或幂等复跑不通过且修正 2 次仍不过;② 目标机 node-exporter 因 textfile 内容异常而抓取失败;③ 新告警规则造成邮件风暴(P1/P2 分级误配)。

G.3 2026-09-30:修正预检并发度配置失效(ops.batch_max_workers)

背景:attribution-split-20260930 的归因测量中,把配置副本的 ops.batch_max_workers 改成 1 后预检并发度毫无变化,据此查出该配置项从未生效——EnvMonitorPreCheck.__init__ 传入 section="precheck",基类 _cfg() 只读该段,而键定义在 ops: 段,取值处 self._cfg().get("batch_max_workers", 5) 恒回落默认值 5。使用侧与校验侧不同源: utils/config_validate.py 校验的正是 ops: 段的同名键。

改了什么

文件 改动
utils/precheck.py host_findings() 的并发度改从 config_data["ops"] 读取,与校验同源;其余逻辑与阈值不变

配置项位置不变(仍只在 ops: 段定义)。第 476 行「度数与 ops.batch_max_workers 同源」 的表述自本版本起才与代码相符,该行文字本身无需修改。

验证

cd /home/rain/Documents/k8s_ins/env-monitor-deploy
# 1) 造一份 workers=1 的配置副本:副本必须留在 config/ 下——project_root 由 config
#    路径上推两级得出,放 /tmp 会把 reports/ 指到根目录
sed 's/^  batch_max_workers: 5 /  batch_max_workers: 1 /' \
    config/env-monitor.yaml > config/env-monitor-w1.yaml
/usr/bin/time -f "workers=1: %e s" .venv/bin/python main.py precheck \
    --refresh-precheck --config config/env-monitor-w1.yaml
rm config/env-monitor-w1.yaml

# 2) 默认配置(workers=5)
/usr/bin/time -f "workers=5: %e s" .venv/bin/python main.py precheck \
    --refresh-precheck --config config/env-monitor.yaml

预期:workers=1 约 46 s(14 台逐台串行),workers=5 约 11 s(批内 5 并发); 两轮结论一致、BLOCK=0。

实测(2026-09-30 14:02–14:03):workers=1 → 47.62 s,workers=5 → 10.87 s, 与附录 E 的 B/C 组基线(46.20 s / 11.06 s)吻合;两轮均 BLOCK=0 WARN=9 INFO=149。 修复前该配置项取任何值耗时都不变(恒 5 并发)。

回滚:见第 7 章「备份位置与台账」,对应条目为 backup/README.md 的 2026-09-30 修复条目(两个 .bak.20260930-140113 文件)。

影响面:只读子命令 precheck 的线程池尺寸,不改变探测内容与判定阈值, 不涉及任何目标机与集群侧状态;deploy 的批次并发走 ops 段,不受本次影响。

G.5 2026-09-30:Slurm 大盘按 EDA 运营视角扩展(节点 / 调度器 / 会计 / 公平性)

背景:P1 的 Slurm 大盘只有「分区级快照 + 作业状态计数」,用户反馈过于单调。EDA 行业看 Slurm 的四个核心视角——排队体验、资源有效性(CPU 效率)、项目公平性(Account/FairShare)、 License 与作业的耦合——在旧盘中全部缺失。根因不在面板数量,而在采集面只调用了 sinfo / squeue / scontrol ping 三条命令,粒度只到分区聚合。

目标:不新增端口、不改抓取配置、不触碰调度器与作业状态,把采集面扩到节点级、调度器内部、 公平性与会计历史,并按 EDA 视角重排大盘。

数据源(均为只读查询命令,均已实测可用;Slurm 26.05.2、slurmdbd active)

命令 提供什么 新增指标(cluster 标签之外)
sinfo -N -h -o '%N\|%t\|%C\|%m\|%e\|%G' 逐节点状态 / CPU 四元组 / 内存 / GRES slurm_node_state、slurm_node_cpus、slurm_node_memory_mb、slurm_node_gres
squeue -h -o '…%M\|%l\|%Q\|%C\|%m' 运行时长、时限、优先级、所需 CPU/内存 slurm_running_job_{runtime,timelimit}_seconds、slurm_running_job_{cpus,memory_mb,priority}、slurm_running_jobs_by_runtime、slurm_pending_job_{cpus,memory_mb}
sdiag 调度器主循环、队列深度、backfill、License 阻塞、RPC 时延 slurm_sched_cycle_seconds、slurm_sched_queue_length、slurm_sched_cycles_per_minute、slurm_sched_blocked_on_licenses、slurm_sched_backfilled_jobs、slurm_sched_rpc_job_info_seconds
sshare -P Account 份额与用量 slurm_account_{norm_shares,raw_usage,fairshare}
sacct -P -X -a -S now-1hour 窗口内完成作业 slurm_finished_jobs_1h、slurm_failed_jobs_1h、slurm_finished_job_elapsed_seconds_1h、slurm_job_cpu_efficiency_1h
sreport cluster utilization -P -t hours 分状态 CPU 小时 slurm_cluster_cpu_hours_1h

改了什么

文件 改动
artifacts/collectors/slurm_metrics.py 新增 5 个采集段(节点 / 调度器 / 公平性 / 会计 / 利用率)与 --acct-window 参数;解析一律走 -P 结构化输出或逐键提取;原有 6 条指标的指标名、标签与语义保持不变
manifests/08-env-slurm-dashboard.yaml 由 8 个面板扩为 27 个,分 7 组:集群与调度器健康 / 队列压力与等待 / 调度器内部与 License 阻塞 / 作业运行与资源效率 / 节点与分区资源 / 公平性与账户 / 跨域联动(FlexLM 许可、Lustre 容量、集群利用率)

配置、systemd 单元、抓取链路与告警规则均未改动;slurm_up 的判定口径不变(仍是 sinfo + squeue)。

分步操作(与实现体等价;实现体:modules/domain_collectors.py)

cd /home/rain/Documents/k8s_ins/env-monitor-deploy

# 1) 门禁
.venv/bin/python main.py precheck
# 预期:BLOCK=0 WARN=9 INFO=149(与既有基线一致)

# 2) 预演(零改动)
.venv/bin/python main.py deploy --dry-run
# 预期:主机侧只列 .16/.17(采集脚本内容不符),其余 6 个采集目标「已就绪,跳过」;
#       集群侧 1 处待下发(08 大盘 ConfigMap)

# 3) 真实变更(主机侧分批 + 批内并发;集群侧 kubectl apply)
.venv/bin/python main.py deploy

# 4) 目标机核对(新指标是否落盘并经 9100 暴露)
ssh root@192.168.31.16 'ls -l /var/lib/node_exporter/textfile/slurm.prom'
ssh root@192.168.31.16 'curl -s http://127.0.0.1:9100/metrics | grep -oE "^slurm_sched_[a-z_]+" | sort -u'

# 5) 集群侧核对:Prometheus 收录 + 大盘对象
kubectl -n monitoring get --raw "/api/v1/namespaces/monitoring/services/kube-prometheus-stack-prometheus:9090/proxy/api/v1/label/__name__/values"
kubectl -n monitoring get cm env-slurm-dashboard -o jsonpath='{.data.env-slurm-dashboard\.json}' | head -c 200

坑与注意事项(实现体已处理,此处留痕)

  1. 同一节点在多个分区重复出现:sinfo -N 按「节点 × 分区」各输出一行(本环境 3 节点 × 4 分区 = 12 行), 不去重会写出重复 series,Prometheus 会拒收整段指标 → 按节点名去重后只保留一条。
  2. sdiag 没有结构化输出:只能逐键提取,且 Last cycle / Max cycle 在 「Main schedule statistics」与「Backfilling stats」两段中各出现一次 → 必须先按段切分, 否则会把 backfill 的 0 误当成主循环耗时。
  3. sdiag 的 cycle 单位是微秒:四舍五入到 4 位小数会把 17 µs 变成 0 → 数值统一保留 6 位小数, 大盘侧再乘 1e6 以 µs 显示。
  4. sshare -P 首行是表头,Account 行与 User 行混排:只取 User 列为空的行, 否则会把每个用户的用量写进 Account 维度(高基数)。
  5. sacct / sreport 依赖 slurmdbd(本环境 active):两段均带 20 s 超时, 任一段失败只跳过该段,不影响其余指标,也不改变 slurm_up 的既有语义。
  6. 零作业时 squeue 无输出:作业类指标统一走「无数据输出 0」分支,大盘显示 0 而非「无数据」。

验证(实测结论)

检查 结果
Prometheus 收录 slurm_ 指标 35 个(新增 24 个);采样 slurm_node_cpus{state="idle"} = 8 / 8 / 8
node-exporter 暴露 .16 的 slurm.prom 7944 B,env-slurm-metrics.timer active
集群侧大盘 ConfigMap 已更新为 27 面板 / 7 分组;Grafana sidecar 日志 Writing /tmp/dashboards/env-slurm-dashboard.json → dashboards/reload 200 OK
幂等复跑 14 主机 + 8 采集目标 + 3 集群侧对象全部「状态一致,无需变更」,退出码 0

回滚:见第 7 章「备份位置与台账」,对应条目见 backup/README.md 的 2026-09-30 EDA 大盘节(工程内制品 backup/artifacts/collectors/slurm_metrics.py.bak.20260930-221849、 backup/manifests/08-env-slurm-dashboard.yaml.bak.20260930-221849、 backup/docs/{SOP,RUNBOOK}.md.bak.20260930-222358);远端脚本备份由实现体自动登记 (.16 / .17 的 env-slurm-metrics.bak.*)。步骤:用备份覆盖上述三个工程内文件 → 重跑 deploy。

风险与残留

  • 作业类指标(slurm_running_job_*、slurm_pending_job_*)在空队列下只验证了 0 值分支: 字段解析(%M / %l / %Q / %C / %m)需在集群实际有作业时再复核一次; 字段本身已用 squeue --helpformat 确认存在。
  • slurm_account_fairshare 当前无输出:本环境各 Account 的 RawUsage 均为 0,sshare 的 FairShare 列为空,产生用量后才会出现。
  • sacct / sreport 每次采集都会查一次 slurmdbd。当前规模下实测无压力; 作业量大时可加大 --acct-window 或调大 collectors.slurm.interval_seconds。

G.6 2026-10-01:LSF 大盘按 EDA 运营视角扩展,并修复采集环境缺失缺陷

背景:本次有两条独立动因,第二条是扩展前实机取证才发现的。

  1. 用户反馈 LSF 大盘「过于单调」:P1 交付时只做了「主机状态 + 队列积压 + 未完成作业数」 共 7 个面板,与同期按 EDA 视角扩展过的 Slurm 大盘(27 面板 / 7 分组,见 G.5)明显不对等。
  2. LSF 领域指标此前从未真正采集成功。.23 上的 lsf.prom 内容是:

lsf_host_status{cluster="lsf",host="Oct",status="1"} 1 lsf_host_jobs{cluster="lsf",host="Oct",type="njobs"} 312346 lsf_host_status{cluster="lsf",host="lsb_init:",status="Failed"} 1

即 LSF 命令的初始化报错文本被当成主机表格解析了 —— host="Oct" 来自日期前缀 Oct 1 00:17:54 2026 ...,host="lsb_init:" 来自报错行本身。

根因(实测证据)

  • LSF 二进制依赖 LSF_ENVDIR / LSF_SERVERDIR 才能找到 lsf.conf;本环境这些变量由 /etc/profile.d/lsf.sh 注入,systemd 非登录环境取不到;
  • 缺失时 env -i <bin>/bhosts -w 实测 rc=255,并把 Oct 1 00:17:54 2026 312728 3 3.4.0 lsb_init: initenv_:fopen(/etc/lsf.conf) failed, No such file or directory. 打到 stdout;补上两个变量后 rc=0 且输出正常表格;
  • 旧采集器对 bhosts / bqueues 两段不检查返回码,只按「列数不小于 N」判断, 于是把报错行的 token 当成 host / status / njobs;
  • lsf_up 旧口径是「任一类指标可用」,被误置为 1 —— 指标停更与集群正常在盘上长得一样,正是 G.1 里强调过的那类盲区。

目标:把 LSF 采集面扩到与 Slurm 对等的 EDA 七视角,并让「采集失败」的判定真实可信。

数据源(均为只读查询命令,均已实机验证;LSF 10.1.0.12 Community Edition,集群 lsfce)

命令 提供什么 新增指标(cluster 标签之外)
lsclusters 集群状态与主备控制端 lsf_cluster_status、lsf_master_up、lsf_cluster_hosts
badmin showstatus 服务器 / 作业 / 用户 / CPU / 槽位计数、mbatchd lsf_sched_servers、lsf_sched_jobs、lsf_sched_users、lsf_sched_capacity、lsf_sched_estimation_{pending,done}、lsf_sched_jobinfo_cache_mb、lsf_sched_server_total、lsf_mbatchd_up
lsload 逐主机负载 lsf_host_load、lsf_host_util_percent、lsf_host_capacity_mb
bqueues -l 队列 FAIRSHARE 份额表 lsf_queue_share
busers all 用户 / 组作业数与限额 lsf_buser_jobs、lsf_buser_limit
bjobs -u all -o … 作业聚合(结构化,分隔符 |) lsf_jobs、lsf_pending_reason、lsf_jobs_{user,by_queue,by_project}、lsf_{running,pending}_job_*、lsf_{pending,dispatch}_wait_seconds、lsf_running_jobs_by_runtime
bacct -u all -C <窗口> 窗口内完成作业会计 lsf_finished_jobs_window、lsf_finished_jobs_throughput_window、lsf_job_window_{seconds,factor}、lsf_scheduler_utilization_window

改了什么

文件 改动
artifacts/collectors/lsf_metrics.py 新增 7 个采集段;_run 增加「初始化失败特征串」判定(命中即整体按失败处理);bhosts / bqueues / lsload 段增加状态列取值集合 + 数值列可解析双重行校验,从源头挡住报错文本;lsf_up 判定收紧为「主机表与队列表均解析成功」(与 slurm_up 同口径)。原有 5 个指标名与语义保持不变
manifests/11-env-lsf-dashboard.yaml 由 7 个面板重做为 30 个 / 7 分组,分组与 Slurm 大盘一一对应
manifests/14-env-lsf-alerts.yaml 新增:12 条 LSF 告警,分「可用性(6)」与「容量与等待(6)」两组
manifests/13-env-domain-alerts.yaml 删除原 env-lsf-alerts 组(3 条规则迁入 14 并扩充),只保留 Lustre / FlexLM,避免同名规则重复发信
modules/domain_collectors.py systemd 单元生成支持 extra_env,输出 Environment= 行
utils/config_validate.py 校验 collectors.<领域>.extra_env(键须为合法环境变量名、值为非空字符串)
config/env-monitor.yaml collectors.lsf.extra_env 注入 4 个 LSF 环境变量;manifests 加 14;resources.prometheus_rule_names 加 env-lsf-alerts

分步操作(与实现体等价;实现体:modules/domain_collectors.py)

cd /home/rain/Documents/k8s_ins/env-monitor-deploy

# 1) 门禁
.venv/bin/python main.py precheck
# 预期:BLOCK=0 WARN=9 INFO=149(与既有基线一致)

# 2) 预演(零改动)
.venv/bin/python main.py deploy --dry-run
# 预期:主机侧只列 .23/.24(脚本与单元内容不符),其余 6 个采集目标「已就绪,跳过」;
#       集群侧 1 处待下发

# 3) 真实变更(主机侧分批 + 批内并发;集群侧 kubectl apply)
.venv/bin/python main.py deploy

# 4) 目标机核对:单元已注入环境变量、产物不再是垃圾
ssh root@192.168.31.23 'systemctl cat env-lsf-metrics.service | grep -E "Environment=|ExecStart"'
ssh root@192.168.31.23 'grep -cE "host=\"(Oct|lsb_init)" /var/lib/node_exporter/textfile/lsf.prom'
# 预期:4 条 Environment=;第二条输出 0

# 5) 集群侧核对
kubectl --kubeconfig /home/rain/.kube/config -n monitoring get prometheusrule env-lsf-alerts
kubectl --kubeconfig /home/rain/.kube/config -n monitoring get cm env-lsf-dashboard -o jsonpath='{.data.env-lsf-dashboard\.json}' | head -c 200

坑与注意事项(实现体已处理,此处留痕)

  1. LSF 命令必须注入环境变量:LSF_ENVDIR / LSF_SERVERDIR(另加 LSF_LIBDIR / LSF_BINDIR)——只给 --bin-dir 不够,这是本次缺陷的根因。
  2. 只看返回码也不够:故同时按行结构过滤(bhosts / lsload 的状态列必须命中 ok|closed|unavail|unreach|unusable|busy|full 前缀),双保险。
  3. bjobs -o 必须显式指定分隔符:pend_reason 的值含空格(如 Waiting for a host), 按空白切列会错位;用 delimiter="|" 后按 | 解析。字段名已实测合法(非法字段会立即报错)。
  4. bacct 的时间格式是 MM/DD/HH:MM:now-1h 一类相对式会被 Bad time specification 拒绝(实测);窗口内无完成作业时输出 No job was found,脚本按 0 输出。
  5. bqueues -l 的 FAIRSHARE 表按队列分布:SHARE_INFO_FOR: <queue>/ 后跟一张用户表, 本环境只有启用 FAIRSHARE 的队列才有该表;用户数会放大基数,脚本设 500 行上限。
  6. badmin showstatus 无结构化输出:按缩进分段解析(顶格 / 4 空格为段头,8 空格为子项), 否则会把 Number of users 与 Number of user groups 混入作业段。
  7. 同一集群被两台控制端各采一份:大盘与告警一律 max by(...) 去重。
  8. closed_Full 不是异常:.28(hpc-submit)是提交节点,EnvLsfHostAbnormal 只匹配 unreach|unavail|unusable。

验证(实测结论)

检查 结果
目标机单元 .23 / .24 的 env-lsf-metrics.service 含 4 条 Environment=
产物变化 lsf.prom 1624 B → 17433 B;垃圾序列(host="Oct" / host="lsb_init:")计数 0
Prometheus 收录 lsf_ 指标 36 个(原 6 个);lsf_up{instance} 两实例均为 1
数值抽查 lsf_sched_servers{state="ok"}=5、closed=1;lsf_host_status{host="hpc-submit",status="closed_Full"}=1;lsf_queue_share{queue="normal",user_group="rain01",metric="priority"}=0.333;lsf_sched_jobinfo_cache_mb=64.26 —— 均与命令行实测一致
集群侧大盘 ConfigMap 已更新为 37 个面板(含 7 个分组行)/ 7 分组
告警规则 env-lsf-alerts 已下发,2 组 12 条;13-env-domain-alerts 仅剩 Lustre / FlexLM 共 6 条;无重名
幂等复跑 14 主机 + 8 采集目标 + 3 集群侧对象全部「状态一致,无需变更」,退出码 0

回滚:见第 7 章「备份位置与台账」,对应条目见 backup/README.md 的 2026-10-01 LSF EDA 大盘节(工程内制品 backup/artifacts/collectors/lsf_metrics.py.bak.20261001-001541、 backup/manifests/11-env-lsf-dashboard.yaml.bak.20261001-001541、 backup/config/env-monitor.yaml.bak.20261001-001541、 backup/docs/SOP.md.bak.20261001-001541、backup/docs/RUNBOOK.md.bak.20261001-001541、 backup/docs/HANDOFF.md.bak.20261001-002642(根级 docs/HANDOFF.md 的备份,原位在此目录)); 远端脚本备份由实现体自动登记 (.23 / .24 的 env-lsf-metrics.bak.*)。步骤:用备份覆盖上述工程内文件 → kubectl delete prometheusrule env-lsf-alerts → 从备份恢复 13 中的 LSF 组 → 重跑 deploy。

风险与残留

  • 作业类指标(lsf_running_job_*、lsf_pending_job_*、lsf_pending_reason、等待时延)在 空队列下只验证了 0 值分支:bjobs -o 的字段名已实测合法,但字段值的解析 (时长 [D-]HH:MM:SS、时间 MMM DD HH:MM、内存 14.8G)需在集群实际有作业时复核一次;
  • lsf_queue_share 只在启用 FAIRSHARE 的队列有数据;
  • bacct 依赖 mbatchd 事件日志,历史被清理后窗口内可能无数据(按 0 输出,不报错);
  • 采集脚本单次执行增加 5 条只读命令(lsclusters / badmin / lsload / busers / bacct), 实测单次仍在秒级,interval_seconds 未变(60s),对目标机压力可忽略。

G.7 2026-10-01:Lustre 大盘按 EDA 运营视角扩展(存储七视角)

背景:用户反馈 Lustre 大盘「过于单调」:P1 交付时只有 7 个面板 / 2 分组(采集状态、 MDT/OST 连接数、客户端容量、OST 操作速率),与同期已按 EDA 视角扩展的 Slurm(27 面板 / 7 分组, 见 G.5)和 LSF(30 面板 / 7 分组,见 G.6)明显不对等。根因不在面板数量,而在采集面: 旧脚本只调用 3 条命令(mdt.*.num_exports、obdfilter.*.num_exports/stats、lfs df -h), 既无元数据侧性能、无 grant/驱逐等写入压力面,也无磁盘层 IO 与客户端体验。

目标:不新增端口、不改抓取配置、不触碰 Lustre 服务与文件系统,把采集面扩到与 Slurm / LSF 对等的 EDA 七视角,并按同一七分组结构重排大盘。

数据源(均为只读查询命令,均实机验证;Lustre 2.17.0,文件系统 edafs)

命令 提供什么 新增指标(cluster 之外的标签)
lctl get_param mdt.*.md_stats 元数据操作次数与累计时延 lustre_mdt_md_ops_total、lustre_mdt_md_time_us_total
lctl get_param mdt.*.eviction_count 客户端驱逐累计 lustre_mdt_evictions
lctl get_param osd-ldiskfs.*.{kbytes,files}{free,total} MDT 容量与 inode(注意不在 mdt.* 层) lustre_mdt_capacity_kbytes、lustre_mdt_inodes
lctl get_param obdfilter.*.stats OST 操作次数与累计时延 lustre_ost_op_time_us_total
lctl get_param obdfilter.*.eviction_count 客户端驱逐累计 lustre_ost_evictions
lctl get_param obdfilter.*.tot_{granted,dirty,pending} 写入授权 / 脏页 / 受阻字节 lustre_ost_grant_bytes
lctl get_param obdfilter.*.{kbytes,files}{free,total} OST 容量与 inode(服务端自查,不依赖客户端) lustre_ost_capacity_kbytes、lustre_ost_inodes
lctl get_param osd-ldiskfs.*.brw_stats 磁盘层 IO 分桶分布(大小 / 延迟 / 队列 / 块映射) lustre_ost_disk_io_ios
lfs df -h / lfs df -i 客户端视角容量与 inode lustre_fs_bytes(保留)、lustre_fs_inodes
lctl get_param llite.*.stats 客户端文件系统操作次数与延迟 lustre_client_ops_total、lustre_client_op_time_us_total
lctl get_param mdc.*.md_stats 客户端元数据操作次数 lustre_mdc_ops_total、lustre_mdc_op_time_us_total
lctl get_param osc.*.{resend_count,cur_grant_bytes,cur_dirty_bytes} RPC 重传与客户端 grant 使用 lustre_client_rpc_resend、lustre_client_grant_bytes
lctl dl 客户端设备状态(mgc/lov/lmv/mdc/osc) lustre_client_device_up

改了什么

文件 改动
artifacts/collectors/lustre_metrics.py 由 4 个指标扩为 22 个;新增 10 个采集段;新增 _split_blocks(按 <path>.md_stats= 切块)、_scalars、_stats_rows、_emit_brw 等解析器;lustre_up 判定收紧为「识别出角色并取到该角色核心数据」。原有 4 个指标名与语义保持不变(含 lustre_ost_ops_samples 对全部 op 计数、不区单位的旧口径)
manifests/10-env-lustre-dashboard.yaml 由 7 面板 / 2 分组重做为 35 面板(含 7 分组行)/ 7 分组:服务与采集健康 / 容量与水位 / 元数据性能(MDS) / 数据 IO 性能(OSS) / 磁盘层 IO(OSD brw_stats) / 客户端体验 / 跨域联动
manifests/13-env-domain-alerts.yaml Lustre 告警由 2 条扩为 11 条,拆为两组:env-lustre-availability(采集失败 / 客户端设备未就绪 / 驱逐 / RPC 重传)与 env-lustre-capacity(OST / MDT / 客户端容量与 inode / grant 受阻 / 元数据与 OST 延迟)。FlexLM 组不变

配置、systemd 单元模板、抓取链路与采集频率(60s)均未改动。

分步操作(与实现体等价;实现体:modules/domain_collectors.py)

cd /home/rain/Documents/k8s_ins/env-monitor-deploy

# 1) 门禁
.venv/bin/python main.py precheck
# 预期:BLOCK=0 WARN=9 INFO=149(与既有基线一致)

# 2) 预演(零改动)
.venv/bin/python main.py deploy --dry-run
# 预期:主机侧只列 .21/.22/.28(脚本内容不符),其余 5 个采集目标「已就绪,跳过」;
#       集群侧 1 处待下发(10 大盘 + 13 告警)

# 3) 真实变更
.venv/bin/python main.py deploy

# 4) 目标机核对:新指标是否落盘并经 9100 暴露
ssh root@192.168.31.21 'wc -c /var/lib/node_exporter/textfile/lustre.prom'
ssh root@192.168.31.22 'curl -s http://127.0.0.1:9100/metrics | grep -oE "^lustre_[a-z_]+" | sort -u | tr "\n" " "'
ssh root@192.168.31.28 'systemctl is-active env-lustre-metrics.timer'

# 5) 集群侧核对:Prometheus 收录 + 大盘对象 + 告警分组
kubectl --kubeconfig /home/rain/.kube/config -n monitoring get --raw \
  "/api/v1/namespaces/monitoring/services/kube-prometheus-stack-prometheus:9090/proxy/api/v1/label/__name__/values" \
  | tr ',' '\n' | grep -c '"lustre_'
kubectl --kubeconfig /home/rain/.kube/config -n monitoring get cm env-lustre-dashboard -o jsonpath='{.data.env-lustre-dashboard\.json}' | head -c 200
kubectl --kubeconfig /home/rain/.kube/config -n monitoring get prometheusrule env-domain-alerts -o jsonpath='{range .spec.groups[*]}{.name}{" "}{end}'

坑与注意事项(实现体已处理,此处留痕)

  1. 服务端 brw_stats 在 osd-ldiskfs.* 层:mdt.*.brw_stats 与 obdfilter.*.brw_stats 在本环境实测均报 No such file or directory,磁盘层 IO 分布只能从 osd 层取。
  2. MDS 没有容量参数:mdt.*.kbytesfree 等不存在(实测报错),MDT 容量与 inode 必须走 osd-ldiskfs.*。这也是旧大盘只有客户端容量视图的原因。
  3. stats 行的单位有 [usecs] 与 [reqs] 两种:操作次数对所有 op 都计数(否则会改变 lustre_ost_ops_samples 的既有语义与旧图形的历史可比性),只有 [usecs] 的 op 才输出累计 时延;ioctl 一行只有 samples 没有 min/max/sum,正则须容忍末四列缺失。
  4. 客户端 osc/mdc 的参数名带后缀:形如 edafs-OST0000-osc-ffff98778250d800,需截断 为 edafs-OST0000 才能与服务端 ost 标签对齐(否则跨面板无法联看)。
  5. brw_stats 空载时只有表头无数据行:解析器按「表头 → 列名行 → 分桶数据行」状态机走, 无数据行即无输出,不产生垃圾序列(实测空载下 .22 未出现该指标)。
  6. lctl dl 的状态列必须严格为 UP 才记 1,其余(DOWN/RECOVERING/NEW)记 0, 否则设备掉线会显示成正常。
  7. cluster 标签由 ServiceMonitor relabel 按节点所属集群生成,会覆盖指标文件里的同名标签: .21/.22 为 lustre、.28 为 hpc。因此大盘把客户端与文件系统容量类面板的 cluster 过滤去掉(它们跨集群),只保留服务端面板的过滤;变量补 allValue: ".*"。 —— 曾尝试让采集器统一输出 cluster=lustre,实测无效(被 relabel 覆盖),已回退。

验证(实测结论)

检查 结果
Prometheus 收录 lustre_ 指标 4 → 22 个;lustre_up=1(3 实例:lustre-mgs-01 / lustre-oss-01 / hpc-submit)
目标机产物 .21 915 B(7 个指标族)/ .22 1315 B(8 个)/ .28 2429 B(9 个);三个 env-lustre-metrics.timer 均 active
数值抽查 lustre_mdt_exports=3、lustre_ost_capacity_kbytes{type="free"}=308693824、lustre_fs_inodes{target="edafs-MDT0000",type="total"}=8388608、lustre_client_device_up 5 个设备全为 1、lustre_ost_grant_bytes{type="granted"}=279104 —— 均与命令行实测一致
本地解析自测 用实机取样驱动 stub lctl/lfs 跑 collect(),26/26 断言通过(含命令全失败时 lustre_up=0 且零垃圾序列)
集群侧大盘 ConfigMap 更新为 35 面板 / 7 分组;Grafana sidecar 日志 Writing /tmp/dashboards/env-lustre-dashboard.json
告警规则 env-domain-alerts 含 env-lustre-availability(4) + env-lustre-capacity(7) + env-flexlm-alerts(4),无重名
幂等复跑 14 主机 + 8 采集目标 + 3 集群侧对象全部「状态一致,无需变更」,退出码 0

回滚:见第 7 章「备份位置与台账」,对应条目见 backup/README.md 的 2026-10-01 Lustre EDA 大盘节(工程内制品 backup/artifacts/collectors/lustre_metrics.py.bak.20261001-003617、 backup/manifests/10-env-lustre-dashboard.yaml.bak.20261001-003617、 backup/manifests/13-env-domain-alerts.yaml.bak.20261001-003617、 backup/modules/domain_collectors.py.bak.20261001-004226(本次尝试后回退,内容与改前一致)、 backup/utils/config_validate.py.bak.20261001-004226、 backup/config/env-monitor.yaml.bak.20261001-004226、 backup/docs/{SOP,RUNBOOK}.md.bak.20261001-003617、 backup/docs/HANDOFF.md.bak.20261001-*(根级 docs/HANDOFF.md 的备份,原位在此目录)); 远端脚本备份由实现体自动登记(.21/.22/.28 的 env-lustre-metrics.bak.*)。 步骤:用备份覆盖上述工程内文件 → 重跑 deploy(告警为同一 PrometheusRule 内的分组, 随 env-domain-alerts 一并回退,无需单独 delete)。

风险与残留

  • 磁盘层 IO 分布(lustre_ost_disk_io_ios)未经实测验证:本环境空载,brw_stats 无数据行, 该指标尚未在 Prometheus 出现。解析器用的是通用「段名 + 分桶标签原样透传」策略,且有 IO 时 才产出;集群有作业负载后需复核一次桶标签形式与数值。
  • 作业级 IO(*.job_stats)本期未采集:空队列下 mdt.*.job_stats 与 obdfilter.*.job_stats 均为空,输出格式无法实测验证。EDA 场景的「按作业/用户归属 IO」是重要视角,待有作业时补采。
  • 延迟类告警阈值(20 ms)取自保守估计:实测元数据操作在几十至几百微秒、OST 操作在毫秒级, 但样本量小(无作业负载)。有真实负载后需按分位数重新校准,见 13-env-domain-alerts.yaml 头部。
  • 采集命令由 3 条增至 13 条(均为 lctl get_param / lctl dl / lfs df 只读查询), 单次实测仍在秒级,interval_seconds 未变(60s)。
  • cluster 变量在两个值之间(lustre / hpc):大盘已按「服务端过滤、客户端不过滤」处理, 但若日后新增 Lustre 客户端挂载到其它集群的主机,需复核各面板的过滤口径是否仍然合适。

G.8 2026-10-01:FlexLM 大盘按 EDA 运营视角扩展(许可七视角)

背景:P1 交付的 FlexLM 大盘只有 6 个面板 / 2 分组(采集状态、服务器 / daemon UP、 许可占用率、各 Feature 明细、denied),是唯一未做 EDA 化改造的领域 —— 同期 Slurm 27 面板、 LSF 30 面板、Lustre 35 面板(见 G.5–G.7)。用户反馈「又拥挤又简单」,两者同源:

  • 拥挤:lmstat -a 实测输出 5698 行 / 2843 个 counted feature(另有 Cadence 604 个 uncounted),旧盘的「各 Feature 许可明细」把上千行一次性渲染;
  • 简单:根因在采集面只解析了 lmstat 的三类字段(服务器 UP / daemon UP / feature 总数与在用), 用户行(谁在用、占了多久)、服务器角色与版本、许可到期日、许可文件与运行服务是否对应, 全部没有采集。

目标:不新增端口、不改抓取配置、不触碰许可服务与许可文件,把采集面扩到与 Slurm / LSF / Lustre 对等的 EDA 七视角,并按同一七分组结构重排大盘。

数据源(均为只读查询,2026-10-01 在 .29 实机验证;FlexNet v11.14.1 / v11.16.4)

来源 提供什么 新增指标(cluster="license" 之外)
lmstat -a 服务器行 UP 状态、角色 (MASTER)、lmgrd 版本 flexlm_server_role{role}、flexlm_license_server_version{version}
lmstat -a daemon 行 daemon 状态与版本 flexlm_vendor_daemon_version{daemon,version}
lmstat -a 用户行【未验证】 持有者、主机、checkout 时间 flexlm_license_user_checkouts{user}、flexlm_license_oldest_checkout_seconds{feature}、flexlm_checkout_parse_failed
lmstat -a Feature 段 有占用的 feature 数、空闲量、利用率、许可类型分布 flexlm_license_features_used、flexlm_license_available_sum、flexlm_license_utilization、flexlm_license_type_features{license_type}
许可文件 FEATURE/INCREMENT 行 到期日、permanent 占比 flexlm_license_expiry_timestamp_seconds、flexlm_license_expiring_features{window}、flexlm_license_permanent_features、flexlm_license_feature_expiry_timestamp_seconds{feature}
许可文件存在性 / 服务对应关系 文件与运行服务是否脱节 flexlm_license_file_present、flexlm_license_file_served、flexlm_license_file_readable
lmgrd 日志可读性 区分「没有拒绝」与「统计不到」 flexlm_license_denied_log_available
采集自省 返回码、耗时、解析健康 flexlm_lmstat_rc、flexlm_collect_success、flexlm_parse_ok、flexlm_collect_duration_seconds

改了什么

文件 改动
artifacts/collectors/flexlm_metrics.py 指标名 10 → 29 个(新增 19 个有样本指标,另有 3 个仅在出现 checkout 或临近到期 feature 时产出);新增 _parse_usage(含用户行状态机)、_license_file_stats、_parse_start;原有 10 个指标名与语义保持不变
manifests/12-env-flexlm-dashboard.yaml 由 6 面板 / 2 分组重做为 30 面板(含 7 分组行)/ 7 分组:服务与采集健康 / 许可池容量与水位 / 占用与持有者 / 拒绝与争抢 / 到期与版本 / 许可结构 / 跨域联动
manifests/13-env-domain-alerts.yaml FlexLM 由 1 组 4 条扩为 2 组 11 条:env-flexlm-availability(7) + env-flexlm-capacity(4)
config/env-monitor.yaml collectors.flexlm.extra_args 新增 --license-file(Synopsys / Cadence / Calibre 三个许可文件)

端口、systemd 单元模板、抓取链路与采集频率(60s)均未改动;Lustre 告警与大盘未受影响。

分步操作(与实现体等价;实现体:modules/domain_collectors.py)

cd /home/rain/Documents/k8s_ins/env-monitor-deploy

# 1) 门禁
.venv/bin/python main.py precheck
# 预期:BLOCK=0 WARN=9 INFO=149(与既有基线一致)

# 2) 预演(零改动)
.venv/bin/python main.py deploy --dry-run
# 预期:主机侧只列 .29(采集脚本内容不符),其余 7 个采集目标「已就绪,跳过」;
#       集群侧「渲染清单与实机存在差异,待下发」(12 大盘 + 13 告警),Alertmanager 跳过

# 3) 真实变更
.venv/bin/python main.py deploy

# 4) 目标机核对:脚本下发、指标落盘、9100 暴露
ssh root@192.168.31.29 'systemctl show -p ExecMainStatus --value env-flexlm-metrics.service'
# 预期:0
ssh root@192.168.31.29 'grep -oE "^[a-z_]+" /var/lib/node_exporter/textfile/flexlm.prom | sort -u | wc -l'
# 预期:29(改造前为 10)
ssh root@192.168.31.29 'curl -s http://127.0.0.1:9100/metrics | grep -c "^flexlm_"'
# 预期:6352

# 5) 集群侧核对:Prometheus 收录 + 大盘对象 + 告警分组
kubectl --kubeconfig /home/rain/.kube/config -n monitoring get cm env-flexlm-dashboard \
  -o jsonpath='{.data.env-flexlm-dashboard\.json}' | python3 -c \
  "import json,sys; d=json.load(sys.stdin); print(len(d['panels']))"
# 预期:37(30 面板 + 7 分组行)
kubectl --kubeconfig /home/rain/.kube/config -n monitoring get prometheusrule env-domain-alerts \
  -o jsonpath='{range .spec.groups[*]}{.name}{" "}{end}'
# 预期:env-lustre-availability env-lustre-capacity env-flexlm-availability env-flexlm-capacity

# 6) 幂等复核
.venv/bin/python main.py deploy
# 预期:14 主机 + 8 采集目标 + 3 集群侧对象全部「状态一致,无需变更」,退出码 0

坑与注意事项(实现体已处理,此处留痕)

  1. Cadence 侧没有容量语义:其实测 604 个 feature 全部是 (Uncounted, node-locked), 只有存在性、没有 issued/in_use;容量类面板与告警对本厂商必然为空,这不是采集缺陷。 「许可类型结构」面板专门用来呈现这一点。
  2. 用户行必须含 , start <时间> 才认作用户行:feature 段里还有 "3D" v…, vendor: … 与 floating license 一类说明行,宽松匹配会把 floating license 误判成「用户 floating、 主机 license」。解析器要求行内含 , start,且匹配失败只计入 flexlm_checkout_parse_failed,不伪造数据。
  3. 用户行格式本环境无法实测:两台服务器长时间空闲,lmstat -a 无任何 checkout, lmgrd 日志中 OUT / IN / denied 均为 0 条。解析器按 FlexNet 11.x 标准格式实现, 按 G.7 对 job_stats 的先例标注【未验证】,并配 EnvFlexlmCheckoutParseFailed 告警兜底。
  4. 许可文件有续行符:INCREMENT 行以 \ 续行,关键字段(feature / 版本 / 到期 / 数量) 都在首行,解析只取首行即可,不必拼接续行。
  5. 序列体量是硬约束:2843 个 counted feature × (total + in_use) 已占 5686 条序列, 许可文件本身 620 KB。新增指标一律固定基数(合计仅 +49 行), 不得再对既有 per-feature 指标乘上用户 / 主机等新维度;feature 级明细在大盘侧用 topk(10, …) 收敛,到期明细只输出 365 天内到期的前 50 个。
  6. file_served=0 不等于「配置脱节」——先看许可文件的类型:本环境 /opt/FlexLM/Cadence/calibre.license.dat 经 2026-10-01 复核,0 条 SERVER 行、0 条 VENDOR 行, 88 条 FEATURE 全部为 HOSTID=ANY(Mentor Graphics,2036-09-26 到期),主机上也没有 mgcld 二进制 —— 它是不需要 license server 的 uncounted / node-locked 型许可文件, 由工具客户端直接读取,file_served=0 属正常。对照 cds.license.dat 含 SERVER 192.168.31.29 525400310e02 5280,才会由 flexlm-cadence.service 提供。 这类不一致不定为告警(属环境既有事实、非故障),只在大盘「许可文件明细」面板呈现。
  7. denied 口径未改动:仍是 lmgrd 日志全文累计,日志被轮转时计数会回落。 面板改用 increase(...[15m]) 看增量以规避该问题;口径原样保留以免破坏历史可比性。

验证(实测结论)

检查 结果
Prometheus 收录 flexlm_ 序列 6352 条;新指标 flexlm_license_total_sum / flexlm_license_expiring_features / flexlm_license_file_served 均可查(job="external-node-exporters")
目标机产物 flexlm.prom 677 KB → 697 KB;9100 暴露 6352 条;单元退出码 0;单次采集实测 3.44 s(周期 60 s)
数值抽查 Synopsys total_sum=281359(2843 个 counted feature)、in_use_sum=0;Cadence 604 个 uncounted、total_sum=0;Calibre file_present=1 / file_served=0;最早到期 Synopsys 2030-12-30、Calibre 2036-09-26
本地解析自测 实机取样驱动 stub lmutil 跑 collect(),15/15 断言通过;另以 stdin 方式在 .29 上实跑(零落盘)得 6354 行,逐项核对取值
集群侧大盘 ConfigMap 更新为 30 面板 / 7 分组(JSON 共 37 对象);Grafana sidecar 日志 Writing /tmp/dashboards/env-flexlm-dashboard.json → reload 200 OK
告警规则 env-domain-alerts 含 env-lustre-*(11) + env-flexlm-availability(7) + env-flexlm-capacity(4),共 22 条,无重名;原 env-flexlm-alerts 组名已取消
幂等复跑 14 主机 + 8 采集目标 + 3 集群侧对象全部「状态一致,无需变更」,退出码 0

回滚:见第 7 章「备份位置与台账」,对应条目见 backup/README.md 的 2026-10-01 FlexLM EDA 大盘节(backup/artifacts/collectors/flexlm_metrics.py.bak.20261001-011858、 backup/manifests/12-env-flexlm-dashboard.yaml.bak.20261001-011858、 backup/manifests/13-env-domain-alerts.yaml.bak.20261001-011858、 backup/config/env-monitor.yaml.bak.20261001-011858、 backup/docs/{SOP,RUNBOOK}.md.bak.20261001-011858)。 步骤:用备份覆盖上述工程内文件 → 重跑 deploy(告警与 Lustre 同处 env-domain-alerts 这一个 PrometheusRule,随它一并回退,无需单独 delete);远端脚本备份由实现体自动登记 (.29 的 env-flexlm-metrics.bak.*)。

风险与残留

  • 「占用与持有者」组的数据未经验证:本环境无 checkout,flexlm_license_user_checkouts / flexlm_license_oldest_checkout_seconds 在 Prometheus 中不存在,面板为空属正常。 有真实作业后必须复核一次用户行格式;若 flexlm_checkout_parse_failed > 0 会触发告警, 届时按实机样本修正解析器(改动只涉及 _RE_USER 与 _START_FORMATS)。
  • denied 的按 feature / 按用户拆分未做:日志当前无 denied 记录,格式无从验证, 因此只保留原有的厂商级累计计数,不硬编码不可验证的日志格式。
  • 到期告警阈值(30 天)为经验值:首批数据为 2030 / 2036 年到期,暂无临近样本, 待有真实临期再校准窗口。
  • 大盘对 2843 个 feature 的渲染已收敛(topk / 窗口过滤),但 per-feature 序列 仍占用较多 TSDB 空间;若日后继续扩展,需先复核 Prometheus 容量预算 (docs/MONITORING-ROADMAP.md 的 G2/G8 缺口)。

G.9 2026-10-01:大盘改为「精简源 + 生成器」产出(消除手写 JSON 样板)

背景:5 个环境大盘(总览 / Slurm / Lustre / LSF / FlexLM)此前是手写 YAML —— ConfigMap 内嵌整段 Grafana JSON,合计 143 KB,约九成是重复样板(datasource 常量、 overrides: []、refId、gridPos、各面板类型的 options/fieldConfig 骨架)。 扩展一个面板要重新输出上万 token 的 JSON,手写 gridPos / id 也容易出错。

改动(大盘自此成为生成物,唯一真源是精简源)

文件 改动
utils/dashboard_builder.py 新增生成器:extract(现网反推精简源)/ render(精简源 → manifests)/ verify(往返等价)/ check(一致性)
dashboards/{overview,slurm,lustre,lsf,flexlm}.yaml 新增精简源(由 extract 从现网自动产出,非手写)
manifests/{03,08,10,11,12}-*.yaml 由生成器重新产出覆盖(语义等价、字节不同:JSON 缩进展开 + 键序规范化)
main.py 新增 build-dashboards 子命令(--check 只校验,不写文件)
deployer.py 预检链 _collect_precheck() 增加「精简源 ↔ manifests 一致」校验,手改 manifests 即 BLOCK

设计要点

  • 面板 id / gridPos 由生成器计算:id 按出现顺序(row 独立从 row_id_base 递增), gridPos 按「面板顺序 + 宽度」自动布局 —— 已实测可 100% 复现现网 5 盘的坐标(0 不匹配);
  • datasource 常量、target refId 由生成器补齐;
  • 各面板类型的样板收进 PANEL_SKELETON,只收录「该类型全部面板都有、且值相同」的键 —— 这是往返无损的前提,verify 是这条约束的验收方(改骨架后必跑)。

改大盘的正确姿势

# 1) 改精简源(只写差异行,不写 id / gridPos / datasource / refId)
#    面板类型骨架见 utils/dashboard_builder.py 的 PANEL_SKELETON
$EDITOR dashboards/slurm.yaml
# 2) 生成 manifests(幂等:内容未变则报「未变」,不写文件)
.venv/bin/python main.py build-dashboards
# 3) 下发
.venv/bin/python main.py deploy

验证(实测结论)

检查 结果
往返等价 5 盘 extract → render 与原 JSON 逐键比对 0 差异(verify 全 OK)
迁移等价 迁移后新旧 manifests 的 JSON 逐键等价(5/5)
精简率 5 盘合计 143 KB → 80 KB(55%);扩展单面板只需在精简源写约 5~8 行
一致性门禁 正向:一致时 exit 0;反向:模拟手改 title 后被拦截(exit 1)
下发 deploy 后 14/14 抓取目标 up、5 组告警规则加载、幂等复跑全跳过、drift exit 0

回滚:用备份覆盖 5 个 manifests 与 main.py / modules/k8s_ingest.py / docs/SOP.md, 删除 dashboards/ 与 utils/dashboard_builder.py → 重跑 deploy。 备份清单见 backup/README.md 的 2026-10-01 大盘模板化节。

风险:生成器与手写 YAML 双轨漂移 —— 已由「pre_check 一致性门禁 + 本节约定」双重约束: 大盘一律由生成器产出,不再手改 manifests。


来源:/home/rain/Documents/k8s_ins/env-monitor-deploy/docs/SOP.md | 最后更新:2026-10-01 01:58(由 build/sync.py 同步,请勿在站点侧修改)