bc49b85b56
build / build (push) Successful in 1m12s
- 矩阵:新增 S06/S06-a/S06-b/V03-c/Neo4j 拒绝行(都有真跑证据);把"封存入口未接线"、 "CLI 库级窗口"从缺口里移走;排空时序旧结论保留但标注被确定性做法取代。 - 死信解除仍缺产品路径,写出最小外部治理契约(保留原始行、审计、验证真实处理成功后 按精确项解除、未解除继续阻断)。 - 台账补 0.1.169–0.1.180 的 SHA 与 run(含 211/215 failure、218 cancelled,不涂改)。 - library-capture / 8088 部署方案同步成本轮真实结果。
136 lines
9.7 KiB
Markdown
136 lines
9.7 KiB
Markdown
# 整库封存(libraryctl)运维手册
|
||
|
||
本文只讲**测试库**的整库封存链:怎么跑、它证明了什么、它**没有**证明什么。生产发布与恢复、清理不在本文范围。
|
||
|
||
## 它做什么
|
||
|
||
`libraryctl capture -scope internal|external`:
|
||
|
||
1. 从**本机真实测试栈**(默认 Compose 项目 `weknora071full`)读来源配置:容器环境里的数据库/检索/对象存储参数,运行时读取,不落盘、不回显。
|
||
2. 用**只读会话**(`default_transaction_read_only=on`)连来源 PostgreSQL;连接串只在进程内存里。
|
||
3. 核对来源已结清:所有文档 `parse_status=completed`、无待处理操作、无死信(任一不满足就拒绝,不看其他 KB 的局部状态)。
|
||
4. 从持久数据推出兼容契约:迁移版本、dirty、租户、Embedding 模型与维度、检索引擎、索引名、索引映射摘要(`text-embedding-v4` / 1024 维 / schema 74)。
|
||
5. 在**维护窗口**内停住声明过的写入者(只对声明为 writer 的服务发 SIGTERM,等它真的停下),导出 PostgreSQL 十表 + Elasticsearch 正文与向量 + 对象资源,做双向覆盖与摘要核验,封存为内容寻址产物。
|
||
6. 无论成败都恢复写入者并等到 healthy;崩溃留下的日志用 `libraryctl recover` 收尾。
|
||
7. 打印快照 ID、分项(kind/name/数量/字节/SHA256)与兼容契约。
|
||
|
||
## 写入者声明(安全边界)
|
||
|
||
`scripts/library-capture.sh` 里逐项声明了项目内**每一个**服务:一个 writer(`app`),其余每个都写了"为什么它不会写知识"。`ProjectController` 的规则:
|
||
|
||
- 整个 Compose 项目被枚举(含停止的),只按声明停机;
|
||
- 项目里出现未声明的服务 → **拒绝执行**,不会"只停已知的那几个";
|
||
- 首次清点后固定容器 ID,容器被重建(同名新实例)→ 拒绝,避免停到一个没被声明的实例;
|
||
- 只发 SIGTERM,没有会在恢复后才触发的 SIGKILL 定时器;
|
||
- 启动后必须等到 healthy 才算恢复。
|
||
|
||
## 命令
|
||
|
||
```bash
|
||
# 接线与来源探针(只读,不停服务)
|
||
./scripts/library-capture.sh status
|
||
# 预检:读来源 + 结清检查 + 候选校验,不写盘、不停服务
|
||
./scripts/library-capture.sh preflight -scope internal
|
||
# 真实封存(停 app → 导出 → 恢复 app)
|
||
./scripts/library-capture.sh capture -scope internal
|
||
./scripts/library-capture.sh capture -scope external
|
||
# 已封存产物
|
||
./scripts/library-capture.sh list
|
||
./scripts/library-capture.sh verify -snapshot <id>
|
||
# 崩溃后的收尾(恢复写入者)
|
||
./scripts/library-capture.sh recover
|
||
```
|
||
|
||
产物默认写到 `~/.local/share/knowledge-governance/library`(0700),日志写到 `~/.local/share/knowledge-governance/freeze`(0700)。api 容器只以只读方式挂载前者;发布管理的「版本与快照」抽屉与系统设置 → 存储管理据此展示。
|
||
|
||
同一个作业号重跑是**幂等重放**:直接返回已封存的同一份候选(`replay=true`、`source_read=false`、`writers_stopped=false`),不读来源、不停写入者;同一作业号换了 scope 或绑定库则明确拒绝。
|
||
|
||
## 实例级维护接线(封存窗口的唯一入口证据)
|
||
|
||
`capture` 与 `recover` 现在都按**实例级维护**开窗口,接线来自与 worker 同一组环境变量
|
||
(名字只在 `internal/librarymaintenance/env.go` 出现一次):
|
||
|
||
```
|
||
LIBRARY_TARGET_INSTANCE 实例名(写进维护记录与目标指纹)
|
||
LIBRARY_TARGET_INGRESS_URL 入口地址(恢复后要能 200)
|
||
LIBRARY_TARGET_INGRESS_SERVICE Compose 里的入口闸门服务名
|
||
LIBRARY_TARGET_INGRESS_DRAIN_FILE 闸门写排空证据的宿主机文件
|
||
LIBRARY_TARGET_REDIS_ADDR/_PASSWORD/_DB 后台队列(asynq)地址
|
||
```
|
||
|
||
另外 `-source-s3-endpoint <URL>` 用来覆盖"源对象存储的访问入口":容器里写的
|
||
`MINIO_ENDPOINT` 往往是服务名(`langfuse-minio:9000`),封存跑在宿主机上时解析不到,
|
||
资源导出会失败(`read library resource`)。覆盖只换入口,桶与对象键不变;`status`
|
||
会如实打印实际使用的 endpoint。
|
||
|
||
为什么不再保留"库级窗口":被代理的 WeKnora 收到 SIGTERM 会直接 `exit(1)`,写者退出码
|
||
永远不能当作"在途请求已排空"。排空证据只能在真正的入口上取——关闸门、等它把在途连接
|
||
放完(`drain.json` 里的 `active_at_close` / `drained`)。因此缺少上面任何一项时 `capture`
|
||
会**在开窗口前拒绝**(报出缺少的变量名),而不是退回一个没有排空证据的窗口。
|
||
|
||
开窗口的顺序仍是:关入口并等在途排空 → 证明后台静默(稳定窗口 + 任务集合指纹)→ 停写入者
|
||
→ 停后复核队列/指纹与入口 → 导出 → 恢复写入者与入口。停写者后仍会**再查一次**源是否结清,
|
||
所以窗口内派生的真实任务必须真的跑完,否则封存被拒(不是等一会儿就算了)。
|
||
|
||
## 从页面发起封存(主机侧 agent)
|
||
|
||
页面点「发起对内/对外封存」只是**投递一个作业**;执行发生在这台主机上的 agent:
|
||
|
||
- `scripts/library-agent-install.sh` 构建并安装 `libraryagent`(systemd **user** 服务),
|
||
监听 `~/.local/share/knowledge-governance/agent/agent.sock`,目录 0700、socket 0600,
|
||
并额外用 `SO_PEERCRED` 要求对端与 agent 同 uid。
|
||
- 维护接线由 `~/.local/share/knowledge-governance/agent.env`(0600,`EnvironmentFile=`)提供;
|
||
没有这个文件安装脚本直接拒绝(`AGENT_ENV_MISSING`),免得装出一个"没有闸门"的 agent。
|
||
源容器/项目/写入者声明可用 `AGENT_SOURCE_APP`、`AGENT_SOURCE_PG`、`AGENT_SOURCE_ES`、
|
||
`AGENT_PROJECT`、`AGENT_WRITERS`、`AGENT_NON_WRITERS` 覆盖,默认是本机 `weknora071full`。
|
||
- agent 自己固定 `libraryctl` 的路径、归档与日志根、Compose 项目与写入者声明;
|
||
请求只能选 `scope`(internal/external)和作业号,**不能**指定路径、项目、容器或命令。
|
||
- 只有 worker 通过挂载的 socket 目录投递作业(`LIBRARY_AGENT_SOCKET`)。容器里
|
||
**没有** Docker socket,**没有**来源凭据:停写入者、读来源、恢复都在主机进程里完成。
|
||
- worker 启动时会先调一次 `recover`,把上次崩溃留下的停机窗口收尾;
|
||
失败会记日志而不是假装就绪。
|
||
- 同一 `scope + 作业号` 重复投递时,agent 走幂等重放,不会第二次停源。
|
||
|
||
安全边界要说清:socket 目录对 worker 可写,因此被攻破的 worker 可以让 agent 执行
|
||
"封存 internal/external"这一类动作(这正是页面能力),但拿不到 Docker API、
|
||
拿不到来源凭据,也不能重定向到别的项目或路径。
|
||
|
||
## 2026-10-28 实测(本机测试栈)
|
||
|
||
| 项 | 对内 | 对外 |
|
||
| --- | --- | --- |
|
||
| 快照 | `f992d64ea45bf737a0721a465b5aef2b150ef7757157eb84f3aaf8b6972d98f9` | `01343b0b310b3977c2fdda05f5b8a6272148c4f7bfa124a6c5944123cf0c51c3` |
|
||
| 文档 / 分块 / 索引 | 4 / 38 / 122 | 0 / 0 / 0 |
|
||
| 资源 / Wiki 页 | 21 / 31 | 0 / 1 |
|
||
| 分项数 / 逻辑字节 | 32 / 55,309,044 | 13 / 1,121 |
|
||
| 策略 | artifact-acceptance-v1 | artifact-acceptance-v1 |
|
||
|
||
两次封存都记录了 `stop_verified=true`、`restored=true`,`WKNFull-app` 恢复后 healthy;来源未做任何写操作。
|
||
|
||
## 2026-10-29 实测:页面入口 → agent → 实例级窗口(真实撤销后的库)
|
||
|
||
- 请求:`POST /api/v1/library/captures {scope:internal, job:lib-internal-20261029T0614}` → 202,
|
||
worker 投递 agent,CLI 以**实例级窗口**执行。
|
||
- 排空证据(确定性顺序,不抢时序):先发一半 multipart 正文 → 闸门日志 `conn:open active=1`
|
||
→ 再发起封存 → 入口拒绝新连接 → 补完正文,应用返回 200 并完成解析/嵌入 →
|
||
`drain.json`:`phase=drained active_at_close=1 served=126 drain_ms=302`。
|
||
- 冻结:`kgmaint-app` 收到 SIGTERM 后 `exit=1`(上游行为),因入口已排空,`drain_verified=true`;
|
||
后台静默 `quiesce.samples=18 stable_ms=1617`,停写者后指纹不变;随后恢复写入者与入口。
|
||
- 产物:快照 `1196c518…`(KB `e64e82b5…`,tenant 10001):1 文档 / 2 分块 / 9 Wiki 页 / 1 资源,
|
||
与源库逐页比对(正文 md5、来源、引用、目录、18 条边)全部一致。
|
||
- 同一份快照随后同步到**独立实例**(自己的 PG/索引/桶)并成功恢复,逐项读回一致。
|
||
|
||
## 它证明了什么 / 没有证明什么
|
||
|
||
已证明(A04 数据完整性口径):这一份整库内容在**写入者停住**之后被完整导出,PostgreSQL 与 Elasticsearch 双向覆盖、生成问题、资源与摘要逐项核验通过,产物可寻址、可复读、可校验。这正是封版所需的完整性口径。
|
||
|
||
用户已确认取消 A06:**不要求**内容、问答、引用/图片效果与性能测试及其报告,也不作为封版/生产发布门槛。因此这里不再把"没有做效果测试"列为缺口。
|
||
|
||
仍然不成立(不许把完整性通过说成这些):
|
||
|
||
- **跨 PG / ES / 资源的一致切点**只在维护窗口内取得;写入者组完整性有检查,但没有独立的切点证据,不能当成已验收的一致性证明;
|
||
- **Wiki 删除贡献的语义重算**没有做;Wiki 队列空不等于语义正确;
|
||
- 对外库当前是空的(0 文档 1 Wiki 页),这份快照只说明"它当时确实是空的";
|
||
- 生产独立预备份、恢复演练、双闸授权、整库同步协调与失败续作都没有接线,本文不涉及;
|
||
- 从页面发起封存、持久作业互斥、启动恢复还没有接线(封存目前只能在目标主机执行命令);
|
||
- 上游父子分块漏正文索引的风险仍在:本次通过是因为库里现有数据已补齐,**以后再重解析**仍可能漏项,届时封存会因覆盖核验失败而阻断(这是设计如此)。 |