Files
dsh_shenxian/dsh-server-docs/04-调整方案/129-日志采集与巡检-方案C实现.md
T
admin d2ef362a98 feat(overlay): 覆盖网络线 序㊾ —— 探针观测面改「两台中继并集」(附 序㊽ 源码/文档补提交)
序㊾(本棒):
- scripts/overlay-probe.cjs:OBS-01 / OBS-08 / OBS-09 的数据源由「只读 47 中继」
  改为「按两台中继取并集」,消除 worker 归属漂移时的假红 / 假 SKIP
  · endpoints 以 network:hostId:port 为键合并,online 取「或」、localPort 取在线那一侧
  · used 按 network/hostId 去重计数(不求和,避免凭空放大在册数)
  · localPort 属中继机回环落点 ⇒ 按归属分机探活(106 侧落点由 106 机上探)
  · derived(OBS-11)保持 47 视角;阈值与判据一律未放宽
  · OBS-16 计数约束:对 47 /status 的读取仍为三次、Δ 只取 47 的 counters;
    对端 106 的采样为独立一次,落在第三次采样之后,不进 (status2, status3] 门窗口
  · 新增 --peer-status-fixture(并集的对端那一半)与「并集不可取证」强制留痕
- 交接单《覆盖网络-序45-低熵块治理-测熵与实现》§16 全节(§8 前前缀逐字未变)
- 参数表 §11.16 补记(§10 现算指纹未变,值格未动)

