一、问题背景:分支乱如麻,发布靠祈祷
在2021年,我们团队规模从40人扩张到200人,代码仓库从单仓裂变为30+子仓。混乱的Git使用方式成为最大瓶颈:
- 分支策略缺失:每个开发随意拉分支,
feature_xxx_final_v2这种名字满天飞。 - Code Review形同虚设:MR直接自我合并,没有任何保护。
- CI与Git脱节:Jenkins定时轮询,构建完也不知道是哪个分支的产物。
最痛的一次,开发A在release-1.2分支上热修,开发B误把dev分支合并进去,导致线上订单服务崩溃40分钟。那一次我们下定决心,必须设计一套适合大型团队的双轨制Git工作流。
二、环境与版本:技术栈基线
- GitLab:Community Edition 15.11(支持合并队列、MR规则引擎)
- Jenkins:2.414.2(Pipeline插件,使用Declarative语法)
- Argo CD:2.8.4(GitOps部署,同步策略为自动+自愈)
- Kubernetes:1.26.3(生产集群3节点,namespace隔离环境)
- Git版本:2.40.1(团队统一使用,禁止
--force-with-lease以外的强推)
三、双轨制方案设计:不是非黑即白
我们调研了Git Flow(Vincent Driessen)和Trunk Based(Paul Hammant),发现极端方案都不适用:
- Git Flow的
develop和release分支,在高速迭代下合并回master冲突极大。 - 纯TBD要求所有特性开开关,但我们的多个长周期项目(3-6个月)无法承受。
最终设计:
| 仓库类型 | 策略 | 适用场景 |
|---|---|---|
| 核心服务(订单、支付) | Trunk Based + 短生命周期特性分支 | 每日多次发布 |
| 业务中台(营销、会员) | Git Flow简化版(仅main/dev/feature) | 双周迭代 |
| 基础组件库(SDK) | 类似Git Flow,但强化release分支 | 月度发版 |
分支命名规范:
feature/{JIRA-ID}-{简短描述}
fix/{JIRA-ID}-{简短描述}
release/{version}
hotfix/{version}-{描述}
关键决策:main分支永远是稳定可部署的。所有合并到main的MR必须通过:2个Approver + 流水线绿灯 + 无未解决的Thread。
四、核心实现:分支保护与MR规则(含真实配置)
4.1 GitLab分支保护(通过GitLab API设置,不再用UI点击)
# 设置main分支保护:禁止任何人直接push,仅允许Maintainer合并
curl --request POST --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
"https://gitlab.example.com/api/v4/projects/${PROJECT_ID}/protected_branches" \
--data "name=main&push_access_level=0&merge_access_level=40&unprotect_access_level=0"
# 对release/*分支:开发者可push,但合并必须由Maintainer执行
curl --request POST --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
"https://gitlab.example.com/api/v4/projects/${PROJECT_ID}/protected_branches" \
--data "name=release/*&push_access_level=30&merge_access_level=40"
4.2 MR模板(.gitlab/merge_request_templates/Default.md)
## 变更描述(必填)
- 关联JIRA: [PROJ-1234](https://jira.example.com/browse/PROJ-1234)
- 变更类型: New Feature / Bugfix / Refactor
- 影响范围: 服务名 + 接口名
## 测试清单(勾选)
- [ ] 单元测试通过(`go test ./...` 覆盖率≥80%)
- [ ] 集成测试通过(`docker-compose -f test/docker-compose.yml up`)
- [ ] 本地已跑通 `make lint` 和 `make build`
## 自测截图/日志(粘贴关键输出)
## 部署说明
- 是否需要DB Migration: 是/否
- 是否有破坏性变更(Breaking Change): 是/否,说明兼容方案
4.3 Code Review强制指令(GitLab Code Owners + Merge Approval)
在仓库根目录添加CODEOWNERS文件:
# 核心模块必须由资深工程师review
/services/payment/* @team-payment-lead @backend-lead
/services/order/* @team-order-lead
# 所有Go文件至少需要两位Approver
*.go @backend-reviewers @xiaoming @xiaohong
在Settings -> Merge Request中开启:
- Approvals required: 2
- Prevent author approval: 开启(作者不能自批)
- Check if merged MR can be reverted: 开启
五、CI/CD联动:从MR到生产只花15分钟
这是整个工作流的精髓。我们使用Jenkins Pipeline监听GitLab事件,实现MR触发测试 → 合并触发镜像构建 → Argo CD自动同步。
5.1 Jenkinsfile(Declarative Pipeline,支持多分支)
pipeline {
agent { label 'go-builder' }
environment {
IMAGE_NAME = "registry.example.com/service/order-service"
IMAGE_TAG = "${env.BRANCH_NAME}-${env.GIT_COMMIT.take(8)}"
}
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Unit Test') {
steps { sh 'go test ./... -coverprofile=coverage.out' }
post {
success { junit '**/report.xml' }
}
}
stage('Build & Push') {
when { branch 'main' } // 只有main分支合并后构建镜像
steps {
sh """
docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
docker push ${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
stage('Update GitOps Repo') {
when { branch 'main' }
steps {
sh """
git clone https://token:${GITOPS_TOKEN}@gitlab.example.com/gitops/config.git
cd config
# 用sed替换镜像tag,然后commit & push
sed -i "s|image: registry.example.com/service/order-service:.*|image: ${IMAGE_NAME}:${IMAGE_TAG}|" overlays/prod/order-service/deployment.yaml
git commit -m "release: update order-service to ${IMAGE_TAG}"
git push origin main
"""
}
}
}
post {
failure {
// 通知飞书机器人
sh 'curl -X POST -H "Content-Type: application/json" -d "{\"msg_type\":\"text\",\"content\":{\"text\":\"[CI] ${JOB_NAME} 构建失败\"}}" https://open.feishu.cn/open-apis/bot/v2/hook/xxx'
}
}
}
5.2 Argo CD Application(GitOps部署,自动回滚)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: order-service-prod
namespace: argocd
spec:
project: default
source:
repoURL: https://gitlab.example.com/gitops/config.git
targetRevision: main
path: overlays/prod/order-service
destination:
server: https://kubernetes.default.svc
namespace: prod
syncPolicy:
automated:
prune: true
selfHeal: true # 自愈,K8s状态漂移自动恢复
allowEmpty: false
syncOptions:
- CreateNamespace=true
healthCheck:
- name: order-service
kind: Deployment
liveState: "deployed"
关键联动逻辑:
1. 开发推送分支 → GitLab触发MR → Jenkins跑单元测试(无分支限制)
2. MR合并到main → GitLab Webhook触发Jenkins的main分支流水线
3. Jenkins构建镜像并推送到Harbor,然后更新GitOps仓库的yaml文件
4. Argo CD检测到GitOps仓库变化,自动kubectl apply到生产集群
5. 如果Pod启动探测失败(LivenessProbe),Argo CD自动回滚到上一个健康版本
六、踩坑与优化:我们走过的弯路
6.1 坑1:Jenkins多分支扫描导致资源耗尽
现象:30个仓库、每个仓库100+分支,Jenkins磁盘和CPU爆满。
解决:设置orphanedItemStrategy丢弃超过7天未活跃的分支任务:
properties([
pipelineTriggers([pollSCM('')]),
disableConcurrentBuilds()
])
// 在Jenkins多分支job配置中,限制扫描深度:
// "Advanced" -> "Suppress automatic SCM triggering" 并设置cron为"H/6 * * * *"
6.2 坑2:Argo CD自动同步导致滚动更新风暴
现象:每次GitOps仓库push,Argo CD会对所有Deployment执行kubectl apply,即使镜像没变。
解决:在Application中加入ignoreDifferences:
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
- /spec/template/spec/containers/0/image
syncPolicy:
automated:
prune: true
selfHeal: true
# 添加这个:只同步有变更的资源
syncOptions:
- ApplyOutOfSyncOnly=true
6.3 坑3:Code Review形式化,MR没人认真看
解决:引入小步合并原则——单个MR改动超过500行自动阻塞,必须拆分。我们在GitLab CI中加了一个check:
# .gitlab-ci.yml 片段
check-mr-size:
stage: test
script:
- |
DIFF_LINES=$(git diff --numstat origin/main...HEAD | awk '{sum+=$1+$2} END {print sum}')
echo "MR改动行数: ${DIFF_LINES}"
if [ "$DIFF_LINES" -gt 500 ]; then
echo "❌ MR超过500行,请拆分为多个MR"
exit 1
fi
only:
- merge_requests
七、效果数据与总结
改造前后对比(基于2022年Q1 vs 2023年Q1数据):
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 发布频率(次/天) | 0.4 | 8 |
| 线上事故回滚时间(分钟) | 25 | 3 |
| 误合并到main的次数(次/周) | 3 | 0.1 |
| MR平均评审周期(小时) | 24 | 4 |
| 新员工上手时间(天) | 7 | 1.5 |
总结:双轨制不是理论上的最优解,但它是200人团队现实中的最优解。核心不在于选择哪个flow,而在于用自动化强制纪律——把分支保护、MR规则、CI/CD联动变成不可绕过的流水线关卡。如果你还在为分支混乱发愁,建议先做两件事:一是把main分支保护起来,二是强制Code Review流水线。这两点落地,至少能减少70%的合并灾难。
行动建议:
- 如果团队100人,建议按业务模块拆仓库,每个仓库独立走流程。
- 永远不要相信开发者的自觉,把规则写进代码和配置里。