为所有 npm ci 增加整命令级有界重试。 补齐 AI 游戏创作 npm 与 Cargo 依赖缓存闭合验证。 更新可信 CI 镜像、缓存漂移告警和 Runner 运维文档。 记录 Actions Gateway CONNECT 断连崩溃的处理与验证边界。
Genarrative 容器化压测、隔离部署与 CI Job 镜像
本目录同时保存两类互不替代的容器资产:本机或预发的容器化模拟压测,以及 Gitea Actions 使用的预构建 CI job 镜像。它们都不替换当前生产 systemd + Nginx + Jenkins 发布路径;生产服务器仍以 deploy/systemd/、deploy/nginx/、scripts/jenkins-*.sh 和 scripts/deploy/production-api-deploy.sh 为准。当前 compose 不包含独立 bgfilter-worker,因此不是完整 BgFilter 预发拓扑,也不覆盖会触发 BgFilter 的现役任务;这里只验证非 BgFilter 路径,或使用 unsupported job 检查队列 claim / fail 回写和 API / worker 进程隔离。
拓扑
Docker Compose
├─ spacetimedb :3101,独立数据卷,供 api-server 连接
├─ nginx :80 -> api-server:8082,负责静态站点、/admin/、/api/ 反代、upstream timing log、连接限制
├─ api-server :8082,Linux release 构建,连接 compose 内 SpacetimeDB
├─ external-generation-worker,独立 worker 进程,消费 external_generation_job 队列
└─ otelcol :4317/4318,debug exporter,接收 traces / metrics / logs
当前容器模拟参数按 genarrative-release 服务器采样值收口为 2 vCPU / 2 GiB RAM / 4096 soft nofile / 768 worker_connections,并已在 compose 里落实到 spacetimedb cpus=1.0 mem_limit=896m、api-server cpus=2.0 mem_limit=1g、external-generation-worker cpus=2.0 mem_limit=1g、nginx cpus=0.5 mem_limit=128m、otelcol cpus=0.25 mem_limit=128m。SpacetimeDB 同时设置 --page_pool_max_size=402653184,给 reducer、订阅与运行时保留更多非 page pool 内存。
容器 api-server 默认 GENARRATIVE_API_WORKER_THREADS=4,用于让 Tokio 在 2 vCPU 配额内有更多 I/O 调度 worker;该值不会突破 compose 里的 cpus=2.0 CPU 上限。
容器默认 GENARRATIVE_EXTERNAL_GENERATION_MODE=queue,用于验证不经过 BgFilter 的 api-server -> external_generation_job -> external-generation-worker 链路;会触发 BgFilter 的任务不属于当前 compose 验收范围。如只想本地同步排查非 BgFilter provider / OSS / SpacetimeDB 写回,可在本机 env 临时改为 inline,但该模式不会覆盖 worker 动态扩缩容验证。
Collector 镜像使用 otel/opentelemetry-collector-contrib:0.151.0。
生产服务器若启用 Collector,则由 deploy/systemd/otelcol-contrib.service 和 deploy/otelcol/genarrative-debug.yaml 托管,不走容器镜像。
默认 host 端口:
http://127.0.0.1:13101:容器 SpacetimeDB。http://127.0.0.1:18080:容器 Nginx。127.0.0.1:4317/127.0.0.1:4318:容器 Collector OTLP gRPC / HTTP。
如端口冲突,可设置:
$env:GENARRATIVE_CONTAINER_SPACETIME_PORT="13102"
$env:GENARRATIVE_CONTAINER_HTTP_PORT="18081"
$env:GENARRATIVE_CONTAINER_OTLP_HTTP_PORT="14318"
$env:GENARRATIVE_CONTAINER_OTLP_GRPC_PORT="14317"
初始化
npm run container:init
该命令会从 deploy/container/api-server.env.example 生成本地 deploy/container/api-server.env。真实 token、库名和外部服务密钥只写本地 env 文件,不提交 Git。
Docker Desktop 下默认通过 http://spacetimedb:3101 连接 compose 内 SpacetimeDB;宿主机只负责用 CLI 发布模块:
GENARRATIVE_SPACETIME_SERVER_URL=http://spacetimedb:3101
GENARRATIVE_SPACETIME_DATABASE=genarrative-loadtest
GENARRATIVE_SPACETIME_TOKEN=
宿主机发布模块时,先用 CLI 向 http://127.0.0.1:13101 发布到 genarrative-loadtest,再启动 npm run container:up。
Linux Docker Engine 若要从宿主机 CLI 连到容器内服务,直接用 http://127.0.0.1:13101;容器内部服务之间统一走 http://spacetimedb:3101。
构建工具链
api-server 容器镜像只构建 Linux release API 二进制,不构建 spacetime-module。当前 api-server -> spacetime-client -> spacetimedb-sdk 2.7.0 依赖链继续兼容 Rust 1.93,因此 deploy/container/api-server.Dockerfile 的 Rust builder 固定为 rust:1.93-bookworm。镜像构建阶段会同时复制 public/,用于满足 API 二进制里 include_bytes! 引用的内置素材;不要把 public/generated-* 放入镜像上下文。如果本机 Docker Hub 拉取失败,可以先在本机准备同名本地 builder 镜像,但不要把临时 bootstrap 容器或私有 registry 凭据写入仓库。
Gitea CI 预构建 Job 镜像
.gitea/workflows/project-ci.yml 的四个 job 统一使用 deploy/container/gitea-ci-job.Dockerfile 构建的 genarrative-ci 环境。镜像固定 Ubuntu job base digest sha256:58ea92624c7c09582e05594d95488331045053d3a3f34cf09649f2a32313a614 和 Rust stage digest sha256:19817ead3289c8c631c73df281e18b59b172f6a31f4f563290f69cddd06c30e9;Node 22.23.1 发行包在解压前执行 SHA-256 校验,Google Linux 主签名指纹固定为 EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796,Chrome 固定为 150.0.7871.181-1。镜像预装 Rust 1.96.0、rustfmt、Chrome、bwrap、rg、ffmpeg、clang/lld 和 Tauri / 后端系统依赖,并设置 RUSTUP_AUTO_INSTALL=0;仓库工具链变更时必须先重建镜像,不允许 job 现场下载补齐。
构建、校验和装入 runner 内层 Docker:
bash scripts/gitea-ci-job-image.sh build
bash scripts/gitea-ci-job-image.sh verify
bash scripts/gitea-ci-job-image.sh export /仓库外受控路径/genarrative-gitea-project-ci-20260807.1.tar.zst
bash scripts/gitea-ci-job-image.sh load-runner
默认构建 tag 为 genarrative/gitea-project-ci:20260807.1。脚本通过 NUL 分隔白名单 tar 流只发送 Dockerfile、checkout 脚本、根与 AI 游戏创作壳的 npm manifests/lock,以及 server-rs、桌面壳和 AI 游戏创作壳的 Cargo manifests/lock;当前构建 context 约 2.13 MB,不会把业务源码、素材或本地私密文件发送给 Docker daemon。镜像除固定工具链外,还按上述五份 lock 预热 npm / Cargo 下载缓存;三个 cargo fetch --locked 最多执行 5 次整命令级有界重试,再分别以断网 cargo fetch --locked 验证缓存闭合,不包含 node_modules 或 Cargo target。build 完成后会自动运行环境校验,load-runner 还会比对宿主和 runner 内层的完整 Image ID,并在内层执行 bwrap 与 Chrome headless canary。当前验证镜像约 1.85 GB,完整 Image ID 为 sha256:8b4b30f5a096522942947927b06cf47bdb1a1016dde9a3573f9780e79d8e40cf。执行这些命令不要求必须使用 root,但执行账号必须有权访问宿主 Docker API 并管理 runner 容器;没有该权限时交给 runner 运维人员执行。
runner 配置保留原 ubuntu-latest 映射,另外增加 genarrative-ci:docker://sha256:8b4b30f5a096522942947927b06cf47bdb1a1016dde9a3573f9780e79d8e40cf。内层 Docker 数据必须持久化,force_pull 保持 false;该精确 Image ID 在内层不存在时 job 应直接失败,不回退到浮动 tag 或现场拉取。四个 job 使用镜像内 genarrative-gitea-checkout 直接从当前 Gitea 拉取事件 commit,带 5 次有界重试,不再运行时下载 GitHub checkout action;随后以 GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 执行 scripts/check-gitea-ci-job-image.sh,同时校验工具链、五份缓存锁命中状态、bwrap 和 Chrome headless。锁不匹配时校验会输出 partial 和醒目的 Actions warning,提示在可信分支落地后刷新镜像。各 job 仍运行干净的 npm ci 以校验当前 lockfile 并隔离 PR 依赖,但统一通过 scripts/ci-npm-ci-with-retry.sh 最多执行 3 次整命令级有界重试,并使用镜像内 npm cache 和 prefer-offline;锁文件新增依赖时允许经受控网络补齐,本阶段不启用共享 Actions cache。
更新顺序固定为:
- 执行
build/verify,记录输出的完整 Image ID;用export将镜像和便携 SHA-256 sidecar 保存到仓库外受控位置。 - 执行
load-runner,确认该 ID 已进入 runner 内层 Docker。 - 确认没有活跃 job,将当前 runner config 备份到仓库外的受控位置;备份不得进入 Git,也不得在文档或日志中回显注册信息。
- 增加或替换
genarrative-ci的精确docker://<Image ID>映射,然后执行docker restart --timeout 660 gitea-runner。 - 重跑真实 PR 的四个 CI job;全部通过且隔离边界复核完成后,才能清理旧镜像。
docker restart --timeout 660 只提供容器停止宽限,不是 Runner drain API;rootless DinD 的 supervisor 可能与 runner 同时停止内层 dockerd。重启前必须同时确认 Gitea 没有 in_progress run 且内层 docker ps 为空,不能依赖该 timeout 等待活跃 job。
回滚时先把 workflow 的 runs-on 改回 ubuntu-latest,再恢复备份的 runner config 并用同一超时重启 runner。不要在真实 CI 验证前删除旧映射或旧镜像。
启动与验证
npm run container:config
npm run container:build
npm run container:up -- spacetimedb
spacetime publish genarrative-loadtest --server http://127.0.0.1:13101 --module-path server-rs/crates/spacetime-module --yes --build-options="--debug"
npm run container:up
npm run container:ps
curl -sS -H 'Authorization: Bearer <access-token>' \
'http://127.0.0.1:18080/api/assets/history?kind=character_visual'
查看日志:
npm run container:logs -- nginx
npm run container:logs -- api-server
npm run container:logs -- external-generation-worker
npm run container:logs -- otelcol
npm run container:config 默认只校验配置,不打印完整 env。排查 compose 展开结果时可临时使用:
npm run container:config -- --print
如果 deploy/container/api-server.env 已写入真实 token,不要把完整展开结果贴到公开渠道。
动态扩缩容外部生成 worker 时,只调整 external-generation-worker service:
npm run container:up -- --scale external-generation-worker=3 external-generation-worker
npm run container:up -- --scale external-generation-worker=1 external-generation-worker
动态扩缩容验证必须保持 GENARRATIVE_EXTERNAL_GENERATION_MODE=queue;inline 模式下生成请求由 api-server 同步执行,不会被这些 worker 实例消费。
外部生成 Worker 隔离 Smoke
如果只想在本机隔离验证 worker 模式,不复用 deploy/container/api-server.env,使用专用脚本:
npm run container:worker-smoke -- smoke
该脚本会生成 gitignored 的 deploy/container/worker-smoke/api-server.env 与端口 state,使用独立 compose project、独立 SpacetimeDB 数据卷和独立 host 端口,完成 build -> up-spacetime -> publish -> up -> enqueue -> api-update -> enqueue。测试 job 使用 worker_smoke_unsupported 类型,不访问真实 VectorEngine、LLM 或 OSS;预期结果是 worker 领取队列任务后按“不支持的任务类型”执行失败分支,从而验证队列 claim、lease、失败回写路径和 API / worker 进程隔离。external_generation_job 是 private table,脚本通过 worker 日志里的 job_id 和 unsupported 记录确认消费,不通过 CLI SQL 绕过权限。smoke 默认只启动 api-server 与 external-generation-worker,避免无关前端 / Nginx 镜像构建;需要同时验证 Nginx 时可分步执行 up --with-nginx。
分步排查时可执行:
npm run container:worker-smoke -- init --force
npm run container:worker-smoke -- build
npm run container:worker-smoke -- up-spacetime
npm run container:worker-smoke -- publish
npm run container:worker-smoke -- up
npm run container:worker-smoke -- enqueue before-update
npm run container:worker-smoke -- api-update
npm run container:worker-smoke -- enqueue after-update
npm run container:worker-smoke -- status
如果隔离端口或库数据需要重置:
npm run container:worker-smoke -- smoke --force
container:worker-smoke 默认会把本机 spacetime 2.7.0 CLI 打成轻量 SpacetimeDB 镜像,避免首次 smoke 必须拉取官方大镜像;普通 npm run container:* 压测默认使用 clockworklabs/spacetime:v2.7.0-hotfix3(容器内二进制报告 2.7.0)。如果 Docker build 阶段在容器内拉取 crates.io 依赖不稳定,可让容器内 Cargo 复用本机 Cargo 缓存构建当前二进制,再打入临时 smoke 镜像。该模式默认使用 rust:1.93-bookworm 作为 builder、Debian bookworm smoke runtime 承载构建产物;需要换 builder 镜像时设置 GENARRATIVE_WORKER_SMOKE_CARGO_IMAGE,需要换运行时基础镜像时设置 GENARRATIVE_WORKER_SMOKE_LOCAL_BASE_IMAGE:
npm run container:worker-smoke -- smoke --local-binary
api-update 只会 --force-recreate api-server,并校验 external-generation-worker 容器 ID 不变;如要同时重建 API 镜像,使用:
npm run container:worker-smoke -- api-update --build
验证 worker 动态扩缩容:
npm run container:worker-smoke -- scale 3
npm run container:worker-smoke -- ps
npm run container:worker-smoke -- enqueue scaled-workers
npm run container:worker-smoke -- scale 1
查看或清理隔离环境:
npm run container:worker-smoke -- logs external-generation-worker
npm run container:worker-smoke -- down -v
停止:
npm run container:down
如需同时清理容器卷:
npm run container:down -- -v
历史压测
旧作品列表 / gallery 的 compose k6 profile 与 container:k6 命令已经随模板业务退役。scripts/loadtest/ 仅保留历史脚本和脱敏样例,不进入当前运行入口;新的容量验收必须针对现役编辑器、项目或素材接口单独设计,不能复用旧作品接口结论。
内存采样
排查 API 容器内存时,优先对比压测前后的 /proc/$pid/smaps_rollup 和 cgroup 当前/峰值,不把 Windows 任务管理器总占用当成单进程结论:
docker exec genarrative-container-loadtest-api-server-1 sh -c 'pid=$(pidof api-server); grep VmRSS /proc/$pid/status; grep RssAnon /proc/$pid/status; cat /proc/$pid/smaps_rollup | grep Anonymous; echo cgroup_current=$(cat /sys/fs/cgroup/memory.current); echo cgroup_peak=$(cat /sys/fs/cgroup/memory.peak)'
/healthz 也能复现的内存尖峰应先按连接层、service clone 或 allocator 高水位排查,不要直接归因到 SpacetimeDB procedure、作品列表 cache 或业务 DTO。2026-05-18 验证:AppState 改为 Arc<AppStateInner> 浅拷贝后,容器内直连 api-server:8082/healthz 的 500 HTTP req/s、PREALLOCATED_VUS=100、30 秒压测完成 15001 次请求,http_req_failed=0、dropped_iterations=0,API 进程 RSS 从约 18 MiB 升至约 52 MiB,cgroup 峰值约 47 MiB,未再出现 1 GiB 级尖峰。
OTLP
容器内 otelcol 默认使用 debug exporter。开启 api-server OTEL:
GENARRATIVE_OTEL_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol:4318
然后重建或重启容器:
npm run container:up
npm run container:logs -- otelcol
Collector 日志会输出 traces / metrics / logs。接 Rider、Jaeger、Tempo、Prometheus、Grafana 或托管平台时,另建独立 Collector 配置,不直接改生产 systemd 或 Nginx 模板。
容器内需要临时转发到 Grafana Cloud 时,切换 Collector 配置并从当前 shell 传入 Grafana Cloud 凭据;真实 token 不写入仓库文件:
$env:GENARRATIVE_CONTAINER_OTELCOL_CONFIG="./otelcol.grafana.yaml"
$env:GRAFANA_CLOUD_OTLP_ENDPOINT="https://..."
$env:GRAFANA_CLOUD_BASIC_AUTH_HEADER="Basic ..."
npm run container:up
npm run container:logs -- otelcol
deploy/container/otelcol.grafana.yaml 会同时保留本地 debug exporter,并通过 otlphttp/grafana 把 traces / metrics / logs 发到 Grafana Cloud。
隔离边界
- 不改生产 systemd 单元。
- 不改 Jenkins 发布主流程。
- 不要求真实 HTTPS 证书。
- 不把真实
.env、.env.local、.env.secrets.local或deploy/container/api-server.env放入 Docker build context。 - 不在容器镜像里内置 SpacetimeDB 数据或 token。