k-skill 生产部署实战:k-skill-proxy 在 gpu01 上的 systemd、自动发布与服务看门狗全解析
【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill
本文基于仓库文档 docs/deploy-k-skill-proxy.md 与 部署脚本、systemd 单元文件、代理服务端实现 展开,讲解 k-skill-proxy 在生产机 gpu01 上的完整部署体系:单跳 Cloudflare Tunnel 拓扑下KSKILL_PROXY_TRUST_PROXY_HOPS=1的由来、每 5 分钟由 cron 驱动的自动发布与回滚流程、防止服务“安静死亡”的看门狗机制、运行时.env的密钥管理与轮换流程,以及 Loki/Promtail/Grafana 用量统计面板的接入方式。读完后你可以理解这套“cron + flock + systemd user service”架构的每个环节为什么这样设计,并能对同类 Fastify 公共代理做同样的部署审计。
生产部署布局:单跳 Tunnel 拓扑
k-skill-proxy的生产环境运行在gpu01上,而不是 Google Cloud Run。公共域名由 Cloudflare Tunnel 转发到绑定在127.0.0.1:8080的 Fastify 进程,整条链路只有“一个本地 Tunnel 跳”。生产布局的关键项如下(完整表格见 docs/deploy-k-skill-proxy.md):
| 项目 | 值 |
|---|---|
| 主机 | gpu01(gpu01.nomadamas.org;hostname -s为marker-multi-gpu-server-01) |
| 公共 URL | https://k-skill-proxy.nomadamas.org |
| 应用目录 | /data/home/jeffrey/apps/k-skill-proxy |
| 源码检出目录 | /data/home/jeffrey/apps/k-skill-proxy-repo |
| 代理服务 | systemctl --user status k-skill-proxy.service |
| Tunnel 服务 | systemctl --user status k-skill-proxy-tunnel.service |
| 运行时环境 | /data/home/jeffrey/apps/k-skill-proxy/.env |
| 已部署版本 | /data/home/jeffrey/apps/k-skill-proxy/deployed-sha |
| 部署脚本 | scripts/deploy-k-skill-proxy-gpu01.sh |
生产监听器绑定127.0.0.1,公共流量只能通过这一个本地 Cloudflare Tunnel 跳进入。文档对此有一个明确的安全约束:只有当部署脚本以KSKILL_PROXY_DEPLOY_ENVIRONMENT=production且生产主机(KSKILL_PROXY_DEPLOY_HOST=gpu01,或hostname -s为gpu01/marker-multi-gpu-server-01)运行时,脚本才会确保运行时环境包含KSKILL_PROXY_TRUST_PROXY_HOPS=1并重启服务;cron 中应显式设置KSKILL_PROXY_DEPLOY_HOST=gpu01。非 production 或非 gpu01 的执行保持应用默认值0。运维人员显式设置的值会被保留,KSKILL_PROXY_ENV_FILE可以把脚本指向非默认的环境文件路径。
文档还特别警告:不要对直接暴露的监听器使用此设置。信任一跳之所以在这里安全,是因为 Fastify 端口只监听 loopback;一旦该端口对外可达,客户端就能提供伪造的X-Forwarded-For值。
从源码看,这一约束是硬编码在脚本里的:deploy-k-skill-proxy-gpu01.sh 中的is_gpu01_production先检查KSKILL_PROXY_DEPLOY_ENVIRONMENT是否为production,再用case匹配gpu01|marker-multi-gpu-server-01两种主机名;只有通过双重校验,ensure_gpu01_production_defaults才会调用ensure_env_default向.env追加缺失的KSKILL_PROXY_TRUST_PROXY_HOPS=1。ensure_env_default(第 36-52 行)先grep检查键是否已存在,存在则直接返回——这正是“显式运维值被保留”的实现。
为什么 TRUST_PROXY_HOPS=1:限流识别的源码原理
生产环境必须设置KSKILL_PROXY_TRUST_PROXY_HOPS=1的原因在 packages/k-skill-proxy/src/server.js 中可以找到:
- 第 288 行将
KSKILL_PROXY_TRUST_PROXY_HOPS解析为trustProxyHops(取Math.max(0, ...),非法值回退 0); - 第 2214 行把它传给 Fastify 构造参数
trustProxy: config.trustProxyHops || false。
如果不信任这一跳,Fastify 的request.ip会一直是127.0.0.1,所有外部客户端会共享同一个限流桶,限流形同虚设。
更进一步的防伪造逻辑在clientRateLimitKey(server.js):当trustProxyHops > 0时,限流键优先取CF-Connecting-IP请求头,而不是X-Forwarded-For——X-Forwarded-For是客户端可以随意追加的,而CF-Connecting-IP由 Cloudflare 在出口重写,客户端无法通过伪造额外 XFF 条目来绕过限流。对应的行为测试在 server.test.js 中覆盖了KSKILL_PROXY_TRUST_PROXY_HOPS为"1"和"2"的场景。
同样地,路由用量日志(routeUsage行)里的clientIp字段也仅在trustProxyHops > 0时写入(server.js),否则记录的全是127.0.0.1,统计失去意义。
默认值与限流相关参数的完整定义见 packages/k-skill-proxy/README.md:KSKILL_PROXY_RATE_LIMIT_WINDOW_MS默认60000、KSKILL_PROXY_RATE_LIMIT_MAX默认60、KSKILL_PROXY_RATE_LIMIT_MAX_CLIENTS默认10000、KSKILL_PROXY_CACHE_TTL_MS默认300000、KSKILL_PROXY_HOST默认127.0.0.1。
自动发布流程:cron + flock + SHA 幂等
gpu01用户的 crontab 在flock保护下每 5 分钟运行一次部署脚本。脚本先fetch origin/main;如果记录的 SHA 与目标一致则直接退出,否则依次执行:
- 在源码检出目录 checkout 目标 SHA;
- 运行
npm ci、proxy lint 和全部 proxy 测试; - 为当前应用创建带时间戳的备份;
- 同步 proxy 及其本地 workspace 依赖;
- 安装生产依赖并重启 systemd user service;
- 检查本地与公共的
/health和/privacy; - 所有检查通过后才记录
deployed-sha。
只要main上有新合并、且新 proxy 测试与冒烟检查通过,就会在一个 cron 周期内完成部署。备份之后的任何失败都会触发自动回滚:恢复上一版文件并重启旧服务。
对照 deploy-k-skill-proxy-gpu01.sh 的实现,几个值得注意的细节:
- cron 环境的 systemd 会话总线:第 4-6 行显式导出
XDG_RUNTIME_DIR和DBUS_SESSION_BUS_ADDRESS,因为 cron 没有 user-session bus,否则systemctl --user无法工作; - 验证先于变更:第 146-150 行在目标 SHA 上依次执行
checkout --detach --force、npm ci --no-audit --no-fund、npm run lint --workspace k-skill-proxy、npm run test --workspace k-skill-proxy——测试失败发生在备份之前,此时还没有任何变更,无需回滚; - 备份内容与回滚陷阱:第 152-159 行用
tar备份packages/k-skill-proxy、packages/parking-lot-search(proxy 的本地 workspace 依赖)、deployed-sha和根package.json/package-lock.json;第 161-168 行用trap rollback ERR注册回滚,回滚动作是解压备份 +npm ci --omit=dev+systemctl --user restart; - 同步范围:第 170-173 行
rsync -a --delete --exclude node_modules同步 proxy 与parking-lot-search两个包,第 174-175 行用install -m 0644覆盖根 workspace 清单; - 带重试的冒烟检查:第 182-187 行对
http://127.0.0.1:8080/health做 5 次、每次间隔 2 秒的重试,之后(第 188-191 行)对本地与公共两个端点各做一次/health与/privacy检查。health_check用node -e断言响应 JSON 的ok === true;privacy_check(第 115-124 行)则断言/privacy页面包含固定版本的 meta 标签name="k-skill-privacy-policy-version" content="2026-08-18"——这意味着隐私政策页面内容被纳入了发布冒烟测试; - 脚本自举:第 193 行把仓库中的部署脚本
install -m 0755到$APP_DIR/deploy-k-skill-proxy-gpu01.sh。因此首次包含该脚本的main部署之后,cron 实际执行的是应用目录里的自拷贝版本。
cron 条目(安装或修复时用):
*/5 * * * * KSKILL_PROXY_DEPLOY_HOST=gpu01 flock -n /tmp/k-skill-proxy-deploy.lock /data/home/jeffrey/apps/k-skill-proxy/deploy-k-skill-proxy-gpu01.sh >> /data/home/jeffrey/apps/k-skill-proxy/deploy.log 2>&1注意flock -n:已有部署在跑时新触发的 cron 直接放弃,而不是排队。SSH 别名是gpu01,而机器上hostname -s是marker-multi-gpu-server-01——部署脚本两者都视为生产,但文档仍要求在 cron 中显式设置KSKILL_PROXY_DEPLOY_HOST=gpu01,避免主机名变更悄悄禁用看门狗。自举之前需要手动安装一次:
install -m 0755 /data/home/jeffrey/apps/k-skill-proxy-repo/scripts/deploy-k-skill-proxy-gpu01.sh \ /data/home/jeffrey/apps/k-skill-proxy/deploy-k-skill-proxy-gpu01.sh服务看门狗:一次重启事故催生的设计
脚本的第一件事(早于任何部署逻辑)是服务看门狗:每次 cron 运行——包括因deployed-sha已匹配而提前退出的运行——都会先确保生产服务处于 active 状态(第 130 行ensure_production_services)。已安装但不 active 的服务会被systemctl --user start拉起(第 82-99 行ensure_service_active先list-unit-files确认单元存在,再is-active --quiet判断)。
这个设计源于一次真实事故:2026-08-29 gpu01 重启后,Cloudflare tunnel 与 Loki/Grafana/Promtail 仪表盘栈挂了约 10 天才被人工发现。根因是这些单元使用Restart=on-failure——它不会恢复一次“干净的 SIGTERM”,而除了这个 cron 之外没有任何东西会再启动它们。proxy 之所以活了下来,纯属巧合:这个 cron 每次运行都会restart它。
被看门狗监视的服务(仅限生产 gpu01,可通过KSKILL_PROXY_WATCHED_SERVICES覆盖,默认列表见 脚本第 20 行):
| 服务 | 角色 |
|---|---|
k-skill-proxy.service | Fastify 代理 |
k-skill-proxy-tunnel.service | 两个公共主机名的 cloudflared tunnel |
k-skill-proxy-loki.service | 日志存储 |
k-skill-proxy-promtail.service | 日志采集 |
k-skill-proxy-grafana.service | 仪表盘 UI |
所有 5 个单元都使用Restart=always加StartLimitIntervalSec=0,保证干净停止或反复失败都不会让重启机制被永久禁用。规范单元文件都在仓库 infra/k-skill-proxy-dashboard/systemd/ 目录下:
| 单元 | 仓库来源 |
|---|---|
| proxy | infra/k-skill-proxy-dashboard/systemd/k-skill-proxy.service |
| tunnel | infra/k-skill-proxy-dashboard/systemd/k-skill-proxy-tunnel.service |
| loki / promtail / grafana | infra/k-skill-proxy-dashboard/systemd/ |
从单元文件内容看两个关键实现:
- k-skill-proxy.service 以
EnvironmentFile=/data/home/jeffrey/apps/k-skill-proxy/.env注入运行时密钥,Restart=always+RestartSec=3,stdout/stderr 直接 append 到proxy.log; - k-skill-proxy-tunnel.service 额外携带
RequiresMountsFor=,分别指向 cloudflared 二进制、其配置和应用目录——这三者都在gpu02:/dataNFS 挂载上,必须等挂载就绪后才能 exec,否则开机时会直接 crash-loop。proxy 单元同样对应用目录设置了RequiresMountsFor。
手动操作与验证命令
日常巡检与手动触发部署(完整命令清单见 docs/deploy-k-skill-proxy.md):
mosh gpu01 /data/home/jeffrey/apps/k-skill-proxy/deploy-k-skill-proxy-gpu01.sh curl -fsS http://127.0.0.1:8080/health curl -fsS https://k-skill-proxy.nomadamas.org/health curl -fsS http://127.0.0.1:8080/privacy curl -fsS https://k-skill-proxy.nomadamas.org/privacy cat /data/home/jeffrey/apps/k-skill-proxy/deployed-sha日志与服务状态:
tail -f /data/home/jeffrey/apps/k-skill-proxy/deploy.log tail -f /data/home/jeffrey/apps/k-skill-proxy/proxy.log systemctl --user status k-skill-proxy.service systemctl --user status k-skill-proxy-tunnel.service验证 trust-proxy 设置时,不要打印整个带密钥的 env 文件,只 grep 目标键:
grep '^KSKILL_PROXY_TRUST_PROXY_HOPS=' /data/home/jeffrey/apps/k-skill-proxy/.env生产环境期望值为KSKILL_PROXY_TRUST_PROXY_HOPS=1。如果该键缺失,下一次 gpu01 生产部署会补上并重启服务;staging、本地或其他主机执行则不会添加。拓扑变化时,只有重新清点可信反向代理跳数之后才应更新显式值。.env文件留在gpu01上,绝不能复制进仓库。
必需运行时环境变量(gpu01)
由于 Cloudflare Tunnel 转发到127.0.0.1:8080,Fastify 必须信任这一跳,否则所有外部客户端共享同一个限流桶。以下键应保留在/data/home/jeffrey/apps/k-skill-proxy/.env中:
KSKILL_PROXY_TRUST_PROXY_HOPS=1 KSKILL_PROXY_RATE_LIMIT_WINDOW_MS=60000 KSKILL_PROXY_RATE_LIMIT_MAX=60 COUPANG_ACCESS_KEY=<coupang-partners-access-key> COUPANG_SECRET_KEY=<coupang-partners-secret-key> KOMSA_MTIS_API_KEY=<komsa-mtis-service-key>可选的 BC Card eat.pl 中转键,仅在启用 Lightsail 固定出口中转时配置。同一个随机 token 在 Lightsail 侧存为EATPL_RELAY_TOKEN。token 永远不要放进 Git、聊天或客户端请求:
BCCARD_EATPL_INST_NM=<partner-institution-code> BCCARD_EATPL_API_BASE_URL=https://api.paybooc.ai/api/mer BCCARD_EATPL_RELAY_URL=https://eatpl-relay.nomadamas.org/v1/search BCCARD_EATPL_RELAY_TOKEN=<same-token-as-lightsail>KSKILL_PROXY_TRUST_PROXY_HOPS=1在生产必需;只有“本地绑定、且不在反向代理之后”的进程才保持不设(默认0)。信任跳数大于 0 时,限流器优先取CF-Connecting-IP而非X-Forwarded-For,客户端无法伪造额外 XFF 条目(实现见 server.js 第 443-451 行)。
两类密钥的启用方式与验证路径:
- Coupang:
COUPANG_ACCESS_KEY+COUPANG_SECRET_KEY启用GET /v1/coupang/products/search。密钥只存在 gpu01 运行时.env,绝不经过 query string、shell 参数、issue 或日志。通过/health→upstreams.coupangConfigured=true验证启用。 - KOMSA MTIS:
KOMSA_MTIS_API_KEY启用GET /v1/komsa/ferry/:dataset。同样只存 gpu01 运行时.env,通过/health→upstreams.komsaConfigured=true验证后,再执行一次只读渡轮查询演练。
ASK Seoul 天气风险路由的密钥交接
部署seoul-weather-risk代理路由前,ASK Seoul 必须先签发专用的、可吊销的服务密钥,不得使用维护者或贡献者的个人 Marketplace 密钥。Marketplace 将该密钥注册为以下固定服务主体与作用域:
| 字段 | 固定值 |
|---|---|
| service principal | k-skill-proxy:seoul-weather-risk |
| scope | skill:seoul-weather-risk:read |
| 允许的 Marketplace API | GET /skill/v1/bundles/seoul-weather-risk、GET /skill/v1/products/weather_place_risk_window、GET /skill/v1/products/weather_place_risk_window/data |
| 排除的 API | 所有/api/v1/*API 与其他所有/skill/v1/*产品 |
只在既有 gpu01 运行时.env中存放这两个值:
ASK_SEOUL_SKILL_API_BASE_URL=https://<ask-seoul-skill-origin> ASK_SEOUL_KSKILL_API_KEY=<dedicated-service-key>该密钥不是用户凭据:它只从代理进程发往固定的 ASK Seoul/skill/v1路径。在 Marketplace 完成服务密钥 scope 迁移并注册该主体之前,不要部署这条路由(代理 README 中askSeoulWeatherRiskConfigured健康标志的含义——仅当 origin 与专用服务键同时设置才为true——见 packages/k-skill-proxy/README.md)。
密钥轮换流程:注册同 scope 的替代密钥 → 只替换 gpu01 运行时密钥 → 重启/冒烟测试代理 → 吊销旧密钥。若代理被禁用或疑似失陷,立即吊销其密钥;Marketplace 必须在任何每日配额核算之前拒绝该密钥。永远不要把密钥粘贴进 issue、PR、shell 参数、URL 或日志。
用量统计面板:Loki + Promtail + Grafana
端点调用统计(routeUsage日志行)由 Promtail 采集进 Loki,再在 Grafana 中可视化。从 server.js 第 2233-2266 行 可以看到日志生产端的设计:onResponsehook 对每个匹配路由计数并输出一条结构化日志,/health被排除(健康检查是运维噪声而非使用量);所有未匹配请求聚合到单一__unmatched__键下,404 的原始路径先经过normalizeUnmatchedPath归一化且 map 上限 100 条——文档注释明确说明这是为了防止任意输入无限撑大内存 map 和 Loki 标签基数。
面板栈的细节(90 天日志保留、{job="k-skill-proxy", route!=""}查询约定、setup-gpu01.sh安装步骤、Grafana 绑定127.0.0.1:3200并复用同一个 cloudflared tunnel 暴露k-skill-proxy-dashboard.nomadamas.org)见 infra/k-skill-proxy-dashboard/README.md。栈以 systemd user service 形式运行(k-skill-proxy-{loki,promtail,grafana}.service),由infra/k-skill-proxy-dashboard/setup-gpu01.sh安装。Grafana 登录即访问控制(禁用匿名、仅管理员账户),管理凭据存放在grafana.env(mode 600,绝不提交)。
一个时序注意点:route标签(以及按端点划分的面板)只有在产出routeUsage日志行的代理构建部署到main之后才会出现——即部署了带routeUsage统计逻辑的版本前,面板上不会有按端点的数据。
小结:这套部署体系的三条经验
- 幂等 + 看门狗合并进同一条 cron:部署脚本既是发布器又是保活器,
flock -n防并发、SHA 对比防重复、每次运行先拉起死掉的服务,用一个 5 分钟周期同时解决“部署”和“活着”两个问题; - 安全默认值写进发布流程而非文档口头约定:
TRUST_PROXY_HOPS=1由脚本在严格的双条件(production + gpu01 主机)下自动补齐,且保留运维显式值,防止误配在 staging/本地扩散; - 发布冒烟测试覆盖到隐私政策版本:
/privacy的 meta 标签版本被privacy_check断言,内容回归同样会阻断发布并触发回滚。
延伸阅读:packages/k-skill-proxy/README.md(端点与全部环境变量、/health上游标志语义)、infra/k-skill-proxy-dashboard/README.md(仪表盘栈完整布局)、docs/deploy-eatpl-relay-lightsail.md(BC Card eat.pl Lightsail 中转的部署文档)。
【免费下载链接】k-skill한국인을 위한 스킬 모음집 - 에이전트를 한국인으로项目地址: https://gitcode.com/GitHub_Trending/ks/k-skill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考