附(前几棒已完成并已部署、但尚未入仓的源码 / 文档):
- src/net/relay/content/*.ts、src/net/relay/index.ts、main.ts:块级寻址 C 域分离
- src/supervisor/orchestrator.ts、src/worker/agent.ts:日志采集与巡检(方案 C)
- test/overlay-content.test.mjs:随附用例(npm test = 200 pass / 0 fail / 1 skipped,Node 22)
- scripts/dshlog.mjs(跨机日志取证)、scripts/overlay-entropy.cjs(熵探针)
- dsh-server-docs/04-调整方案/129、133;INDEX.md / docs-manifest.json / 交接单 README 登记
2026-09-19 05:35:35 +08:00

14 KiB
Raw Blame History

日志采集 · 实时巡检 · 事后溯源(方案 C 实现)

  • 版本:v1 实现稿(2026-09-18)
  • 状态:✅ 已实现 · 本机实测通过(工具 = 代码仓 scripts/dshlog.mjs)
  • 上游决策:方案 C —— 不装新日志服务(不装 Amber / 不装 Loki+Alloy),靠现有 journald + 自研观测面
  • 取消的候选:Amber(无预编译产物 / 单维护者 / 落盘异步不去重 ⇒ 与"日志原文行数"类判据冲突)· Loki+Alloy(组件数与内存代价,规模未到)
  • 复跑入口:"E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe" scripts/dshlog.mjs help

0. 一句话

一条 CLI 把 47 / 106 的 journald 拉回本机按天归档;跨机时间线一键重建;巡检规则命中即出判据表与非零退出码。 服务器侧零安装、零新增端口、零常驻进程 —— 远端只用系统自带的 journalctl。


1. 现状取证(为什么只能走这条路)

事实 实测值(2026-09-18) 含义
47 日志分布(当日) dshs 29274 · dshs-worker 7312 · sshd 3830 · init.scope 3155 · dshs-relay 830 主角是四层单元:Manager / Worker / relay / PG
106 日志分布 user@0 2604 · init 1575 · crond 728 · sshd 339 同一套单元命名,靠 host 字段区分
journald 形态 两机均 persistent(/var/log/journal/<machine-id>/);47 = 769 MB · 106 = 130 MB 可直接增量拉取;但两机都没设上限(默认吃磁盘 10%)
时钟 两机 NTP 均已同步(timedatectl NTPSynchronized=yes) 跨机时间线不需要自造校时
用户实例(dsh --profile) _SYSTEMD_UNIT=dsh-<uid>-<hash>.scope 无任何条目;journalctl _PID=<实例pid> 无条目;实例 fd/1 指向 socket:[…] 🔴 实例 stdout 不进 journald —— 它被 Worker 用 socket 接管,只有 Worker 愿意转发的那部分才落在 dshs-worker 里
journalctl 版本 47 = systemd 239 · 106 = 255 --output-fields 两机都支持(用 awk NR==1 取版本号,别用正则猜)

2. 架构(三层,无新增常驻组件)

[采集]  47 / 106 的 journalctl -o json          ← 远端只读,不写任何文件
          │  ssh -C(压缩,见 §6.1)
          ▼
[归档]  E:/dsh-logs/<host>/<YYYY-MM-DD>.ndjson.gz   ← gzip 多成员追加,按天分片
        E:/dsh-logs/state.json                      ← 每节点 cursor / lastTs / NTP 状态
        E:/dsh-logs/hosts.json                      ←(可选)节点清单,缺省用内置
          ▼
[使用]  q(查询)· timeline(溯源)· watch(巡检)· stats / ls / prune(运维)

三条不可让步的设计约束

  1. 零新增常驻服务 / 零新增监听口 —— 不装 agent、不开端口、远端不落任何脚本(bash -s 走 stdin);符合 R5 的"权限只准收窄"。
  2. 日志原文保真 —— msg 字段逐字落盘(本项目大量判据依赖日志原文的行数/字节数);非 UTF-8 字节转义为 \xNN 保留。
  3. "拉取失败" 与 "确无日志" 永不混淆 —— 数据走 stdout、元信息走 stderr 哨兵 __DSHLOG_EOF__ rc= errbytes= + __DSHLOG_LINES__ n;两条通道物理分离。

3. 三个目标 → 怎么满足

目标 命令 判据
不同节点/设备的日志存储 collect(增量 cursor 续拉 / --since 回填) 四态:OK / EMPTY / MISMATCH(远端报数与本地解析数不等) / FAIL;非 OK 即退出码 2
快速排查线上运行问题 q <词|/正则/> · timeline q 支持跨机跨单元、时间窗、正则;timeline 把多机日志按时间戳合并成单条时间线
实时 bug 监控 watch(默认先自动增量续拉再巡检) 8 条规则 → 判据表 PASS/FAIL;有 FAIL ⇒ 退出码 2(可作自动化判据)
事后溯源分析 timeline --from --to --grep --out 带毫秒排序 + host/unit 归属 + pri;输出可落文件归档

4. 用法

N="E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe"

# ① 拉取:首次回填 / 之后增量
$N scripts/dshlog.mjs collect --since 6h --chunk 2h     # 回填(自动去重)
$N scripts/dshlog.mjs collect                            # 增量:只搬 cursor 之后的新行

# ② 查询
$N scripts/dshlog.mjs q "EADDRINUSE" --since 24h
$N scripts/dshlog.mjs q "/no-trusted-keys|信任链/" --host 106 --limit 20

# ③ 事后溯源(把一次故障从两机日志里拼出来)
$N scripts/dshlog.mjs timeline --from 2026-09-18T16:00 --to 2026-09-18T17:00 \
   --grep "overlay|relay" --out E:/dsh-logs/tl-1617.log

# ④ 巡检(实时监控入口)
$N scripts/dshlog.mjs watch --since 30m --report      # 有 FAIL ⇒ rc=2
$N scripts/dshlog.mjs watch --since 30m --json        # 机器可读

# ⑤ 运维
$N scripts/dshlog.mjs stats --detail     # 归档分布(按节点/日期/单元 top4)
$N scripts/dshlog.mjs prune --keep 14    # 保留策略(默认干跑,--apply 才删)

扩展新节点(用户说的"不同设备"):在 E:/dsh-logs/hosts.json 加一条即可,命令无需改动。

{ "47": { "ssh": ["-p", "22", "[email protected]"], "label": "Manager + w-47" },
  "106": { "ssh": ["-p", "22", "test106"], "label": "w-106" } }

5. 实测基线(2026-09-18 21:0x–21:2x)

项 实测
当日归档量 47 = 35,603 行 · 106 = 5,988 行(gzip 后 47 ≈ 0.3 MB)
回填 6 h(3 块 × 2 h) 2 分 28 秒(含首次去重重读);单块上限受链路带宽支配
增量续拉(1 块,含去重) 19.5 秒
单块原始体积 47 ≈ 1.6 MB / 小时(未压缩,全字段)
巡检扫描 41,226 行(6 h 窗口) < 3 秒(纯本地 gzip 顺序扫)
跨机时间线 6 h 窗口内 41k 行,毫秒级排序,秒级完成

压缩是最大杠杆(见 §6.1):47 出方向未压缩实测 ~20 KB/s(1.6 MB 要 84 秒),开 ssh -C 后同样数据 11 秒。


6. 关键设计(每条都有实测出处,别改回去)

6.1 🔴 ssh -C 不可省 —— 7.6× 提速

实测同一块数据(1 h,1.6 MB)真传到本机:不带 -C = 84 s vs 带 -C = 11 s。 JSON 日志压缩率极高;-C 的开销是两端 CPU(1.6 MB 约几十毫秒),完全值得。 ⚠️ 不加 -C 时表现为"像是卡死了"(第一版跑 3 h 回填超过 5 分钟被外层超时杀掉,误判为代码 hang)。

6.2 数据/元信息双通道

远端脚本把 journalctl 的 stdout 逐行 awk 转发(数据),同时把行数写 stderr(__DSHLOG_LINES__),末尾再补 __DSHLOG_EOF__ rc=… errbytes=…(元信息)。 ⇒ "确无日志"(0 行) 与 "拉取失败"(无哨兵 / rc≠0) 在同一份输出里天然可分 —— 这是本项目反复踩过的坑,此处按机制而非纪律解决。

6.3 归档是 gzip 多成员追加,且必须同步写

  • 每次 appendFileSync 追加一个完整 gzip 成员 ⇒ gunzip / node createGunzip 都能顺序读回(已实测:596 + 248 两个成员读出 844 行)。
  • ⛔ 不能用 createWriteStream 异步管道 —— 进程收尾时可能未 flush,静默丢最后一批。

6.4 回填必须去重,续拉必须用 cursor

  • 续拉(无 --since):journalctl --after-cursor=<cursor> —— 精确、不重不漏。
  • 回填(有 --since):先把已有分片的 __CURSOR 读进 Set,落盘时跳过 ⇒ 实测一次回填跳过 8100 行重复。 ⛔ 不去重会让"行数类判据"直接失真。

6.5 校时用 NTP 状态,不要自造往返估算

第一版用 (远端date + 本地往返/2) 估偏移,实测给出 +1337 ms 的假偏移(47 的 ssh RTT 达 3.7 s 且往返不对称)⇒ 拿它做校正会制造错序。 改为读 timedatectl show -p NTPSynchronized:两机都是 yes ⇒ 不校正,timeline 默认输出原始系统时间戳,并把 NTP 状态打在末尾。

6.6 版本号解析必须 awk 'NR==1{…}'

journalctl --version 是多行输出;不限定行会让变量变成 "239\n0" ⇒ [: integer expression expected ⇒ 静默退回全字段(体积翻倍)。 症状是"结果没错但慢一倍",只在 err: 里留一行噪声。

6.7 巡检规则要收紧到指向本项目故障(误报比漏报更贵)

实测踩过的两条反例:

  • fatal 裸写 ⇒ sshd 的 ssh_dispatch_run_fatal(客户端网络断)天天命中 ⇒ 收紧为 PANIC|FATAL ERROR|unhandled… + 排除 sshd/crond/systemd-logind 噪声单元。
  • Stopped .* 裸写 ⇒ 实例正常退出(用户关会话)会打 Stopped /usr/bin/bwrap … ⇒ 收紧为 Failed with result|start request repeated|Main process exited, code=…。

当前 8 条规则:进程级致命 / 内存被杀 / 端口连接失败 / 权限属主 / 磁盘写入 / 覆盖网络信任链被拒 / HTTP 5xx / 服务异常终止。

实测有效:WATCH-06 在 106 上命中 ⚠ 取目录全部失败 ⇒ 回落到内置种子地址本身 —— 正是序 ㉗ 记录的 E3 缺口(106 候选链退化为单点),属真报。


7. 边界(红线遵守情况)

项 状态
新增公网监听口 ⛔ 0 个(只读 ssh,未改 nft / nginx / 任何监听)
服务器侧新增常驻进程 / 安装 ⛔ 0 个(只用系统自带 journalctl;脚本走 bash -s stdin,不在远端落文件)
生产值改动 ⛔ 0 处
权限 只读:journalctl 读取无需提权以外的任何放行;ssh 用既有连接
归档位置 E:/dsh-logs/(E 盘;不在 git 仓库内)

8. 已知限制与未做项(如实登记)

  1. 🔴 实例层(dsh --profile)日志目前抓不到 —— 实例 stdout 被 Worker 用 socket 接管,不进 journald(_SYSTEMD_UNIT=…scope 与 _PID= 均为空,已双重取证)。当前只能拿到 Worker 转发的部分。 ⇒ 这是本方案最大的剩余缺口;要补需改 Worker 的 stdout 接管方式(属改代码,另立一单),或让实例自己写日志文件(需确认官方 dsh 是否支持日志落文件,受 R2 约束)。
  2. 本机(WorkBuddy 开发机)未纳入 —— 本机是 Windows,无 journald;其日志在 ~/.workbuddy/logs/,与"线上运行"关联弱,暂不入归档。
  3. 全文检索是线性扫描 —— 当前量级(每日 4 万行)毫秒级;若到 GB 级需换索引(届时正是上 Loki 的信号,见上游评估的触发条件)。
  4. 未设置 journald 上限 —— 两机 /etc/systemd/journald.conf 均为空(默认 10% 磁盘)。47 已 769 MB。建议后续加 SystemMaxUse=,避免日志本身成为磁盘事故源。
  5. --threshold 目前是全规则统一阈值,未做逐规则独立阈值。

8.1 日志保留策略 = 3 天(2026-09-18 落地)

服务器侧(两机均写 drop-in,⛔ 不覆盖主配置 /etc/systemd/journald.conf):

# /etc/systemd/journald.conf.d/10-retention.conf
[Journal]
MaxRetentionSec=3d      # 时间上限(用户口径"只保留3天的日志")
SystemMaxUse=512M       # 47  / 106 用 192M —— 容量兜底,防单日风暴吃满磁盘
SystemMaxFileSize=32M   # 47  / 106 用 8M  —— 压小分片,提高"3 天"判定精度

本机归档:dshlog prune --keep 3 --apply(默认干跑;默认保留 3 天,与服务器同口径)。

🔴 关键实测:--vacuum-time 的判据是「分片起始记录时间」,且按整片删除

⇒ 实际保留期 = 3 天 −(0 ~ 一片跨度)。分片越大,"3 天"缩水越严重。 这解释了为什么必须同时压小 SystemMaxFileSize:

机 分片跨度(默认 128M 时) 清理后实际保留 压小分片后预期
47 ~128M ≈ 1.5 天/片 1.35 天 ❌ 32M ≈ 6h/片 ⇒ 2.75~3 天
106 ~47M ≈ 3 天/片 2.44 天 ❌ 8M ≈ 9h/片 ⇒ 2.6~3 天

⚠️ 一次执行的偏差记录(如实留档):首次清理未预判该判据,按"末记录时间"估算,实测多删了一片 —— 47 释放 608 MB(768→160 MB)、106 释放 59.5 MB(130→70 MB),但保留期分别只有 1.35 天 / 2.44 天,未达 3 天。 受影响而永久丢失的区间:47 = 09-15 13:33 ~ 09-17 13:05;106 = 09-13 12:54 ~ 09-16 11:29(本机归档当时也尚未覆盖该区间 ⇒ 无副本)。 ⇒ 47 的保留跨度会随新数据每日增长约 1 天,约 1.7 天后自然恢复到 3 天并稳定(无需人工干预)。


9. 待你拍板

是否挂周期自动化做"实时"巡检:

A. 挂 automation 每 30 分钟跑一次 watch,仅 FAIL 时出报告(推荐) 优点:真"实时",异常半小时内可见,且只在有 FAIL 时才有可读产出。 缺点:每天 48 次新会话,按本项目实测的自动化成本(每轮 5–9 积分)估算约 250–430 积分/天,成本可观。

B. 挂 automation 每天 1 次汇总(如 08:00) 优点:成本低(约 5–9 积分/天),能发现"过夜积累"的问题。 缺点:不是实时;白天的突发故障要等次日,或靠人工跑 watch。

C. 不挂,保持按需手动执行 优点:零成本,需要时一条命令 10–20 秒出结果;当前平台规模小(47 仅 0–2 实例),"现拉现看"足够。 缺点:无人自动发现问题,依赖你或我主动去查。

我的倾向:先 C,等出现一次"事后才发现"的线上问题再上 A —— 理由是本项目已有 overlay-probe.cjs 这类主动探针承担"判据式巡检",watch 的价值主要在事后取证;而实时性的成本(积分)与当前规模不匹配。若你认为线上稳定性优先于积分,直接上 A(我按"每 30 分钟 + 仅 FAIL 出报告"落地)。