Git 工作流中的 熔断 最佳实践
上周五下午四点多,一个 merge 把主干的单测干红了,但因为走的是 merge queue 排队合并,等发现的时候后面已经叠了七八个 PR,谁也说不清是从哪一单开始坏的。最后俩人对着 CI 日志倒着翻了快一个小时才定位到,回滚完各回各家。事后复盘我就一直在想,服务治理里有熔断这玩意儿,Git 工作流里凭什么只能靠人吼?主干一旦确认坏了,应该先把合入通道掐了,止损,再慢慢查原因。
下面是我们仓库里实测在用的一套做法,不复杂,但出事的时候真的能省命。
先说清楚这个“熔断”是什么
借用微服务里的概念:下游挂了就切断调用,别让雪崩扩大。放到 Git 这边,“下游”就是 main 的健康状态,“调用”就是往主干合代码的动作。所以 Git 熔断拆开就三步:
- 发现主干坏了(CI 红、冒烟挂、线上报警);
- 止损:把合入通道冻住,把坏提交摘出去;
- 修完合闸,恢复日常。
听起来平平无奇,但没有这套东西的时候,上面每一步都在靠嗓门大。
第一档:git revert,永远的第一反应
多数情况不用花活。找到那个 merge commit,直接:
git revert -m 1 abc1234
-m 1 别忘,merge commit 不带这个参数会直接报错拒干,第一次遇到的人基本都会懵一下。
还有个经典坑必须提一嘴:revert 掉一个 merge 过后,如果后面想把这个分支原样重新合进来,直接 re-merge 是不行的,Git 会认为这些提交已经在历史里了,啥也不给你带进来。正确姿势是先把那条 revert commit 再 revert 掉,或者让作者 rebase 过后重新提 PR。所以我们的规矩是回滚完必须在原 PR 下面留个言,不然半个月后绝对有人一脚踩进去。
第二档:给合入通道装个开关
熔断的关键是冻住入口,不是冻住所有人写代码。做法很土:仓库里放个标记文件 .ci/BREAKER,CI 的第一个 job 就检查它,并且设成 required check,它一挂,merge queue 就不会再放行任何 PR。
jobs:
breaker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: check breaker
env:
TITLE: ${{ github.event.pull_request.title }}
run: |
if [ -f .ci/BREAKER ]; then
if [[ "$TITLE" == *"[fix]"* ]]; then
echo "熔断期间的修复 PR,放行"
exit 0
fi
echo "主干熔断中,原因和时间如下:"
cat .ci/BREAKER
exit 1
fi
文件里老老实实写清楚时间、原因、负责人,一行行来,别嫌麻烦,事后复盘全靠这个。[fix] 那段是给修复 PR 留的后门,不然主干红了,修复 PR 自己也进不来,直接死锁。
至于怎么把 BREAKER 文件推上去,我们没搞什么优雅方案:给两三个值班同学留了直推权限,出事直接 git push,不走 PR。这权限只在事故时用,平时谁手痒谁请奶茶,目前执行得还行...
事前也顺手卡两道
熔断是事后止损,入口平时也得看住:
main开 required status checks,这个没得商量;- merge queue 的分组大小调小,分得越大,一单坏了连坐的越多,上周那次就是这么倒霉的。代价是 CI 跑得更频繁,没准儿还得给 runner 加预算,自己权衡;
- CI 配置、构建脚本这类文件用 CODEOWNERS 多加一个 reviewer,它们坏起来影响面最大。
直推也要拦住:服务端 hook
上面的开关管 PR,管不住直推。而且客户端 hook 没用,一个 --no-verify 就绕过去了,这东西只能提醒自己,管不了别人。真要拦直推,得做在服务端。
自建 Git 的话(Gitea、GitLab 都行),挂个 pre-receive:
#!/bin/bash
while read oldrev newrev refname; do
if [ "$refname" = "refs/heads/main" ] && [ -f /srv/git/BREAKER ]; then
echo "main 处于熔断状态,禁止直推,请联系值班同学"
exit 1
fi
done
exit 0
注意这个开关是服务器上的一个文件,不进仓库,拉闸删文件就行,和上面的 .ci/BREAKER 是两道门,各管各的。一般由管 Git 服务的人握着,或者提前给值班同学配好权限,别到时候现找。各家的 hook 目录路径不一样,自己翻文档。
半自动熔断:目标是没人拉闸
理想状态是 CI 挂了自动熔断,不等值班同学反应过来。目前的草图是一个监听 main 的 workflow,冒烟失败就直接往 main 推一个 BREAKER 文件:
name: auto-breaker
on:
workflow_run:
workflows: ["smoke"]
types: [completed]
jobs:
trip:
if: github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.head_branch == 'main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.BREAKER_TOKEN }} # <--- 能绕过分支保护的 PAT,别用默认 GITHUB_TOKEN
- run: |
echo "$(date -Iseconds) smoke 挂了,自动熔断,触发于 ${GITHUB_SHA}" > .ci/BREAKER
git config user.name "breaker-bot"
git config user.email "breaker-bot@noreply"
git add .ci/BREAKER
git commit -m "trip: auto breaker"
git push origin main
坦白说这步我们还没全量开。主要卡在阈值:runner 抽风、网络抖一下都可能是误伤,自动熔断错一次比晚熔断十分钟还烦。现在是折中方案,CI 挂了先发通知,人确认过再手动拉闸。连续失败 N 次自动拉闸这个功能,阈值还没调出来,打算先定 2,没准儿过俩月又改回 1 了...
踩过的坑
- revert merge 之后想重新合入,得先 revert the revert,前面说过,再说一遍因为它真的坑;
- 开关要想清楚“谁能开、谁能关”,别出事的时候值班同学对着仓库干瞪眼;
- 有 release 分支的话,主干熔断时 release 往往也得跟着冻,不然修复代码合不进主干、热修全堆在 release 上,场面很难看;
- 所有拉闸、合闸都走 commit,别用口头通知,
git log就是最好的事故记录。
最后
拆开看全是土办法:一个文件、一段 CI、一个 hook。但它解决的不是技术问题,是“出事的时候谁喊停、怎么停得下来”。工具越简单越好,前提是团队先认一件事:主干红了,拉闸的人不需要跟任何人商量。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。