持续交付流水线从45分钟降到8分钟,我踩过的5个坑和3个反常识观点

🔑 关键词:持续交付, CI/CD流水线优化, GitOps, ArgoCD, 数据库迁移

📖 摘要:一个后端老鸟的持续交付实战笔记:为什么你的CD流水线越搞越慢?从Jenkins到GitLab CI再到ArgoCD,具体参数、镜像瘦身、测试分片、数据库回滚,还有那些没人告诉你的手动审批陷阱。

持续交付流水线从45分钟降到8分钟,我踩过的5个坑和3个反常识观点

图片

先说结论:我所在的小团队5个人,维护一个日活3万左右的SaaS后端。两年前我们的持续交付流水线跑一次要45分钟,现在稳定在8分钟以内。不是因为我们用了什么黑科技,而是把一堆“看起来正确”的做法给砍了。网上那些持续交付教程,动不动就让你全自动化、每天部署100次,说实话,那是给谷歌、Netflix看的。小团队照搬,死得很快。

坑1:把持续交付当成持续部署,结果半夜被叫醒

我最早的理解就是:代码合并到main,自动跑测试、自动构建、自动部署到生产。听起来很爽对吧?结果上线第三周,一个前端同事改了个按钮颜色,合并后自动部署,把生产环境的支付回调地址给覆盖了(因为环境变量文件被误提交)。凌晨2点被运维电话叫醒,回滚花了40分钟。

后来我坚持在流水线最后加一道手动审批门。不是不信任自动化,而是生产环境的总有几个人为判断:这次变更影响面多大?要不要先发10%流量?有没有对应的监控看板?手动审批不是落后,是给团队一个“深呼吸”的机会。ArgoCD的Sync Policy里可以设置automated: {prune: true, selfHeal: true},但我把生产环境的Application设成了手动Sync,只允许预发环境自动同步。这个改动之后,半夜告警少了70%。

图片

坑2:流水线慢,80%的时间花在重复下载依赖上

我们最早用Jenkins,每个stage都重新npm install和mvn dependency:resolve。一次构建45分钟里,有28分钟在下载依赖。后来换GitLab CI,加了缓存,但缓存key设计错了——用了$CI_COMMIT_REF_NAME,导致每个分支都重新下。改成基于package-lock.json和pom.xml的hash作为cache key,命中率从12%飙到87%。具体配置:

cache:
  key:
    files:
      - package-lock.json
      - pom.xml
  paths:
    - node_modules/
    - .m2/repository/

还有并行度。我们原来串行跑单元测试、集成测试、lint,后来拆成3个job并行,测试分片用--shard=1/3,单元测试从6分钟降到2分10秒。但注意,并行不是越多越好,我们试过拆成8个,结果GitLab Runner的CPU争抢反而让总时间多了1分钟。最后稳定在4个并行job。

坑3:Docker镜像1.2G,拉取就要3分钟

图片

有段时间每次部署,Kubernetes拉镜像都要3-4分钟。查了一下,镜像1.2G。里面塞了完整的JDK、Maven、甚至还有测试用的Chrome。后来改成多阶段构建,运行阶段只用eclipse-temurin:17-jre-alpine,把编译工具全部扔掉。镜像降到280M,拉取时间变成25秒。

具体做法:

# 构建阶段
FROM maven:3.8-eclipse-temurin-17 AS build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

# 运行阶段
FROM eclipse-temurin:17-jre-alpine
COPY --from=build /app/target/app.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

图片

另外,docker build一定要加--cache-from,在CI里把上一次的镜像作为缓存源,构建时间从4分钟降到50秒。

坑4:数据库迁移,那个让我凌晨3点回滚的Flyway脚本

持续交付里最容易被忽略的就是数据库变更。我们用了Flyway,但有一次开发写了个ALTER TABLE加索引,没加CONCURRENTLY,直接锁表8分钟。生产环境写入全挂。回滚?Flyway不支持自动回滚,只能手动写反向SQL。那次之后,我定了三条规矩:

  1. 所有DDL必须走flyway migrate,禁止手动改生产库。
  2. 加索引必须用CREATE INDEX CONCURRENTLY,并且单独一个迁移文件。
  3. 每个迁移脚本必须配一个undo脚本,放在db/undo/目录,虽然Flyway社区版不自动执行,但至少出事了有东西可查。

图片

现在我们的数据库迁移平均执行时间从12分钟降到90秒,因为把大表变更拆成了小批次,每次只改一个字段。

坑5:环境不一致,预发没问题生产就崩

预发环境用Docker Compose,生产用Kubernetes,配置全靠环境变量。结果有一次预发环境JAVA_OPTS是-Xmx512m,生产是-Xmx2g,导致GC行为完全不同,一个内存泄漏在预发没暴露,生产跑了6小时OOM。

后来我们强制用同一套Helm Chart,通过values.yaml区分环境,并且用kubectl diff在部署前检查差异。ArgoCD的Application资源里开syncPolicy: {syncOptions: ["CreateNamespace=true", "PruneLast=true"]},每次同步前先diff,人工确认。这个习惯让环境差异导致的事故从每月2-3次降到几乎为零。

三个反常识观点

图片

第一,持续交付的瓶颈不是技术,是团队对失败的容忍度。 我们花了大量时间做自动化测试,但真正让发布频率从每月2次变成每周5次的,是老板同意“允许每天挂一次,但必须5分钟内回滚”。回滚能力比发布能力更重要。

第二,小批量发布比全自动化更有效。 我们试过一天部署20次,结果监控看板全是噪音。后来改成每次只改一个功能,用Feature Flag控制开关,发布变成“合并代码+打开开关”两件事。LaunchDarkly太贵,我们用Unleash自建,成本每月不到20美元。

第三,不是所有服务都需要持续交付。 我们的核心支付服务还是手动审批+蓝绿部署,因为一次故障的代价太高。而内部管理后台,直接自动部署到生产,挂了也没人骂。区别对待,别一刀切。

最后说个具体数字:从45分钟到8分钟,我们主要做了三件事——缓存命中率提到87%、镜像从1.2G减到280M、测试并行拆成4片。没有重写流水线,没有换云厂商。如果你也在折腾持续交付,先打开你的CI日志,看看时间到底花在哪,别急着上ArgoCD、Tekton那些花哨玩意。

🏷️ 标签: