补充 robots、sitemap、SEO 元信息、结构化数据与首页语义内容 统一三套 Nginx 与 Pingora 的 62 条 SPA 路由和真实 404 行为 新增路由一致性检查、网关测试并同步技术文档与项目记忆
138 KiB
Pingora 独立网关试点
目标
本试点新增 server-rs/crates/pingora-gateway 独立二进制 crate,用 Pingora 复刻当前生产 Nginx 的核心反向代理与静态路由口径。当前阶段只作为影子网关验证,不绑定公网 80/443,不替代 deploy/nginx/genarrative.conf。
运行边界
- 默认监听
127.0.0.1:18081,只用于本机或容器内 smoke。可选配置GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN、TLS_CERT_FILE与TLS_KEY_FILE后会额外挂载 Rustls HTTPS 监听;可选配置GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN后会额外挂载 HTTP 入口,除 ACME challenge 外统一 301 到 HTTPS。两类直连入口默认关闭,不改变 Nginx canary / shadow 路径。 - 默认转发
api-server到127.0.0.1:8082。 - 默认转发最小 SpacetimeDB 公网路由到
127.0.0.1:3101。 - 默认静态目录为
/srv/genarrative/web,维护开关文件为/var/lib/genarrative/maintenance/enabled。 - 静态响应会显式写入
Cache-Control:HTML / SPA fallback 默认no-cache,Vite 指纹静态资源默认public, max-age=31536000, immutable,其它静态资源默认no-cache。三档分别由GENARRATIVE_PINGORA_GATEWAY_HTML_CACHE_CONTROL、GENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL和GENARRATIVE_PINGORA_GATEWAY_STATIC_CACHE_CONTROL覆盖,配置值禁止换行或 NUL,避免响应头注入。静态文件还会按 metadata 写入弱ETag、Last-Modified与Accept-Ranges: bytes,并对GET/HEAD的If-None-Match、If-Modified-Since返回304;单段Range: bytes=返回206 + Content-Range,越界范围返回416 + Content-Range: bytes */<len>,保持 HTMLno-cache下的浏览器协商缓存和 Nginx 直连体验一致。 /api通用路由同时按Content-Length与实际流式请求体累计字节数执行大小上限,避免客户端省略长度头绕过网关保护。GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN非空时,/__genarrative_pingora/healthz可用同名探针 header 做本机 shadow 健康检查;未带 token 或 token 不匹配时仍返回 404。api-server代理响应会写入X-Accel-Buffering: no,便于后续由 Nginx 反代到 Pingora canary 时继续保持 API / SSE 低缓冲口径。api-server代理请求会对齐当前 Nginx 头语义:透传Host,写入X-Forwarded-Host、配置化的X-Forwarded-Proto、TCP 对端 IP 作为X-Real-IP,并把 TCP 对端 IP 追加到X-Forwarded-For。TRUST_X_FORWARDED_FOR只影响接流保护 client key,不改变传给上游的真实 TCP 对端归因;直连公网时仍必须保持关闭。- 代理路径上的上游连接失败会返回统一 JSON;
502使用GATEWAY_UPSTREAM_ERROR,504使用GATEWAY_UPSTREAM_TIMEOUT,避免 shadow / canary 阶段把框架默认错误页透给前端或巡检。 - 上游超时显式配置在 Pingora peer 上:连接超时默认
3000ms,没有 Nginx 显式长超时的代理路由读取默认60s,通用/api、公开列表 / 详情和 SpacetimeDB subscribe 读取默认3600s,写入超时默认3600s。读 / 写 / 连接超时统一映射为 JSON504 GATEWAY_UPSTREAM_TIMEOUT。 - 默认开启 gzip 响应压缩,
GENARRATIVE_PINGORA_GATEWAY_GZIP_LEVEL=5和GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES=1024对齐当前 Nginxgzip_comp_level 5/gzip_min_length 1024;GENARRATIVE_PINGORA_GATEWAY_GZIP_ENABLED=false时禁用。GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS=gzip是当前唯一允许的压缩算法白名单;网关会在进入 Pingora compression 模块前把Accept-Encoding收敛为 gzip,避免未验收的br/zstd被隐式打开。压缩能力由check:pingora-gateway-smoke用小响应不压缩、图片资源不压缩、大响应Accept-Encoding: gzip、Accept-Encoding: br, gzip、Content-Encoding: gzip、Vary: Accept-Encoding和解压后的响应体一起验证。Brotli 不进入当前 Pingora 正式化口径,仍由 Nginx / 前置代理能力探测承担;直连 Pingora 时不把 Brotli parity 作为切换门禁。 - 默认开启单进程内接流保护,按客户端 IP 与路由组执行并发上限、RPS 和 burst 限制。保护组默认对齐当前 Nginx:
admin_api=64/30rps/16burst、gallery_list=320/5000rps/4096burst、gallery_detail=32/300rps/32burst、api=64/300rps/64burst、spacetime=256/1000rps/256burst。超限返回 JSON429并带Retry-After: 1。 - 当前接流保护的默认正式口径是单 Pingora 实例。
GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT默认1;若GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED=true且GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1,必须先落地共享限流 / 共享并发保护层,并显式设置GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED=true,否则网关启动和目标机 direct preflight 都会失败。若关闭网关保护后横向多实例运行,则全局接流保护必须由前置 Nginx / LB 承担。 - 默认以 TCP 对端 IP 作为限流 client key;只有在 Pingora 前置代理已经清洗
X-Forwarded-For时,才允许开启GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=true使用首个转发 IP。开启时必须同时设置GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=true,否则网关会拒绝启动。若 Pingora 直接监听公网地址,TRUST_X_FORWARDED_FOR必须保持false,目标机 direct preflight 会在看到公网监听加该开关时失败。 - 可选配置
GENARRATIVE_PINGORA_GATEWAY_GITEA_HOSTS和GENARRATIVE_PINGORA_GATEWAY_GITEA_UPSTREAM后,网关会先按Host归一化匹配 Gitea 域名,命中时整站代理到 Gitea 上游。Gitea 路由不走应用维护页、API 请求体上限或网关接流保护,避免影响 git clone / push。用于同一公网 IP 同时承载dev.genarrative.world与git.genarrative.world的直连切换时,TLS 证书必须同时覆盖两个域名;当前单 listener 配置只加载一组 cert/key。 - 网关启动会做配置校验:监听地址不能和上游地址相同,TLS 入口必须同时给出可读取的证书和私钥文件,HTTP redirect 入口必须依附已配置的 TLS 入口且不能复用同一监听地址,redirect scheme 当前只允许
https,MAX_API_BODY_BYTES必须大于0,上游 timeout 必须大于0,GZIP_LEVEL必须在0..=9,GZIP_MIN_LENGTH_BYTES必须大于0,COMPRESSION_ALGORITHMS当前只允许gzip,shadow probe token 不能是占位值或短 token,access log 不能指向目录,BURST>0时对应RATE_PER_SECOND不能为0,开启网关保护时GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1必须已确认共享保护层。 - 结构化日志会写入
request_id、method、path、uri、host、client_ip、status、route、proxy_target、upstream、content_length、body_bytes_seen和elapsed_ms,用于和 Nginx access log 对照 shadow 行为。 - 配置
GENARRATIVE_PINGORA_GATEWAY_ACCESS_LOG_FILE后,网关会额外写入 tab-separated access log。影子 systemd 模板默认允许写/var/log/genarrative,Server-Provision 会安装deploy/logrotate/genarrative-pingora-gateway到/etc/logrotate.d/,避免 canary 日志无限增长。 - 当前已具备受配置保护的 TLS 监听和 HTTP 到 HTTPS 重定向入口,但默认 systemd service 仍不具备绑定低端口的 capability;需要直连
80/443时必须人工启用deploy/systemd/genarrative-pingora-gateway-direct-entry.confdrop-in。网关仍不承接 Certbot / ACME 自动化和跨进程 / 跨实例全局限流;Brotli 明确留在 Nginx / 前置代理,不作为 Pingora 直连接管的阻断项。Jenkins / systemd / 健康巡检默认仍按影子发布与巡检模板处理,不作为公网切换依据。
启动命令
Pingora 0.8.1 会经 pingora-core -> flate2(zlib-ng) 构建 libz-ng-sys,构建机必须预装 cmake 与 C/C++ 编译器;缺少 cmake 时 cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml 会停在 libz-ng-sys build script。
本机影子验证时先启动 SpacetimeDB 与 api-server,再运行:
GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT=/srv/genarrative/web \
cargo run -p pingora-gateway --manifest-path server-rs/Cargo.toml
不依赖完整 dev stack 的本地回归使用仓库脚本:
npm run check:pingora-gateway-smoke
npm run check:nginx-spa-routes
npm run check:pingora-route-parity
npm run check:nginx-pingora-canary
npm run check:pingora-canary-docker
npm run check:pingora-canary-access-log-parity
npm run check:pingora-direct-preflight
npm run check:pingora-direct-live
npm run check:pingora-direct-enable
npm run check:pingora-direct-rollback
npm run check:pingora-current-release-audit
npm run check:pingora-direct-rehearsal-status
npm run check:pingora-cutover-status-snapshot
npm run check:pingora-cutover-evidence-bundle
npm run check:pingora-cutover-evidence-verify
npm run check:pingora-cutover-evidence-audit
npm run check:pingora-release-readiness
check:pingora-gateway-smoke 会临时启动 mock api-server、mock SpacetimeDB、mock Gitea 和 pingora-gateway,覆盖精确主站 SPA fallback、大小写与尾部斜杠兼容、同前缀未知路径真实 404、后台静态路由、HTML / 普通静态资源 no-cache、Vite 指纹静态资源 immutable 缓存、静态 ETag / Last-Modified 与 304 协商缓存、静态 HEAD 响应、静态 Range、静态 access log method/path/status 对账、gzip 最小长度、小响应不压缩、图片资源不压缩、大响应压缩、ACME、TLS 直连、HTTP/2 ALPN、HTTP 到 HTTPS 重定向、内部路由拒绝、shadow probe、API 代理头(Host / X-Forwarded-Host / X-Forwarded-Proto / X-Real-IP / X-Forwarded-For)、Gitea Host 整站转发、请求体上限、429 接流保护、上游断连 / 超时 JSON 错误、维护模式、维护模式不拦截 Gitea Host 和 SpacetimeDB WebSocket Upgrade,并复用 check-pingora-direct-live.mjs 对临时 HTTPS / HTTP redirect / WSS subscribe 入口做 live smoke。该本地 fixture 会让首页同时引用普通静态资源和 Vite 指纹静态资源,direct live JSON 必须确认指纹资源 GET / HEAD / Range: bytes=0-0 以及 access log method/path/status 证据,避免正式直连前只证明普通静态读取。排查失败时可追加 -- --verbose 输出网关 stderr / stdout;已确认二进制无需重编时可追加 -- --skip-build。
check:nginx-spa-routes 从 appPageRoutes.ts 的 STAGE_ROUTE_ENTRIES / APP_RUNTIME_ROUTES、appRoutes.tsx 的精确路由判断和兼容恢复路径 /creation/rpg/agent 提取当前主站 SPA allowlist,确认生产、开发和容器三套 Nginx 模板集合一致,并验证大小写、尾部斜杠和 /creation/not-exist、/runtime/not-exist、/puzzle/not-exist 等未知反例。
check:pingora-route-parity 会先执行同一 Nginx SPA 路由门禁,再读取 deploy/pingora/nginx-route-parity.matrix.json,静态确认生产 / 开发 Nginx 模板、Pingora Rust 路由 allowlist / 单测和本文档都覆盖同一组核心路由。cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml matches_nginx_route_parity_matrix 会读取同一份矩阵,逐条断言 classify_path 的路由结果、body limit 和接流保护分组。
check:nginx-pingora-canary 会静态校验 deploy/nginx/snippets/genarrative-pingora-canary.conf 的本机来源限制、handoff 响应头、probe token 占位、前缀 rewrite、低缓冲和 WebSocket Upgrade 设置,也会校验 deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf 只能作为独立 loopback server 片段使用、默认监听 127.0.0.1:18083、写独立 access log、没有 rewrite、覆盖真实 /api / /v1 / /assets 代表路径。本机安装了 Nginx 时脚本会额外把两个 snippet 包进临时 http {} 执行 nginx -t;需要在 CI / 目标 agent 上强制要求真实 Nginx 语法检查时执行 node scripts/check-nginx-pingora-canary.mjs --require-nginx。
check:pingora-canary-docker 会启动 mock api-server、mock SpacetimeDB、真实 pingora-gateway 和 Docker Nginx,把前缀 canary 与真实路径 canary 两份 snippet 都渲染到临时 Nginx 中,再复用 check:pingora-canary-live 验证 Nginx -> Pingora -> 上游的 handoff 链路。临时 Nginx 使用生产同口径 genarrative_upstream access log,live smoke 后会继续调用 scripts/check-pingora-canary-access-log-parity.mjs,按同一 request_id 对账 canary healthz、代表性 API、SpacetimeDB identity 和静态资源路径;前缀模式确认 Nginx rewrite 后路径与 Pingora access log 一致,真实路径模式除 healthz 探针外要求 Nginx path 与 Pingora path 完全一致。默认不拉取镜像;缺少 Docker daemon 或 nginx:1.27-alpine 镜像时跳过。CI / 目标 agent 上需要把它作为硬门禁时执行 node scripts/check-pingora-canary-docker.mjs --require-docker --pull。
check:pingora-canary-access-log-parity 会烟测只读脚本 scripts/check-pingora-canary-access-log-parity.mjs,用于目标机 canary 后按 request_id 对照 Nginx handoff access log 和 Pingora tab-separated access log。真实目标机前缀 canary 执行时使用 /opt/genarrative/current/scripts/check-pingora-canary-access-log-parity.mjs --nginx-log-file /var/log/nginx/genarrative.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log --path /__genarrative_pingora_canary/healthz --path /__genarrative_pingora_canary/api/creation-entry/config;真实路径 canary 执行时追加 --realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log --path /__genarrative_pingora_realpath_canary/healthz --path /api/creation-entry/config --path /v1/identity --path /assets/app.js。脚本只读日志,不修改日志、不 reload Nginx 或 Pingora。
check:pingora-direct-preflight 默认只检查仓库内主 service、direct-entry drop-in 和 env 示例,适合本机提交前护栏。目标机直连切换窗口必须提供真实 env 并打开现场检查:
npm run check:pingora-direct-preflight -- \
--env-file /etc/genarrative/pingora-gateway.env \
--require-live-env \
--systemd-cat \
--check-cert-readable \
--check-service-env-file \
--check-service-user-cert-readable \
--check-service-binary-executable \
--check-ports-free
该命令只在切换窗口中用于直连入口验收,必须确认 Nginx 或其它进程已从 80/443 摘掉,--check-cert-readable 会先证明当前执行用户可读 TLS 文件,--check-service-env-file 会确认 service 模板读取的 EnvironmentFile= 包含本次 --env-file,并在同时启用 --systemd-cat 时复核 systemd 最终生效配置也读取同一份 env,--check-service-user-cert-readable 会按 genarrative-pingora-gateway.service 的 User= 推导服务用户并验证其可读证书链和私钥,--check-service-binary-executable 会从 service ExecStart= 推导 current release 的 pingora-gateway 路径并确认文件存在且可执行,--check-ports-free 会在授予低端口能力前证明 TLS / HTTP redirect 监听端口当前可绑定。preflight 还会复核 GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT:开启网关保护且 GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1 时,必须同时设置 GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED=true,否则视为把进程内保护错误放大到多实例并直接失败。
check:pingora-direct-enable 会用临时模板和 drop-in 路径验证 pingora-direct-enable.sh 的默认 dry-run 不安装文件、会打印安装 drop-in / systemctl daemon-reload / 重启 Pingora / 启用后 systemctl cat 生效核验命令,并覆盖启用前 direct preflight、模板 / 目标 drop-in / preflight env 相对路径负例,以及 --apply 缺少 --preflight-env-file、--preflight-check-cert-readable、--preflight-check-service-env-file、--preflight-check-service-user-cert-readable、--preflight-check-service-binary-executable 或 --preflight-check-ports-free 时必须失败。启用脚本 apply 后会以 --json 运行 direct live smoke,并解析 stdout 中的 direct-access-log 结构化结果;缺少该结果、matchedCount != checked、missingCount != 0 或 mismatchCount != 0 都会让启用失败,避免只看 direct live 退出码。check:pingora-direct-rollback 会用临时 drop-in 文件验证 pingora-direct-rollback.sh 的默认 dry-run 不修改文件、会打印移除 drop-in / systemctl daemon-reload / 重启 Pingora / 回退后 systemctl cat 移除核验 / systemctl show ... ExecStart 指向核验 / nginx -t / reload Nginx / reload 后 systemctl is-active / Nginx smoke 命令,并覆盖相对路径负例、--apply 缺少 --reload-nginx 或 --nginx-smoke-url 必须失败、nginx -t 失败时不能先删除 drop-in、systemd ExecStart 指向旧 release 时必须失败,以及 Nginx smoke 失败必须失败。两类检查都不接触 /etc/systemd,用于保证切换启用和失败回退路径不是只停留在文档里。
check:pingora-current-release-audit 会烟测 scripts/ops/pingora-current-release-audit.mjs 的只读边界、current release 必备脚本 / 配置目录、可选 pingora-gateway 可执行性和 --systemd-show 下的 ExecStart 指向。正式直连 runbook 会先执行 /opt/genarrative/current/scripts/ops/pingora-current-release-audit.mjs --release-root /opt/genarrative/current --require-pingora-gateway --systemd-show,在采集状态快照前确认目标机 current release 本身自包含;显式 --timeout-ms 和 GENARRATIVE_PINGORA_CURRENT_RELEASE_TIMEOUT_MS 必须是正整数,非法值直接失败,不静默回退默认 5000。GENARRATIVE_PINGORA_CURRENT_RELEASE_REQUIRE_GATEWAY、GENARRATIVE_PINGORA_CURRENT_RELEASE_SYSTEMD_SHOW 只接受 true/false、1/0、yes/no、on/off 或空值,非法值直接失败,不再按 false 继续执行;--release-root 必须是绝对路径且不能是文件系统根目录,也不能包含换行或 NUL 字符,--systemd-service 同样不能包含换行或 NUL 字符;启用 --systemd-show 时,脚本会在执行 systemctl show 前复核子命令可执行文件和参数不含换行或 NUL,避免把污染参数带进只读审计命令。
check:pingora-direct-rehearsal-status 会烟测 scripts/ops/pingora-direct-rehearsal-status.mjs 的只读彩排状态采集。目标机切换前可执行:
node -- /opt/genarrative/current/scripts/ops/pingora-direct-rehearsal-status.mjs \
--release-root /opt/genarrative/current \
--expect-public-gateway nginx \
--require-pingora-shadow \
--require-realpath-canary \
--require-current-release-gateway \
--fail-on-critical
该脚本只读读取 health patrol env、Pingora env、systemctl、ss -H -ltnp、realpath canary 配置和 current release 自审结果,输出 JSON 总览;不会写 /etc、不会 reload systemd,也不会修改 Nginx 或 Pingora。--expect-public-gateway nginx 会要求公网 80/443 仍由 Nginx 监听,--require-pingora-shadow 会要求 127.0.0.1:18081 由 Pingora shadow 监听,--require-realpath-canary 会要求 127.0.0.1:18083 有 Nginx realpath canary,--require-current-release-gateway 会复用 current release 自审确认 pingora-gateway、checksum、manifest 和 systemd ExecStart。该脚本用于直连前确认现场状态已经进入“可彩排但未切公网”的安全边界,不替代后续证据包,也不做任何启用或回退动作。
check:pingora-cutover-status-snapshot 会烟测 scripts/ops/pingora-cutover-status-snapshot.mjs 的只读边界、release artifact 采集、health patrol env 复核、systemd capability / EnvironmentFile 判断和 --fail-on-critical 行为。正式切换窗口由 /opt/genarrative/current/scripts/ops/pingora-cutover-evidence-bundle.mjs 调用随包状态快照脚本,按 --phase pre-cutover、--phase post-enable、--phase post-rollback 生成时间戳证据目录,保存 manifest.json、snapshot.json、snapshot.stdout.txt、snapshot.stderr.txt 和 snapshot-command.json;状态快照 stdout 解析失败时保存 snapshot-parse-error.txt 并在 manifest / 最终 stdout 中给出路径,启用后 direct live stdout 解析失败时保存 direct-live-parse-error.txt 并同样索引。状态快照会把 --pingora-env-file 与 systemctl cat genarrative-pingora-gateway.service 的 EnvironmentFile= 做精确匹配,支持 EnvironmentFile=-/path 和一行多个文件,但不接受路径前缀误判;未包含本次 pingora env 时 systemd.pingoraUnit.environmentFileMatchesPingoraEnvFile=false 并标记 CRITICAL。启用后 direct live 结果中的 direct-access-log 必须保留 scannedLineCount、matchedCount、missing[]、mismatches[] 以及每个 request_id 的预期 / 实际 method、path 与 status,避免证据包只留下计数或终端 stderr;静态 GET / HEAD / 304 / Range 结果还必须在 direct-live.json 中保留白名单响应头,字段限于 cache-control、etag、last-modified、accept-ranges、content-range、content-length 和 content-encoding,便于复盘缓存分档、校验器和 Range 语义,同时避免把 API / WSS 原始响应头落进正式证据。证据包还会把 directLiveAccessLog 和 directLiveStaticHeaders 摘要提升到 manifest.summary,前者用于快速确认 request_id 对账,后者用于快速确认普通静态 / Vite 指纹静态的 Cache-Control、ETag、Last-Modified、Content-Length、Range Content-Range 与 304 状态证据;缺少 direct-access-log 结构化结果、缺少可判定的静态头摘要,或静态摘要缺少缓存头、校验头、Range 206 + Content-Range、ETag 304 / Last-Modified 304 证据时,证据包都会直接记为 CRITICAL。维护模式、非 HTML 或首页确实没有构建资产引用时,direct live 会把静态检查标记为 skipped,此时允许证据包继续记录跳过原因。snapshot-command.json 与 manifest 的 commands[] 同时保留脱敏后的可读命令和结构化 executable / args[],便于路径或参数包含空格时复盘;证据包在执行状态快照或 direct live 子命令前也会拒绝任何带换行或 NUL 字符的子命令参数,避免污染后的 args[] 先落盘再等总审计兜底。--run-direct-live 使用的 HTTPS / HTTP base URL、Host、redirect Host、probe token、SpacetimeDB 数据库名、Pingora access log 路径和 access log tail 行数也会在证据包配置层先拒绝换行或 NUL,且 --direct-pingora-access-log 必须是绝对路径并且不能是文件系统根目录,避免进入 snapshot 或 direct live 子命令后才暴露参数污染。--phase 只允许 ASCII 字母、数字、点、下划线和短横线,非法阶段名直接失败,不做隐式清洗,避免目录名和 manifest 阶段漂移;状态快照和证据包的显式 --timeout-ms 以及 GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_TIMEOUT_MS 必须是正整数,非法值直接失败,不再静默回退默认 5000。GENARRATIVE_HEALTH_PATROL_REQUIRE_EMPTY_PUBLIC_HOST、GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_RUN_HEALTH_PATROL、GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_REQUIRE_PINGORA_GATEWAY、GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_FAIL_ON_CRITICAL 只接受 true/false、1/0、yes/no、on/off 或空值,非法值直接失败,避免 run / require / fail 开关拼写错误后静默按 false 采集证据。状态快照脚本只读采集 summary、healthPatrolEnv、pingoraEnv、releaseArtifacts、systemd 和 checks;probe token 和其他 env 敏感值只记录是否存在,不输出原文,健康巡检、current release 自审等子检查的 stdout / stderr 在进入快照 JSON 前也必须按 env 敏感值脱敏,证据包里的 manifest、snapshot、stdout 和命令记录都必须通过自测确认不落盘 token 原文;状态快照脚本的 --release-root、--health-patrol-env-file 和 --pingora-env-file 都必须是绝对路径、不能是文件系统根目录,也不能包含换行或 NUL 字符,且执行 systemctl、current release 自审、health patrol env 复核或生产巡检子命令前还会复核子命令参数不含换行或 NUL;证据包脚本只写 --output-root 下的新目录,且证据包所有显式路径参数也不能是文件系统根目录,--output-root 及其已存在上级路径不能是符号链接,已存在的 --output-root 必须是真实目录;路径异常时会在执行状态快照前失败,不写入软链目标、不覆盖既有文件,证据目录权限固定为 0750,证据文件权限固定为 0640,不写 /etc、不 reload systemd,也不修改 Nginx 或 Pingora。
直连 runbook 的三个证据包阶段都会透传 --require-pingora-gateway,让 checks.current-release-audit.details 同步归档 Pingora 二进制、sha256、release manifest 和 systemd ExecStart 自审结果,避免证据包只有巡检状态而缺少发布物可信度证明。每个证据包生成后都要用 /opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs --bundle-dir <本阶段bundleDir> 做只读验真;该脚本只接受 schemaVersion=1 的 manifest,并把 manifest.files 视为闭集,只读取 manifest.files 中的 { path, sizeBytes, sha256 },拒绝路径逃逸、符号链接证据目录、非目录证据目录、非元数据对象文件条目,以及任何未登记的额外普通文件、目录或符号链接,发现缺文件、大小漂移或 sha256 漂移时退出失败;--allow-extra-files 只用于人工排障时显式放行额外条目,正式切换归档不应使用。验真脚本不修改证据目录、不 reload systemd,也不访问 Nginx 或 Pingora。enable apply / rollback apply 的命令证据生成后也要立即用同一个随包 verifier 验真,将上一步 stdout 的 bundleDir 分别填入 <enable-apply-bundle-dir> / <rollback-apply-bundle-dir>,确认 command.stdout.txt、command.stderr.txt、command-record.json 与 manifest 元数据一致,再继续后续 health patrol 或回退复核。三阶段证据包都生成并分别验真、enable / rollback apply 命令证据和三条 env 变更命令证据也即时验真后,还要执行 /opt/genarrative/current/scripts/ops/pingora-cutover-evidence-audit.mjs --evidence-root <证据根目录> --require-phase pre-cutover --require-phase post-enable --require-phase post-rollback --require-phase-direct-live-access-log post-enable --require-phase-direct-live-static-headers post-enable --require-command enable-apply:pingora-direct-enable-apply --require-command post-enable:pingora-health-patrol-direct-env-switch --require-command rollback-prep:pingora-gateway-shadow-env-switch --require-command rollback-prep:pingora-health-patrol-nginx-env-switch --require-command rollback-apply:pingora-direct-rollback-apply --require-command-executable enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --require-command-executable post-enable:pingora-health-patrol-direct-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs --require-command-executable rollback-prep:pingora-gateway-shadow-env-switch:/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs --require-command-executable rollback-prep:pingora-health-patrol-nginx-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs --require-command-executable rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh --require-command-arg enable-apply:pingora-direct-enable-apply:--apply --require-command-arg post-enable:pingora-health-patrol-direct-env-switch:--apply --require-command-arg post-enable:pingora-health-patrol-direct-env-switch:pingora-direct --require-command-arg rollback-prep:pingora-gateway-shadow-env-switch:--apply --require-command-arg rollback-prep:pingora-health-patrol-nginx-env-switch:--apply --require-command-arg rollback-prep:pingora-health-patrol-nginx-env-switch:nginx --require-command-arg rollback-apply:pingora-direct-rollback-apply:--apply --require-cutover-run-id <本次cutoverRunId> --timeline-max-span-ms 86400000,由脚本按 manifest.phase 找到每个阶段最新证据目录、按 manifest.phase + manifest.commandName 找到五条真实切换命令证据,并复核五条真实切换命令都来自 current release 随包脚本;所有候选证据都必须带合法 manifest.generatedAt,格式必须是 new Date().toISOString() 产出的 UTC 毫秒格式 YYYY-MM-DDTHH:mm:ss.sssZ,最新选择和后续时间线证明只使用该字段,不依赖目录 mtime;随后复用随包 verifier 的 --require-summary-ok 严格模式再次只读验真,输出可归档的阶段与命令总表;阶段证据必须是不带 manifest.commandName 的状态快照证据包,命令证据不能冒充同名 phase 的阶段证据。总审计输出会把阶段 manifest 中的 directLiveAccessLog 与 directLiveStaticHeaders 一并带到对应 phases[] 项,启用后阶段可直接看到 request_id 对账数量、普通静态和指纹静态的缓存头、校验头、Range Content-Range 与 304 状态摘要,不必再逐个打开 bundle manifest;正式 runbook 还会通过 --require-phase-direct-live-access-log post-enable 和 --require-phase-direct-live-static-headers post-enable 把缺少 request_id 对账摘要、缺少静态头摘要、摘要被跳过或摘要缺少缓存头 / 校验头 / Range / 304 证据的启用后证据判为失败,避免旧 post-enable 证据包混入最终归档。总审计的证据根目录默认只能包含带 manifest.json 的证据目录,夹带普通文件、无 manifest 子目录或符号链接都会失败,--allow-extra-root-entries 只用于人工排障显式放行,正式切换归档不应使用。总审计不只验文件 hash,还要求所有候选证据 manifest.schemaVersion=1,阶段证据 manifest.summary.status=OK,命令证据 manifest.summary.status=OK、manifest.summary.exitCode=0 且无 signal;命令证据的顶层 manifest.commandName 和内嵌 manifest.command.name 只要存在就必须各自是安全非空命令名,且两者同时存在时必须一致,否则按坏 manifest 处理;并在标准八段证据都被要求时校验每段审计状态都是 OK,以及 pre-cutover -> enable-apply -> post-enable:pingora-health-patrol-direct-env-switch -> post-enable -> rollback-prep:pingora-gateway-shadow-env-switch -> rollback-prep:pingora-health-patrol-nginx-env-switch -> rollback-apply -> post-rollback 的 manifest.generatedAt 顺序和默认 24 小时最大跨度,防止从不同切换窗口拼出一组看似完整的最新证据;缺失、非法或非规范格式的 manifest.generatedAt 会直接失败,不能靠目录 mtime 兜底;最终总审计还会在 --require-command-arg 下要求 enable / rollback 命令证据的 manifest.command.args 和独立 command-record.json.args 都包含 --apply,并在 --require-cutover-run-id 下要求所有阶段和命令证据的 manifest.cutoverRunId 与本次 runbook 一致;确需跨更长维护窗口时,只能在生成 runbook 时显式传 --cutover-evidence-timeline-max-span-ms <ms>,让最终总审计 JSON 可见本次放宽值;证据根目录、verifier 路径、阶段名、命令名或 cutover run id 不安全时直接失败。
正式 runbook 的五个即时验真步骤(pre-cutover、enable-apply、post-enable、rollback-apply、post-rollback)都必须在随包 verifier 后追加 --require-summary-ok,让刚生成的证据包除文件完整性外也立即要求 manifest.summary.status=OK;缺少 summary 或状态为 CRITICAL 时要先修现场状态或重新生成证据,不能等最终根目录总审计才发现。最终 pingora-cutover-evidence-audit.mjs 仍是证据根目录、命令身份、--apply 参数、时间线和 cutoverRunId 的二次总审计,不替代即时验真;它内部复用 verifier 时也必须启用 --require-summary-ok。
证据根目录总审计选择每个阶段或命令的“最新证据”时,最新 manifest.generatedAt 必须唯一;如果同一阶段或同一命令存在两个或多个候选拥有相同的最新 generatedAt,总审计会输出 AMBIGUOUS_LATEST 并列出重复目录,不能按目录名排序打平。出现这种情况时应重新归档该阶段 / 命令证据,或把旧证据移出正式证据根目录后重新总审计。
标准八段时间线只要任一阶段或命令证据带 manifest.cutoverRunId,八段就必须全部带同一个值;缺字段或混入其它 cutoverRunId 都会让总审计失败。八段顺序固定为 pre-cutover -> enable-apply -> post-enable:pingora-health-patrol-direct-env-switch -> post-enable -> rollback-prep:pingora-gateway-shadow-env-switch -> rollback-prep:pingora-health-patrol-nginx-env-switch -> rollback-apply -> post-rollback。任何证据 manifest 只要显式写入 cutoverRunId 字段,就必须是安全非空 ID,不能用空字符串伪装成缺省字段。正式 runbook 仍必须显式传 --require-cutover-run-id <本次cutoverRunId>,这条一致性检查只是防止人工临时总审计忘带该参数时把不同切换批次拼在一起。
标准八段时间线失败时,总审计 JSON 的 timeline.failedCount 不只是一个笼统红灯,而是按可操作失败项累计;timeline.failureBreakdown 会分别输出 nonOkItems、missingGeneratedAt、cutoverRunIdMismatch、outOfOrder 和 spanExceeded 数量,值班人员应按 breakdown 先修对应证据或现场状态,再重新生成总审计。
最终总审计 JSON 的 summary 是切换窗口的人工入口:先看 summary.status 和 summary.failedItems[] 定位失败阶段、命令或根目录诊断,再看 summary.directLiveEvidence[] 里的 accessLog.ok/reason 与 staticHeaders.ok/reason 判断 post-enable 是否缺 request_id 对账或静态头证据;完整字段仍保留在 phases[]、commands[] 与 timeline 中供机器归档和深挖复盘。
--warn-only、--allow-extra-files 和 --allow-extra-root-entries 只允许值班人员在正式 runbook 之外作为独立排障命令使用;plan:pingora-direct-cutover 生成的正式切换计划不得携带这些放行参数。若需要放行额外证据条目或忽略 CRITICAL,应先在证据根目录外保存人工说明、修正现场或重新生成正式证据,而不是把排障开关写进切换 runbook。
check:pingora-release-readiness 是正式切换前的聚合门禁,默认串行执行 cargo test -p pingora-gateway、mock 上游 smoke、路由矩阵 parity、Nginx canary snippet 校验、Docker handoff smoke、canary access log 对账烟测、realpath canary 启停烟测、直连入口静态预检、直连启用 / 回退 dry-run 行为检查、current release 自审烟测、直连彩排状态烟测、release readiness 计划自检、生产运维护栏、Pingora cutover 状态快照烟测、Pingora cutover 证据包烟测、Pingora cutover 命令证据烟测、Pingora cutover 证据 manifest 验真烟测、Pingora cutover 证据根目录审计烟测、API release build 烟测、Pingora production release 真实构建烟测和 API deploy release 烟测。普通本机执行时 Docker / Nginx 能力仍按子脚本默认口径跳过;切换窗口或 CI 必须执行:
目标机 current release 上的启用前基础门禁和启用后 --require-direct 复核使用同一个聚合脚本,但必须追加 --release-runtime-only。该模式只执行发布包内可自包含的运行时复核:current release 自审、启用前直连彩排状态复核、live canary、真实 access log 对账、direct preflight、health patrol env 复核和 direct live smoke;不会运行 Cargo、npm、Docker 或 Nginx 源码 / 构建环境门禁,并会拒绝 --require-docker、--pull-docker 与 --require-nginx。未带 --require-direct 的启用前基础门禁会自动执行 scripts/ops/pingora-direct-rehearsal-status.mjs --expect-public-gateway nginx --require-pingora-shadow --require-realpath-canary --require-current-release-gateway --fail-on-critical,确认公网 80/443 仍由 Nginx 接流、Pingora shadow 与 realpath canary 高端口在线;启用后 --require-direct 复核不再要求 Nginx 接公网的彩排状态,改为检查 direct preflight、health patrol 直连模式和 direct live smoke。
正式 runbook 的 direct enable apply / rollback apply 命令证据必须额外传 --expected-executable <current release 随包脚本绝对路径> 和 --require-arg --apply;命令证据脚本会在执行前拒绝与预期脚本不一致、缺少必需 --apply 参数,或任一真实命令参数包含换行 / NUL 字符的真实命令,并把 expectedExecutable 写入 manifest / command-record。命令名只用于审计分类,不能替代真实脚本身份和真实 apply 参数校验。
最终证据根目录总审计还必须传五条真实切换命令的脚本身份和关键参数要求:--require-command-executable enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh、--require-command-executable post-enable:pingora-health-patrol-direct-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs、--require-command-executable rollback-prep:pingora-gateway-shadow-env-switch:/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs、--require-command-executable rollback-prep:pingora-health-patrol-nginx-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs、--require-command-executable rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh,并要求 enable-apply:pingora-direct-enable-apply:--apply、post-enable:pingora-health-patrol-direct-env-switch:--apply、post-enable:pingora-health-patrol-direct-env-switch:pingora-direct、rollback-prep:pingora-gateway-shadow-env-switch:--apply、rollback-prep:pingora-health-patrol-nginx-env-switch:--apply、rollback-prep:pingora-health-patrol-nginx-env-switch:nginx、rollback-apply:pingora-direct-rollback-apply:--apply 这些 --require-command-arg 均存在。总审计会复核命令证据里的 manifest.expectedExecutable、manifest.command.executable 与独立 command-record.json 的 executable 都指向 current release 随包脚本,并要求 manifest.command.args 与独立 command-record.json.args 都包含必需参数,且每个 args 字符串都不含换行或 NUL 字符。--require-command-executable 的路径参数本身也必须是安全绝对路径,不能是文件系统根目录,也不能包含换行或 NUL 字符。总审计还要求 manifest.command 与 command-record.json 的 schemaVersion / phase / commandName / cutoverRunId / exitCode / signal / startedAt / finishedAt / durationMs / stdoutPath / stderrPath / args / command / cwd / error 等关键字段一致;其中两份命令记录的 schemaVersion 都必须是 1,stdoutPath / stderrPath 还必须分别与 manifest.files.stdout.path / manifest.files.stderr.path 指向同一份归档文件,args 与 command 用于证明真实 apply 参数未被替换成 dry-run 或其它动作。命令记录时间必须满足 finishedAt >= startedAt、durationMs == finishedAt - startedAt,且 manifest.generatedAt 不能早于命令 finishedAt。这些参数会隐式要求对应命令证据存在;缺命令证据、旧证据缺少 schemaVersion 或 expectedExecutable、缺少必需参数、人工同名证据的 executable 漂移、命令 stdout / stderr 引用漂移、命令参数漂移、命令参数控制字符污染或命令时间线漂移都会失败。
命令证据 manifest 只要出现 manifest.expectedExecutable 或 manifest.command.expectedExecutable 字段,就必须是安全绝对路径;空字符串、相对路径、文件系统根目录或包含换行 / NUL 的值都会按坏 manifest 处理,不能退化成“未声明 expectedExecutable”。生成端 pingora-cutover-command-evidence.mjs --expected-executable 也会在执行真实命令前拒绝相对路径、文件系统根目录和带换行 / NUL 的路径,避免先生成坏命令证据再等最终总审计失败。
命令证据声明 expectedExecutable 后,即使最终总审计没有额外传 --require-command-executable,manifest.command.executable 和独立 command-record.json.executable 也必须是同一个绝对路径;二者一致但都指向相对路径,或真实 executable 与 expectedExecutable 不一致,都会失败。
命令证据必须同时在 manifest.command.executable 和独立 command-record.json.executable 中记录真实可执行文件绝对路径;只保留可读 command 字符串、缺少结构化 executable 字段,或两份记录的 executable 任一缺失 / 非绝对路径,都不能进入正式证据链。
pingora-cutover-command-evidence.mjs 生成证据时,-- 后面的真实命令也必须直接传绝对路径,不能传 node、bash、脚本名等 PATH 裸命令名;真实命令不能是文件系统根目录,真实命令和每个真实命令参数都不能包含换行或 NUL 字符,否则生成端会在执行前失败,不创建正式命令证据。runbook 中的 enable / rollback apply 因此必须始终指向 current release 下的绝对脚本路径。
聚合门禁、dry-run plan、cutover runbook、直连启用脚本、直连回退脚本和本机 gateway smoke 可以把 probe token 参数真实传给子脚本,但所有 JSON 输出和命令日志必须显示 <redacted>,不得把 direct probe token 或 rollback shadow probe token 原文写入终端、CI 日志或证据包命令记录。
node scripts/check-pingora-release-readiness.mjs \
--require-docker \
--pull-docker \
--require-nginx \
--require-live \
--live-base-url http://127.0.0.1 \
--live-host <域名> \
--live-nginx-access-log /var/log/nginx/genarrative.access.log \
--live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log
该命令要求 Docker Nginx handoff、目标机 nginx -t、目标 Nginx live canary 和真实 canary access log 对账全部通过。--require-live 前必须已经人工 include canary snippet 并 reload Nginx;否则 live smoke 会按预期失败。--require-live 会强制要求 --live-host,避免只访问 127.0.0.1 命中 Nginx 默认 vhost 而不是正式域名 vhost;live smoke 成功后会立即用 scripts/check-pingora-canary-access-log-parity.mjs 按 request_id 对照 Nginx 与 Pingora access log,默认读取 /var/log/nginx/genarrative.access.log 和 /var/log/genarrative/pingora-gateway.access.log。--live-nginx-access-log 与 --live-pingora-access-log 必须是绝对路径且不能是文件系统根目录;单独运行 canary access log 对账脚本时,--nginx-log-file 与 --pingora-log-file 也遵循同一规则。
Pingora 直连入口切换窗口必须追加:
node scripts/check-pingora-release-readiness.mjs \
--require-direct \
--direct-https-base-url https://127.0.0.1 \
--direct-http-base-url http://127.0.0.1 \
--direct-host <域名> \
--direct-redirect-host <域名或host:port> \
--direct-spacetime-database <库名> \
--direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log \
--direct-health-patrol-env-file /etc/genarrative/health-patrol.env \
--direct-preflight-env-file /etc/genarrative/pingora-gateway.env \
--direct-preflight-systemd \
--direct-preflight-check-cert-readable \
--direct-preflight-check-service-env-file \
--direct-preflight-check-service-user-cert-readable \
--direct-preflight-check-service-binary-executable
--require-direct 用于 Pingora 已接管后的直连复核,不再要求 --direct-preflight-check-ports-free;切换前释放 80/443 的证明只放在 --dry-run-cutover、启用前 preflight 和 pingora-direct-enable.sh --apply 中。
正式执行直连切换前先生成只读 runbook,供当班人员逐条审阅。该命令只打印 JSON,不启动检查、不修改 systemd:
npm run plan:pingora-direct-cutover -- \
--require-direct \
--direct-https-base-url https://127.0.0.1 \
--direct-http-base-url http://127.0.0.1 \
--direct-host <域名> \
--direct-redirect-host <域名或host:port> \
--direct-spacetime-database <库名> \
--direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log \
--direct-health-patrol-env-file /etc/genarrative/health-patrol.env \
--direct-preflight-env-file /etc/genarrative/pingora-gateway.env \
--direct-preflight-systemd \
--direct-preflight-check-cert-readable \
--direct-preflight-check-service-env-file \
--direct-preflight-check-service-user-cert-readable \
--direct-preflight-check-service-binary-executable \
--direct-preflight-check-ports-free \
--cutover-evidence-output-root /var/log/genarrative/pingora-cutover-evidence \
--rollback-nginx-smoke-url https://<域名>/ \
--rollback-nginx-smoke-expect-body '<!doctype html>' \
--rollback-health-patrol-public-base-url <切换前Nginx巡检入口>
runbook 会列出 Host 与回退巡检入口确认、切换前 current release 自包含自审、切换前状态快照证据包、切换前证据 manifest 只读验真、current release 随包 preflight、启用前基础门禁、direct enable dry-run、direct enable apply、启用命令证据 manifest 只读验真、用 current release 随包 scripts/deploy/pingora-health-patrol-env-switch.mjs --apply 把 health patrol 切到 pingora-direct、启用后 health patrol env 直连复核、启用后状态快照证据包、启用后证据 manifest 只读验真、启用后 --require-direct 复核、rollback dry-run、用 current release 随包 scripts/deploy/pingora-gateway-env-shadow-switch.mjs --apply 在 rollback apply 前把 Pingora env 预置回 shadow、用 health patrol env 切换脚本在 rollback apply 前把 health patrol 预置回 nginx、rollback apply、回退命令证据 manifest 只读验真、回退后 health patrol env Nginx 模式复核、回退后状态快照证据包、回退后证据 manifest 只读验真和切换证据根目录三阶段总审计。rollback dry-run / apply 必须显式传 --nginx-smoke-expect-body,该片段必须来自切换前真实 Nginx 入口响应;dev 实测证明默认 /healthz 并不总是公网 Nginx 可用端点,因此不要把 http://127.0.0.1/healthz 或固定 "ok":true 当作通用回退 smoke。启用前 current release 自审使用 --require-pingora-gateway --systemd-show,先确认发布包自包含、api-server.sha256 / pingora-gateway.sha256 匹配、release-manifest.api-server.json 已登记 pingora-gateway、pingora-gateway 可执行且 systemd ExecStart 指向 current release;启用前基础门禁不带 --require-direct,因为此时 systemd direct-entry drop-in 尚未生效;启用后复核必须带 --require-direct,但不再检查 --direct-preflight-check-ports-free,因为此时 80/443 应由 Pingora 直连入口占用。若切换参数提供 --direct-probe-token,启用后复核也会继续透传该 token 检查内部探针,JSON runbook 只显示 <redacted>。三个状态快照证据包都使用 current release 随包脚本,显式传 --output-root <证据根目录> 和同一个 --cutover-run-id,并带 --require-pingora-gateway --run-health-patrol --fail-on-critical,用于把切换前基线、启用后 direct 状态、回退后 Nginx 状态和当前发布物自审结果留成可归档证据;其中启用后证据包还会额外传 --run-direct-live,把 direct live smoke 的 direct-live.json、stdout / stderr、命令记录和 Pingora access log request_id 反查结果一起写入同一证据目录,且 direct-access-log JSON 要保留匹配数量、缺失明细和 method/path/status 漂移明细,避免只在终端输出里保留直连证据;post-enable 证据包会传 --expected-pingora-env-mode direct,post-rollback 证据包会传 --expected-pingora-env-mode shadow。证据包 manifest 会给已生成的 snapshot、direct live、stdout / stderr、命令记录和 parse-error 文件记录 path、sizeBytes 与 sha256,并把 Pingora env 的 listen、tlsListen、httpRedirectListen、tlsCertFile、tlsKeyFile、mode 和 shadowReady 提升到 manifest.summary.pingoraEnvShadow,用于证明回退后 active env 已恢复到 127.0.0.1:18081 shadow,且低端口 TLS / HTTP redirect / 证书路径均已清空;direct enable apply / rollback apply 通过命令证据脚本归档时,命令证据 manifest 也会给 command.stdout.txt、command.stderr.txt 和 command-record.json 同步记录 sizeBytes 与 sha256。每次复制或归档证据目录后,都要把对应阶段 stdout 中的 bundleDir 填入 /opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs --bundle-dir <bundleDir> 做只读验真,确认文件未在归档过程中损坏或被替换;enable / rollback apply 命令证据生成后也要分别把 stdout 中的 bundleDir 填入 verifier 的 <enable-apply-bundle-dir> / <rollback-apply-bundle-dir> 占位符,先验真命令证据,再继续 health patrol 切换或回退后复核;三阶段证据、enable / rollback apply 命令证据和三条 env 变更命令证据都完成并即时验真后,再运行 /opt/genarrative/current/scripts/ops/pingora-cutover-evidence-audit.mjs --evidence-root <证据根目录> --require-phase pre-cutover --require-phase post-enable --require-phase post-rollback --require-phase-direct-live-access-log post-enable --require-phase-direct-live-static-headers post-enable --require-phase-pingora-env-shadow post-rollback --require-command enable-apply:pingora-direct-enable-apply --require-command post-enable:pingora-health-patrol-direct-env-switch --require-command rollback-prep:pingora-gateway-shadow-env-switch --require-command rollback-prep:pingora-health-patrol-nginx-env-switch --require-command rollback-apply:pingora-direct-rollback-apply --require-command-executable enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --require-command-executable post-enable:pingora-health-patrol-direct-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs --require-command-executable rollback-prep:pingora-gateway-shadow-env-switch:/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs --require-command-executable rollback-prep:pingora-health-patrol-nginx-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs --require-command-executable rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh --require-command-arg enable-apply:pingora-direct-enable-apply:--apply --require-command-arg post-enable:pingora-health-patrol-direct-env-switch:--apply --require-command-arg post-enable:pingora-health-patrol-direct-env-switch:pingora-direct --require-command-arg rollback-prep:pingora-gateway-shadow-env-switch:--apply --require-command-arg rollback-prep:pingora-health-patrol-nginx-env-switch:--apply --require-command-arg rollback-prep:pingora-health-patrol-nginx-env-switch:nginx --require-command-arg rollback-apply:pingora-direct-rollback-apply:--apply --require-cutover-run-id <本次cutoverRunId> --timeline-max-span-ms 86400000 生成总审计 JSON,并在 JSON 中记录 requiredCutoverRunId、requiredCommandExecutables、requiredCommandArgs、timeline.maxSpanMs 与 timeline.spanMs。若 snapshot 或 direct live stdout 解析失败,证据包会保留对应 *-parse-error.txt 并写入 manifest 索引。--cutover-release-root 与 --cutover-evidence-output-root 必须是绝对路径,且不能是文件系统根目录,证据根目录默认 /var/log/genarrative/pingora-cutover-evidence;--cutover-evidence-timeline-max-span-ms 默认 86400000,仅用于明确批准超过 24 小时的维护窗口。
runbook 中“启用前基础门禁”和“启用后 --require-direct 复核”都调用 current release 随包 scripts/check-pingora-release-readiness.mjs --release-runtime-only。前者不带 --require-direct,因为 direct-entry drop-in 尚未生效,并会额外跑直连彩排状态复核,确认 Nginx 仍接公网、Pingora shadow / realpath canary 高端口和 current release 自审均可用;后者必须带 --require-direct,不再要求 Nginx 接公网彩排状态,并复核 direct preflight、health patrol env 直连模式和 direct live smoke。
证据 verifier / 总审计的入口路径、verifier 脚本路径和 manifest 登记文件名都不能包含换行或 NUL 字符,避免污染 JSON 证据、终端输出或归档复盘;遇到此类失败应重新生成证据或修正 runbook 参数,不要手工改 manifest 兜底。
命令证据生成端的 --output-root 同样必须是安全绝对路径,不能是文件系统根目录、符号链接或包含换行 / NUL 字符;失败时应先修正证据根目录参数,不能让被污染的路径进入 bundleDir / manifestPath 输出。
最终证据根目录总审计命令必须同时带两条真实脚本身份要求:--require-command-executable enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh 和 --require-command-executable rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh。这两个参数的 executable 段必须是安全绝对路径,不能是文件系统根目录,也不能包含换行或 NUL 字符;否则总审计会在读取证据前失败,避免把被控制字符污染的审计要求带入正式证据链。总审计 JSON 会记录 requiredCommandExecutables,用于复盘本次 enable / rollback apply 证据到底绑定了哪两个 current release 随包脚本。
--live-host、--direct-host、--direct-redirect-host 和 --rollback-nginx-smoke-host 都只接受域名或 host:port,不要填 https:// URL、路径或查询。--direct-redirect-host 用于校验 HTTP redirect Location 的目标 host,本机单独 smoke 使用非标准端口验证 redirect 时可填 127.0.0.1:18443;但 --dry-run-cutover 代表正式切换 runbook,会要求 --direct-redirect-host、--rollback-nginx-smoke-host 和 --direct-host 使用同一 hostname,只允许端口不同,避免 redirect 和回退 smoke 打到不同入口。Host 与回退巡检入口确认步骤会同时展示 rollback-health-patrol-public-base-url 和 rollback-health-patrol-public-host,当班人员必须确认回退巡检入口就是切换前记录值。--rollback-health-patrol-public-base-url 必须填写切换前 GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL 的原值;如果切换前 Nginx 巡检需要 Host 覆盖,再追加 --rollback-health-patrol-public-host <切换前Host>,否则 runbook 会要求回退后清空 GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST。若希望回退步骤同时证明 Pingora shadow 高端口仍存活,可给 runbook 追加 --rollback-pingora-shadow-probe-url 和 --rollback-pingora-shadow-probe-token;两者必须成对出现,URL 必须是 http(s),JSON runbook 会隐藏 token 原文。生产不应使用 --direct-insecure-tls,--require-direct 会直接拒绝 insecure TLS。
--require-direct 会强制要求同时提供 --direct-https-base-url、--direct-http-base-url、--direct-host、--direct-redirect-host、--direct-pingora-access-log、--direct-health-patrol-env-file、--direct-preflight-env-file、--direct-preflight-systemd、--direct-preflight-check-cert-readable、--direct-preflight-check-service-env-file、--direct-preflight-check-service-user-cert-readable、--direct-preflight-check-service-binary-executable 和 --direct-spacetime-database,避免只验证 HTTPS / WSS 而漏掉 HTTP 301、ACME challenge、正式域名 Host/SNI、redirect Location host、Pingora access log 的 request_id 落盘证据、health patrol 仍按 Nginx 模式巡检、systemd drop-in 生效、service 实际读取 env 与 preflight env 一致性、当前用户证书可读性、服务用户证书私钥可读性、current release 二进制可执行性或误用默认 SpacetimeDB 库。--direct-preflight-check-ports-free 只属于切换前端口释放门禁,--dry-run-cutover、启用前 preflight 和 enable apply 必须带它,启用后 --require-direct 复核不能再带它。--direct-pingora-access-log、--direct-health-patrol-env-file 和 --direct-preflight-env-file 必须是绝对路径且不能是文件系统根目录;单独运行 scripts/check-pingora-direct-live.mjs --pingora-access-log 或 scripts/check-pingora-direct-preflight.mjs --env-file 时同样不能把路径指向 /。
check-pingora-release-readiness.mjs --help 的正式直连和只生成 runbook 示例也必须显式带 --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log,确保值班人员复制示例命令时不会因为缺少 direct access log 参数而被 --require-direct 拦住。
前缀 canary 已在目标 Nginx 中人工 include 且 reload 后,执行 live smoke:
GENARRATIVE_PINGORA_CANARY_BASE_URL=http://127.0.0.1 \
GENARRATIVE_PINGORA_CANARY_HOST=<域名> \
npm run check:pingora-canary-live
该脚本只读访问 GET /__genarrative_pingora_canary/*,检查 healthz、代表性 API、SpacetimeDB identity、静态资源和 generated 禁止入口,并强制校验 X-Genarrative-Nginx-Handoff: pingora-canary。本机用 http://127.0.0.1 验证时仍必须设置 GENARRATIVE_PINGORA_CANARY_HOST=<域名> 命中正式 vhost;base URL、canary prefix、Host、额外 path 和 timeout 不能包含换行或 NUL,脚本会在发起 canary 请求前失败,避免污染参数进入 URL、Host header 或 JSON 输出。环境变量示例见 deploy/env/pingora-canary-live.env.example。live smoke 后用 current release 随包 scripts/check-pingora-canary-access-log-parity.mjs 对照 Nginx 与 Pingora access log,确认同一 request_id 已在两侧落盘且 method/status/path 没有漂移。
如果已经显式配置 GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN,并准备验证 Pingora 不经 Nginx 的直连入口,执行 direct live smoke:
GENARRATIVE_PINGORA_DIRECT_HTTPS_BASE_URL=https://127.0.0.1 \
GENARRATIVE_PINGORA_DIRECT_HTTP_BASE_URL=http://127.0.0.1 \
GENARRATIVE_PINGORA_DIRECT_HOST=<域名> \
GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST=<域名或host:port> \
npm run check:pingora-direct-live
该脚本只读检查 HTTPS 根路径、HTTP/2 ALPN、代表性 API、SpacetimeDB identity、WSS /v1/database/<database>/subscribe 握手、generated 禁止入口、公开 /healthz 拒绝、可选 shadow probe,以及 HTTP 入口到 HTTPS 的 301 和 ACME challenge 静态读取;真实 SpacetimeDB 对 GET /v1/identity 返回 405 Method Not Allowed 属于可接受语义,direct live 只把它作为路径转发代表,不要求该 GET 创建 identity;配置 --pingora-access-log 后还会为每个请求生成 X-Request-Id 并反查 Pingora access log,逐条比对同一 request_id 的 method、path 与 status,证明直连流量真实进入 Pingora,且未发生日志侧方法 / 路径 / 状态漂移。HTTP 响应必须带 X-Genarrative-Gateway: pingora-shadow,TLS ALPN 必须协商到 h2,WSS 成功握手时必须保留 v2.bsatn.spacetimedb 子协议。release readiness --require-direct 会自动要求 WSS 返回 101,并要求提供 direct HTTP base URL、正式域名 Host/SNI、redirect Location host、显式 SpacetimeDB 数据库名和 Pingora access log 路径,让 HTTP/2、HTTP redirect / ACME 入口、正式证书域名、跳转目标域名、目标库 subscribe 和 request_id + method/path/status 对账证据都纳入硬门禁;单独运行 direct live smoke 时也可加 --require-wss-upgrade 强制同一口径。direct live 在首页发现 /assets/ 或 /admin/assets/ 引用时会额外执行静态资源 GET、HEAD、If-None-Match / If-Modified-Since 条件请求和 Range: bytes=0-0 探针,确认静态头、HEAD 头响应、304 协商缓存、206 + Content-Range 以及不压缩 Range / 304 响应均进入同一 access log 证据链;这些静态 GET / HEAD / 304 / Range 的 JSON 结果会写入白名单 headers,只保留 cache-control、etag、last-modified、accept-ranges、content-range、content-length 和 content-encoding,方便切换后复盘缓存和 Range 响应头。若发现 Vite 指纹资源,还会额外确认 Cache-Control: public, max-age=31536000, immutable 及其 GET / HEAD / 304 / Range method/path/status 证据,避免旧 tab chunk 缓存口径在直连后退化。direct live 的 HTTPS / HTTP base URL、Host、redirect Host、probe token、额外 path、SpacetimeDB 数据库名、access log 路径、timeout 和布尔 env 都不能包含换行或 NUL;脚本会在发起 HTTPS / HTTP / WSS 请求前失败,避免污染参数进入请求头、URL、日志对账或 JSON 证据。环境变量示例见 deploy/env/pingora-direct-live.env.example。生产证书必须可被系统信任;--insecure-tls 只允许本机自签证书 smoke 使用。--skip-wss 只允许单独 direct live 临时排障,release readiness --require-direct 会直接拒绝。
生产发布包默认不携带 Pingora,避免现有 API 流水线被影子网关构建依赖影响。需要部署影子服务时,构建机先确保有 cmake、C/C++ 编译器和 Rust target,再显式执行;build-production-release.sh 会在真实构建 Pingora 前 fail-fast 检查 cmake、C 编译器和 C++ 编译器,Jenkins INCLUDE_PINGORA_GATEWAY=true 时也会先检查这些工具:
npm run build:production-release -- --component api-server --name <version> --include-pingora-gateway
Jenkins Genarrative-Api-Build 对应参数是 INCLUDE_PINGORA_GATEWAY,默认关闭;勾选后才会归档 pingora-gateway 与 pingora-gateway.sha256,触发后续 Genarrative-Api-Deploy 时也会传递该布尔参数并复制这两个可选产物。无论是否打包 Pingora 二进制,API release 都必须携带 build/<version>/scripts/deploy/production-api-deploy.sh、同目录的 maintenance-on.sh / maintenance-off.sh、/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh、pingora-direct-rollback.sh、pingora-tls-cert-sync.mjs、scripts/ops/pingora-current-release-audit.mjs、scripts/ops/pingora-cutover-status-snapshot.mjs、scripts/ops/pingora-cutover-evidence-bundle.mjs、scripts/ops/pingora-cutover-command-evidence.mjs、scripts/ops/pingora-cutover-evidence-verify.mjs、scripts/ops/pingora-cutover-evidence-audit.mjs、scripts/check-pingora-direct-preflight.mjs、scripts/check-pingora-direct-live.mjs、scripts/check-pingora-canary-access-log-parity.mjs、scripts/check-production-health-patrol-env.mjs、deploy/systemd/、deploy/env/ 和 deploy/pingora/。Genarrative-Api-Deploy 只能从上游构建归档复制并执行 build/<version>/scripts/deploy/production-api-deploy.sh,不能继续执行部署工作区根部脚本;维护脚本必须与 deploy 脚本来自同一发布包同一目录,避免 Jenkins 工作区里的旧脚本掩盖 release 包布局缺陷。直连启用脚本从 current release 执行时默认读取 /opt/genarrative/current/deploy/systemd/genarrative-pingora-gateway-direct-entry.conf,必须能自包含完成 preflight、systemd 模板读取和 direct live smoke,不依赖 Jenkins 工作区、目标机源码 checkout 或 /etc 参考模板;current release 自审、TLS 证书同步、health patrol env 复核、canary access log 对账、canary live、direct live、状态快照、证据包、命令证据、证据验真和证据根目录审计也必须来自 current release 随包脚本与 deploy/env/。/etc/genarrative/pingora/genarrative-pingora-gateway-direct-entry.conf 只作为 Server-Provision 安装的人工审阅 / 手动覆盖模板;确需使用时显式传 --template-path 或 GENARRATIVE_PINGORA_DIRECT_TEMPLATE_PATH。npm run check:production-api-release 会用临时 CARGO_TARGET_DIR 和假 api-server / pingora-gateway release binary 跑 build-production-release.sh --component api-server --skip-api-build,动态验证默认 API release 不登记 Pingora,显式 --include-pingora-gateway --skip-pingora-gateway-build 时则必须包含 pingora-gateway、pingora-gateway.sha256 并写入 manifest,同时验证 Pingora 直连依赖、TLS 证书同步脚本、current release 自审脚本、状态快照脚本、证据包脚本、命令证据脚本、证据验真脚本、证据根目录审计脚本、canary access log 对账脚本、健康巡检 env 复核脚本、env 示例目录、API deploy 执行入口自包含和 deploy/pingora/pingora-gateway.env.example 的生产安全默认值:gzip-only、TRUST_X_FORWARDED_FOR=false、TRUSTED_FRONT_PROXY_CONFIRMED=false、接流保护默认开启、probe token 为空;该检查还会读取发布包 README,并直接运行发布包内 scripts/check-pingora-release-readiness.mjs --dry-run-cutover,确认最终证据根目录总审计步骤仍带两条 --require-command-executable ... /opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh / pingora-direct-rollback.sh current release 脚本身份要求。npm run check:pingora-production-release-build 会用假 api-server、临时 CARGO_TARGET_DIR 和真实 cargo build -p pingora-gateway --release --target x86_64-unknown-linux-gnu 验证显式 include 路径能构出可执行 pingora-gateway、checksum 和 manifest 登记,避免只靠假二进制布局 smoke。production-api-deploy.sh 对这些 Pingora 直连依赖采取 fail-fast:上游发布产物缺失时保持维护模式并停止部署,不再从部署工作区兜底复制;release-manifest.json 必须存在且登记 api-server artifact,若发布包包含 pingora-gateway,manifest 也必须登记 pingora-gateway artifact,否则部署会在切换 current 前失败。API deploy 要求 --release-root、--current-link、--api-env-file 使用绝对路径,--version 必须以数字或字母开头并拒绝点目录,会先写 ${RELEASE_ROOT}/.${VERSION}.staging.$,全部校验和复制完成后用非合并语义提升为 ${RELEASE_ROOT}/${VERSION},再用固定替换语义切换 current 符号链接;同版本正式 release 已存在、提升前竞态出现或 current 路径不是符号链接时都会拒绝合并 / 覆盖,失败时清理 staging 并保持维护模式。本机用 npm run check:production-api-deploy 通过临时 release、fake systemctl / curl 动态验证从发布产物内执行 deploy 脚本后 current release 自洽,并覆盖缺少 TLS 证书同步脚本、current release 自审脚本、状态快照脚本、证据包脚本、证据验真脚本、证据根目录审计脚本、canary access log 对账脚本、健康巡检 env 复核脚本、deploy/env/、release manifest、api-server manifest artifact、Pingora manifest artifact、相对 release root / current link / api env file、点目录或点开头 version、同版本 release 目录已存在、current 路径不是符号链接或提升前 release 目录竞态出现时必须失败,同时复核 current release 内 Pingora env 示例仍保持同一组生产安全默认值。
API release 还必须携带 scripts/check-pingora-release-readiness.mjs、scripts/check-pingora-canary-live.mjs 与 scripts/ops/pingora-direct-rehearsal-status.mjs。前者支撑 current release 的 --release-runtime-only 聚合复核,canary live 脚本支撑目标 Nginx canary live smoke,直连彩排状态脚本支撑目标机切换前只读确认 Nginx 仍接公网、Pingora shadow / realpath canary 高端口和 current release 自审均可用;缺少任一脚本时 check:production-api-release、check:production-api-deploy 和生产运维护栏都必须失败。
发布包会额外包含 pingora-gateway 与 pingora-gateway.sha256。production-api-deploy.sh 看到这两个文件时会校验并把 pingora-gateway / pingora-gateway.sha256 一起复制到 /opt/genarrative/current,供 shadow systemd 模板和 current release 自审使用;api-server.sha256 也必须随 api-server 一起进入 current release。没有这两个 Pingora 文件时现有 API 发布行为不变。API 发布仍只重启 genarrative-api.service;包含 Pingora 时,部署脚本会在提升 release 和切换 current 前读取 systemctl cat genarrative-pingora-gateway.service 与其 EnvironmentFile,拒绝已出现 CAP_NET_BIND_SERVICE direct-entry capability、拒绝 GENARRATIVE_PINGORA_GATEWAY_LISTEN 不是 127.0.0.1:18081、拒绝已配置 TLS_LISTEN 或 HTTP_REDIRECT_LISTEN,确认仍是本机 shadow 高端口后才切换 current,并执行 systemctl restart genarrative-pingora-gateway.service 后复核 active。该自动拉起只覆盖 shadow 服务,不会启用公网 80/443 直连入口;若现场已经处于 direct-entry 状态,应走正式直连 runbook 或先回退到 shadow 后再执行 API deploy。
也可以复制 deploy/pingora/pingora-gateway.env.example 到部署环境的非 Git 配置文件,由 systemd 或容器注入。仓库提供 deploy/systemd/genarrative-pingora-gateway.service 作为影子服务模板,默认读取 /etc/genarrative/pingora-gateway.env,仍只应监听本机高端口,再由 Nginx 或本机 smoke 主动访问。Server-Provision 会把主 service 安装到 /etc/systemd/system/genarrative-pingora-gateway.service,并把直连低端口 drop-in 模板安装到 /etc/genarrative/pingora/genarrative-pingora-gateway-direct-entry.conf 作为参考和手动覆盖来源;该模板不会默认生效,/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh 默认使用随 current release 发布的 deploy/systemd/genarrative-pingora-gateway-direct-entry.conf。deploy/nginx/snippets/genarrative-pingora-canary.conf 是可选的 Nginx -> Pingora 前缀 canary 模板,Server-Provision 会安装到 /etc/nginx/snippets/,但主站配置默认不 include;启用前必须把 __GENARRATIVE_PINGORA_PROBE_TOKEN__ 替换为真实 token,并确认 allow/deny 来源边界符合当次验证窗口。
直连公网入口切换窗口如果需要让非 root 的 genarrative 用户绑定 80/443,先确认 /etc/genarrative/pingora-gateway.env 已显式设置 TLS / redirect 入口和证书路径,并确认 genarrative 用户可读取证书链和私钥;生产 80/443 还必须先从 Nginx 或其它进程释放,非切换窗口建议先用 18443/18080 这类高端口验证。若证书来自 Certbot / Let’s Encrypt,不要放宽 /etc/letsencrypt/live 或 archive 的目录 / 私钥权限;先用随包 pingora-tls-cert-sync.mjs 把 live symlink 解析后的真实证书复制到 /etc/genarrative/pingora-tls/<域名>/,再把 env 中 TLS_CERT_FILE / TLS_KEY_FILE 指向该私有副本:
sudo -n node -- /opt/genarrative/current/scripts/deploy/pingora-tls-cert-sync.mjs \
--apply \
--source-cert-file /etc/letsencrypt/live/<域名>/fullchain.pem \
--source-key-file /etc/letsencrypt/live/<域名>/privkey.pem \
--target-dir /etc/genarrative/pingora-tls/<域名>
该脚本默认 dry-run,--apply 才原子写入 fullchain.pem / privkey.pem,目标目录默认 root:genarrative 0750,文件默认 root:genarrative 0640,并复核 genarrative 服务用户可读;目标目录和目标文件不能是符号链接。随后先 dry-run 直连启用脚本:
npm run plan:pingora-direct-cutover -- --require-direct --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log --direct-health-patrol-env-file /etc/genarrative/health-patrol.env --direct-preflight-env-file /etc/genarrative/pingora-gateway.env --direct-preflight-systemd --direct-preflight-check-cert-readable --direct-preflight-check-service-env-file --direct-preflight-check-service-user-cert-readable --direct-preflight-check-service-binary-executable --rollback-nginx-smoke-url https://<域名>/ --rollback-nginx-smoke-expect-body '<!doctype html>' --rollback-health-patrol-public-base-url <切换前Nginx巡检入口>
/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --no-status
plan:pingora-direct-cutover 只打印切换 runbook,不执行命令。默认 enable dry-run 会展示 apply 前执行 current release 自审,以及安装 /opt/genarrative/current/deploy/systemd/genarrative-pingora-gateway-direct-entry.conf 到 /etc/systemd/system/genarrative-pingora-gateway.service.d/direct-entry.conf 的命令。只有需要临时使用 Server-Provision 安装到 /etc/genarrative/pingora/ 的参考模板时,才显式传 --template-path /etc/genarrative/pingora/genarrative-pingora-gateway-direct-entry.conf。切换窗口覆盖 --preflight-script、--direct-live-script、--current-release-audit-script、--template-path、--service-unit-path、--dropin-path 或 env 文件路径时必须使用绝对路径且不能是文件系统根目录;--apply 会在安装 drop-in 前确认 current release 自审、direct preflight 和 direct live smoke 脚本都真实存在,脚本缺失时直接失败。直连启用脚本的 service、路径、URL、Host、probe token、access log、数据库名、tail 行数和 timeout 参数都不能包含换行或 NUL;脚本会在 current release 自审、preflight、drop-in 写入和 systemctl 前失败。--apply 还会拒绝符号链接形式的 drop-in 目录或 drop-in 目标文件,并拒绝已存在但不是普通文件的目标,避免把直连 capability 写入非预期 systemd 位置。
确认 direct preflight 已通过且命令符合预期后,再显式执行:
/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh \
--apply \
--preflight-env-file /etc/genarrative/pingora-gateway.env \
--preflight-check-cert-readable \
--preflight-check-service-env-file \
--preflight-check-service-user-cert-readable \
--preflight-check-service-binary-executable \
--preflight-check-ports-free \
--direct-https-base-url https://127.0.0.1 \
--direct-http-base-url http://127.0.0.1 \
--direct-host <域名> \
--direct-redirect-host <域名或host:port> \
--direct-spacetime-database <库名> \
--direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log
systemctl cat genarrative-pingora-gateway.service | grep -E 'AmbientCapabilities|CapabilityBoundingSet'
grep -E 'GENARRATIVE_PINGORA_GATEWAY_(TLS_LISTEN|HTTP_REDIRECT_LISTEN|TLS_CERT_FILE|TLS_KEY_FILE)=' \
/etc/genarrative/pingora-gateway.env
仓库工作区也可以执行 npm run deploy:pingora-direct-enable -- --apply --preflight-env-file /etc/genarrative/pingora-gateway.env --preflight-check-cert-readable --preflight-check-service-env-file --preflight-check-service-user-cert-readable --preflight-check-service-binary-executable --preflight-check-ports-free --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log。--apply 会先执行随包 scripts/ops/pingora-current-release-audit.mjs --require-pingora-gateway --systemd-show;自审失败、direct preflight 脚本缺失、direct live smoke 脚本缺失、drop-in 目录是符号链接、drop-in 目标是符号链接或已存在目标不是普通文件时不会安装 direct-entry drop-in。--apply 缺少 --preflight-env-file、--preflight-check-cert-readable、--preflight-check-service-env-file、--preflight-check-service-user-cert-readable、--preflight-check-service-binary-executable、--preflight-check-ports-free、--direct-https-base-url、--direct-http-base-url、--direct-host、--direct-redirect-host、--direct-pingora-access-log 或 --direct-spacetime-database 会直接失败,避免跳过 env / 当前用户证书可读 / service EnvironmentFile 一致性 / 服务用户证书可读 / service 二进制可执行 / 端口释放预检、正式域名 Host/SNI、HTTP redirect Location host、Pingora access log request_id 落盘或 WSS 目标库验证;--apply 安装并重启后还会用 systemctl cat 核验 AmbientCapabilities=CAP_NET_BIND_SERVICE、CapabilityBoundingSet=CAP_NET_BIND_SERVICE 和 EnvironmentFile=/etc/genarrative/pingora-gateway.env 已进入 systemd 最终配置,用 systemctl show genarrative-pingora-gateway.service --property=ExecStart --value --no-pager 确认最终 ExecStart 指向随包主 service 模板中的 current release pingora-gateway,用 systemctl is-active 确认 Pingora service 为 active,再执行 direct live smoke,覆盖 HTTPS 根路径、首页引用静态资产自动发现探测、静态资产 HEAD 与 Range: bytes=0-0、HTTP/2 ALPN、API、SpacetimeDB identity、HTTP redirect / ACME、generated / healthz 拒绝、WSS 101 和 Pingora access log request_id 落盘。首页返回 200 且 HTML 中包含 /assets/ 或 /admin/assets/ 引用时,direct live 会额外请求该静态资源,确认 Cache-Control、ETag、Last-Modified、Accept-Ranges: bytes 和 HEAD 头响应,再用 Range: bytes=0-0 验证 206 + Content-Range 且不压缩,并纳入 direct-access-log method/path/status 对账;维护模式、非 HTML 或发布包首页没有资产引用时该项标记为 skipped,不阻断维护窗口。启用脚本会解析 direct live JSON 中的 direct-access-log 结果,要求 matchedCount == checked 且 missingCount=0、mismatchCount=0,因此 direct live 退出 0 但缺少结构化 access log 证据也会被视为启用失败。直连启用前必须已把 active /etc/genarrative/pingora-gateway.env 提升为直连 env;回退前则必须用 node -- /opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs --apply --env-file /etc/genarrative/pingora-gateway.env 把同一文件恢复为 shadow 高端口 env(GENARRATIVE_PINGORA_GATEWAY_LISTEN=127.0.0.1:18081 且清空 TLS / HTTP redirect 低端口监听),否则移除 capability 后重启 Pingora 可能仍按 80/443 配置失败。启用后单独复核可执行 npm run check:pingora-direct-live;启用后 --require-direct 聚合复核不再带 --direct-preflight-check-ports-free。验证失败时优先执行随 release 携带的直连回退脚本,移除 drop-in、systemctl daemon-reload 并重启 Pingora,让公网入口回到 Nginx:
正式 runbook 的状态快照证据包必须显式传 --expected-pingora-env-mode:post-enable 使用 direct,证明 active /etc/genarrative/pingora-gateway.env 已是直连姿态;post-rollback 使用 shadow,证明回退后的 active env 已恢复 shadow 姿态。pingora-gateway-env-shadow-switch.mjs --apply 不只清空 GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN / HTTP_REDIRECT_LISTEN,还必须同时清空 GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE / TLS_KEY_FILE;否则 Pingora 会看到“有证书路径但无 TLS listen”的半直连 env,回退证据也会被判为 CRITICAL。
/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh \
--apply \
--reload-nginx \
--nginx-smoke-url https://<域名>/ \
--nginx-smoke-host <域名> \
--nginx-smoke-expect-body '<!doctype html>'
在仓库工作区也可以执行 npm run deploy:pingora-direct-rollback -- --apply --reload-nginx --nginx-smoke-url https://<域名>/ --nginx-smoke-host <域名> --nginx-smoke-expect-body '<!doctype html>';不带 --apply 时只打印将执行的命令,适合切换前复核。回退脚本 --apply 必须同时带 --reload-nginx 和 --nginx-smoke-url,且 smoke URL 必须是 http(s) URL;执行时会先拒绝带换行或 NUL 的 service、路径、Nginx smoke URL / Host / 响应片段、health patrol 复核参数、shadow probe URL / token 和二进制 override,并拒绝路径参数指向文件系统根目录,再拒绝符号链接形式的 drop-in 目录或目标文件,并拒绝已存在但不是普通文件的 drop-in 目标,通过后才运行 nginx -t、移除 direct-entry drop-in、systemctl daemon-reload、重启 Pingora;随后用 systemctl cat 确认 AmbientCapabilities=CAP_NET_BIND_SERVICE 与 CapabilityBoundingSet=CAP_NET_BIND_SERVICE 已从最终 unit 配置中消失,避免只删除文件但 systemd 仍保留旧能力,并用 systemctl show ... ExecStart 确认最终 service 仍指向随包主 service 模板中的 current release pingora-gateway,避免回退后 shadow 服务继续跑旧发布包;最后 reload Nginx、确认 Nginx service 仍为 active,并用 smoke URL 证明 Nginx 入口真实可访问;传入 --nginx-smoke-expect-body 时会额外要求响应体包含预期片段,避免 HTTP 200 命中错误入口。回退 smoke URL 和响应片段必须来自切换前真实 Nginx 响应;dev 当前首页可用 https://dev.genarrative.world/ 与 <!doctype html>,不要沿用固定 /healthz + "ok":true。--nginx-smoke-url 指向本机 127.0.0.1、localhost 或 ::1 时,--apply 必须同时传 --nginx-smoke-host <域名>,且该值只能是 host 或 host:port,避免回退 smoke 命中默认 vhost。覆盖 --nginx-binary 或 --curl-binary 时可以传裸命令名;如果值包含路径分隔符,则必须使用绝对路径,避免回退窗口受 cwd 影响。若 health patrol env 已先切回 Nginx,可追加 --health-patrol-env-file /etc/genarrative/health-patrol.env --health-patrol-expected-public-base-url <切换前Nginx巡检入口> --health-patrol-require-empty-public-host,脚本会在 Nginx smoke 之后调用 current release 的 scripts/check-production-health-patrol-env.mjs,确认 GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx 且 public base URL / Host 已恢复为切换前记录值;若切换前 Nginx 巡检需要 Host 覆盖,则把 --health-patrol-require-empty-public-host 换成 --health-patrol-expected-public-host <切换前Host>。若需要同时证明回退后 Pingora 仍作为 shadow 高端口服务存活,可追加 --pingora-shadow-probe-url http://127.0.0.1:18081/__genarrative_pingora/healthz --pingora-shadow-probe-token <token>;脚本会隐藏 token,并要求探针响应包含 gateway=pingora-shadow。同一能力也可由 plan:pingora-direct-cutover 的 --rollback-pingora-shadow-probe-url / --rollback-pingora-shadow-probe-token 写进 rollback dry-run 和 apply 步骤。
生产健康巡检脚本支持可选 shadow probe。将 deploy/env/health-patrol.env.example 复制到 /etc/genarrative/health-patrol.env 后,同时配置:
GENARRATIVE_HEALTH_PATROL_PINGORA_BASE_URL=http://127.0.0.1:18081
GENARRATIVE_HEALTH_PATROL_PINGORA_PROBE_TOKEN=<与 pingora-gateway.env 一致的 token>
此时 genarrative-health-patrol.service 会额外请求 GET /__genarrative_pingora/healthz 并校验返回 {"ok":true,"gateway":"pingora-shadow"};未配置时现有生产巡检行为不变。
Pingora 直连接管公网入口后,巡检口径也必须从 Nginx 切到直连网关,避免因为 nginx.service 已停止而误报,同时继续证明正式 Host 语义可用。切换窗口不要手工编辑这三项,使用 current release 随包脚本写入并立即复核:
node -- /opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs \
--apply \
--env-file /etc/genarrative/health-patrol.env \
--gateway-mode pingora-direct \
--public-base-url https://127.0.0.1 \
--public-host <域名>
该模式检查 genarrative-api.service、spacetimedb.service、genarrative-pingora-gateway.service 和 Pingora 直连 public probe;脚本只管理 GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE、GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL、GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST,保留其它巡检配置,并会先对权限固定为 0600 的临时目标 env 调用 /opt/genarrative/current/scripts/check-production-health-patrol-env.mjs 复核,复核通过后才按真实 env 原权限、owner/group 原子替换,复核失败不会落盘。env 切换脚本的 --env-file、--check-script 以及 health patrol env 复核脚本的 --env-file 都必须是绝对路径且不能是文件系统根目录,也不能包含换行或 NUL;env 切换脚本的 --public-base-url 与 --public-host 同样不能包含换行或 NUL。--apply 的 --env-file 还必须直接指向真实普通文件,不能是符号链接;如果现场 /etc/genarrative/health-patrol.env 是链接,应先确认真实目标路径后把真实路径传给脚本,避免切换窗口替换链接本身或写入非预期目标。生产巡检脚本自身的显式 --timeout-ms、--slow-ms、GENARRATIVE_HEALTH_PATROL_TIMEOUT_MS 和 GENARRATIVE_HEALTH_PATROL_SLOW_MS 必须是正整数,非法值直接失败,不静默回退默认 5000ms / 3000ms,避免切换窗口因写错阈值而误判巡检质量。回退到 Nginx 前用同一脚本把 mode 和 public base URL / Host 恢复为切换前记录值;若切换前 Nginx 巡检不需要 Host 覆盖,用 --clear-public-host:
node -- /opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs \
--apply \
--env-file /etc/genarrative/health-patrol.env \
--gateway-mode nginx \
--public-base-url <切换前Nginx巡检入口> \
--clear-public-host
若切换前 Nginx 巡检需要 Host 覆盖,则把最后一项换成 --public-host <切换前Host>。脚本默认 dry-run,只有 --apply 才写入 env;Pingora direct 模式使用 https://127.0.0.1 这类本机 public base URL 时必须带 --public-host <域名>。
生产巡检、health patrol env 复核和 env 切换脚本读取的布尔 env 只接受 true/false、1/0、yes/no、on/off 或空值,非法值直接失败,不再按 false 继续。
Nginx canary handoff
当前 canary 只提供人工验证入口,不做随机流量抽样。启用步骤:
- 在本地或 CI 先执行
npm run check:pingora-canary-docker;需要强制真实 Docker Nginx 链路时执行node scripts/check-pingora-canary-docker.mjs --require-docker --pull。该脚本会同时验证 Docker Nginx access log 与 Pingora access log 的request_id对账,不只看 handoff 响应头。 - 确认
genarrative-pingora-gateway.service已作为持久 shadow service 运行在127.0.0.1:18081,GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN非空,并已用真实api-server、SpacetimeDB、静态 Web 目录和 access log 做过本机回环验收;不要直接从一次性/tmp进程进入 Nginx canary。 - 在目标机复制
/etc/nginx/snippets/genarrative-pingora-canary.conf为临时启用版本,替换__GENARRATIVE_PINGORA_PROBE_TOKEN__。 - 在
server {}内人工 include 该 snippet,并保持allow 127.0.0.1; allow ::1; deny all;或改成当次可信来源。 - 执行
npm run check:nginx-pingora-canary;目标机或 CI 有 Nginx 时执行node scripts/check-nginx-pingora-canary.mjs --require-nginx,再执行nginx -t && nginx -s reload。 - 执行
GENARRATIVE_PINGORA_CANARY_BASE_URL=http://127.0.0.1 GENARRATIVE_PINGORA_CANARY_HOST=<域名> npm run check:pingora-canary-live,确认 healthz、API、SpacetimeDB identity、静态资源和拒绝入口都带X-Genarrative-Nginx-Handoff: pingora-canary。 - 必要时再用同一前缀访问其它代表性路由,例如
/__genarrative_pingora_canary/api/creation-entry/config、/__genarrative_pingora_canary/v1/identity和/__genarrative_pingora_canary/assets/app.js,并对照直连 Nginx 正常入口和 Pingora shadow 日志。 - 执行 current release 随包
scripts/check-pingora-canary-access-log-parity.mjs --nginx-log-file /var/log/nginx/genarrative.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log --path /__genarrative_pingora_canary/healthz --path /__genarrative_pingora_canary/api/creation-entry/config,确认同一request_id、path、status和proxy_target与 Nginx access log 可对齐。对账脚本的日志路径、canary prefix、必需路径和--since-lines/GENARRATIVE_PINGORA_CANARY_ACCESS_LOG_SINCE_LINES都不能包含换行或 NUL;若 access log 行里解析出的 URI / path 含控制字符,脚本也会把对应行记为失败,避免污染值进入 JSON 对账输出。 - 验证结束后移除 include 并 reload Nginx;不要把该前缀入口当作正式公网 URL。
dev shadow service 验收记录
2026-06-17 已在 dev 机安装 genarrative-pingora-gateway.service 作为持久 shadow service。服务使用 /opt/genarrative/current/pingora-gateway,读取 /etc/genarrative/pingora-gateway.env,只监听 127.0.0.1:18081,上游保持 api-server=127.0.0.1:8082、SpacetimeDB=127.0.0.1:3101、静态目录 /srv/genarrative/web,access log 写入 /var/log/genarrative/pingora-gateway.access.log。probe token 只保存在目标机 env 文件,不进入仓库、终端日志或证据包。
本阶段验收只证明 shadow service 和真实上游链路可用,不启用 Nginx include,不 reload Nginx,不绑定公网 80/443。验收结果:
systemctl is-active genarrative-pingora-gateway.service返回active,NRestarts=0。GET /__genarrative_pingora/healthz带X-Genarrative-Pingora-Probe返回200和gateway=pingora-shadow,不带 token 返回404。GET /api/creation-entry/config经 Pingora 转发到真实api-server,返回200 application/json。GET /从真实 Web 目录返回200 text/html。GET /v1/identity经 Pingora 转发到真实 SpacetimeDB,返回405 Method Not Allowed;这是当前 SpacetimeDB 对 GET identity 的真实语义,只作为路径转发代表。GET /v1/database/genarrative-prod/subscribe?compression=BrotliWebSocket Upgrade 经 Pingora 转发到真实 SpacetimeDB,返回101 Switching Protocols,并带X-Genarrative-Gateway: pingora-shadow。/var/log/genarrative/pingora-gateway.access.log已记录 healthz、API、静态、SpacetimeDB identity 和 WSS subscribe 的request_id、path、status、proxy_target与upstream。
dev 根盘空间在安装后曾接近满盘;2026-06-17 进入 canary 前已清理旧 /tmp/genarrative-* 临时部署目录、apt cache,并将 journald 收敛到约 512M,df -h / 从 100% 降到约 92%。后续 canary / 证据归档前仍应复核 df -h /,避免 access log、Nginx reload 或证据归档阶段被磁盘空间干扰。
dev Nginx prefix canary 验收记录
2026-06-17 已在 dev 机做过一次临时 Nginx prefix canary。步骤是将 deploy/nginx/snippets/genarrative-pingora-canary.conf 渲染到 /etc/nginx/snippets/genarrative-pingora-canary.conf,只在 dev.genarrative.world 的本机 HTTP server {} 内临时 include,执行 nginx -t && nginx -s reload 后用 http://127.0.0.1 与 Host: dev.genarrative.world 做 loopback 验收。验证结束后已恢复 /etc/nginx/conf.d/genarrative.conf 备份并 reload Nginx;当前正常公网入口仍由 Nginx 原配置承接,Pingora 继续只作为 127.0.0.1:18081 shadow service 运行。
本阶段验收只证明 Nginx -> Pingora 的前缀 handoff 可用,不做真实路径 canary,不切 80/443 到 Pingora。验收结果:
node /tmp/check-pingora-canary-live.mjs --base-url http://127.0.0.1 --host dev.genarrative.world --json通过:healthz200、API config200、SpacetimeDB identity405、静态代表路径404、generated 拒绝路径404,全部带X-Genarrative-Nginx-Handoff: pingora-canary。- 通过 Nginx 前缀 canary 访问
/__genarrative_pingora_canary/v1/database/genarrative-prod/subscribe?compression=Brotli,WebSocket Upgrade 返回101 Switching Protocols,并带sec-websocket-protocol: v2.bsatn.spacetimedb、X-Genarrative-Nginx-Handoff: pingora-canary和X-Genarrative-Gateway: pingora-shadow。 node /tmp/check-pingora-canary-access-log-parity.mjs --nginx-log-file /var/log/nginx/genarrative.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log ... --json对账6/6 matched,missingCount=0,mismatchCount=0;覆盖 healthz、API config、SpacetimeDB identity、WSS subscribe、静态代表路径和 generated 拒绝路径。- 恢复后
grep genarrative-pingora-canary /etc/nginx/conf.d/genarrative.conf无匹配,GET http://127.0.0.1/__genarrative_pingora_canary/healthz在正常 Nginx HTTP 入口回到301,证明临时 canary include 已移除;genarrative-pingora-gateway.service仍为active且NRestarts=0。
dev Nginx realpath canary 验收记录
2026-06-17 已在 dev 机做过一次临时 Nginx realpath canary。步骤是将 deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf 渲染到 /etc/nginx/snippets/genarrative-pingora-realpath-canary.conf,并在 Nginx http 上下文临时 include 一个只监听 127.0.0.1:18083 的独立 server {};执行 nginx -t && nginx -s reload 后,用 http://127.0.0.1:18083 与 Host: genarrative-pingora-realpath-canary.local 做 loopback 验收。验证结束后已恢复 /etc/nginx/conf.d/genarrative.conf 备份并 reload Nginx;127.0.0.1:18083 已释放,正常公网入口仍由 Nginx 原配置承接,Pingora 继续只作为 127.0.0.1:18081 shadow service 运行。
本阶段验收只证明真实路径 Nginx -> Pingora handoff 可用,不切 80/443 到 Pingora。验收结果:
node /tmp/check-pingora-canary-live.mjs --realpath --base-url http://127.0.0.1:18083 --host genarrative-pingora-realpath-canary.local --json通过:healthz200、API config200、SpacetimeDB identity405、静态代表路径404、generated 拒绝路径404,全部带X-Genarrative-Nginx-Handoff: pingora-realpath-canary。- 通过真实路径 canary 访问
/v1/database/genarrative-prod/subscribe?compression=Brotli,WebSocket Upgrade 返回101 Switching Protocols,并带sec-websocket-protocol: v2.bsatn.spacetimedb、X-Genarrative-Nginx-Handoff: pingora-realpath-canary和X-Genarrative-Gateway: pingora-shadow。 node /tmp/check-pingora-canary-access-log-parity.mjs --realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log --pingora-log-file /var/log/genarrative/pingora-gateway.access.log ... --json对账6/6 matched,missingCount=0,mismatchCount=0;除 healthz 探针映射到__genarrative_pingora/healthz外,API config、SpacetimeDB identity、WSS subscribe、静态代表路径和 generated 拒绝路径的 Nginx path 与 Pingora path 完全一致。- 恢复后
grep genarrative-pingora-realpath-canary /etc/nginx/conf.d/genarrative.conf无匹配,ss -ltnp | grep :18083无监听,证明临时 realpath canary server 已移除;genarrative-pingora-gateway.service仍为active且NRestarts=0。
同日下一阶段在 current release /opt/genarrative/releases/dev-pingora-api-20260617230221 上重新启用 loopback realpath canary,并用正式 Host dev.genarrative.world 验收。第一次把独立 server 写成 /etc/nginx/conf.d/genarrative-pingora-realpath-canary.conf 时,因它早于 genarrative.conf 加载,nginx -t 报 unknown log format "genarrative_upstream";改为 /etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf 后 nginx -t 和 reload 均通过。随后:
node -- /opt/genarrative/current/scripts/check-pingora-canary-live.mjs --realpath --base-url http://127.0.0.1:18083 --host dev.genarrative.world --json通过:healthz200、API config200、SpacetimeDB identity405、静态代表路径404、generated 拒绝路径404,全部带X-Genarrative-Nginx-Handoff: pingora-realpath-canary。node -- /opt/genarrative/current/scripts/check-pingora-canary-access-log-parity.mjs --realpath ... --json对账12/12 matched,missingCount=0,mismatchCount=0。node -- /opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only --require-realpath-live ...通过 current release 自包含自审、realpath live smoke 和 realpath access log 对账,最终对账18/18 matched。- 本阶段没有启用前缀 canary,因此不能同时传
--require-live;否则正式 HTTP 入口会按 Nginx 策略返回301,并缺少X-Genarrative-Nginx-Handoff: pingora-canary。当前 public80/443仍由 Nginx 承接,realpath canary 只保留在127.0.0.1:18083用于后续 loopback 复核。 - 后续 realpath canary 启停统一走 current release 随包脚本:
/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-enable.sh --apply --probe-token <token> --host <域名> --base-url http://127.0.0.1:18083和/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-disable.sh --apply。启用脚本固定写入/etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf,并在nginx -t、reload 或 live smoke 失败时回滚;关闭脚本在nginx -t或 reload 失败时恢复删除前配置。
dev API release 正式路径验收记录
2026-06-17 已在 dev 机用正式 API release 路径部署包含 Pingora 的发布包 dev-pingora-api-20260617140915。发布包由分支 codex/pingoranginx 的 68bf3b698a885ee2cd248dcba81e4b57460000f8 构建,包含 api-server、api-server.sha256、pingora-gateway、pingora-gateway.sha256、release-manifest.json 和随包部署 / 自审脚本。部署命令从上传到 /tmp/dev-pingora-api-20260617140915 的发布包内执行 scripts/deploy/production-api-deploy.sh,目标 release root 为 /opt/genarrative/releases,current link 为 /opt/genarrative/current。
本阶段验收只证明 API release 的正式打包、复制、current 切换、服务重启和 shadow Pingora 随 current 重启可用,不切 80/443 到 Pingora。验收结果:
production-api-deploy.sh进入维护模式后完成api-server.sha256和pingora-gateway.sha256校验,复制二进制、checksum、manifest 和 Pingora 直连依赖到新 release,再把/opt/genarrative/current切到/opt/genarrative/releases/dev-pingora-api-20260617140915,最后退出维护模式。- 部署时
genarrative-pingora-gateway.service仍是 shadow 配置,脚本在 current 切换前拒绝 direct-entry capability 和公网监听 env,切换后执行systemctl restart genarrative-pingora-gateway.service并复核 active;genarrative-api.service、外部生成 worker 和 worker controller 也完成重启和 active 等待。 /opt/genarrative/current/release-manifest.api-server.json记录api-server和pingora-gateway两个 artifact,cd /opt/genarrative/current && sha256sum -c api-server.sha256 && sha256sum -c pingora-gateway.sha256均为OK。/opt/genarrative/current/scripts/ops/pingora-current-release-audit.mjs --release-root /opt/genarrative/current --require-pingora-gateway --systemd-show返回summary.status=OK,确认 current release 自包含、checksum 匹配、pingora-gateway可执行,且 systemdExecStart指向/opt/genarrative/current/pingora-gateway。/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only通过,证明目标机可以只依赖 current release 运行 runtime-only 发布自审,不需要源码 checkout 或 Jenkins 工作区。- 部署后
nginx、genarrative-api、genarrative-pingora-gateway、spacetimedb、genarrative-external-generation-controller.service和genarrative-external-generation-worker@1.service均为active,API / Pingora / worker 相关服务NRestarts=0。 GET http://127.0.0.1:8082/readyz返回{"ok":true,"ready":true};GET http://127.0.0.1:8082/api/creation-entry/config返回200。GET http://127.0.0.1:18081/api/creation-entry/config经 Pingora shadow 返回200;GET http://127.0.0.1:18081/v1/identity返回真实 SpacetimeDB 语义405。- 使用完整 WebSocket 握手头
Sec-WebSocket-Protocol: v2.bsatn.spacetimedb访问http://127.0.0.1:18081/v1/database/genarrative-prod/subscribe?compression=Brotli,HTTP code 为101;curl 的超时退出只发生在 WebSocket 升级后连接保持阶段。少传该子协议时 SpacetimeDB 会返回400 no valid protocol selected,不能作为网关失败证据。 - Nginx 仍监听公网
80/443,Pingora 仍只监听127.0.0.1:18081;127.0.0.1:18083无监听。https://dev.genarrative.world/与https://dev.genarrative.world/api/creation-entry/config通过本机--resolve验收均为200,证明正常公网入口仍由 Nginx 承接。 - 上传到
/tmp/dev-pingora-api-20260617140915的临时发布包已删除;新 release 目录约57M,清理后 dev 根盘约92%使用率。后续正式证据归档前仍需复核磁盘余量。
2026-06-18 已在 dev 机把 current release 提升到 dev-pingora-realpath-readiness-20260618151219,发布包由分支 codex/pingoranginx 的 e79cc5bd0909291431892f5dc7e3e68f839ae52a 构建,包含最新 check:pingora-realpath-canary-toggle 默认门禁。部署后:
/opt/genarrative/current/release-manifest.api-server.json记录source_commit=e79cc5bd0909291431892f5dc7e3e68f839ae52a,并登记api-server与pingora-gateway两个 artifact。grep 'Pingora realpath canary 启停烟测' /opt/genarrative/current/scripts/check-pingora-release-readiness.mjs可确认 realpath canary 启停烟测已进入随包聚合门禁。nginx、genarrative-api、genarrative-pingora-gateway、spacetimedb、genarrative-external-generation-worker@1.service和genarrative-external-generation-controller.service均为active;/healthz与/readyz返回正常。- 公网
80/443仍由 Nginx 监听,Pingora shadow 仍只监听127.0.0.1:18081,真实路径 canary 仍由 Nginx 监听127.0.0.1:18083,本阶段没有执行pingora-direct-enable.sh --apply。 /opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only --require-realpath-live ...通过 current release 自审、直连彩排状态、realpath live smoke 和 realpath access log 对账,最终对账47/47 matched。- 部署前 dev 根盘约
99%,先清理旧/tmp发布包,再删除不再作为当前回滚点的旧数字 release52、53、54、55、57,清理后df -h /约剩3.2G,避免证据包阶段被磁盘空间干扰。
同日进入正式直连前证据链彩排,但仍未执行公网直连切换:
- 使用 cutover run id
pingora-direct-dev-20260618T1520-realpath-readiness生成pre-cutover证据包:/var/log/genarrative/pingora-cutover-evidence/20260618T071853Z-pre-cutover。 node -- /opt/genarrative/current/scripts/ops/pingora-cutover-evidence-verify.mjs --bundle-dir /var/log/genarrative/pingora-cutover-evidence/20260618T071853Z-pre-cutover --require-summary-ok返回ok=true,checkedCount=4,failedCount=0;manifest summary 为OK,criticalCount=0,warningCount=0。node -- /opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --dry-run-cutover --require-direct ... --cutover-run-id pingora-direct-dev-20260618T1520-realpath-readiness ...生成 22 步 JSON runbook,覆盖 Host 一致性确认、current release 自审、pre-cutover / post-enable / post-rollback 三阶段证据包、enable / rollback 命令证据、health patrol env 切换、启用后 direct live 证据和最终证据根目录总审计;最终总审计包含两条 current release 脚本身份护栏:--require-command-executable enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh与--require-command-executable rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh。- 当前 dev 的正式
/etc/genarrative/pingora-gateway.env还没有进入 direct 模式:缺少GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN、GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN、GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE、GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE,GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO仍为http,且 systemd 最终配置未包含AmbientCapabilities=CAP_NET_BIND_SERVICE/CapabilityBoundingSet=CAP_NET_BIND_SERVICE。check-pingora-direct-preflight.mjs --require-live-env --systemd-cat --check-cert-readable --check-service-env-file --check-service-user-cert-readable --check-service-binary-executable --check-ports-free因上述缺口失败,符合正式直连前应阻断的预期。 - dev 已有证书副本
/etc/genarrative/pingora-tls/dev.genarrative.world/fullchain.pem与privkey.pem,权限为root:genarrative 0640,可用于后续 loopback 高端口 direct rehearsal 或正式切换前 env 准备;不要直接放宽/etc/letsencrypt/live/archive权限。
同日已完成不触碰公网 80/443 的高端口直连真实演练:
- 基于
/etc/genarrative/pingora-gateway.env生成临时 env/tmp/pingora-direct-highport-20260618T1535.env,把 shadow 监听改到127.0.0.1:18084,直连 HTTPS / HTTP redirect 分别设为127.0.0.1:18443/127.0.0.1:18080,证书指向/etc/genarrative/pingora-tls/dev.genarrative.world/,FORWARDED_PROTO=https,access log 独立写入/var/log/genarrative/pingora-direct-highport-20260618T1535.access.log。 node -- /opt/genarrative/current/scripts/check-pingora-direct-preflight.mjs --env-file /tmp/pingora-direct-highport-20260618T1535.env --require-live-env --check-cert-readable --check-service-user-cert-readable --check-ports-free --allow-loopback-only --json通过,确认两个高端口可绑定,当前用户和genarrative用户都可读 TLS cert/key。- 独立演练进程通过临时 systemd unit
genarrative-pingora-direct-highport-pingora-direct-highport-20260618T1535.service启动/opt/genarrative/current/pingora-gateway,只读取临时 env,不改正式genarrative-pingora-gateway.service、不安装 direct-entry drop-in、不重启 Nginx。 node -- /opt/genarrative/current/scripts/check-pingora-direct-live.mjs --https-base-url https://127.0.0.1:18443 --http-base-url http://127.0.0.1:18080 --host dev.genarrative.world --redirect-host dev.genarrative.world --redirect-base-url https://dev.genarrative.world --spacetime-database genarrative-prod --pingora-access-log /var/log/genarrative/pingora-direct-highport-20260618T1535.access.log --access-log-since-lines 4000 --require-wss-upgrade --insecure-tls --json返回OK:HTTPS 根路径200、API config200、SpacetimeDB identity405、HTTP redirect301到正式域名、ACME 静态404、HTTP/2 ALPNh2、WSS subscribe101,静态资源Cache-Control/ETag/Last-Modified/Accept-Ranges、HEAD、Range 206、ETag / Last-Modified304全部通过,access log 对账19/19 matched。- 演练结束后停止并清理临时 unit;因 Pingora 默认优雅退出窗口较长,临时 unit 曾短暂停在
stop-sigterm,确认高端口无监听后对该临时 unit 执行systemctl kill -s SIGKILL收尾。收尾后127.0.0.1:18080、127.0.0.1:18084、127.0.0.1:18443均无监听,正式公网80/443仍由 Nginx 监听,正式 Pingora shadow 仍为127.0.0.1:18081,realpath canary 仍为 Nginx127.0.0.1:18083。 - 复核
http://127.0.0.1:8082/healthz与/readyz正常,curl --resolve dev.genarrative.world:443:127.0.0.1 https://dev.genarrative.world/返回 NginxHTTP/2 200,http://127.0.0.1:18081/__genarrative_pingora/healthz带 probe token 返回{"ok":true,"gateway":"pingora-shadow","maintenance":false}。 - 经验:临时 env 含
GENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL=public, max-age=31536000, immutable这类带空格值,不能用 shellsource直接加载;演练进程应交给 systemdEnvironmentFile=或使用安全 env 解析器,否则 shell 会把max-age=31536000,当命令执行。
同日继续进入正式窗口前 final-prep,不执行公网切换:
node -- /opt/genarrative/current/scripts/ops/pingora-direct-rehearsal-status.mjs --release-root /opt/genarrative/current --expect-public-gateway nginx --require-pingora-shadow --require-realpath-canary --require-current-release-gateway --fail-on-critical返回summary.status=OK:公网80/443为 Nginx,Pingora shadow 为127.0.0.1:18081,realpath canary 为 Nginx127.0.0.1:18083,health patrol env 为nginx模式且 public base URL 为https://dev.genarrative.world。node -- /opt/genarrative/current/scripts/ops/pingora-current-release-audit.mjs --release-root /opt/genarrative/current --require-pingora-gateway --systemd-show返回summary.status=OK,确认 current release 自包含、api-server.sha256/pingora-gateway.sha256匹配、manifest 登记pingora-gateway,且genarrative-pingora-gateway.service的ExecStart指向/opt/genarrative/current/pingora-gateway。- 生成正式 direct env 候选
/tmp/pingora-direct-dev-20260618T1548-final-prep.pingora-gateway.env,与正式/etc/genarrative/pingora-gateway.env的差异仅为FORWARDED_PROTO=https,追加TLS_LISTEN=0.0.0.0:443、HTTP_REDIRECT_LISTEN=0.0.0.0:80、TLS_CERT_FILE=/etc/genarrative/pingora-tls/dev.genarrative.world/fullchain.pem、TLS_KEY_FILE=/etc/genarrative/pingora-tls/dev.genarrative.world/privkey.pem、HTTP_REDIRECT_TARGET_SCHEME=https。该候选 env 的check-pingora-direct-preflight.mjs --require-live-env --check-cert-readable --check-service-user-cert-readable通过,证明 direct env 内容和证书权限就绪。 - 生成正式 dry-run cutover runbook
/tmp/pingora-direct-dev-20260618T1548-final-prep.runbook.json,共 22 步,cutover run id 为pingora-direct-dev-20260618T1548-final-prep,覆盖 pre-cutover 证据、enable/rollback 命令证据、post-enable/post-rollback 证据和最终根目录总审计。后续 runbook 已追加回退前 Pingora env shadow 预置确认,因此步数会大于该早期 final-prep 版本。 - 注意:该 runbook 仍按正式切换窗口读取 active
/etc/genarrative/pingora-gateway.env。因此正式执行前必须在维护窗口内先把已审阅的 direct env 提升为/etc/genarrative/pingora-gateway.env,确认 Nginx 释放80/443,再进入 runbook 第 5 步 preflight;否则第 5 步会继续按当前 shadow env 失败。当前阶段未替换 active env、未安装 direct-entry drop-in、未释放 Nginx 端口、未执行pingora-direct-enable.sh --apply。
同日真实 dev 直连 80/443 切换已完成一次启用和回退验证:
- 切换前必须释放所有占用公网低端口的进程,不只是主站 Nginx;dev 上还需要临时释放 Gitea vhost,回退时再恢复。
- Pingora direct 接管
0.0.0.0:80/443后,HTTPS 根路径、API、静态资源HEAD/Range/304、HTTP/2 ALPN、WSS subscribe 和 Pingora access log 对账均通过,direct access log19/19 matched。 - 启用后 release readiness 不能再要求
80/443空闲;此时端口应由 Pingora 占用。 - 回退 smoke 不能使用固定
http://127.0.0.1/healthz和"ok":true。dev 实测应使用切换前真实 Nginx 首页,例如https://dev.genarrative.world/与<!doctype html>。 - 回退前必须把
/etc/genarrative/pingora-gateway.env从 direct 低端口配置恢复为 shadow 配置;回退后 dev 恢复为 Nginx 监听0.0.0.0:80/443、Pingora shadow127.0.0.1:18081、realpath canary127.0.0.1:18083,genarrative-api.service、spacetimedb.service、nginx.service和genarrative-pingora-gateway.service均 active。
同日继续推进正式 dev 切换时发现 dev.genarrative.world 和 git.genarrative.world 解析到同一公网 IP;旧 Pingora 只按 path 分流,若直接绑定 0.0.0.0:80/443 会把 git.genarrative.world 也落到主站静态 / API 路由。新版补齐 Host 分流后,dev 直连切换不应再“临时释放 Gitea vhost 后让 Gitea 不可用”,而应在 direct env 中配置:
GENARRATIVE_PINGORA_GATEWAY_GITEA_HOSTS=git.genarrative.worldGENARRATIVE_PINGORA_GATEWAY_GITEA_UPSTREAM=127.0.0.1:3000GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE/TLS_KEY_FILE指向同时覆盖dev.genarrative.world和git.genarrative.world的证书副本。
当前 dev 的单域名证书 /etc/letsencrypt/live/dev.genarrative.world/fullchain.pem 只覆盖 dev.genarrative.world,/etc/letsencrypt/live/git.genarrative.world/fullchain.pem 只覆盖 git.genarrative.world。由于当前 Pingora TLS listener 只加载一组 cert/key,正式直连接管两个 Host 前必须先准备一张覆盖两个域名的证书并同步到 /etc/genarrative/pingora-tls/<combined-name>/,或另行实现 SNI 多证书支持。
环境变量
| 变量 | 默认值 | 说明 |
|---|---|---|
GENARRATIVE_PINGORA_GATEWAY_LISTEN |
127.0.0.1:18081 |
Pingora 监听地址。 |
GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN |
空 | 可选 HTTPS 监听地址;启用时必须同时设置 TLS_CERT_FILE 和 TLS_KEY_FILE。 |
GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE |
空 | 可选 HTTPS 证书链文件;必须是网关运行用户可读取的文件。Certbot 证书建议先同步到 /etc/genarrative/pingora-tls/<域名>/fullchain.pem。 |
GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE |
空 | 可选 HTTPS 私钥文件;必须是网关运行用户可读取的文件。Certbot 私钥建议先同步到 /etc/genarrative/pingora-tls/<域名>/privkey.pem。 |
GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN |
空 | 可选 HTTP 重定向监听地址;启用时必须已配置 TLS 入口,ACME challenge 仍静态读取。 |
GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_TARGET_SCHEME |
https |
HTTP 重定向目标 scheme,当前只允许 https。 |
GENARRATIVE_PINGORA_GATEWAY_API_UPSTREAM |
127.0.0.1:8082 |
api-server 上游地址。 |
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_UPSTREAM |
127.0.0.1:3101 |
SpacetimeDB 上游地址。 |
GENARRATIVE_PINGORA_GATEWAY_GITEA_HOSTS |
空 | 可选 Gitea Host 白名单,逗号分隔;匹配时整站代理到 Gitea。 |
GENARRATIVE_PINGORA_GATEWAY_GITEA_UPSTREAM |
空 | 可选 Gitea 上游;配置 Gitea Host 时必须同时设置。 |
GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT |
/srv/genarrative/web |
前端静态文件根目录。 |
GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT |
/var/www/html |
ACME challenge 静态目录。 |
GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_FILE |
/var/lib/genarrative/maintenance/enabled |
存在即进入维护模式。 |
GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO |
http |
写入 X-Forwarded-Proto 的值。 |
GENARRATIVE_PINGORA_GATEWAY_MAX_API_BODY_BYTES |
67108864 |
/api 通用路由的 Content-Length 上限。 |
GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS |
gzip |
当前唯一允许的压缩算法白名单;Pingora 正式化口径固定为 gzip-only,Brotli 继续由 Nginx / 前置代理承担。 |
GENARRATIVE_PINGORA_GATEWAY_GZIP_ENABLED |
true |
是否启用 gzip 响应压缩。 |
GENARRATIVE_PINGORA_GATEWAY_GZIP_LEVEL |
5 |
gzip 压缩等级,必须在 0..=9。 |
GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES |
1024 |
gzip 最小响应长度,默认对齐 Nginx gzip_min_length 1024,必须大于 0。 |
GENARRATIVE_PINGORA_GATEWAY_HTML_CACHE_CONTROL |
no-cache |
HTML、目录 index 和 SPA fallback 的缓存头,避免入口 HTML 被长期缓存。 |
GENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL |
public, max-age=31536000, immutable |
/assets/* 与 /admin/assets/* 中带 Vite 指纹文件名的静态资源缓存头。 |
GENARRATIVE_PINGORA_GATEWAY_STATIC_CACHE_CONTROL |
no-cache |
非指纹静态资源和 ACME challenge 的默认缓存头。 |
GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_CONNECT_TIMEOUT_MS |
3000 |
连接上游的超时,必须大于 0。 |
GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_DEFAULT_READ_TIMEOUT_SECONDS |
60 |
没有 Nginx 显式长超时的代理路由读取超时,必须大于 0。 |
GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_API_READ_TIMEOUT_SECONDS |
3600 |
通用 /api 路由读取超时,对齐当前 Nginx proxy_read_timeout 3600s。 |
GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_LONG_READ_TIMEOUT_SECONDS |
3600 |
公开列表 / 详情和 SpacetimeDB subscribe 长连接读取超时。 |
GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_WRITE_TIMEOUT_SECONDS |
3600 |
写上游请求头 / 请求体超时,对齐当前 Nginx proxy_send_timeout 3600s 口径。 |
GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR |
false |
是否用 X-Forwarded-For 首个 IP 作为接流保护 client key;公网直连 Pingora 时必须保持 false,direct preflight 会阻断公网监听误开启。 |
GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED |
false |
开启 TRUST_X_FORWARDED_FOR 时必须显式设为 true,表示前置代理会清洗 X-Forwarded-For。 |
GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED |
true |
是否启用单进程接流保护。 |
GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT |
1 |
当前接流保护覆盖的 Pingora 实例数;必须是正整数。开启网关保护且大于 1 时必须确认共享保护层。 |
GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED |
false |
多实例仍启用网关保护时必须显式设为 true,表示已落地共享限流 / 共享并发保护层。 |
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_MAX_CONCURRENT |
64 |
/admin/api/* 每 client 并发上限;0 表示不限制并发。 |
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_RATE_PER_SECOND |
30 |
/admin/api/* 每 client token bucket 回填速率;0 表示不限制 RPS。 |
GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_BURST |
16 |
/admin/api/* 每 client 额外 burst。 |
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_MAX_CONCURRENT |
320 |
公开列表路由每 client 并发上限。 |
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_RATE_PER_SECOND |
5000 |
公开列表路由每 client RPS。 |
GENARRATIVE_PINGORA_GATEWAY_GALLERY_LIST_BURST |
4096 |
公开列表路由每 client burst。 |
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_MAX_CONCURRENT |
32 |
公开详情兼容路由每 client 并发上限。 |
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_RATE_PER_SECOND |
300 |
公开详情兼容路由每 client RPS。 |
GENARRATIVE_PINGORA_GATEWAY_GALLERY_DETAIL_BURST |
32 |
公开详情兼容路由每 client burst。 |
GENARRATIVE_PINGORA_GATEWAY_API_MAX_CONCURRENT |
64 |
通用 /api 路由每 client 并发上限。 |
GENARRATIVE_PINGORA_GATEWAY_API_RATE_PER_SECOND |
300 |
通用 /api 路由每 client RPS。 |
GENARRATIVE_PINGORA_GATEWAY_API_BURST |
64 |
通用 /api 路由每 client burst。 |
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_MAX_CONCURRENT |
256 |
SpacetimeDB 公开最小路由每 client 并发上限。 |
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_RATE_PER_SECOND |
1000 |
SpacetimeDB 公开最小路由每 client RPS。 |
GENARRATIVE_PINGORA_GATEWAY_SPACETIME_BURST |
256 |
SpacetimeDB 公开最小路由每 client burst。 |
GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN |
空 | 内部 shadow 探针 token;为空时探针端点关闭。 |
GENARRATIVE_PINGORA_GATEWAY_LOG |
info,pingora=info,pingora_gateway=info |
tracing 过滤器。 |
GENARRATIVE_PINGORA_GATEWAY_ACCESS_LOG_FILE |
空 | 可选 access log 文件路径;生产 shadow 示例使用 /var/log/genarrative/pingora-gateway.access.log。 |
GENARRATIVE_PINGORA_GATEWAY_OTEL_ENABLED |
false |
是否启用共享 OpenTelemetry 初始化。 |
当前路由口径
| 路由 | 行为 |
|---|---|
/.well-known/acme-challenge/* |
从 GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT 精确读取静态文件,默认 Cache-Control: no-cache,并带 ETag / Last-Modified / Accept-Ranges: bytes。 |
/admin |
301 到 /admin/。 |
/admin/api/* |
转发到 api-server。 |
/admin/assets/* |
从 Web 根目录精确读取静态文件;带 Vite 指纹的文件默认长期缓存,其它文件默认 no-cache,并支持条件请求返回 304 与单段 Range: bytes= 返回 206 / 越界返回 416。 |
/admin/* |
先读取静态文件或目录 index,失败回退 /admin/index.html,HTML 默认 no-cache,并支持条件请求返回 304 与单段 Range: bytes= 返回 206 / 越界返回 416。 |
/assets/* |
从 Web 根目录精确读取静态文件;带 Vite 指纹的文件默认长期缓存,其它文件默认 no-cache,并支持条件请求返回 304 与单段 Range: bytes= 返回 206 / 越界返回 416。 |
/api/runtime/puzzle/gallery、/api/runtime/custom-world-gallery |
转发到 api-server。 |
/api/runtime/puzzle/gallery/{id}、/api/runtime/custom-world-gallery/{profile}/{owner} |
转发到 api-server。 |
/api、/api/* |
转发到 api-server,按配置执行 Content-Length 与流式 body 累计上限检查。 |
/v1/database/{db}/subscribe、/v1/identity* |
转发到 SpacetimeDB,保留 WebSocket Upgrade 头。 |
/__genarrative_pingora/healthz |
仅在携带 X-Genarrative-Pingora-Probe 且匹配配置 token 时返回 shadow JSON,否则 404。 |
/v1/*、/generated-*、/healthz*、/readyz* |
返回 404,保持生产公网不暴露口径。 |
| 主站 SPA allowlist | 只对当前前端完整路由及兼容恢复路径 /creation/rpg/agent 失败回退 /index.html;匹配大小写不敏感并允许一个尾部斜杠,HTML 默认 no-cache。 |
| 其它 Web 路径 | 只读取真实静态文件或目录 index,缺失时返回真实 404;/creation/not-exist、/runtime/not-exist、/puzzle/not-exist 不进入 SPA fallback。 |
维护模式下,API-like 路由返回 JSON 503,Web 静态路由优先返回 maintenance.html,不存在时返回纯文本 503。
代理失败时,API / SpacetimeDB 等代理路由返回统一 JSON 网关错误;本地静态路由仍保持对应 HTTP 错误状态。
静态 Range 只支持单段 bytes range;多段 range 暂按完整文件返回,避免在正式替换前引入 multipart 响应面。If-None-Match / If-Modified-Since 优先于 Range 判定,命中时仍返回 304;If-Range 日期匹配时继续返回 206,日期旧于文件或弱 ETag 校验器时回完整 200;206 / 304 / 416 不做 gzip 压缩,避免 Content-Range 语义被响应体改写破坏。Gateway smoke 会用固定 X-Request-Id 对账静态 304、405、206、416 的 Pingora access log 行,确认本地响应状态也进入正式切换证据链。
静态路由只允许 GET / HEAD 读取;其它方法在确认命中静态候选后返回 405 并写入 Allow: GET, HEAD,缺失文件仍返回 404,避免直连后错误客户端把静态入口当作可写接口。
后续替换前验收
- 每次网关行为变更后运行
npm run check:pingora-gateway-smoke,确认 mock 上游下的静态、gzip、API 代理头、维护模式、429 保护、上游断连错误和 WebSocket 行为仍通过。 - 涉及 Nginx 模板、Pingora 路由、限流分组或路由文档时,同步更新
deploy/pingora/nginx-route-parity.matrix.json,并运行npm run check:pingora-route-parity与cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml matches_nginx_route_parity_matrix。 - 容器内使用同一份 Web 产物、同一组真实上游地址跑 Pingora smoke,并继续对照
deploy/nginx/genarrative.conf扩展真实上游路由 parity 自动测试。 - 使用
deploy/nginx/snippets/genarrative-pingora-canary.conf做 Nginx 前缀 canary;启用前先跑npm run check:nginx-pingora-canary和npm run check:pingora-canary-docker,有 Nginx 或 Docker 的目标环境分别强制跑node scripts/check-nginx-pingora-canary.mjs --require-nginx与node scripts/check-pingora-canary-docker.mjs --require-docker --pull,其中 Docker handoff 会自动对账临时 Nginx 与 Pingora access log。启用后跑npm run check:pingora-canary-live,再用 current release 随包 access log parity 脚本对账目标机 Nginx 与 Pingora access log。canary live 的 base URL、prefix、Host、额外 path 和 timeout 不能包含换行或 NUL;canary live timeout 和 access logsince-lines必须是正整数,非法值直接失败。 - 前缀 canary 通过后,再使用 current release 随包
/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-enable.sh --apply --probe-token <token> --host <域名> --base-url http://127.0.0.1:18083启用真实路径 canary;它会把deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf渲染成独立本机server,写入/etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf,不能 include 到生产443server 内。该片段使用access_log ... genarrative_upstream,文件名必须保证晚于定义log_format genarrative_upstream的主站配置加载;否则nginx -t会报unknown log format "genarrative_upstream"。启用脚本会先执行nginx -t、reload Nginx,再默认运行 realpath live smoke,任一阶段失败都会恢复写入前配置。关闭时执行/opt/genarrative/current/scripts/deploy/pingora-realpath-canary-disable.sh --apply,脚本会在nginx -t或 reload 失败时恢复删除前配置。启用后跑node -- /opt/genarrative/current/scripts/check-pingora-canary-live.mjs --realpath --base-url http://127.0.0.1:18083 --host <域名>,再用 current release 随包 access log parity 脚本传--realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log对账/api/creation-entry/config、/v1/identity和/assets/app.js等真实路径。 - 目标机 canary include 后必须跑正式切换聚合门禁,并按现场已启用的 canary 入口选择参数:前缀 canary 已启用时,源码 checkout / CI / 构建环境执行
node scripts/check-pingora-release-readiness.mjs --require-docker --pull-docker --require-nginx --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log,目标机 current release 执行/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log;真实路径 canary 已启用时,追加或单独使用--require-realpath-live --realpath-live-base-url http://127.0.0.1:18083 --realpath-live-host <域名> --realpath-live-nginx-access-log /var/log/nginx/genarrative-pingora-realpath-canary.access.log --realpath-live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log。如果现场只启用了真实路径 canary,不要同时传--require-live;缺少 Host 会直接失败,避免 live canary 误测默认 vhost。live smoke 后还会按request_id对账 Nginx 与 Pingora access log,缺少同一请求的 Pingora 日志、状态码、方法或 path 漂移都会失败。 - 如需评估 Pingora 直连公网入口,必须显式配置
TLS_LISTEN、证书、私钥和HTTP_REDIRECT_LISTEN;Certbot 证书先用随包scripts/deploy/pingora-tls-cert-sync.mjs同步到/etc/genarrative/pingora-tls/<域名>/,不要直接 chmod Let’s Encrypt live/archive 原路径;同一 IP 上还有 Gitea 域名时,还必须配置GITEA_HOSTS/GITEA_UPSTREAM并确认 TLS 证书覆盖所有由 Pingora 直连接管的 Host。绑定80/443时还必须人工启用genarrative-pingora-gateway-direct-entry.confdrop-in 授予CAP_NET_BIND_SERVICE。随后用npm run check:pingora-gateway-smoke覆盖 TLS / HTTP/2 ALPN / redirect / WSS subscribe / Gitea Host 分流;目标机必须先跑npm run check:pingora-direct-preflight -- --env-file /etc/genarrative/pingora-gateway.env --require-live-env --systemd-cat --check-cert-readable --check-service-env-file --check-service-user-cert-readable --check-service-binary-executable --check-ports-free,再跑npm run check:pingora-direct-live或 release readiness 的--require-direct,且--require-direct必须带 direct HTTPS base URL、direct HTTP base URL、正式域名 Host/SNI、redirect Location host、Pingora access log 文件、health patrol env 文件、direct preflight env 文件、systemd drop-in 生效检查、service EnvironmentFile 一致性检查、当前用户和 systemd 服务用户证书可读检查、service 二进制可执行检查、显式 SpacetimeDB 数据库名,并会拒绝--skip-wss。高端口 rehearsal 使用https://127.0.0.1:<高端口>打入但期望 HTTP redirect Location 指向正式域名默认 HTTPS 入口时,额外传--direct-redirect-base-url https://<域名>;--direct-redirect-host仍必须保留,用于 runbook Host 一致性约束。direct live 会用生成的request_id反查 Pingora access log;缺少对应日志、method 漂移、path 漂移或 status 漂移都算直连门禁失败。direct preflight 会拒绝开启网关保护但未确认共享保护层的GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1配置;--env-file、--systemd-service、服务用户和 env 中的 listen / cert / key 值都不能包含换行或 NUL,执行systemctl cat或sudo -u <serviceUser> test -r <file>前还会复核子命令参数,避免污染参数进入目标机预检命令;direct live 的 URL、Host、redirect base URL、probe token、额外 path、数据库名、access log 路径、timeout 和布尔 env 也不能包含换行或 NUL,且会在发起请求前失败;direct live timeout 必须是正整数,直连相关布尔 env 只接受true/false、1/0、yes/no、on/off或空值,非法值直接失败。证书申请与续期仍由 Certbot / 外部自动化承担,网关只读取现有文件。 - 正式直连 runbook 的启用前基础门禁和启用后
--require-direct复核必须调用/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only。该脚本、scripts/check-pingora-canary-live.mjs、scripts/ops/pingora-direct-rehearsal-status.mjs、realpath canary 启停脚本和deploy/nginx/必须进入生产 API release、Jenkins API Build 归档、Jenkins API Deploy 复制清单和目标机 current release;缺失时部署应 fail-fast,切换窗口不能依赖源码 checkout 或 Jenkins workspace。 - direct preflight / direct live timeout 必须是正整数;直连相关布尔 env 和 Pingora gateway env 里的
TRUST_X_FORWARDED_FOR/TRUSTED_FRONT_PROXY_CONFIRMED只接受true/false、1/0、yes/no、on/off或空值,非法值直接失败。 - 最终证据根目录总审计必须带
--require-phase-pingora-env-shadow post-rollback;post-enable证据包必须通过--expected-pingora-env-mode direct证明 active Pingora env 是直连姿态,post-rollback证据包必须通过--expected-pingora-env-mode shadow证明 active Pingora env 已恢复 shadow 姿态。manifest.summary.pingoraEnvShadow必须证明listen=127.0.0.1:18081,mode=shadow,shadowReady=true,且tlsListen/httpRedirectListen/tlsCertFile/tlsKeyFile均为空,避免回退后仍残留 direct 低端口或证书路径 env。 - Pingora 正式化口径固定为 gzip-only;Brotli 继续由 Nginx / 前置代理承担,直连 Pingora 不以 Brotli parity 作为切换门禁。如需多实例,开启网关接流保护时必须先引入共享限流 / 共享并发保护层并设置
GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED=true,否则保持GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT=1;关闭网关保护的多实例方案必须明确由前置 Nginx / LB 承担全局限流。 - 前缀 canary 稳定后,再评估是否做真实路径 canary;真实路径 canary 稳定后,再评估是否让 Pingora 直接承接公网入口。