site logo

Marico's space

看了 GitHub Actions 账单后,我在家里搭了一套 CI + 安全基座

服务器技术 2026-08-20 14:50:10 4

2026年6月底,团队GitHub Actions撞到了账单上限。每个PR(pull request,代码评审请求)的CI(持续集成,指每次代码变更自动运行测试的系统)检查都在2秒内失败,日志里空空如也。与此同时,Slack里的插件发布通知也停了。代码一行都没动过,是外部服务因为账单问题直接把我的基础设施掐断了。

当时我跑的是GitHub Actions免费额度。但免费额度用完了,CI被卡住,要继续开发就只能按量付费(超出免费分钟数后的计量收费)。"免费"到此为止,从那之后不管怎样都得往外掏钱了——这是起点。所以问题不是"减少测试来省钱",而是"继续给GitHub付费,还是自己搭一套?"GitHub是按量计费,提交越多、代码越复杂,月账单就越高。而家用CI服务器(ci1/ci2级别,约人民币67,900一台)呢,一次性投入加电费,用多少都不涨。持续增长的成本 vs 一次性的固定成本——这个结构性差异是决定性因素。而且随着代码规模增长,每次CI运行时间也在涨。实测:某个产品的GitHub Actions运行时间从今年3月平均约1分钟,涨到了8月的平均约16分钟,最长超过一小时。我这边每月活跃提交大约2,400次。按GitHub的按量计费(Linux 0.008美元/分钟),光是超出免费额度的部分每月就要150到200美元,而且随着测试时间越来越长只会越涨越多。一台约人民币67,900的家用CI服务器,不到半年就能追平,之后永远更便宜(提交数和运行时间是实测,月费是按免费额度和平均分钟数估算的)。除此之外,我计划加的安全扫描工具(DefectDojo之类的)也得有地方跑。所以最优解是:在家搭一套统一的基础设施,测试、安全扫描、监控全放上去。这就是这次决策的记录。根据当时的记录,这套基础设施的monorepo(多个代码库合并到一个仓库的布局)从首次提交到330次提交、35个ADR(约一个月)。

实际的ci1/ci2——两台MINISFORUM UM880 Plus(AMD Ryzen 7 8845HS,32GB,每台约人民币67,900)。没用GitHub的按量账单,而是在这两台上跑了CI、安全扫描和监控

先把结论放前面(全文约11分钟)。

  • 所有运行瞬间失败(2秒)且日志为空,是GitHub Actions账单封停的指纹——外部服务某天会悄无声息地关掉你的基础设施。汇报失败机制不能依赖会失败的那个机制。
  • 搭了自己的基座不停,原来的也没扔——切换到self-hosted(自托管)runner,保留GitHub Actions侧的workflow,万一账单恢复可以零改动回去。
  • 一个分布式安全workflow把所有PR都卡住了——gitleaks(全量历史扫描检测代码中泄露的密钥的工具)做全分支扫描会把正常的PR也搞挂。卡点不仅要验证"能不能拦住",还要验证"能正常通过",否则就变成让所有人停工的武器。
  • HA(高可用,指一个节点挂了系统还能继续跑的架构)的通过线是真实硬件断电断网测试——单元测试全绿的情况下,上真实硬件才发现3个bug。

症状:所有运行瞬间失败,日志为空

一开始我怀疑是自己的workflow。但两个CI检查都稳定地在2秒内FAILED。打开日志,里面什么都没有。隔离排查,先怀疑Slack通知路径,直接curl webhook。

# webhook本身还活着吗?→ HTTP 200,Slack收到 = 代码没问题
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$SLACK_WEBHOOK" \ -H 'content-type: application/json' \ -d '{"text":"probe from local"}'
# => 200

webhook返回200,本地通知脚本也没问题。通知逻辑是清白的。接下来用gh run view看CI侧,每个运行都是2秒结束,日志为空。问题逐渐清晰:runner根本没启动。原因是GitHub Actions被账单封停了,整个组织的GitHub Actions完全停掉。每个运行在启动前就瞬间失败(2秒),日志里什么都没有——这个行为本身,回过头看就是账单封停的指纹。更坑的是这个停掉的机制是静默的。因为"汇报失败"的机制——Slack通知——本身也依赖Actions所以安静了,过了好几天才发现。

gh run view里每个运行都瞬间失败(2秒),日志为空。runner根本没启动;超账单后组织的GitHub Actions悄无声息地停了(重建终端;主机名/ID已匿名化)

转折:搭一套不依赖托管的CI,"用代码"

外部SaaS(软件即服务)会因为自己的账单问题,某天毫无预兆地关掉你的基础设施。那就只有一条路:掌握一套自己的不停机的基座。既然要在家搭基座,只放CI是浪费。测试、计划加的安全扫描、监控——全塞到同一套基础设施上。这是最佳决策。计划是:"停止SSH进去直接敲命令的老习惯,用Ansible/Docker Compose/Terraform(都是把配置定义为代码来管理的工具)声明式管理家庭服务器集群。"把唯一一台现有CI服务器的状态收进一个monorepo,把self-hosted runner、漏洞台账、通过Grafana/Loki/Prometheus(监控可视化+日志+指标的监控栈)实现可观测性、日志平台,一股脑用代码固化下来,几乎在第一两天内全部搞定。

