9.3 KiB
Genarrative 容器化压测与隔离部署方案
本目录只服务本机或预发的容器化模拟压测,不替换当前生产 systemd + Nginx + Jenkins 发布路径。生产服务器仍以 deploy/systemd/、deploy/nginx/、scripts/jenkins-*.sh 和 scripts/deploy/production-api-deploy.sh 为准。
拓扑
Docker Compose
├─ spacetimedb :3101,独立数据卷,供 api-server 连接
├─ nginx :80 -> api-server:8082,负责静态站点、/admin/、/api/ 反代、upstream timing log、连接限制
├─ api-server :8082,Linux release 构建,连接 compose 内 SpacetimeDB
├─ 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、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 上限。
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.2.0 依赖链要求 Rust 1.93,因此 deploy/container/api-server.Dockerfile 的 Rust builder 固定为 rust:1.93-bookworm。如果本机 Docker Hub 拉取失败,可以先在本机准备同名本地 builder 镜像,但不要把临时 bootstrap 容器或私有 registry 凭据写入仓库。
启动与验证
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
查看日志:
npm run container:logs -- nginx
npm run container:logs -- api-server
npm run container:logs -- otelcol
npm run container:config 默认只校验配置,不打印完整 env。排查 compose 展开结果时可临时使用:
npm run container:config -- --print
如果 deploy/container/api-server.env 已写入真实 token,不要把完整展开结果贴到公开渠道。
停止:
npm run container:down
如需同时清理容器卷:
npm run container:down -- -v
压测
k6 在 compose 网络内访问 http://nginx,避免 Windows 本机直连连接模型干扰 Linux 容器结果:
npm run container:k6
作品列表脚本一次 iteration 默认请求两个公开列表接口,因此目标 500 HTTP req/s 对应 PEAK_RPS=250:
$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 服务,再发布模块:
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 任务管理器总占用当成单进程结论:
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。