247 lines
16 KiB
Markdown
247 lines
16 KiB
Markdown
# 云测试发布目标:部署与验证(父执行)
|
||
|
||
目标:把本机治理平台的"整库发布执行"搬到云测试主机,与目标容器同机;
|
||
本机只保留封存源、平台与 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>
|
||
```
|
||
|
||
两条命令都只认部署注入的配置:不接受调用方指定远端路径/容器/上游地址。
|