From f629acbcf5f4183b56a5f56d23caf6dc38a34583 Mon Sep 17 00:00:00 2001 From: NEDONION Date: Wed, 12 Aug 2026 00:40:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=E9=83=A8=E7=BD=B2=20job=20=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20artifact=20=E4=BC=A0=E9=85=8D=E7=BD=AE=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=20checkout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首次自动部署失败:两个镜像构建推送成功,但部署这一步挂在 actions/checkout。 fatal: unable to access 'https://github.com/NEDONION/rag-agent-platform/': Failed to connect to github.com port 443 after 130394 ms The process '/usr/bin/git' failed with exit code 128 actions/checkout 会在 runner 所在机器上执行 git clone github.com,而服务器到 github.com:443 的连接正是 runner tarball 那个 25 KB/s 的问题,三次重试全部超时。 关键观察:runner 与 GitHub 的通信完全正常(它成功领到了任务),那条链路走的是 *.actions.githubusercontent.com;不通的只有 github.com:443 这一个入口。 部署这一步其实只需要 docker-compose.yml 和 deploy/nginx.conf,不需要整个仓库。 新增 config job 在云端打包成 artifact,deploy job 用 download-artifact 取用—— 走的正是那条已被证明可用的链路。 文档同步更新: - 新增 3.10 记录本问题 - 新增第 12 条经验:受限网络里把需要外网的步骤留在云端 job,只把必须本机执行的 放到自托管 job,中间用 artifact 传递。actions/checkout 是最容易忽略的一个—— 它看起来只是"取代码",实际会在执行它的那台机器上发起网络请求 - 按要求为全部主要章节添加「一句话人话」导读(共 10 处),用大白话概括每节内容, 技术细节保留在下方不变 验证:workflow YAML 可解析,三个 job 依赖关系正确,deploy job 已无 checkout, 仍不含 pull_request 触发器,全部 run 步骤通过 bash -n。 Co-Authored-By: Claude Opus 5 --- .github/workflows/deploy.yml | 33 +++++++++-- docs/TROUBLESHOOTING_LOG.md | 109 +++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a45ae30..0281223 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -68,21 +68,46 @@ jobs: cache-from: type=gha,scope=${{ matrix.component }} cache-to: type=gha,mode=max,scope=${{ matrix.component }} + config: + name: 打包部署配置 + # 在云端 runner 上取出部署所需的配置文件,作为 artifact 传给自托管 runner。 + # 目的是让部署那一步完全不必访问 github.com——见下方 deploy job 的说明。 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/upload-artifact@v4 + with: + name: deploy-config + path: | + docker-compose.yml + deploy/nginx.conf + retention-days: 1 + deploy: name: 部署到轻量服务器 - needs: build + needs: [build, config] # 跑在服务器自己的 runner 上,所有命令都是本机执行 runs-on: [self-hosted, deploy] steps: - - uses: actions/checkout@v4 + # 这里**不能**用 actions/checkout:它会在服务器上执行 git clone github.com, + # 而国内服务器到 github.com:443 的 git 连接极不稳定,实测三次重试全部超时: + # fatal: unable to access '...': Failed to connect to github.com port 443 + # + # runner 与 GitHub 的通信走的是 *.actions.githubusercontent.com,那条链路是通的 + # (它能正常领到任务)。artifact 下载同样走该链路,因此改用 artifact 传配置文件。 + - uses: actions/download-artifact@v4 + with: + name: deploy-config + path: deploy-config - name: 同步部署配置 # 只同步这两个文件。服务器上的 .env 存着密钥,绝不覆盖。 run: | set -euo pipefail mkdir -p "$DEPLOY_PATH/deploy" - cp docker-compose.yml "$DEPLOY_PATH/docker-compose.yml" - cp deploy/nginx.conf "$DEPLOY_PATH/deploy/nginx.conf" + cp deploy-config/docker-compose.yml "$DEPLOY_PATH/docker-compose.yml" + cp deploy-config/deploy/nginx.conf "$DEPLOY_PATH/deploy/nginx.conf" env: DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} diff --git a/docs/TROUBLESHOOTING_LOG.md b/docs/TROUBLESHOOTING_LOG.md index 4ed3085..fcb43d3 100644 --- a/docs/TROUBLESHOOTING_LOG.md +++ b/docs/TROUBLESHOOTING_LOG.md @@ -6,6 +6,9 @@ **最后更新**:2026-08-12 +> 📖 **怎么读**:每一节开头都有一段 **「一句话人话」**,用大白话说清这节讲什么。 +> 只想知道发生了什么,看那一句就够;要动手修或深究原理,再往下看细节。 + --- ## 目录 @@ -25,6 +28,7 @@ - 3.1 仓库名对不上 · 3.2 CI nginx 校验失败 · 3.3 不是 git 仓库 - 3.4 docker 权限不足 · 3.5 令牌明文泄露 · 3.6 download.docker.com 不通 - 3.7 apt 源太慢 · 3.8 no tracking information · 3.9 runner tarball 25 KB/s + - 3.10 部署 job checkout 失败 - [附录:经验教训](#附录经验教训) --- @@ -57,6 +61,9 @@ ## 1.2 根因一:LLM 客户端超时是 1 小时 +> 💬 **一句话人话**:代码里写着「等模型回复最多等 1 小时」。1 小时约等于永远不超时—— +> 对面挂了,我们这边还在傻等,页面就一直转圈,连个错误都不报。 + **位置**:`src/main/java/org/lucas/infrastructure/llm/factory/LLMProviderFactory.java` 四处模型构造全部写着: @@ -87,6 +94,10 @@ private static final Duration STREAMING_TIMEOUT = timeoutFromEnv("LLM_STREAM_TIM ## 1.3 根因二:进度提示与实际执行不同步 +> 💬 **一句话人话**:页面显示「正在检索文档」,但这时候程序其实已经去干别的了—— +> 它要连着问大模型三四个问题(理解意图、改写问题…)才会更新提示。 +> 所以你看到「卡在检索」,其实压根不是检索慢,是卡在这几次提问上。 + **位置**:`src/main/java/org/lucas/application/rag/service/RagQaDatasetAppService.java` 这是「卡在检索」这个现象的直接来源。发完 `"正在数据集中检索..."` 这条 SSE 事件之后,代码**连着执行了 3~4 次同步 LLM 调用**才发下一条事件: @@ -125,6 +136,11 @@ effectiveQuestion = rewriteQuestion(...); ## 1.4 根因三:并发被串行化 +> 💬 **一句话人话**:Java 有个默认的「公共任务池」,这台机器只分到 1.2 个 CPU, +> 算下来这个池子同时只能干 1 件事。而问答大部分时间是在干等模型返回(不占 CPU), +> Java 却看不出来这是在等,不会多派人手。结果就是**两个人同时提问,第二个必须排队**—— +> 这就是「一开始卡死、过一会儿又好了」的真相。 + 这是「一开始直接卡死,过一会儿又好了」的真正来源,也是最隐蔽的一个。 两处流式问答用的是**无参**的 `CompletableFuture.runAsync(...)`: @@ -171,6 +187,10 @@ private final ExecutorService ragStreamExecutor = Executors.newFixedThreadPool(8 ## 1.5 根因四:nginx 回源降级到 HTTP/1.0 +> 💬 **一句话人话**:nginx 转发请求给后端时用了老版协议(HTTP/1.0), +> 而「一个字一个字往外吐」这种流式回复在老协议下传不可靠。 +> 配置文件里前端那段写对了,后端那段漏了一行。 + **位置**:`deploy/nginx.conf` ```nginx @@ -209,6 +229,9 @@ location /api/ { ## 1.6 根因五:SSE 超时形同虚设 +> 💬 **一句话人话**:浏览器和服务器之间那根「长连接」也设了个近乎无限的超时。 +> 这是最后一道保险,保险设成永不触发,等于没装。 + | 位置 | 修改前 | 修改后 | |---|---|---| | `RagQaDatasetAppService`(两处) | `new SseEmitter(Long.MAX_VALUE)` | 10 分钟 | @@ -222,6 +245,11 @@ location /api/ { ## 2.1 为什么不用 SSH +> 💬 **一句话人话**:SSH 部署等于「让 GitHub 主动敲你服务器的门」, +> 那你就得把门(22 端口)对全世界开着,还得把钥匙(私钥)交给 GitHub 保管。 +> 公司里都反过来做:**让服务器主动去问 GitHub「有活儿吗」**, +> 门一直关着,也不用把钥匙交出去。 + 最初的方案是 GitHub Actions 通过 SSH 登录服务器执行部署。这是**推(push)模式**:CI 主动连进服务器,所以服务器必须开放入站端口,CI 里必须保存服务器私钥。 公司环境几乎不这么做,因为生产机器通常压根不对公网开放。常见的替代: @@ -281,6 +309,9 @@ location /api/ { ## 2.3 实现中的三个坑 +> 💬 **一句话人话**:这三个坑都源于同一件事——干活的 runner 自己也是个容器, +> 它「站在容器里操作容器」,所以自杀、路径、网络三方面都容易出岔子。 + ### 坑 1:runner 会把自己重启掉 runner 自己也在 `docker-compose.yml` 里。如果部署命令写成裸的 `docker compose up -d`,它会**连同 runner 一起重建,把正在执行的部署任务当场杀掉**。 @@ -413,6 +444,10 @@ read -s -p "粘贴令牌然后回车: " T && echo "GITHUB_PAT=$T" >> .env && uns ## 3.6 构建 runner 镜像卡在 download.docker.com +> 💬 **一句话人话**:装 docker 命令行工具时要先从 docker 官网下个密钥, +> 但那个网址国内连不上。好在同一时刻 Docker Hub 是通的—— +> 于是改成直接从官方镜像里把现成的程序拷过来,绕开那个网址。 + ``` curl: (35) Recv failure: Connection reset by peer ------ @@ -441,6 +476,9 @@ COPY --from=dockercli /usr/local/libexec/docker/cli-plugins/docker-compose \ ## 3.7 apt 官方源太慢 → 换阿里云镜像 +> 💬 **一句话人话**:装系统软件时默认从国外服务器下载,只有 352 KB/s。 +> 服务器本来就在阿里云上,改成从阿里云自己的镜像站下载,快到几乎不用等。 + 构建日志里: ``` @@ -498,6 +536,12 @@ git branch --set-upstream-to=origin/feat/cicd feat/cicd ## 3.9 runner tarball 下载 25 KB/s —— 用镜像站 + 校验和 +> 💬 **一句话人话**:要下一个 215MB 的程序包,直连 GitHub 只有 25 KB/s, +> 得下 2 个半小时。换成加速站 3 分钟搞定。 +> **但加速站是别人的服务器,怎么保证他没在包里塞东西?** +> 办法是拿 GitHub 官方公布的「指纹」(SHA256)比对—— +> 文件哪怕改了一个字,指纹就对不上,构建当场失败。 + actions-runner 的 tarball 有 **215.5 MB**,从国内直连 github.com 实测: ``` @@ -582,6 +626,63 @@ echo 'RUNNER_DOWNLOAD_BASE=https://ghfast.top/https://github.com/actions/runner/ 代价是有先后顺序问题:需要 workflow 先合入 main 才能手动触发,而合并本身会触发 部署、此时 runner 尚不存在。绕得开,但比镜像站这条路复杂。当前规模下镜像站够用。 +## 3.10 部署 job 在服务器上 checkout 失败 + +> 💬 **一句话人话**:自动部署的最后一步要先把代码拉到服务器上, +> 但服务器连 GitHub 拉代码就是不通(还是那个网速问题),重试三次全超时。 +> 解法是——**这一步根本不需要整个代码仓库**,只要两个配置文件。 +> 于是让云端先把这两个文件打包好,服务器直接取包,完全不碰 GitHub 的代码服务。 + +首次自动部署时,两个镜像都构建推送成功,但部署这一步失败: + +``` +fatal: unable to access 'https://github.com/NEDONION/rag-agent-platform/': + GnuTLS recv error (-110): The TLS connection was non-properly terminated. +... +fatal: unable to access 'https://github.com/NEDONION/rag-agent-platform/': + Failed to connect to github.com port 443 after 130394 ms: Couldn't connect to server +The process '/usr/bin/git' failed with exit code 128 +``` + +**原因**:`actions/checkout` 会在 runner 所在机器上执行 `git clone github.com`, +而服务器到 `github.com:443` 的连接正是 3.9 里那个 25 KB/s 的问题,三次重试全部超时。 + +**关键观察**:runner 与 GitHub 的通信**完全正常**——它成功领到了这个任务。 +那条链路走的是 `*.actions.githubusercontent.com`,是通的; +**不通的只有 `github.com:443` 这一个入口**。 + +> 又一次印证了 3.6 的教训:把「网络不行」精确到「哪个域名不行」,解法就出来了。 + +**修复**:部署这一步其实只需要 `docker-compose.yml` 和 `deploy/nginx.conf` 两个文件, +根本不需要整个仓库。让云端 runner 打包成 artifact,自托管 runner 下载即可—— +artifact 走的正是那条已被证明可用的链路。 + +```yaml + config: + runs-on: ubuntu-latest # 云端,github.com 畅通 + steps: + - uses: actions/checkout@v4 + - uses: actions/upload-artifact@v4 + with: + name: deploy-config + path: | + docker-compose.yml + deploy/nginx.conf + + deploy: + needs: [build, config] + runs-on: [self-hosted, deploy] # 服务器,github.com 不通 + steps: + - uses: actions/download-artifact@v4 # ← 不用 checkout + with: + name: deploy-config + path: deploy-config +``` + +> **一般化的经验**:自托管 runner 在受限网络里,要尽量把「需要访问外网」的活 +> 留在云端 job,只把「必须在本机执行」的活放到自托管 job。 +> 两者之间用 artifact 传递产物。 + --- # 附录:经验教训 @@ -648,6 +749,14 @@ Ubuntu 24.04 的 apt 源改成了 DEB822 格式,路径从 `/etc/apt/sources.li **改完一定要验证结果**,而不是看命令有没有报错。 +### 12. 受限网络里,让云端干需要外网的活 + +自托管 runner 所在的机器往往网络受限。把「要访问外网」的步骤留在云端 job, +只把「必须在本机执行」的步骤放到自托管 job,中间用 artifact 传递产物。 + +`actions/checkout` 是最容易忽略的一个——它看起来只是「取代码」, +实际会在**执行它的那台机器上**发起 git 网络请求。 + ### 11. 引导时先看是不是自己埋的坑 `no tracking information` 这个报错,根源是前一步用 `git checkout -B FETCH_HEAD`