云测试发布目标:部署与验证(父执行)
目标:把本机治理平台的"整库发布执行"搬到云测试主机,与目标容器同机; 本机只保留封存源、平台与 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:
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 全项目(那会把无关服务一起重建)。
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,但不能混用到自动传输配置。
# 本机(只读归档;输出了每个 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 后建反向转发:
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. 执行器
# 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. 只读预检(任何时候都可跑)
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. 回退
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。
因此"这份版本是谁"跨主机一致,而"这份版本的字节现在在哪台机器"没有被记录。
后果(真实缺口):
- 本机页面能看到云端执行器登记的版本证据行,但看不到云端预备份/快照原件;
点"按历史版本恢复"时,若该快照不在执行器所在机器上,作业会在装配阶段失败
(
librarysnapshot.Load读不到),错误信息只会说"快照不可读"。 - 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 只读核对命令(父可用)
# 本机:把某个已封存快照推到执行器(会打印 bundle 每个 blob 的 sha256,可对拍)
LIBRARY_HANDOFF_...=... libraryctl snapshot-push -snapshot <id> -scope internal -tenant <n> -kb <uuid>
# 本机:把云端快照取回本机归档
libraryctl snapshot-fetch -snapshot <id>
两条命令都只认部署注入的配置:不接受调用方指定远端路径/容器/上游地址。