小Cの已经记不起来的博客

Git 工作流中的 熔断 最佳实践

上周五下午四点多,一个 merge 把主干的单测干红了,但因为走的是 merge queue 排队合并,等发现的时候后面已经叠了七八个 PR,谁也说不清是从哪一单开始坏的。最后俩人对着 CI 日志倒着翻了快一个小时才定位到,回滚完各回各家。事后复盘我就一直在想,服务治理里有熔断这玩意儿,Git 工作流里凭什么只能靠人吼?主干一旦确认坏了,应该先把合入通道掐了,止损,再慢慢查原因。

下面是我们仓库里实测在用的一套做法,不复杂,但出事的时候真的能省命。

先说清楚这个“熔断”是什么

借用微服务里的概念:下游挂了就切断调用,别让雪崩扩大。放到 Git 这边,“下游”就是 main 的健康状态,“调用”就是往主干合代码的动作。所以 Git 熔断拆开就三步:

听起来平平无奇,但没有这套东西的时候,上面每一步都在靠嗓门大。

第一档: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。这权限只在事故时用,平时谁手痒谁请奶茶,目前执行得还行...

事前也顺手卡两道

熔断是事后止损,入口平时也得看住:

直推也要拦住:服务端 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 了...

踩过的坑

最后

拆开看全是土办法:一个文件、一段 CI、一个 hook。但它解决的不是技术问题,是“出事的时候谁喊停、怎么停得下来”。工具越简单越好,前提是团队先认一件事:主干红了,拉闸的人不需要跟任何人商量。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放