@@ -1,6 +1,6 @@
# 本地开发验证与生产运维
更新时间:`2026-05-1 5`
更新时间:`2026-06-0 5`
## 标准开发流程
@@ -47,7 +47,7 @@ npm run dev:api-server
Linux 本机多用户并发开发时,`npm run dev` 和 `npm run dev:*` 单模块命令会先在系统级端口段注册表里给当前用户分配一个端口段,再把该段映射为 `web = start` 、`api = start + 1` 、`spacetime = start + 2` 、`admin-web = start + 3` 。默认注册表目录是 `/var/tmp/genarrative-dev-port-ranges/` ,其中 `registry.json` 记录各用户的活跃段,`registry.lock` 负责串行化分配;可以用 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。系统自动分配时从 `10000-10099` 开始,每次占用 100 个端口块,后续块按 `10100-10199` 、`10200-10299` 递增;`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range` 只在 Linux 上生效, Windows 仍按原来的 3000 / 8082 / 3101 / 3102 端口探测与漂移逻辑运行,不读这个系统级注册表。
后端日志默认写入 `logs/api-server/` 。后端 API smoke 使用 `npm run dev:api-server` 并检查 `/healthz` ;不要使用旧 `api-server:maincloud` 或任何 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 口径。
后端日志默认写入 `logs/api-server/` 。后端 API smoke 使用 `npm run dev:api-server` 并检查 `/healthz` ; 需要确认实例可接生产流量时检查 `/readyz` 。 不要使用旧 `api-server:maincloud` 或任何 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 口径。
开发态 `npm run dev` 与 `npm run dev:api-server` 会默认注入 `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true` ,因此密码登录在本地开发环境可直接注册未知手机号账号;生产环境仍按 `api-server` 配置默认关闭该开关。
@@ -69,6 +69,8 @@ spacetime sql <database> "SELECT * FROM puzzle_gallery_card_view LIMIT 1" --serv
本地 `.env` 、`.env.local` 或 `.env.secrets.local` 修改后必须重启 `api-server` 才会生效;若已经通过 `npm run dev` 启动完整联调,可在该终端输入 `rs api-server` 。排查 RPG / 拼图 / 抓大鹅等 VectorEngine 生图链路时,确认 `VECTOR_ENGINE_BASE_URL` 、`VECTOR_ENGINE_API_KEY` 和 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 只在本地或服务器密钥文件中配置,不能写入 Git。VectorEngine `gpt-image-2` 图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 `server-rs/crates/platform-image` ; `api-server` 只做配置、玩法编排、OSS / asset 持久化、计费和失败审计落库。开局 CG 故事板、首图、背景和图集都属于长耗时图片请求;后端默认会把 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 下限收口到 `1000000` ,旧进程仍可能沿用重启前的短超时。若 VectorEngine 在 `send()` 阶段失败且日志显示 `SendRequest` ,先看同一 `request_id` 的 provider 日志字段 `source` 、`source_chain` 、`source_chain_depth` ,再查 `external_api_call_failure.metadata_json.errorSource` ;当前 multipart `/v1/images/edits` 单独强制 HTTP/1.1。拼图关卡资产按 `level_scene -> ui_spritesheet -> level_background` 顺序生成,日志会带 `slot` 、`asset_kind` 和 `elapsed_ms` 。
VectorEngine 图片生成 / 编辑在 `request_send` 阶段出现 `timeout` 或 `connect` 错误时,`platform-image` 会对同一请求最多发送 3 次; multipart 图片编辑每次重试都会重新构造 form, 避免复用已消费的 body。日志中 `VectorEngine 图片请求发送失败,准备重试` 表示本次失败已进入下一次尝试;最终仍失败时才会写入 `external_api_call_failure` 并返回 504。排查生产失败时应同时统计 retry 前的尝试日志和最终 audit, 避免把一次用户请求内的多次发送误判成多个用户请求。
查看本地 Rust / SpacetimeDB 日志:
``` bash
@@ -242,26 +244,27 @@ Jenkins 按 web / api / Spacetime module / build / deploy / publish 拆分
`Genarrative-Web-Build` 打包 `web.tar.gz` 前、`Genarrative-Web-Deploy` 解包后都会把 Web 静态目录规范为目录 `755` 、文件 `644` 。如果前端页面能打开但 public 图片、字体或音频返回 `403 Forbidden` ,优先检查当前 `/srv/genarrative/web` 指向的 release 中对应文件权限是否被异常归档为 `600` ,临时恢复可对该 release 的 `web` 目录执行目录 `755` 、文件 `644` 的权限修正。
生产 Jenkins 的 `Pipeline script from SCM` 仍 由 Jenkins controller 读取 Jenkinsfile, SCM URL 继续使用 `https://git.genarrative.world/GenarrativeAI/Genarrative.git` 。现在所有生产 流水线 j ob 的首次 checkout 都先走 `http://127.0.0.1:3000/GenarrativeAI/Genarrative.git` ,失败后回退到 `https://git.genarrative.world/GenarrativeAI/Genarrative.git` ;两层 checkout 都必须保留单分支 refspec、`shallow=true` 、`depth=1` 、`noTags=true` 与 `honorRefspec=true` ,后续二次源码确认继续走 `scripts/jenkins-checkout-source.sh` 。
生产 Jenkins 的 `Pipeline script from SCM` 由 Jenkins controller 读取 Jenkinsfile。`Genarrative-Server-Provision` 是服务器初始化 流水线, J ob 配置里的 SCM URL 必须使用 controller 本机可访问的仓库路径或内网 Gitea 地址,不能使用 `https://git.genarrative.world/...` ;否则日志一开始的 `Checking out git ... to read jenkins/Jenkinsfile.production-server-provision` 就会先从公网拉 Jenkinsfile。其它构建 / 发布流水线仍按各自 Jenkinsfile 的 checkout 口径执行;所有 `GitSCM checkout` 都必须保留单分支 refspec、`shallow=true` 、`depth=1` 、`noTags=true` 与 `honorRefspec=true` 。
`Genarrative-Stdb-Module-Publish` 在 `Pipeline script from SCM` 阶段如果一开始就报 `No such DSL method ' pipeline'` ,优先检查 `jenkins/Jenkinsfile.production-stdb-module-publish` 是否带 UTF-8 BOM。Jenkins Declarative Pipeline 的首个 token 必须是纯 `pipeline` ;仓库中的 Jenkinsfile 应保存为 UTF-8 without BOM, 只有临时写给 Windows PowerShell 5.1 `-File` 执行的 `.ps1` 才需要按对应 helper 转成带 BOM。验证时可检查文件前三字节不再是 `EF BB BF` ,并运行 `validateDeclarativePipeline` 或重放该流水线。
`Genarrative-Stdb-Module-Build` 或 SpacetimeDB module 构建失败若出现 Rust `E0425 cannot find function migrate_*` ,优先排查 `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs` 等同文件内默认种子迁移 helper 是否在分支合并时只保留了调用、漏掉了函数定义。`Genarrative-Stdb-Module-Build` 现在运行在 `linux && genarrative-build` 节点上, Checkout 与 Build 都走 bash + cargo + sccache, 不再依赖 Windows PowerShell 或 Git Bash。修复时不要直接删除迁移调用; 应恢复只纠偏历史默认种子且不覆盖后台手动配置的 helper, 并用 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 复现 Jenkins module 编译路径。
`Genarrative-Server-Provision` 现在也运行在 `linux && genarrative-build ` / `linux && genarrative-release-deploy` 节点上, `Prepare Provision Tools` 会在 Linux build 节点直接 准备 SpacetimeDB 与 `otelcol-contrib` 交付件,再 stash 给后续发布阶段;旧 Windows 下载 helper 已退役。`Genarrative-Stdb-Module-Build` 、`Genarrative-Server-Provisio n` 和 `Genarrative-Notify-Email` 都不再需要单独的 Windows 节点口径 。
`Genarrative-Server-Provision` 只做服务器初始化,不再承担构建职责。流水线全程运行在目标服务器 agent: `DEPLOY_TARGET=development` 使用 `linux && genarrative-dev-deploy` , `DEPLOY_TARGET=release ` 使用 `linux && genarrative-release-deploy` ; `Prepare Provision Tools` 也在同一个目标 agent 工作区内 准备 SpacetimeDB 与 `otelcol-contrib` 交付件,不再切到 `linux && genarrative-build` ,也不再 stash 给后续阶段。`SOURCE_GIT_REMOTE_URL` 必须显式填写为目标 agent 可访问的本机路径、`file:///` 地址、localhost / 127.0.0.1、RFC1918 内网 HTTP Git 地址、单标签内网主机名或 `.local` / `.la n` / `.internal` 地址;这条流水线不配置公网 Git 备用地址,目标 agent 拉不到内网源就应直接失败。真实初始化会写入 `/etc` / systemd / Nginx、创建系统用户并修改服务, 目标 dev / release agent 非 dry-run 时都必须具备 root 权限 。
生产环境变量模板:`deploy/env/api-server.env.example` 。真实密钥只放服务器,不提交 Git, 不写入文档示例。
`Genarrative-Server-Provision` 会安装基础构建依赖、 systemd 模板和 Nginx 站点模板。Ubuntu / apt 目标机会额外安装 `libnginx-mod-http-brotli-filter` 与 `libnginx-mod-http-brotli-static` ,随后由 `scripts/jenkins-server-provision.sh` 通过临时 `nginx -t` 配置探测 Brotli 指令是否可用;该临时配置必须先 `include /etc/nginx/modules-enabled/*.conf` ,因为 apt 安装的 Brotli 是动态模块,不会出现在普通 `nginx -V` 编译参数里。探测成功才在渲染后的 `deploy/nginx/genarrative.conf` / `genarrative-dev-http.conf` 中启用 Brotli, 避免未安装模块的机器直接写入无效配置。Provision 写入 Genarrative Nginx 站点时会把 `/etc/nginx/sites-enabled/default*` 移到 `/etc/nginx/sites-disabled/` ,避免 Debian / Certbot 默认站点继续占用 `genarrative.world` / `www.genarrative.world` 并在 `nginx -T` 中出现 `conflicting server name ... ignored` 。如果 `nginx -t` 失败,脚本会恢复写入前的 Genarrative 配置和被移动的默认站点。
`Genarrative-Server-Provision` 会安装 systemd 模板和 Nginx 站点模板,不再安装 clang / lld / pkg-config / OpenSSL headers / sccache 等构建链依赖 。Ubuntu / apt 目标机会额外安装 `libnginx-mod-http-brotli-filter` 与 `libnginx-mod-http-brotli-static` ,随后由 `scripts/jenkins-server-provision.sh` 通过临时 `nginx -t` 配置探测 Brotli 指令是否可用;该临时配置必须先 `include /etc/nginx/modules-enabled/*.conf` ,因为 apt 安装的 Brotli 是动态模块,不会出现在普通 `nginx -V` 编译参数里。探测成功才在渲染后的 `deploy/nginx/genarrative.conf` / `genarrative-dev-http.conf` 中启用 Brotli, 避免未安装模块的机器直接写入无效配置。Provision 写入 Genarrative Nginx 站点时会把 `/etc/nginx/sites-enabled/default*` 移到 `/etc/nginx/sites-disabled/` ,避免 Debian / Certbot 默认站点继续占用 `genarrative.world` / `www.genarrative.world` 并在 `nginx -T` 中出现 `conflicting server name ... ignored` 。如果 `nginx -t` 失败,脚本会恢复写入前的 Genarrative 配置和被移动的默认站点。
50 HTTP req/s 首版压测优化口径:
- `api-server` 生产模板默认 `GENARRATIVE_API_LISTEN_BACKLOG=1024` 、`GENARRATIVE_API_WORKER_THREADS=4` ;本地未设置 worker threads 时继续使用 Tokio 默认值。
- `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512` 开启应用内 HTTP 并发背压;`GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320` 、`GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS=64` 、`GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS=16` 分别限制公开列表、公开详情和后台 API 热路径。超过许可时直接返回 `429 Too Many Requests` 和 `Retry-After: 1` , `/healthz` 不受该限制。这些值不是 RPS 限速;如果压测中 429 上升但内存和 p95 收敛,说明背压正在保护进程。直连 `api-server` 的极高 RPS 压测若出现 `connection refused` ,通常已经打到 TCP 监听 / accept 层,应同时检查 backlog、Nginx upstream keepalive 和前置限流。
- `genarrative-api.service` 设置 `LimitNOFILE=65535` 、`TasksMax=2048` ;上线后用 `systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax` 和 `cat /proc/$(pidof api-server)/limits` 核对 。
- Server provision 不再通过 Windows helper 下载。 `G enarrative-Server-Provision ` 的 `Prepare Provision Tools` 在 Linux build 节点直接准备 `spacetime-x86_64-unknown-linux-gnu.tar.gz` 和 `otelcol-contrib_0.151.0_linux_amd64.tar.gz` ,再 stash `provision-tools/` 给后续发布阶段;如果 build 节点需要代理,在 `PROVISION_DOWNLOAD_PROXY` 配置 Linux 侧可访问的 HTTP 代理。后续 Linux 目标节点只消费 `provision-tools/` ,不再回退到外网下载 。
- `Genarrative-Stdb-Module-Build` 、`Genarrative-Web-Build` 、`Genarrative-Api-Build` 、`Genarrative-*Deploy` 、`Genarrative-Database-Import/Export` 、`Genarra tiv e-Full-Build-And-Deploy` 和 `Genarrative-Notify-Email` 的生产流水线现都以 Linux agent 为主,统一走 `http://127.0.0.1:3000/GenarrativeAI/Genarrative.git` 优先、`https://git.genarrative.world/GenarrativeAI/Genarrative.git` 备用的 checkout 口径 。
- `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512` 开启应用内 HTTP 并发背压;`GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320` 、`GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS=64` 、`GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS=16` 分别限制公开列表、公开详情和后台 API 热路径。超过许可时直接返回 `429 Too Many Requests` 和 `Retry-After: 1` , `/healthz` 与 `/readyz` 不受该限制。这些值不是 RPS 限速;如果压测中 429 上升但内存和 p95 收敛,说明背压正在保护进程。直连 `api-server` 的极高 RPS 压测若出现 `connection refused` ,通常已经打到 TCP 监听 / accept 层,应同时检查 backlog、Nginx upstream keepalive 和前置限流。
- `api-server` 正常运行时 `/healthz` 返回进程存活状态,`/readyz` 返回是否仍接收新流量;收到 `SIGINT` / `SIGTERM` 后会先把 readiness 标记为不可用,再让 Axum 停止接新连接并等待已有 HTTP 请求排空。systemd 仍以 `KillSignal=SIGINT` 停服务,`TimeoutStopSec=90` 作为长请求排空上限 。
- `g enarrative-api.service ` 设置 `LimitNOFILE=65535` 、`TasksMax=2048` ;上线后用 `systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax -p TimeoutStopUSec` 和 `cat /proc/$(pidof api-server)/limits` 核对 。
- Server provision 不再通过 Windows helper 下载,也不再通过 Linux build 节点中转工具包。`Prepare Provision Tools` 在目标 dev / release agent 工作区内准备 `space tim e-x86_64-unknown-linux-gnu.tar.gz` 和 `otelcol-contrib_0.151.0_linux_amd64.tar.gz` 并生成 `provision-tools/` ;如果目标服务器下载需要代理,在 `PROVISION_DOWNLOAD_PROXY` 配置目标机可访问的 HTTP 代理 。
- 除 `Genarrative-Server-Provision` 外,`Genarrative-Stdb-Module-Build` 、`Genarrative-Web-Build` 、`Genarrative-Api-Build` 、`Genarrative-*Deploy` 、`Genarrative-Database-Import/Export` 、`Genarrative-Full-Build-And-Deploy` 和 `Genarrative-Notify-Email` 的生产流水线现都以 Linux agent 为主,仍按各自 Jenkinsfile 的 checkout 口径执行。Server provision 不使用公网备用 Git 源。
- `otelcol-contrib.service` 作为可选系统服务加入 provision, 默认监听 `127.0.0.1:4317/4318` 并使用 `deploy/otelcol/genarrative-debug.yaml` 。api-server 是否发送 OTLP 仍由 `GENARRATIVE_OTEL_ENABLED` 控制,服务 unit 见 `deploy/systemd/otelcol-contrib.service` 。
- Nginx `/api/` 与 `/admin/api/` 通过 `genarrative_api` upstream 代理到 `127.0.0.1:8082` , upstream keepalive 为 64; `limit_conn` 负责连接 / 并发保护,`limit_req` 负责入口 RPS 快拒绝。当前模板把公开 gallery list 单独放到 `genarrative_gallery_rps` ,默认 `rate=5000r/s` 、`burst=4096` 、`limit_conn=320` ;公开详情和普通 API 放到 `genarrative_api_rps` ,后台 API 放到 `genarrative_admin_rps` 。通用 `/api` location 设置 `client_max_body_size 64m` 是反代兜底,防止拼图入口页 / 新增关卡本地参考图 Data URL 或旧兼容请求在到达 `api-server` 前被默认 1 MiB 上限拦截;拼图本地参考图前后端统一限制 6MB, 历史图片仍提交 `referenceImageAssetObjectId(s)` 。若线上出现 `413 Request Entity Too Large` 且 access log 中 `request_time=0.000` 、`upstream_status=-` ,说明请求在 Nginx 层被拦截,先用 `nginx -T | grep client_max_body_size` 检查 release 模板是否已渲染并 reload, 同时检查前端是否超出 6MB 或错误提交了未压缩大图。`limit_conn_status 429` 和 `limit_req_status 429` 必须在 HTTP 与 HTTPS server 中同时生效;若线上压测看到 `limiting connections by zone "genarrative_api_conn"` 却返回 503, 优先检查 `nginx -T` 里 HTTPS server 是否缺少这些状态码,以及 `/api/runtime/puzzle/gallery` 是否误落到通用 `location ~ ^/api` 的 `limit_conn=64` 。压测时看 `/var/log/nginx/genarrative.access.log` 中的 `request_time` 、`upstream_connect_time` 、`upstream_header_time` 、`upstream_response_time` 、`upstream_status` 、`request_id` 。
- 作品列表 K6 脚本一次 iteration 默认请求两个公开接口,因此约 50 HTTP req/s 的目标命令使用 `SCENARIO=spike START_RPS=5 PEAK_RPS=25 HOLD=60s END_RPS=5 DETAIL_RATIO=0 npm run loadtest:k6:works` 。
@@ -279,7 +282,7 @@ npm run container:k6
npm run container:down
```
容器方案默认暴露 `http://127.0.0.1:18080` , `api-server` 在容器内监听 `0.0.0.0:8082` , Nginx 通过 `api-server:8082` upstream 反代 `/api/` 和 `/admin/api/` 。SpacetimeDB 也纳入 compose, 容器内由 `spacetimedb:3101` 提供服务,宿主机通过 `http://127.0.0.1:13101` 进行模块发布; Collector 镜像使用 `otel/opentelemetry-collector-contrib:0.151.0` 。生产 provision 侧现在由 Linux build 节点直接 准备 `provision-tools/otelcol-contrib` , 再交给后续 Linux 发布阶段 安装本机 `otelcol-contrib.service` , 真实库名、token 和外部服务密钥只写本地 `deploy/container/api-server.env` ,不提交 Git。完整拓扑、端口、k6 参数和 OTLP debug exporter 使用方法见 `deploy/container/README.md` 。
容器方案默认暴露 `http://127.0.0.1:18080` , `api-server` 在容器内监听 `0.0.0.0:8082` , Nginx 通过 `api-server:8082` upstream 反代 `/api/` 和 `/admin/api/` 。SpacetimeDB 也纳入 compose, 容器内由 `spacetimedb:3101` 提供服务,宿主机通过 `http://127.0.0.1:13101` 进行模块发布; Collector 镜像使用 `otel/opentelemetry-collector-contrib:0.151.0` 。生产 provision 侧现在由目标 dev / release agent 自己 准备 `provision-tools/otelcol-contrib` , 并 安装本机 `otelcol-contrib.service` , 真实库名、token 和外部服务密钥只写本地 `deploy/container/api-server.env` ,不提交 Git。完整拓扑、端口、k6 参数和 OTLP debug exporter 使用方法见 `deploy/container/README.md` 。
`npm run container:config` 默认只做 quiet 校验,避免把本地 env 中的 token 展开到终端;确需排查完整 compose 时再传 `-- --print` 。
OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs, 但本地日志与 Nginx 文件日志仍保留:
@@ -292,7 +295,8 @@ OpenTelemetry 现阶段默认开启 OTLP traces / metrics / logs, 但本地日
- debug exporter / Rider 转发都会同时接收 traces、metrics 和 logs。
- api-server 会随 metrics 发送进程级指标:`process.memory.usage` 、`process.memory.virtual` 、`process.cpu.time` 、`genarrative.process.cpu.usage_percent` 、`process.thread.count` 、`genarrative.process.memory.private` ; Windows 额外发送 `process.windows.handle.count` , Linux 额外发送 `process.unix.file_descriptor.count` 。这些指标只描述当前进程,不携带请求、用户或作品 label。
- HTTP 运行态补充发送 `genarrative.http.server.response_bodies.in_flight` 与 `genarrative.http.server.request_permits.available` ,后者带低基数 `pool=default|gallery|detail|admin` label, 用于区分业务 handler / 背压 permit 是否仍被占用;拼图广场热点缓存补充发送 `genarrative.puzzle_gallery.cache.*` 指标,记录 fresh hit、stale hit、未命中、后台刷新开始 / 失败、重建耗时和预序列化 data JSON 字节数。
- 外部 API 失败统一发送 OTLP 并落库。当前 VectorEngine `gpt-image-2` 图片生成 / 编辑失败由 `platform-image` provider 输出低基数字段 结构化日志,字段包括 provider、endpoint、failure_stage、status、source、source_chain、source_chain_depth、timeout、retryable、latency_ms、prompt_chars、reference_image_count、image_model 和 raw_excerpt ; `api-server` 再记录指标 `genarrative.external_api.failures{provider,failure_stage,status_class,retryable}` ,并写入 `tracking_event` , `event_key = external_api_call_failure` 、`module_key = external-api` 、`scope_kind = module` 、`scope_id = provider` 。调用方能拿到身份上下文时,失败事件还会在行级 `user_id` / `owner_user_id` / `profile_id` 和 `metadata_json.userId` / `metadata_json.profileId` / `metadata_json.requestId` / `metadata_json.errorSource` 中记录触发者、草稿 / 作品作用域、请求标识和传输错误链。排障时先按 provider / failureStage 聚合,再下钻 userId / profileId, 最后结合 request 日志、errorSource 和上游响应 excerpt 判断是限流、超时、解析失败还是未返回图片。
- 外部 API 失败统一发送 OTLP 并落库。当前 VectorEngine `gpt-image-2` 图片生成 / 编辑失败由 `platform-image` provider 输出结构化日志字段 ,字段包括 provider、endpoint、failure_stage、status、source、source_chain、source_chain_depth、timeout、retryable、latency_ms、prompt_chars、reference_image_count、image_model、request_params 和 raw_excerpt; 图片编辑请求参数日志还会带 reference_image_bytes_total, 并在 request_params.referenceImages 中记录每个 multipart `image` part 的 fileName、mimeType 和 bytes, 不记录 API key 或原始图片 bytes ; `api-server` 再记录指标 `genarrative.external_api.failures{provider,failure_stage,status_class,retryable}` ,并写入 `tracking_event` , `event_key = external_api_call_failure` 、`module_key = external-api` 、`scope_kind = module` 、`scope_id = provider` 。调用方能拿到身份上下文时,失败事件还会在行级 `user_id` / `owner_user_id` / `profile_id` 和 `metadata_json.userId` / `metadata_json.profileId` / `metadata_json.requestId` / `metadata_json.errorSource` 中记录触发者、草稿 / 作品作用域、请求标识和传输错误链。排障时先按 provider / failureStage 聚合,再下钻 userId / profileId, 最后结合 request 日志、errorSource 和上游响应 excerpt 判断是限流、超时、解析失败还是未返回图片。
- OSS 平台适配器也输出结构化日志,覆盖 `sign_post_object` 、`sign_get_object_url` 、`head_object` 和 `put_object` 。排查资产签名、上传或确认失败时,先按 `provider=aliyun-oss` 与 `operation` 过滤,再看 `object_key` / `key_prefix` 、`status` 、`status_class` 、`error_kind` 、`content_length` 、`content_type` 和 `elapsed_ms` ;日志不得包含 AccessKey、policy、signature、Authorization header 或完整 signed URL。
- SpacetimeDB 观测分为两类: procedure / reducer 调用继续用 `genarrative.spacetime.procedure.*` ,订阅本地 cache 读使用 `genarrative.spacetime.read.*` 。`read=list_puzzle_gallery` 表示拼图广场当前从 `puzzle_gallery_card_view` 本地 cache 读取,不再每个 HTTP 请求调用 `list_puzzle_gallery` procedure。
- 本地 Windows 直连压测的内存高水位要结合 K6 VU / 连接数解释。250 RPS 下过高 `PREALLOCATED_VUS` 可能让 300 个本地 Established 连接把 `api-server` private memory 瞬时推到 GB 级,且 `/healthz` 小响应也能复现;若压测结束后回落、`response_bodies.in_flight` 和背压 permit 未显示业务积压,应优先按连接 / 发送链路高水位处理,而不是判断为 SpacetimeDB 或 JSON 缓存泄漏。
- Rider 的 Logs 面板只展示 log event 自身字段,不会自动展开父 span 的全部 attributes; 请求完成日志会直接带 `request_id` 、`http.request.method` 、`http.route` 、`url.scheme` 、`url.path` 、`http.response.status_code` 、`status_class` 、`latency_ms` 和 `slow_request` ,完整链路继续到 Traces 面板按 trace/span 查看。
@@ -378,7 +382,7 @@ ORDER BY failures DESC, last_seen DESC
LIMIT 100 ;
```
VectorEngine `request_send` 且 `timeout = true` 的记录表示 `reqwest::Error::is_timeout()` 判定为超时,常见于连接、发送请求体、等待上游首包或上游长时间无响应;`errorSource` 会保存 reqwest 底层错误链,若只看到 `client error (SendRequest)` ,表示 Hyper 只暴露到发送请求阶段,仍不等于最终根因。若 `statusCode` 为空,应优先查同一 `requestId` 的 `api-server` request 日志、provider 日志 `source_chain` 、Nginx / 出口网络、VectorEngine 可用性和请求体大小;若已有 `502` 、`429 moderation_blocked` 等状态码,则按上游网关或内容审核失败单独处理,不要和传输超时混为一类。
VectorEngine `request_send` 且 `timeout = true` 的记录表示 `reqwest::Error::is_timeout()` 判定为超时,常见于连接、发送请求体、等待上游首包或上游长时间无响应;`errorSource` 会保存 reqwest 底层错误链,若只看到 `client error (SendRequest)` ,表示 Hyper 只暴露到发送请求阶段,仍不等于最终根因。若 `statusCode` 为空,应优先查同一 `requestId` 的 `api-server` request 日志、provider 日志 `source_chain` 、request_params、reference_image_bytes_total、 Nginx / 出口网络、VectorEngine 可用性和请求体大小;若已有 `502` 、`429 moderation_blocked` 等状态码,则按上游网关或内容审核失败单独处理,不要和传输超时混为一类。
tracking outbox 默认配置:
@@ -388,9 +392,10 @@ GENARRATIVE_TRACKING_OUTBOX_DIR=/var/lib/genarrative/tracking-outbox
GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE = 500
GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS = 1000
GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES = 268435456
GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS = 5000
```
outbox 采用 NDJSON 文件保存原始事件。达到 `BATCH_SIZE` 时会立刻把当前 active 文件原子封存为 sealed 文件,并马上切到新的 active 继续写入;后台 worker 异步 flush sealed 文件, HTTP 请求线程不等待 SpacetimeDB。`FLUSH_INTERVAL_MS` 只负责兜底封存长时间未满批的 active 文件。SpacetimeDB 批量 procedure 返回成功后删除 sealed 文件,失败则保留文件并重试。`MAX_BYTES` 是磁盘保护阈值,不是 flush 阈值;超过后低价值 route tracking 可以被丢弃并记录日志 / 指标, 关键同步事件不进入该丢弃路径。sealed 文件若出现无法解析的坏行,会重命名为 `corrupt-*` 隔离并记录 `genarrative.tracking_outbox.files.corrupt` 指标,避免一个坏文件阻塞后续批量入库。该机制提供至少一次投递语义,依赖 `tracking_event.event_id` 幂等跳过重复事件。
outbox 采用 NDJSON 文件保存原始事件。达到 `BATCH_SIZE` 时会立刻把当前 active 文件原子封存为 sealed 文件,并马上切到新的 active 继续写入;后台 worker 异步 flush sealed 文件, HTTP 请求线程不等待 SpacetimeDB。`FLUSH_INTERVAL_MS` 只负责兜底封存长时间未满批的 active 文件。SpacetimeDB 批量 procedure 返回成功后删除 sealed 文件,失败则保留文件并重试。`MAX_BYTES` 是磁盘保护阈值,不是 flush 阈值;超过后低价值 route tracking 可以被丢弃并记录日志 / 指标, 关键同步事件不进入该丢弃路径。sealed 文件若出现无法解析的坏行,会重命名为 `corrupt-*` 隔离并记录 `genarrative.tracking_outbox.files.corrupt` 指标,避免一个坏文件阻塞后续批量入库。api-server 收到退出信号后会在 `GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS` 窗口内封存 active 文件并尽力 flush sealed 文件,超时或 SpacetimeDB 暂不可用时保留本地文件给下次启动继续投递。 该机制提供至少一次投递语义,依赖 `tracking_event.event_id` 幂等跳过重复事件。
release 机器如果日志每秒刷 `tracking outbox ... Permission denied (os error 13)` ,先检查 `/etc/genarrative/api-server.env` 是否缺少 `GENARRATIVE_TRACKING_OUTBOX_DIR` 。缺少时 `api-server` 会回退到本地开发默认相对路径 `server-rs/.data/tracking-outbox` ,而 systemd 的工作目录是只读发布目录 `/opt/genarrative/releases/<version>` , `genarrative` 用户无法在其中创建 `server-rs` 。修复顺序: