持续交付流水线从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。那次之后,我定了三条规矩:
- 所有DDL必须走
flyway migrate,禁止手动改生产库。 - 加索引必须用
CREATE INDEX CONCURRENTLY,并且单独一个迁移文件。 - 每个迁移脚本必须配一个
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那些花哨玩意。