# Genarrative 容器化压测与隔离部署方案 本目录只服务本机或预发的容器化模拟压测,不替换当前生产 `systemd + Nginx + Jenkins` 发布路径。生产服务器仍以 `deploy/systemd/`、`deploy/nginx/`、`scripts/jenkins-*.sh` 和 `scripts/deploy/production-api-deploy.sh` 为准。 ## 拓扑 ```text 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 └─ k6 profile=loadtest 时临时启动,在 compose 网络内压 nginx ``` 当前容器模拟参数按 `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`、`k6 cpus=1.0 mem_limit=512m`。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`,用于验证 `api-server -> external_generation_job -> external-generation-worker` 链路;如只想本地同步排查 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。 如端口冲突,可设置: ```powershell $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" ``` ## 初始化 ```bash 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 发布模块: ```env 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.4.1` 依赖链要求 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 凭据写入仓库。 ## 启动与验证 ```bash 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 http://127.0.0.1:18080/api/runtime/puzzle/gallery ``` 查看日志: ```bash 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 展开结果时可临时使用: ```bash npm run container:config -- --print ``` 如果 `deploy/container/api-server.env` 已写入真实 token,不要把完整展开结果贴到公开渠道。 动态扩缩容外部生成 worker 时,只调整 `external-generation-worker` service: ```bash 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`,使用专用脚本: ```bash 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`。 分步排查时可执行: ```bash 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 ``` 如果隔离端口或库数据需要重置: ```bash npm run container:worker-smoke -- smoke --force ``` `container:worker-smoke` 默认会把本机 `spacetime` 2.4.1 CLI 打成轻量 SpacetimeDB 镜像,避免首次 smoke 必须拉取官方大镜像;普通 `npm run container:*` 压测仍默认使用 `clockworklabs/spacetime:v2.4.1`。如果 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`: ```bash npm run container:worker-smoke -- smoke --local-binary ``` `api-update` 只会 `--force-recreate api-server`,并校验 `external-generation-worker` 容器 ID 不变;如要同时重建 API 镜像,使用: ```bash npm run container:worker-smoke -- api-update --build ``` 验证 worker 动态扩缩容: ```bash 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 ``` 查看或清理隔离环境: ```bash npm run container:worker-smoke -- logs external-generation-worker npm run container:worker-smoke -- down -v ``` 停止: ```bash npm run container:down ``` 如需同时清理容器卷: ```bash npm run container:down -- -v ``` ## 压测 k6 在 compose 网络内访问 `http://nginx`,避免 Windows 本机直连连接模型干扰 Linux 容器结果: ```bash npm run container:k6 ``` 作品列表脚本一次 iteration 默认请求两个公开列表接口,因此目标 500 HTTP req/s 对应 `PEAK_RPS=250`: ```powershell $env:SCENARIO="spike" $env:START_RPS="25" $env:PEAK_RPS="250" $env:HOLD="60s" $env:END_RPS="25" $env:PREALLOCATED_VUS="100" $env:MAX_VUS="500" $env:DETAIL_RATIO="0" npm run container:k6 ``` 容器内 `api-server` 资源上限与 Nginx 连接模型已经按 `genarrative-release` 的 2C / 2G / `nofile=4096` / `worker_connections=768` 收口;如果你要改成别的机器,就先重新采样再改这里。 SpacetimeDB 容器默认只提供运行时,不自动发布模块。首次启动或清理 `spacetime-data` 卷后,先只启动 `spacetimedb` 服务,再发布模块: ```bash 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:k6`。如果 `deploy/container/api-server.env` 里的 `GENARRATIVE_SPACETIME_DATABASE` 改成了别的库名,发布命令里的库名也要同步修改。 如果要压 1000 HTTP req/s,把 `PEAK_RPS` 调到 `500`;如果要压 5000 HTTP req/s,把 `PEAK_RPS` 调到 `2500`,并同时提高 `PREALLOCATED_VUS` / `MAX_VUS`,观察是否先被带宽、Nginx `limit_conn` / `limit_req` 或 api-server 分组背压限制。当前容器 Nginx 对公开 gallery list 使用 `genarrative_gallery_rps`,公开详情和普通 API 使用 `genarrative_api_rps`,后台 API 使用 `genarrative_admin_rps`;api-server 侧对应 `GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS`、`GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS` 和 `GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS`。 2026-05-19 的 2C / 2G 容器压测结论:公开 gallery list 的 `limit_conn=320`、`limit_req rate=5000r/s burst=4096` 与 `GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320` 是当前发布口径。用宿主机 k6 打 `http://127.0.0.1:18080`,`PEAK_RPS=2500` 等价于约 5000 HTTP req/s 的两接口组合压测;连续 10 轮不重启 SpacetimeDB 的平均实际吞吐约 `4219 HTTP req/s`,总计 `1,897,357` 个 200、`212,542` 个 429、`0` 个 5xx,200 请求平均 `p95=123ms`、`p99=234ms`。该档会让 SpacetimeDB 内存从约 `366MiB` 累积到约 `885MiB / 896MiB`,下游内存先到危险区。当前不要为了降低“剩余 CPU”继续抬公开列表并发;下一步应减少成功列表请求后的 SpacetimeDB tracking 写入或优化下游连接 / 订阅状态,而不是放大入口并发。 ### 内存采样 排查 API 容器内存时,优先对比压测前后的 `/proc/$pid/smaps_rollup` 和 cgroup 当前/峰值,不把 Windows 任务管理器总占用当成单进程结论: ```bash 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` 浅拷贝后,容器内直连 `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: ```env GENARRATIVE_OTEL_ENABLED=true OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol:4318 ``` 然后重建或重启容器: ```bash 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 不写入仓库文件: ```powershell $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。