Merge branch 'codex/container-simulate'

# Conflicts:
#	.hermes/shared-memory/decision-log.md
#	server-rs/crates/api-server/src/puzzle.rs
#	server-rs/crates/spacetime-client/src/mapper.rs
This commit is contained in:
kdletters
2026-05-19 10:07:45 +08:00
40 changed files with 2795 additions and 165 deletions

View File

@@ -52,16 +52,57 @@
- 影响范围:`jenkins/Jenkinsfile.production-stdb-module-build` 及后续所有同类 Windows 构建流水线。
- 验证方式Jenkins 日志中应能看到 `[jenkins-powershell] user:``[jenkins-powershell] exe:`Checkout 阶段会打印当前 `HEAD` 与请求 commit并在 `COMMIT_HASH` 为空或一致时直接继续;不再停在 `PipelineNodeTreeScanner... Cannot run program "powershell"` 或重复 `git clean` 的退出码 5。
- 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md``.hermes/shared-memory/pitfalls.md`
## 2026-05-19 tracking outbox 改为 rotate 后异步 flush
- 背景:普通 route tracking 写入压力上来后,不能让 HTTP 请求线程等待 SpacetimeDB 批量入库。
- 决策:`api-server` tracking outbox 达到 `BATCH_SIZE` 时立即封存当前 active 文件并切新 activesealed 文件交给后台 worker 异步 flush`FLUSH_INTERVAL_MS` 只做长时间未满批的兜底封存;`MAX_BYTES` 只做磁盘保护阈值;成功后删除 sealed失败保留重试坏文件隔离为 `corrupt-*`
- 影响范围:`api-server` tracking outbox、埋点文档、压测口径和后续排障记忆。
- 验证方式HTTP route 请求在 SpacetimeDB 短暂不可用时仍可返回;恢复后 sealed 文件会被批量写入并清理。
- 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
## 2026-05-19 OTLP 默认开启但日志本地输出保留
- 背景:生产和容器环境需要默认把 OTLP 接到本机 Collector但压测或排障时也要能显式关闭。
- 决策:生产与容器 `api-server` env 模板默认 `GENARRATIVE_OTEL_ENABLED=true`;生产 endpoint 用 `http://127.0.0.1:4318`,容器 endpoint 用 `http://otelcol:4318``OTEL_EXPORTER_OTLP_ENDPOINT` 只填 Collector HTTP base endpoint不填 gRPC `4317` 或 Rider 端口本地日志、Nginx 日志和 `GENARRATIVE_API_LOG` / `RUST_LOG` 仍保留。
- 影响范围:`deploy/env/api-server.env.example``deploy/container/api-server.env.example``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md``scripts/loadtest/README.md`
- 验证方式:检查 env 模板默认值与端点口径;压测若要关闭 OTLP必须显式设置 `GENARRATIVE_OTEL_ENABLED=false`
- 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md``scripts/run-otelcol.mjs`
## 2026-05-19 容器 collector 可切 Grafana Cloud
- 背景:容器隔离压测时除了本地 debug exporter还需要临时把 traces / metrics / logs 转发到 Grafana Cloud 做可视化验证。
- 决策:`deploy/container/docker-compose.loadtest.yml` 里的 `otelcol` 支持通过 `GENARRATIVE_CONTAINER_OTELCOL_CONFIG=./otelcol.grafana.yaml` 切换配置;`deploy/container/otelcol.grafana.yaml` 同时保留 debug exporter并通过 `GRAFANA_CLOUD_OTLP_ENDPOINT``GRAFANA_CLOUD_BASIC_AUTH_HEADER` 转发到 Grafana Cloud。
- 影响范围:`deploy/container/docker-compose.loadtest.yml``deploy/container/otelcol.grafana.yaml``deploy/container/README.md`
- 验证方式:容器 `otelcol` 启动日志应能看到 OTLP receiver readydebug exporter 仍可输出本地链路Grafana Cloud 转发凭据只通过当前 shell 环境变量传入,不写入 Git。
- 关联文档:`deploy/container/README.md``scripts/loadtest/README.md`
## 2026-05-17 容器化方案只作为隔离压测与预发模拟路径
- 背景Windows 本机直连极高 VU 压测会放大本地连接与发送缓冲行为,和线上 Linux + Nginx + systemd 拓扑不一致;需要一个更接近生产网络层的模拟方案,但不能扰动当前生产发布链路。
- 决策:新增 `deploy/container/` 容器化方案,使用 Docker Compose 组合 Linux release `api-server`、容器 Nginx、`otelcol-contrib` debug exporter 和可选 k6。该方案只用于本机或预发压测模拟不替换当前生产 `systemd + Nginx + Jenkins` 路径。
- 决策:新增 `deploy/container/` 容器化方案,使用 Docker Compose 组合 Linux release `api-server`、容器 SpacetimeDB、容器 Nginx、`otelcol-contrib` debug exporter 和可选 k6。该方案只用于本机或预发压测模拟不替换当前生产 `systemd + Nginx + Jenkins` 路径。
- 服务器模拟参数2026-05-18 通过 `ssh genarrative-release` 采样,目标机器为 2 vCPU / 约 2 GiB RAM / Ubuntu 24.04 / Nginx `worker_connections=768`;容器方案按待发布运行口径使用 `nofile=4096`,并在 compose 中限制 `spacetimedb cpus=1.0 mem_limit=768m``api-server cpus=2.0 mem_limit=1g``nginx cpus=0.25 mem_limit=128m``otelcol cpus=0.25 mem_limit=128m``k6 cpus=0.5 mem_limit=512m`Collector 镜像默认使用 `otel/opentelemetry-collector-contrib:0.151.0`
- 隔离边界:容器方案使用独立 `deploy/container/api-server.env`、独立 Nginx 配置、独立 compose 命令和默认 `18080` 端口;真实 token 不进入镜像、不提交 Git生产 systemd 单元、Jenkins 发布脚本和 `deploy/nginx/` 模板仍是正式线上来源。
- 生产 Collectorserver-provision 可安装 `otelcol-contrib.service` 和本机 debug exporter 配置,但二进制由 Jenkins 构建机先准备 `provision-tools/otelcol-contrib` 再上传到 release 部署 agent目标机不从 GitHub 下载api-server 是否发送 OTLP 仍由 `GENARRATIVE_OTEL_ENABLED` 控制。
- 影响范围:`deploy/container/``scripts/container-compose.mjs``package.json` 容器命令、开发运维文档和容器 build context 排除规则。
- 验证方式:执行 `npm run container:config` 展开 compose 配置;需要真实运行时再执行 `npm run container:build``npm run container:up``npm run container:k6`,并结合容器 Nginx log 与 OTLP debug exporter 判断瓶颈。
- 关联文档:`deploy/container/README.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 2026-05-18 生产 provision 改为构建机准备工具包再上传安装
- 背景:目标 release 服务器无法访问 GitHub之前的 server provision 默认仍假设 `spacetime``otelcol-contrib` 已经存在于目标机本地路径,和真实运维条件不符。
- 决策Jenkins 新增 `Prepare Provision Tools` 阶段,在 `linux && genarrative-build` 构建机执行 `scripts/prepare-server-provision-tools.sh`,通过官方 SpacetimeDB 安装入口和 OpenTelemetry release 包生成 `provision-tools/`,再用 `stash/unstash` 带到 release 部署 agent`scripts/jenkins-server-provision.sh` 只从工作区工具包复制安装,不再要求目标机自己下载或预装二进制。
- 影响范围:`jenkins/Jenkinsfile.production-server-provision``scripts/prepare-server-provision-tools.sh``scripts/jenkins-server-provision.sh`、生产运维文档。
- 验证方式Jenkins 构建机可完成工具包准备release 部署 agent 只消费工作区文件;目标机不再依赖 GitHub 外网下载。
- 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 2026-05-19 公开 gallery 入口发布限流以快拒绝保护后端
- 背景:容器 2C / 2G 压测中,公开作品列表在约 5000 HTTP req/s 目标下可以保持 200 请求低延迟,但 SpacetimeDB 内存会随 api-server 重连和高压请求累积到容器上限附近。
- 决策:发布配置采用公开 gallery list 专用入口限流Nginx `genarrative_gallery_rps rate=5000r/s``burst=4096`、gallery list `limit_conn=320`api-server 对应 `GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320`,公开详情维持更低的 `GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS=64`。超过容量时接受明确 `429`,不继续扩大入口并发。
- 影响范围:`deploy/nginx/` 发布模板、`deploy/env/api-server.env.example``deploy/container/` 隔离压测模板和生产运维文档。
- 验证方式:容器连续 10 轮不重启 SpacetimeDB 压测,`PEAK_RPS=2500` 等价约 5000 HTTP req/s平均实际吞吐约 `4219 HTTP req/s`,总计 `0` 个 5xx200 请求平均 `p95=123ms``p99=234ms`;同时观察 SpacetimeDB 内存高水位,后续优化先处理连接 / 订阅 / tracking 下游状态。
- 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md``deploy/container/README.md`
## 2026-05-16 公开作品列表短期由 BFF 订阅读模型缓存
- 背景:作品列表压测和实时性讨论中,曾考虑让浏览器前端直接订阅公开作品列表,减少 HTTP 拉取和 BFF 压力。
@@ -72,8 +113,6 @@
- 验证方式:新增公开作品列表订阅能力时,检查前端只消费专用 public read model 或 BFF HTTP DTO检查源表 row shape、权限判断和跨玩法聚合没有下沉到前端页面。
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 2026-05-16 api-server OpenTelemetry 统一补齐 traces metrics logs
- 背景:压测与运行观测需要把 HTTP、SpacetimeDB 调用和应用日志串起来,同时保留本地 `journalctl` / 文件日志做故障排障。
- 决策:`api-server` 通过 OTLP HTTP base endpoint 发送 traces、metrics 和 logsCollector 统一用 `otelcol-contrib``npm run otel:debug` 负责 debug 采集,`npm run otel:rider` 负责转发到 RiderRider 只是接收与可视化端,不直接替代 Collector。
- 日志口径Rider Logs 面板只展示 log event 自身字段,请求完成日志需要直接携带 `request_id`、HTTP method、规范化 route、scheme、path、status、status_class、latency 和 slow_request更完整的 request attributes 仍以 trace/span 为准。
@@ -589,3 +628,11 @@
- 影响范围:用户侧任务中心、后台任务配置、运营查询、埋点查询、钱包流水。
- 验证方式:非 `user` scope 的个人任务配置应被 API 和领域构造层拒绝;任务查询与埋点查询分别放在 `docs/operations/``docs/tracking/`
- 关联文档:`PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md``RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md``ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md`
## 普通 route tracking 先写本机 outbox 再批量入库
- 背景:公开作品列表压测中,成功响应后的全局 route tracking 会逐条调用 SpacetimeDB导致数据库内存和事务压力先到边界。
- 决策:普通 HTTP route tracking 先写入 `api-server` 本机 NDJSON outbox后台按数量或时间阈值批量调用 SpacetimeDB`daily_login``work_play_start`、支付、任务领奖、钱包等关键事件保持同步直写。
- 默认阈值:每批 500 条或 1 秒 flush 一次outbox 磁盘上限 256 MiB超过后丢弃低价值 route 事件并记录指标 / 日志。
- 影响范围:`api-server` tracking 中间件、SpacetimeDB tracking procedure、部署数据目录、OTLP 指标和运维排障。
- 验证方式:数据库不可用时公开 route 请求不失败且 outbox 文件保留;恢复后批量写入成功并删除本地 sealed 文件;关键事件仍立即影响任务 / 统计。

View File

@@ -22,6 +22,22 @@
- 验证:拼图入口测试仍可通过,且新组件可通过不同页面复用而不需要复制上传卡实现。
- 关联:`src/components/common/CreativeImageInputPanel.tsx``src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`
## OTLP 端点只填 Collector HTTP base endpoint
- 现象:生产或容器 env 里把 `OTEL_EXPORTER_OTLP_ENDPOINT` 填成 `4317`、Rider 端口或别的非 HTTP base endpoint 后api-server 发不出 OTLP或者链路被错误转发。
- 原因api-server 当前走 OTLP HTTP不是 gRPCCollector 才是接收和转发边界。
- 处理:生产模板用 `http://127.0.0.1:4318`,容器模板用 `http://otelcol:4318`;需要关闭时显式设 `GENARRATIVE_OTEL_ENABLED=false`,不要通过改 endpoint 绕开 Collector 语义。
- 验证:检查 env 模板和运行态配置都指向 Collector HTTP base endpoint日志仍通过 `journalctl` / 文件日志保留。
- 关联:`deploy/env/api-server.env.example``deploy/container/api-server.env.example``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## tracking outbox 到批量阈值后先封存再异步 flush
- 现象route tracking 高峰时如果主请求线程要等 SpacetimeDB 批量入库,接口延迟会被 outbox 写入链路拖长。
- 原因outbox 的职责是把普通 HTTP route tracking 从请求线程切走,不能把 flush 结果回写成同步阻塞。
- 处理:达到 `BATCH_SIZE` 立即封存 active 文件并切新 active`FLUSH_INTERVAL_MS` 只做兜底封存,后台 worker 异步 flush sealed 文件;成功删文件,失败保留重试,坏文件隔离为 `corrupt-*``MAX_BYTES` 只做磁盘保护。
- 验证:普通 route 请求在 SpacetimeDB 不可用时仍能返回,恢复后 sealed 文件会继续被清理。
- 关联:`server-rs/crates/api-server/src/tracking_outbox.rs``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 汪汪声浪入口不要再回到独立配置阶段
- 现象:汪汪声浪入口如果继续切换到独立配置阶段,会和拼图、抓大鹅的创作页内嵌结构不一致,用户会感觉入口跳页。
@@ -107,6 +123,22 @@
- 验证:对照打 `/api/runtime/puzzle/gallery``/healthz`;对比 `PREALLOCATED_VUS=300 MAX_VUS=800``PREALLOCATED_VUS=20 MAX_VUS=40`;压测结束后继续采样 10 秒确认 private memory 回落。
- 关联:`scripts/loadtest/README.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md``server-rs/crates/api-server/src/process_metrics.rs``server-rs/crates/api-server/src/telemetry.rs`
## 容器高 VU 下 `/healthz` RSS 尖峰先查 Axum state 深拷贝
- 现象:容器 Linux release `api-server``/healthz`500 HTTP req/s、`PREALLOCATED_VUS=100` 只跑 1 秒也能把 RSS 推到约 1 GiB同样问题与作品列表、SpacetimeDB procedure、业务 cache 和请求日志等级无关。
- 原因:`AppState` 曾直接 `#[derive(Clone)]` 大结构体里面包含配置、SpacetimeDB client、平台服务、认证服务和多组 cache。Axum/Hyper 会在 router/service/connection 路径频繁 clone state高并发 keepalive 下会放大为状态深拷贝高水位。
- 处理:`server-rs/crates/api-server/src/state.rs``AppState` 必须保持 `Arc<AppStateInner>` 浅拷贝壳;新增共享状态字段时放入 `AppStateInner`,不要把外层改回大结构体 clone。
- 验证:用容器内 k6 直连 `api-server:8082/healthz`500 HTTP req/s、`PREALLOCATED_VUS=100`、30 秒压测后采样 `/proc/$pid/status``/proc/$pid/smaps_rollup` 和 cgroup `memory.current/memory.peak`。2026-05-18 修复后结果为 `15001` 请求、`http_req_failed=0``dropped_iterations=0`RSS 约 18 MiB -> 52 MiBcgroup peak 约 47 MiB。
- 关联:`server-rs/crates/api-server/src/state.rs``deploy/container/README.md``deploy/container/api-server.Dockerfile`
## Gallery 压测延迟升高先查入口过量放行和 TTL 边界刷新
- 现象:公开作品列表在 500-1000 HTTP req/s 附近可能吞吐没有明显提升,但 p95 变高、VU 上升,甚至出现排队和 dropped iterations。
- 原因Nginx、Axum 和缓存刷新边界如果同时允许过多请求进入压力会先堆在连接、service 和 cache rebuild 周围;这类延迟不等同于数据库连接池不足。
- 处理Nginx 按 endpoint 使用 `limit_req` 快拒绝api-server 按 `default/gallery/detail/admin` 分组 semaphore 快拒绝;拼图广场 TTL 过期时已有缓存先返回 stale 响应,只允许一个后台 refresh 任务重建,冷启动无缓存时才同步构建。
- 验证OTLP 看 `genarrative.http.server.request_permits.available{pool=...}``genarrative.puzzle_gallery.cache.stale_hits``refreshes_started``refreshes_failed`Nginx access log 看 `request_time``upstream_response_time` 是否同步收敛;超过容量时应明确 429而不是长时间排队或新增 502。
- 关联:`deploy/nginx/genarrative.conf``deploy/container/nginx.conf``server-rs/crates/api-server/src/backpressure.rs``server-rs/crates/api-server/src/puzzle_gallery_cache.rs`
## 多玩法公开广场列表优先订阅 public view / read model
- 现象:抓大鹅、方洞挑战、视觉小说、大鱼吃小鱼等公开列表如果沿用 `list_*_works` procedure即使只读已发布作品也会在每个 HTTP 请求里回到 SpacetimeDB WASM 侧扫描、反序列化配置并组装列表50RPS 以上容易变成热点。
@@ -824,6 +856,22 @@
- 验证:执行 `cargo test -p api-server jsapi_order_request_sets_wechat_required_http_headers --manifest-path server-rs/Cargo.toml`
- 关联:`server-rs/crates/api-server/src/wechat_pay.rs``docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md`
## 容器公开列表压测不要靠继续抬并发吃满 CPU
- 现象2C / 2G 容器压测公开 gallery list 时,`api-server` CPU 仍有余量,看起来像可以继续提高 `GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS` 或 Nginx `limit_conn`
- 原因:当前瓶颈不是 Tokio worker 线程数。`/api/runtime/puzzle/gallery``/api/runtime/custom-world-gallery` 成功响应后会走全局 route tracking继续向 SpacetimeDB 写 `record_tracking_event_and_return`;入口并发从 320 抬到 336 / 352 时SpacetimeDB 内存先逼近 `896m` 容器上限200 请求 p95 变差429 比例没有改善。
- 处理2C / 2G 容器模拟里公开 gallery list 暂以 `limit_conn=320``GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320` 作为稳定上限。若要继续提升吞吐,优先减少高频公开 GET 的 tracking 写入、做采样或改成批量/异步聚合;不要单纯放大入口并发。
- 验证:宿主机 k6 打 `http://127.0.0.1:18080``PEAK_RPS=1000` 等价约 2000 HTTP req/s320 档无 dropped iterations、无 5xx、无 OOM200 请求 `request_time p95` 约 0.292s。336 / 352 档 p95 升到约 0.31s / 0.32sSpacetimeDB 内存尾部可到约 `880MiB / 896MiB`
- 关联:`deploy/container/nginx.conf``deploy/container/api-server.env.example``deploy/container/README.md``server-rs/crates/api-server/src/tracking.rs`
## tracking outbox 成功入库后删除 sealed 文件
- 现象:普通 route tracking 改为本机 outbox 后,容易误以为入库成功只需要清空文件内容。
- 原因:清空文件会扩大崩溃窗口,进程在 truncate 和确认之间异常退出时可能丢失未确认事件。
- 处理:当前 active NDJSON 达到数量或时间阈值后原子 rename 为 sealed 文件;后台批量 flush sealed 文件SpacetimeDB 返回成功后直接删除该文件失败则保留文件等待重试。sealed 文件如果出现无法解析的坏行,重命名为 `corrupt-*` 隔离并记录指标,避免阻塞后续批量入库。该路径是至少一次投递,重复事件由 `tracking_event.event_id` 幂等跳过。
- 验证:模拟 SpacetimeDB 不可用时 sealed 文件保留;恢复后批量 procedure 成功sealed 文件消失,`tracking_event``tracking_daily_stat` 均更新。
- 关联:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md``server-rs/crates/api-server/src/tracking.rs``server-rs/crates/spacetime-module/src/runtime/profile.rs`
## 后台表查询展示 SpacetimeDB 枚举时不要套用 Option 解码
- 现象:后台“表查询”查看 `profile_recharge_order` 时,`kind``status` 显示为空数组 `[]`,例如充值订单原始行里 `points_60` 的类型和状态都不可读。