每个产品的workflow从托管runner机械地重定向到self-hosted runner。

# 去掉托管的(ubuntu-latest)依赖,指向家庭runner的标签
jobs: test: runs-on: [self-hosted, linux, docker] # 家庭runner集群的标签 steps: - uses: actions/checkout@v4 - run: pnpm install --frozen-lockfile && pnpm test

保留了Actions侧的workflow而不是删掉,万一账单恢复可以零改动回去。"坏了切别的基座,原来的也不扔"——这成了基本策略。

坑1:分布式安全workflow把整个组织的PR全卡住了

迁移后不久,这个广泛分发的安全workflow引发了事故,把目的地的所有PR都堵死了。当时这个workflow分发到了组织102个非归档仓库中的25个,影响范围不小。真凶是gitleaks的使用方式。

gitleaks detect --source .扫描的是目标的所有refs(相当于git log --all)。这意味着它会把旧分支上留的测试密钥和历史泄露记录都扫出来,所以哪怕是完全健康的PR也必然失败。雪上加霜的是self-hosted runner上的工作区污染产生了误报,检测到"那个PR里不该存在的文件"。修复方案是把扫描范围限制在PR的diff上。

# 别扫所有refs导致PR总是失败。只看diff。
gitleaks detect --source . --redact \ --log-opts="origin/${BASE_REF}..HEAD"

保险起见,我还准备了一个自测workflow,对应该PASS的分支和应该FAIL的分支都做E2E(端到端)验证。教训是:安全门禁如果只能"拦住"是没用的——不验证它能"正常通过",它就变成让所有人停工的武器

坑2:切到self-hosted后Docker构建附带挂了

迁移也带来了技术债。完全移除托管runner后,CDK(用代码定义云基础设施的工具)的Docker打包在好几个产品上不工作了。在"runner跑在容器里 + 用宿主机的docker socket"这种架构下,打包用的volume挂载是空的。有个产品的部署workflow一度彻底废掉,我在本地Mac上手动部署撑了一段时间。解决方案是用本地依赖展开替代Docker打包,在runner上用pip install --target,然后实测连续5次部署workflow成功才关闭。基座迁移了,上面跑的构建前提也跟着悄悄塌了。这里也是,确认"能用"只能靠实测。

HA:触发了脑裂,当天断真机修复

单机runner挂了CI就停了。于是上了2节点架构,结果最大的事故来了。故障切换从ci1切到ci2加NVMe(高速存储标准)之后,重启ci1导致两个节点同时激活,抢同一个任务队列打成一团——脑裂(两个系统同时变成主节点),半夜才发现。任务从网页提交到ci2却跑到ci1上执行,这状态很让人不安。

原因是结构性问题:"起家系统没有门禁判断自己是不是指定的主节点"。ci1自启动时不知道ci2已经激活了。当天就实现了原子租约隔离(确保旧主节点可靠锁定的机制防止双活)——fence-agent常驻双节点,心跳2s/TTL 6s/宽限6s——然后过了真实硬件杀测试(真正断电断网)。自动切换18秒;自隔离+接管NFS分区17秒;数据分歧精确为零。这里最大的教训是切换 ≠ 隔离。手动回切会被忘掉。而且即使bats(bash测试框架)全绿,也有3个bug只在真机断电断网时才发现(宽限期内心跳互相让步导致无法前进的活锁;硬挂载导致的无限IO阻塞;不看对方租约直接抢主的盲抢)。这一天我把"通过线是真机断电断网测试,不是设计文档"写进了任务手册。

Infra Portal:一个界面汇总所有产品

最后我搞了一个自研portal(叫Infra Portal),一个界面汇总:CI状态、部署升级、多云商成本、备份充足度、服务器和本地LLM(大语言模型)的存活状态。有两个设计理念。一:"不信注册,从仓库和运行环境自动探测能力靠实测"。二:"不把HTTP 200当存活——读实际响应的含义"。前者能抓到自报的空档;后者能抓到"服务器回了但内容坏了"。这里也有个搞笑翻车:有个节点的卡片定格在62小时前,因为HA安装不对称——我忘了在备用侧放上报脚本。修复方案是让双节点对称。

家庭CI基座的Infra Portal。HA 2节点(ci1主/ci2备)和CI runner、安全扫描、监控统一到一个基座。节点名在重建中已隐藏;HA数据已实测验证

可复用的经验

  • 测试省不掉。所以我算清成本, consolidation到一套自己的基座。不是挤在免费额度里,而是按持续运行的假设估算成本,把CI、安全、监控画到一块基础设施上。
  • 所有运行瞬间失败日志为空是账单封停的指纹。外部服务某天悄无声息地关掉你的基础设施。汇报失败机制不能依赖会失败的那个机制。
  • 自己搭基座,原来的也不扔。切换到self-hosted但保留托管的workflow,可以零改动回去。
  • 分布式门禁要验证到"能正常通过"的程度。像gitleaks的全分支扫描,只能拦人的门禁最后变成让所有人停工。
  • HA的通过线是真机断电断网测试。切换 ≠ 隔离。即使bats全绿,也有bug只在真实硬件上才露头。