diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 4d146e1f..3393cfe0 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -95,12 +95,12 @@ - 验证方式:Jenkins 构建机可完成工具包准备,release 部署 agent 只消费工作区文件;目标机不再依赖 GitHub 外网下载。 - 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 -## 2026-05-19 otelcol-contrib 改为 Jenkins 手动上传归档再解包 +## 2026-05-19 provision 工具包由 Jenkins 构建机本地准备后上传 -- 背景:Genarrative-Server-Provision 中 `otelcol-contrib` 的构建机下载步骤耗时较长,且本机已经提前准备好安装包。 -- 决策:`jenkins/Jenkinsfile.production-server-provision` 新增 `OTELCOL_CONTRIB_ARCHIVE` 手动上传参数,默认要求上传 `otelcol-contrib_0.151.0_linux_amd64.tar.gz`;`scripts/prepare-server-provision-tools.sh` 优先从上传归档解包生成 `provision-tools/otelcol-contrib`,不再默认联网下载 OpenTelemetry release 包。 +- 背景:目标 release 机器不应自己下载 SpacetimeDB 或 `otelcol-contrib`,但 Jenkins 构建机可以先准备二进制工具包,再把结果带到目标部署 agent。 +- 决策:`jenkins/Jenkinsfile.production-server-provision` 不要求人工上传安装包;`Prepare Provision Tools` 阶段在 `linux && genarrative-build` 构建机本地执行 `scripts/prepare-server-provision-tools.sh`,下载 SpacetimeDB 官方安装器和 OpenTelemetry release 包并生成 `provision-tools/`,随后通过 `stash/unstash` 上传到 release 部署 agent。若构建机下载较慢,可通过 `PROVISION_DOWNLOAD_PROXY` 显式指定该构建机可访问的代理地址。 - 影响范围:`jenkins/Jenkinsfile.production-server-provision`、`scripts/prepare-server-provision-tools.sh`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 -- 验证方式:Jenkins 日志应显示使用手动上传的 otelcol 包,`MANIFEST.txt` 记录 source 为 manual archive;当 `ENABLE_OTELCOL=false` 时可以跳过 collector 工具包准备。 +- 验证方式:Jenkins 日志应显示目标部署 agent 只消费 `server-provision-tools` stash;目标机不直接访问 `install.spacetimedb.com` 或 OpenTelemetry GitHub release 下载地址。 - 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 ## 2026-05-19 公开 gallery 入口发布限流以快拒绝保护后端 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 8bc5ec79..e18049d0 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -160,7 +160,8 @@ Windows Stdb module 构建流水线运行在 Jenkins `windows` 节点上。该 - `api-server` 生产模板默认 `GENARRATIVE_API_LISTEN_BACKLOG=1024`、`GENARRATIVE_API_WORKER_THREADS=4`;本地未设置 worker threads 时继续使用 Tokio 默认值。 - `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512` 开启应用内 HTTP 并发背压;`GENARRATIVE_API_GALLERY_MAX_CONCURRENT_REQUESTS=320`、`GENARRATIVE_API_DETAIL_MAX_CONCURRENT_REQUESTS=64`、`GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS=16` 分别限制公开列表、公开详情和后台 API 热路径。超过许可时直接返回 `429 Too Many Requests` 和 `Retry-After: 1`,`/healthz` 不受该限制。这些值不是 RPS 限速;如果压测中 429 上升但内存和 p95 收敛,说明背压正在保护进程。直连 `api-server` 的极高 RPS 压测若出现 `connection refused`,通常已经打到 TCP 监听 / accept 层,应同时检查 backlog、Nginx upstream keepalive 和前置限流。 - `genarrative-api.service` 设置 `LimitNOFILE=65535`、`TasksMax=2048`;上线后用 `systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax` 和 `cat /proc/$(pidof api-server)/limits` 核对。 -- Server provision 不在目标机下载 SpacetimeDB 或 `otelcol-contrib`。Jenkins 的 `Prepare Provision Tools` 阶段在 `linux && genarrative-build` 构建机执行 `scripts/prepare-server-provision-tools.sh`:SpacetimeDB 仍通过官方安装入口 `https://install.spacetimedb.com` 准备;`otelcol-contrib` 默认要求在 Jenkins 参数 `OTELCOL_CONTRIB_ARCHIVE` 手动上传 `otelcol-contrib_0.151.0_linux_amd64.tar.gz`,再从上传包解出 `provision-tools/otelcol-contrib`。最终工具包通过 `stash/unstash` 上传到 release 部署 agent。目标机上的 `scripts/jenkins-server-provision.sh` 只从该工作区工具包安装 `/stdb/spacetime`、`/stdb/bin/current/*` 和 `/usr/local/bin/otelcol-contrib`。注意 `scripts/jenkins-checkout-source.sh` 会执行 `git reset --hard` / `git clean`,因此被直接执行的新增脚本必须以 Git `100755` 模式提交,或在二次 checkout 之后再补 `chmod +x`。 +- Server provision 不在目标机下载 SpacetimeDB 或 `otelcol-contrib`。Jenkins 的 `Prepare Provision Tools` 阶段在 `linux && genarrative-build` 构建机执行 `scripts/prepare-server-provision-tools.sh`,先在该构建机本地通过官方 SpacetimeDB 安装入口 `https://install.spacetimedb.com` 和 OpenTelemetry release 包生成 `provision-tools/`,再通过 `stash/unstash` 上传到 release 部署 agent。目标机上的 `scripts/jenkins-server-provision.sh` 只从该工作区工具包安装 `/stdb/spacetime`、`/stdb/bin/current/*` 和 `/usr/local/bin/otelcol-contrib`。注意 `scripts/jenkins-checkout-source.sh` 会执行 `git reset --hard` / `git clean`,因此被直接执行的新增脚本必须以 Git `100755` 模式提交,或在二次 checkout 之后再补 `chmod +x`。 +- 构建机下载慢时,在 `Genarrative-Server-Provision` 参数 `PROVISION_DOWNLOAD_PROXY` 填写构建机可访问的 HTTP 代理,例如 `http://:7890`。不要填写目标 release 机器视角的 `127.0.0.1`,除非代理确实运行在该构建机本机。 - `otelcol-contrib.service` 作为可选系统服务加入 provision,默认监听 `127.0.0.1:4317/4318` 并使用 `deploy/otelcol/genarrative-debug.yaml`。api-server 是否发送 OTLP 仍由 `GENARRATIVE_OTEL_ENABLED` 控制,服务 unit 见 `deploy/systemd/otelcol-contrib.service`。 - Nginx `/api/` 与 `/admin/api/` 通过 `genarrative_api` upstream 代理到 `127.0.0.1:8082`,upstream keepalive 为 64;`limit_conn` 负责连接 / 并发保护,`limit_req` 负责入口 RPS 快拒绝。当前模板把公开 gallery list 单独放到 `genarrative_gallery_rps`,默认 `rate=5000r/s`、`burst=4096`、`limit_conn=320`;公开详情和普通 API 放到 `genarrative_api_rps`,后台 API 放到 `genarrative_admin_rps`。压测时看 `/var/log/nginx/genarrative.access.log` 中的 `request_time`、`upstream_connect_time`、`upstream_header_time`、`upstream_response_time`、`upstream_status`、`request_id`。 - 作品列表 K6 脚本一次 iteration 默认请求两个公开接口,因此约 50 HTTP req/s 的目标命令使用 `SCENARIO=spike START_RPS=5 PEAK_RPS=25 HOLD=60s END_RPS=5 DETAIL_RATIO=0 npm run loadtest:k6:works`。 diff --git a/jenkins/Jenkinsfile.production-server-provision b/jenkins/Jenkinsfile.production-server-provision index 98757f54..b78e2791 100644 --- a/jenkins/Jenkinsfile.production-server-provision +++ b/jenkins/Jenkinsfile.production-server-provision @@ -23,6 +23,7 @@ pipeline { string(name: 'SERVER_NAME', defaultValue: 'genarrative.example.com', description: '证书主域名;也作为 Nginx server_name 的第一个域名') string(name: 'SERVER_ALIASES', defaultValue: '', description: '可选,额外 Nginx server_name,多个用空格或逗号分隔,例如 www.genarrative.world') string(name: 'PROVISION_TOOLS_DIR', defaultValue: 'provision-tools', description: '构建机准备并上传到目标机工作区的工具包目录') + string(name: 'PROVISION_DOWNLOAD_PROXY', defaultValue: '', description: '可选,构建机下载 SpacetimeDB 和 otelcol-contrib 时使用的代理地址,例如 http://proxy-host:7890;留空不设置代理') string(name: 'SPACETIME_DOWNLOAD_ROOT', defaultValue: 'https://github.com/clockworklabs/SpacetimeDB/releases/latest/download', description: '构建机下载 SpacetimeDB 官方安装产物的根地址;目标机不访问该地址') string(name: 'SPACETIME_ROOT', defaultValue: '/stdb', description: 'SpacetimeDB root-dir') string(name: 'RELEASE_ROOT', defaultValue: '/opt/genarrative/releases', description: 'release 根目录') @@ -34,7 +35,6 @@ pipeline { booleanParam(name: 'ENABLE_SERVICES', defaultValue: true, description: '启用并启动 spacetimedb 与 api-server systemd 服务') booleanParam(name: 'ENABLE_OTELCOL', defaultValue: true, description: '安装并启用本机 OpenTelemetry Collector;api-server 模板默认开启 OTLP,如需关闭请在 API_ENV_FILE 中将 GENARRATIVE_OTEL_ENABLED 改为 false') string(name: 'OTELCOL_VERSION', defaultValue: '0.151.0', description: 'otelcol-contrib 版本') - stashedFile 'OTELCOL_CONTRIB_ARCHIVE' } stages { @@ -70,22 +70,13 @@ pipeline { if (!(params.PROVISION_TOOLS_DIR.trim() ==~ /^[0-9A-Za-z._\/-]+$/) || params.PROVISION_TOOLS_DIR.startsWith('/') || params.PROVISION_TOOLS_DIR.contains('..')) { error("PROVISION_TOOLS_DIR 只能是工作区内的相对目录,不能包含绝对路径或连续点号: ${params.PROVISION_TOOLS_DIR}") } + def provisionDownloadProxy = params.PROVISION_DOWNLOAD_PROXY?.trim() + if (provisionDownloadProxy && !(provisionDownloadProxy ==~ /^https?:\/\/\S+$/)) { + error("PROVISION_DOWNLOAD_PROXY 只能填写 http:// 或 https:// 开头的代理地址,当前值: ${params.PROVISION_DOWNLOAD_PROXY}") + } if (!(params.OTELCOL_VERSION?.trim() ==~ /^[0-9]+\.[0-9]+\.[0-9]+$/)) { error("OTELCOL_VERSION 格式应为 x.y.z: ${params.OTELCOL_VERSION}") } - def otelcolArchiveFilename = env.OTELCOL_CONTRIB_ARCHIVE_FILENAME?.trim() - def expectedOtelcolArchiveFilename = "otelcol-contrib_${params.OTELCOL_VERSION.trim()}_linux_amd64.tar.gz" - if (params.ENABLE_OTELCOL) { - if (!otelcolArchiveFilename) { - error("ENABLE_OTELCOL=true 时必须在 OTELCOL_CONTRIB_ARCHIVE 上传 ${expectedOtelcolArchiveFilename}。") - } - if (otelcolArchiveFilename != expectedOtelcolArchiveFilename) { - error("OTELCOL_CONTRIB_ARCHIVE 文件名必须是 ${expectedOtelcolArchiveFilename},当前上传: ${otelcolArchiveFilename}") - } - } - if (!params.ENABLE_OTELCOL && otelcolArchiveFilename) { - echo "ENABLE_OTELCOL=false,已上传的 OTELCOL_CONTRIB_ARCHIVE 将不会被安装。" - } if (!params.SPACETIME_DOWNLOAD_ROOT?.trim()) { error('SPACETIME_DOWNLOAD_ROOT 不能为空。') } @@ -145,18 +136,6 @@ pipeline { chmod +x scripts/prepare-server-provision-tools.sh BASH ''' - script { - if (params.ENABLE_OTELCOL) { - echo "准备使用手动上传的 otelcol-contrib 包: ${env.OTELCOL_CONTRIB_ARCHIVE_FILENAME}" - sh 'bash -lc "rm -rf manual-provision-tool-upload && mkdir -p manual-provision-tool-upload"' - dir('manual-provision-tool-upload') { - unstash 'OTELCOL_CONTRIB_ARCHIVE' - } - env.OTELCOL_ARCHIVE_SOURCE = 'manual-provision-tool-upload/OTELCOL_CONTRIB_ARCHIVE' - } else { - env.OTELCOL_ARCHIVE_SOURCE = '' - } - } sh ''' bash <<'BASH' set -euo pipefail @@ -164,7 +143,7 @@ BASH PROVISION_TOOLS_DIR="${PROVISION_TOOLS_DIR:-provision-tools}" \ OTELCOL_VERSION="${OTELCOL_VERSION:-0.151.0}" \ PREPARE_OTELCOL="${ENABLE_OTELCOL:-true}" \ - OTELCOL_ARCHIVE_SOURCE="${OTELCOL_ARCHIVE_SOURCE:-}" \ + PROVISION_DOWNLOAD_PROXY="${PROVISION_DOWNLOAD_PROXY:-}" \ SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT:-https://github.com/clockworklabs/SpacetimeDB/releases/latest/download}" \ scripts/prepare-server-provision-tools.sh BASH diff --git a/scripts/prepare-server-provision-tools.sh b/scripts/prepare-server-provision-tools.sh index 2e73a5f4..5a19434a 100755 --- a/scripts/prepare-server-provision-tools.sh +++ b/scripts/prepare-server-provision-tools.sh @@ -4,10 +4,11 @@ set -euo pipefail PROVISION_TOOLS_DIR="${PROVISION_TOOLS_DIR:-provision-tools}" OTELCOL_VERSION="${OTELCOL_VERSION:-0.151.0}" PREPARE_OTELCOL="${PREPARE_OTELCOL:-${ENABLE_OTELCOL:-true}}" -OTELCOL_ARCHIVE_SOURCE="${OTELCOL_ARCHIVE_SOURCE:-}" OTELCOL_DOWNLOAD_ROOT="${OTELCOL_DOWNLOAD_ROOT:-https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download}" SPACETIME_INSTALLER_URL="${SPACETIME_INSTALLER_URL:-https://install.spacetimedb.com}" SPACETIME_DOWNLOAD_ROOT="${SPACETIME_DOWNLOAD_ROOT:-https://github.com/clockworklabs/SpacetimeDB/releases/latest/download}" +PROVISION_DOWNLOAD_PROXY="${PROVISION_DOWNLOAD_PROXY:-}" +PROVISION_NO_PROXY="${PROVISION_NO_PROXY:-127.0.0.1,localhost}" PROVISION_TOOLS_TMP_PARENT="${PROVISION_TOOLS_TMP_PARENT:-${WORKSPACE:-$(pwd)}/.tmp/server-provision-tools}" TMP_DIR_TO_CLEAN="" OTELCOL_SOURCE_DESCRIPTION="skipped" @@ -26,6 +27,22 @@ require_cmd() { fi } +configure_download_proxy() { + if [[ -z "${PROVISION_DOWNLOAD_PROXY}" ]]; then + return + fi + + export HTTP_PROXY="${PROVISION_DOWNLOAD_PROXY}" + export HTTPS_PROXY="${PROVISION_DOWNLOAD_PROXY}" + export ALL_PROXY="${PROVISION_DOWNLOAD_PROXY}" + export http_proxy="${PROVISION_DOWNLOAD_PROXY}" + export https_proxy="${PROVISION_DOWNLOAD_PROXY}" + export all_proxy="${PROVISION_DOWNLOAD_PROXY}" + export NO_PROXY="${PROVISION_NO_PROXY}" + export no_proxy="${PROVISION_NO_PROXY}" + echo "[prepare-provision-tools] 已配置下载代理: ${PROVISION_DOWNLOAD_PROXY%%://*}://***" +} + download_file() { local url="$1" local output="$2" @@ -57,25 +74,14 @@ prepare_otelcol() { local archive="${tmp_dir}/otelcol-contrib.tar.gz" local extract_dir="${tmp_dir}/otelcol-contrib" local url="${OTELCOL_DOWNLOAD_ROOT}/v${OTELCOL_VERSION}/otelcol-contrib_${OTELCOL_VERSION}_linux_amd64.tar.gz" - local source_archive="${OTELCOL_ARCHIVE_SOURCE}" local target="${PROVISION_TOOLS_DIR}/otelcol-contrib" require_cmd tar + echo "[prepare-provision-tools] 下载 otelcol-contrib: ${url}" mkdir -p "${extract_dir}" - if [[ -n "${source_archive}" ]]; then - if [[ ! -f "${source_archive}" ]]; then - echo "[prepare-provision-tools] 上传的 otelcol-contrib 包不存在: ${source_archive}" >&2 - exit 1 - fi - echo "[prepare-provision-tools] 使用手动上传的 otelcol-contrib 包: ${source_archive}" - cp "${source_archive}" "${archive}" - OTELCOL_SOURCE_DESCRIPTION="manual archive ${source_archive}" - else - echo "[prepare-provision-tools] 下载 otelcol-contrib: ${url}" - download_file "${url}" "${archive}" - OTELCOL_SOURCE_DESCRIPTION="download ${url}" - fi + download_file "${url}" "${archive}" + OTELCOL_SOURCE_DESCRIPTION="download ${url}" tar -xzf "${archive}" -C "${extract_dir}" if [[ ! -x "${extract_dir}/otelcol-contrib" ]]; then @@ -123,6 +129,8 @@ main() { require_cmd mktemp require_cmd rm + configure_download_proxy + mkdir -p "${PROVISION_TOOLS_TMP_PARENT}" tmp_dir="$(mktemp -d "${PROVISION_TOOLS_TMP_PARENT%/}/run.XXXXXX")" TMP_DIR_TO_CLEAN="${tmp_dir}"