Files
knowledge-governance/deploy/cloud-test/README.md
T
2026-10-30 22:37:18 +08:00

247 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 云测试发布目标:部署与验证(父执行)
目标:把本机治理平台的"整库发布执行"搬到云测试主机,与目标容器同机;
本机只保留封存源、平台与 capture 角色的 worker。本文所有命令都在**目标机上**执行,
除了明确标注"本机"的两步。
## 0. 前置(缺一条都别继续)
- 云端 compose 项目 `weknora-test-v071` 当前健康,且已确认 `weknora-test.dobot.cn` 的维护窗口可接受。
- 已交付镜像:`knowledge-governance-worker:<与验收提交同 sha>`(含 `/app/libraryctl`)。
- `/srv/kgtest-gate/{gate.py,state}`、`/srv/knowledge-governance/{library,target-journal,bundles,release-executor.env}`。
- 治理库接入方式已定:SSH 加密反向隧道(`deploy/cloud-test/pg-forward.sh`,默认 streamlocal
unix socket)。本机 PG 在 Docker 内、无 5432 宿主机发布,**不要**写 `localhost:5432`。
## 1. 云端入口闸门(一次)
**先做 compose 身份核对(不能盲 `compose up`)**:父的现场记录里,某历史容器的 label
引用了已不存在的 `/tmp/weknora-test-compose.wiki-graph.yaml`。对这类容器执行
`docker compose up` 会让 compose 按"缺失的配置链"重建它,等于在没有可验证配置的情况下
重建业务容器。先逐容器读回配置链,只对**配置链文件都在**的服务做 `up -d`:
```bash
for c in $(docker ps -q --filter label=com.docker.compose.project=weknora-test-v071); do
docker inspect "$c" --format '{{.Name}} files={{index .Config.Labels "com.docker.compose.project.config_files"}}'
done
```
规则:`app`/`frontend` 的配置链齐全才允许 `up -d`;`ingress` 是本片段新增,没有历史包袱。
任何服务的 config_files 里有缺失文件 → **停手**,先补回可验证配置(或让父按运维流程处理),
不要用 `compose up` 把它"顺便"重建。只用显式服务名 `up -d app ingress frontend`,
不要 `up -d` 全项目(那会把无关服务一起重建)。
```bash
install -d -m 0755 /srv/kgtest-gate /srv/kgtest-gate/state
install -m 0644 deploy/cloud-test/gate.py /srv/kgtest-gate/gate.py
# 叠加片段来自本仓库 deploy/cloud-test/compose.release-overlay.yaml
cd /opt/weknora-test-v071 # 现有 compose.yaml 所在目录
docker compose -f compose.yaml -f /path/to/compose.release-overlay.yaml config >/dev/null
docker compose -f compose.yaml -f /path/to/compose.release-overlay.yaml up -d app ingress frontend
curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/health # 期望 200
docker inspect weknora-test-v071-app-1 --format '{{json .HostConfig.PortBindings}}' # 期望 null
```
要点:`app` 不再发布宿主机端口(写入者发布端口 = 绕开闸门的直连,拓扑校验会拒绝);
`ingress` 是宿主机 8080 上唯一的 HTTP 入口,`SIGTERM` 后写 `drain.json` 并 `exit 0`;
`restart: "no"` 保证维护窗口停下的入口不会被 Docker 自己拉回来。
### 1.1 入口不止 frontend:四条路径都要落到闸门上
只把 frontend 改成 `APP_HOST=ingress` **不等于**入口唯一。现场实测四条能到 app 的路径:
| 入口 | 现状 | 必须改成 | 谁做 |
|---|---|---|---|
| 宿主机 nginx `weknora-test.dobot.cn` → `127.0.0.1:8088` | frontend | 不用改;frontend 的 `/api` 由 `APP_HOST` 决定 | 父不改 |
| frontend 容器 → `app:8080` | `APP_HOST=app` | overlay 已改为 `APP_HOST=ingress` | overlay |
| knowledge-adapter 容器 → `http://app:8080`(双网络) | 直连 app | adapter 的 `WEKNORA_BASE_URL=http://ingress:8080` | 父改 adapter 部署 |
| DobotAI(host 网络)→ `http://127.0.0.1:8080` | 走宿主机发布端口 | 不用改;overlay 后该端口就是闸门 | 无 |
`scripts/verify_cloud_release_target.py` 的 `entry/*` 三项就是在核对这张表
(adapter 仍指 `app:8080` 时报 FAIL —— 它就是一条真实存在的绕闸通道)。
## 2. 快照(本机打包 → 云端导入)
自动 handoff 使用 `RELEASE_BUNDLE_DIR`(默认 `/srv/knowledge-governance/bundles`);启动器创建 0700 目录并在执行器内同路径挂载。本机 API/worker 的 `LIBRARY_HANDOFF_BUNDLE_DIR` 必须与它一致,因为 SSH 在宿主机写入、libraryctl 在容器中读取同一路径。手工单次导入可像下例单独映射 `/bundles`,但不能混用到自动传输配置。
```bash
# 本机(只读归档;输出了每个 blob 的 sha256,可与云端对拍)
libraryctl snapshot-pack -snapshot <snapshot_id> -bundle /var/tmp/<snapshot_id>.tar
# 传到云端
scp /var/tmp/<snapshot_id>.tar weknora-test:/srv/knowledge-governance/bundles/
# 云端:校验 + 原子装入(版本清单是提交点;重复导入幂等)
docker run --rm \
-v /srv/knowledge-governance/library:/var/lib/knowledge-governance/library \
-v /srv/knowledge-governance/bundles:/bundles:ro \
knowledge-governance-worker:<sha> \
/app/libraryctl snapshot-unpack \
-archive /var/lib/knowledge-governance/library \
-bundle /bundles/<snapshot_id>.tar \
-snapshot <snapshot_id>
```
导入失败时不会留下版本:blob 以 sha256 命名且先落盘,版本清单最后才 Link。
执行器每次使用快照前还会 `Load` 逐 blob 重算 sha256,所以"传输不可变"有两道证据。
## 2.5 治理库连接(SSH 加密反向隧道)
本机治理 PG 在 Docker 内且没有 5432 宿主机发布,不能写 `localhost:5432`。
`deploy/cloud-test/pg-forward.sh`(本机运行)解析 PG 容器的真实 IP 后建反向转发:
```bash
MODE=streamlocal bash deploy/cloud-test/pg-forward.sh # 推荐:云侧只出现 unix socket
MODE=tcp bash deploy/cloud-test/pg-forward.sh # 云侧 127.0.0.1:15433,仅宿主进程可用
```
- **streamlocal(默认)**:云侧生成 `/run/kgtest-pg/.s.PGSQL.5432`,执行器容器把该目录挂进去,
`DATABASE_URL` 用 `host=/run/kgtest-pg&port=5432`。没有 TCP 端口、不受"容器看不到宿主
127.0.0.1"影响。
- **tcp**:`ssh -R 127.0.0.1:15433` 只在云**宿主机** loopback 上;执行器容器里的
127.0.0.1 是它自己,**不能**直接用。要用就必须在云侧再加 docker bridge → 127.0.0.1
的桥接(socat/haproxy),那是父的运维决定,脚本不替你开。
- 隧道断 = 执行器拿不到租约 = 作业 fail-closed,目标保持维护态(不会"假装成功")。
### 治理凭据与挂载
目标机准备非空 0600 `/srv/knowledge-governance/governance.pgpass`,只读挂载为
`/run/secrets/governance_pgpass`;env 必须显式设置 `PGPASSFILE` 为该容器路径。
Unix socket 的 pgpass 主机匹配使用 `localhost`,字段中的冒号和反斜线须按 pgpass 规则转义。
不要把本机无密码 DATABASE_URL 当成已携带凭据。启动器挂载 `/run/kgtest-pg`,
默认网络为 `weknora-test-v071-network`;目录或密码文件不满足要求时在重建前拒绝。
可用 `RELEASE_PG_SOCKET_DIR` / `RELEASE_PGPASS_FILE` 覆盖宿主路径,容器路径不变。
执行器显式启动 `/app/worker`,不能沿用镜像默认的 `/app/api`;仅该执行器以
`--user 0:0` 运行,访问 root 私有 socket、0600 凭据和 0700 归档。挂载 Docker socket
意味着它具有宿主容器控制权限,并非只读权限;不得把该身份用于对外 API 或前端。
镜像必须预先加载,启动使用 `--pull never`,不允许现场隐式拉取替代产物。
## 3. 执行器
```bash
# 1) 填写 /srv/knowledge-governance/release-executor.env(0600 root),
# 字段见 deploy/cloud-test/release-executor.env.example。
# 2) 先让本机变成 capture 角色(否则两边都注册 release,谁抢到按谁的配置执行):
# 本机 compose 加 LIBRARY_WORKER_ROLE=capture 重启 worker,日志应出现
# role=capture release=false source=true。
# 3) 起云端执行器:
RELEASE_EXECUTOR_IMAGE=knowledge-governance-worker:<sha> bash deploy/cloud-test/run-release-executor.sh
docker logs --tail 30 kgtest-release-executor # 期望:execution domain admitted / role=release-executor
```
## 4. 只读预检(任何时候都可跑)
```bash
python3 scripts/verify_cloud_release_target.py --host weknora-test --snapshot <snapshot_id>
```
判据:ssh、项目服务清单、写入者无发布端口、闸门 healthy 且发布 8080、经闸门 /health=200、
drain 目录可写且无残留、redis/ES/RDS 可达、归档根 0700、快照清单摘要自证、执行器角色。
`MANUAL target-graph` 必须人工确认目标 KB `graph_enabled=false`(发布路径不写 Neo4j)。
有 FAIL 即退出码 1。
## 5. 回退
```bash
docker rm -f kgtest-release-executor
# 本机 worker 改回 LIBRARY_WORKER_ROLE=full(或保持 capture)并重启
cd /opt/weknora-test-v071
docker compose -f compose.yaml -f /path/to/compose.release-overlay.yaml down # 或去掉 -f overlay 重新 up:
docker compose -f compose.yaml up -d app frontend
```
治理库不需要回滚:维护记录与版本证据都带 `target_fingerprint` 与实例身份,
跨目标不会被误认。云端归档里的快照与预备份保留,不自动删除。
## 6. 已知边界(交付时写明,不当成已完成)
- 本机 UI 的"版本与快照"读的是本机归档:云端预备份/快照原件在页面上看不到,
只能看到治理库里的版本证据行。需要页面展示云端原件时再单独做只读读回链路。
- 目标 KB `graph_enabled` 没有 fail-closed 检查(当前只有源侧导出拒绝)。
- 维护窗口内 `weknora-test.dobot.cn` 会不可用(这是语义正确的关闭)。
- **归档自动传输未实现**:今天只有 `libraryctl snapshot-pack / snapshot-unpack`(人工两步)。
UI 里点"发布/恢复"不会把本机快照送到云端执行器,也不会把云端预备份读回本机页面。
一致寻址的现状与阻塞见本文 §7。
## 7. 归档寻址:现状、缺口与阻塞(不许当成已完成)
**寻址口径(现状)**:快照是内容寻址的(`snapshot_id` = 版本清单 sha256),本机与云端
各自有独立 `LIBRARY_ARCHIVE_ROOT`;治理库的版本证据行记录 `snapshot_id` 与 `source_job_id`。
因此"这份版本是谁"跨主机一致,而"这份版本的字节现在在哪台机器"**没有**被记录。
后果(真实缺口):
1. 本机页面能看到云端执行器登记的版本证据行,但**看不到云端预备份/快照原件**;
点"按历史版本恢复"时,若该快照不在执行器所在机器上,作业会在装配阶段失败
(`librarysnapshot.Load` 读不到),错误信息只会说"快照不可读"。
2. UI 无法触发传输:本机 API 没有读归档/推送归档的接口,云端也**不能**主动连本机
(公网 22 关闭、无入站);今天唯一通路是人工 `pack` → `scp` → `unpack`。
**为什么不用脚本硬糊**:定时 rsync/scp 无法保证"版本清单与 blob 同时可见 + 字节可校验",
而发布前的 `Load` 会逐 blob 重算 sha256,半拷贝的归档会让发布在维护窗口内失败,
正好落在最不该失败的时刻。
**建议的最小协议(未实现,需父批准)**:给执行器一条**出站**拉取路径 ——
`GET /api/v1/library/snapshots/{id}/bundle`(本机 API 侧,仅服务已封存且未过期的快照,
返回带 sha256 的流)+ 在版本证据里补 `archive_host` 字段。执行器在装配前若本地缺快照,
按证据里的 `archive_host` 标记走到该 API 拉取并复用 `Unpack` 的校验/原子落位。
工作量约:API 1 个只读端点 + 证据字段 + 执行器一次拉取调用(~300 行 + 权限/审计)。
通路本身可以复用已有的 `ssh -R`(再加一条把本机 API 映到云侧 loopback/unix socket 的
反向转发),所以阻塞不在"连不上",而在:① 该端点等于让云侧凭据读走整库快照,
需要单独的鉴权与审计口径;② 版本证据要新增 `archive_host`,否则 UI 无法判断"该去哪台机器取";
③ 拉取失败必须与"目标已进维护窗口"的时序绑定,不能先开窗口再发现取不到。
这三条都要父拍板,属于新协议而非配置。
## 8. 自动快照搬运(run03 已实现,需按本节接线)
没有这一步,UI 点"发布/恢复"只会在执行器上以"快照不在本机归档"失败(fail-closed,不会开维护窗口)。
接线后链路变成:**授权 → 搬运并双向校验 → 才投递发布作业**。
### 8.1 接线(API 与 capture worker 都要有同一组值)
| 变量 | 含义 |
|---|---|
| `LIBRARY_HANDOFF_SSH_CONFIG` | 0600 ssh 配置(与 `BACKUP_HOST_SSH_CONFIG` 同一份即可,指向 `weknora-test`) |
| `LIBRARY_HANDOFF_TARGET` | ssh 配置里的 Host 别名(`weknora-test`) |
| `LIBRARY_HANDOFF_EXECUTOR` | 云端执行器容器名(`kgtest-release-executor`) |
| `LIBRARY_HANDOFF_BUNDLE_DIR` | 云端暂存目录(`/srv/knowledge-governance/bundles`,只放传输中的 bundle) |
| `LIBRARY_HANDOFF_REMOTE_ARCHIVE` | 云端归档根(必须等于执行器 env 的 `LIBRARY_ARCHIVE_ROOT`) |
| `LIBRARY_HANDOFF_STAGING_DIR` | 本机暂存目录(容器内可见的 0700 目录) |
| `LIBRARY_HANDOFF_MAX_BYTES` | 单份 bundle 上限(默认 8GiB) |
| `LIBRARY_HANDOFF_TIMEOUT` | 单个远端动作超时(默认 10m) |
| `LIBRARY_HANDOFF_PULL_INTERVAL` | 云端版本回传本机的扫描间隔(默认 5m;游标跨轮保留,重启后从头核对) |
目标/源身份(run05 起必需,二者**必须不同**):
`LIBRARY_PRODUCTION_TENANT_ID` + `LIBRARY_PRODUCTION_KB_INTERNAL/EXTERNAL` = 云测试验收专用 KB
(对内 `a8e9c8ad-411c-4c53-8421-599d29e9b179`、对外 `2e980686-2233-4c4e-b5a6-8b0069477b8e`,tenant 10000);
`LIBRARY_SOURCE_TENANT_ID` + `LIBRARY_SOURCE_KB_INTERNAL/EXTERNAL` = 源 WKNFull 的 KB。
搬运按版本台账判定原产地:封存快照用源绑定核对,预备份用目标绑定核对;没有捕获事件或两类都有的快照一律拒绝。
原有业务四 KB(graph=true)不关闭、不覆盖;验收专用 KB 的 graph_enabled=false。
任一项缺失但其余存在 ⇒ **启动失败**(半接线最坏:API 以为会搬运、worker 搬不动)。
全都不配 ⇒ 保持旧行为(本机目标直接发布),不受影响。
### 8.2 行为
- 发布/恢复作业先入 `library.snapshot.handoff`(capture 角色执行):本机 `Load` 逐 blob 校验 →
`Pack` → SSH 流式写云端暂存 → 原子改名 → 云端 `libraryctl snapshot-unpack`(再逐 blob 校验)→
读回版本清单摘要自证 → 身份(scope/tenant/KB)与治理库作业一致 → **才**投递 `library.sync|restore`
(ID 仍是 `QueueJobIDFor(jobID)`,接管/续作路径不变)。
- 失败即作业失败,文案写明"未进入维护窗口、未写任何知识";发布队列行不存在。
- 云端已有同 ID 且校验通过 ⇒ 复用(不发数据);同 ID 异内容 ⇒ 拒绝。
- 同一快照并发搬运:本机 flock 串行化,云端内容寻址 + Link 幂等。
- 暂存 bundle 用后即删(只删自己这次的传输文件);归档本体(versions/blobs)**永不自动清理**。
- 本机回传:capture 角色按 `library_jobs` 里已成功作业的 `snapshot_id` 扫描,本机缺哪个就
`Fetch` 哪个(云端 pack → SSH 流回 → 本机 `Unpack` 逐 blob 校验)。回传成功后本机既有
API/页面路径(列表/详情/差异/存储/历史恢复)读到的就是真实字节;回传失败则版本不出现在本机,
页面不会把它显示成可恢复。
### 8.3 只读核对命令(父可用)
```bash
# 本机:把某个已封存快照推到执行器(会打印 bundle 每个 blob 的 sha256,可对拍)
LIBRARY_HANDOFF_...=... libraryctl snapshot-push -snapshot <id> -scope internal -tenant <n> -kb <uuid>
# 本机:把云端快照取回本机归档
libraryctl snapshot-fetch -snapshot <id>
```
两条命令都只认部署注入的配置:不接受调用方指定远端路径/容器/上游地址。