周四下午四点半,我提交了这周的上线申请。按流程,运维同事会在周五晚上操作,顺利的话凌晨前能发完,然后我守着手机等告警——这已经算快的了。遇到测试环境跟生产环境有差异、配置文件漏改、流量切不过来这些问题,一个晚上就交代进去了。后来我把这套流程从头到尾重做了一遍,从提交代码到生产环境新的服务实例真正开始接流量,现在稳定在3分钟左右,而且全程不用人工盯着。这篇文章把我当时的改造思路、每一步的取舍、以及跑起来之后踩过的坑都写清楚,给还在手工上线泥潭里的团队一个参考。
1. 动手改造前,先算清一天时间都花在了哪里
很多团队一提"上线提速",第一反应就是去买Jenkins、买GitLab CI、上K8s,结果工具买了一堆,流程还是慢。我的经验恰恰相反:先别急着上工具,先把"一天"这24小时拆开,看看到底被什么东西吃掉了。
我当时让团队把一次完整上线的全过程记录下来,从提交代码到线上验证通过为止,每一段操作都标上人和时间。整理出来大概是这样的:
| 环节 | 耗时 | 谁在做 | 时间去哪了 |
|---|---|---|---|
| 打包 | 20分钟 | 开发本地 | Maven下载依赖,本地环境不一致导致反复打包 |
| 上传服务器 | 10分钟 | 开发 | scp传jar包,网络波动重传 |
| 备份旧包 | 15分钟 | 运维 | 手动cp、记录版本号、写回滚说明 |
| 停服 | 5分钟 | 运维 | 通知在线用户、kill进程 |
| 替换配置 | 30分钟 | 开发+运维 | 逐个核对环境差异,经常漏改 |
| 启动+日志确认 | 20分钟 | 运维 | 启动失败回滚再来一遍 |
| 冒烟测试 | 40分钟 | 测试 | 手工点主流程,发现bug再返工 |
| 等待与沟通 | 6小时以上 | 所有人 | 等审批、等别人干完、半夜被叫醒确认 |
这个表列完,所有人都沉默了。真正花在"打包上传"上的时间其实不到1小时,剩下的全在等待、沟通、重复劳动和返工上。其中最致命的三件事:
- 环境不一致导致"我本地是好的"这个问题反复出现,一出现就是半小时起步的排查。
- 配置散落在服务器上,每次上线都要人工改,改错一个端口或开关就是一次事故。
- 没有自动化的验证手段,上线后靠人肉点页面,发现问题时已经过去了半小时。
所以后面所有改造,本质上都是在解决这三个问题。自动化只是手段,不是目的。
2. 提速的地基:先把"可变的东西"全部固化
很多人一上来就搭流水线,流水线跑通了,但上线还是慢,因为他在流水线里做的还是以前那套手工操作——只不过手动变成了半自动。我没有急着写pipeline,而是先花了两周时间做标准化。这一部分看着不性感,但恰恰是后面能把时间压到3分钟的关键。
2.1 镜像化:让"环境差异"这个词从字典里消失
以前每个环境都有一台"有感情的服务器",配置改来改去,谁也说不清生产上到底是什么状态。我做的第一件事,是把应用直接容器化,所有环境跑同一个镜像。
具体来说:
- 代码里只保留一套配置模板,环境相关的变量全部提取成环境变量或者配置中心条目。
- Dockerfile只做一件事:把构建好的产物打进镜像,不包含任何环境信息。
- 开发、测试、预发、生产这四个环境,用同一份镜像启动,差异只体现在环境变量上。
这样做的好处,表面上是"环境更一致了",实际上是把排查环境问题的成本从每次上线的常规操作里彻底删掉了。"测试好好的,上生产就挂"这种事,镜像化之后再也没发生过,因为跑的东西物理上就是同一个。
2.2 基础设施代码化:服务器不是"改出来的",是"定义出来的"
服务器上乱七八糟的部署目录、启动脚本、定时任务,以前靠人肉维护。我把这些都收编成了代码,放在仓库里,用IaC工具去管理。每次要改什么,先改代码、走评审、再应用到目标环境。
这一步的收益不是马上能看到的,但它是后面自动化流水线的唯一地基。没有它,流水线就算能自动部署,也依然是盲人摸象——不知道线上最终长什么样。
2.3 一键回滚能力:敢提速的前提
做完了标准化,我还特意做了个"一键回滚"的预案。回滚这个能力,平时用不上,但没有它,谁都不敢把上线流程改成全自动。我当时的做法是:保留最近N个镜像版本,回滚操作就是重新拉起前一个版本的镜像,整个操作在流水线里是一个已封装好的"回滚按钮"。这个按钮我一直到后面才公开给团队用,但它的存在让所有人对自动化上线有了信心。
3. 流水线怎么设计:从push到生产,中间只留关键关卡
标准化做完之后,我才开始搭真正的发布流水线。这条流水线从开发者git push触发,到生产环境滚动更新完成,前后一共跑11个步骤,其中需要人工确认的只有一步。
3.1 流水线的整体结构
我用的是GitLab CI,但也无所谓具体工具,思路是一样的。整个流程如下:
stages: - lint - test - build - scan - image - release - deploy - health每一步具体做什么:
- lint阶段:跑静态代码检查,发现低级错误直接拦截,MR都合不进来。
- test阶段:跑单元测试。我们要求核心模块覆盖率不低于80%,低于就失败。
- build阶段:编译打包,这一步的结果会被后面直接使用。
- scan阶段:镜像安全扫描,检查基础镜像和依赖库有没有已知漏洞,有高危的就卡住。
- image阶段:把构建产物打进镜像,push到私有镜像仓库,打上git短SHA的tag。
- release阶段:生成发布单,记录本次变更的commit范围、镜像地址、涉及的配置项。到这里会等一个负责人的approval,这是全程唯一的人工关卡。
- deploy阶段:K8s滚动更新,新的Pod先起来,Ready之后再摘旧的。
- health阶段:自动跑健康检查和冒烟接口,全部通过才标记本次上线成功。
这条流水线,从代码push到镜像推完,大概需要2分钟出头(后面细说为什么这么快)。人工批准后,部署和健康检查总共花不到1分钟。所以整体下来,真正的手按按钮到完成,就是3分钟的量级。
3.2 为什么只保留一道人工关卡
以前上线要经过好几道审批,开发领导签、测试领导签、运维领导签,签完黄花菜都凉了。我保留了"release阶段"的一次人工确认,原因是:自动化能保证"无故障",但保证不了"这件事该不该此刻做"。比如周五晚上十一点发起上线,哪怕流水线全绿,也未必该发。所以流程可以自动化,决策依然要留给人。
这道关卡我设计成跟流水线解耦的:负责人在手机上点一下就放行,不用登录服务器、不用看日志,所有的信息——改了哪些代码、哪些服务受影响、回滚方案是什么——都已经在发布单里自动生成好了。决策者只需要回答一个问题:现在发,可以吗?
4. 3分钟账单:每一步从多少秒压到了多少秒
很多人不信3分钟能上线,觉得构建一个Java应用都不止3分钟。我当时的优化思路不是"拼命压每一个环节",而是搞清楚哪些环节可以砍掉,哪些环节可以并行,哪些环节必须保留。
4.1 砍掉的环节:从源头消灭等待
原来的流程里,打包、上传、备份、改配置、停服、启动、冒烟,这些步骤之间存在大量的串行等待:A做完了要通知B,B做完了再通知C。流水线把这些环节全部串起来自动跑,步骤之间零等待。这是最粗暴也最有效的一刀。
4.2 压下去的环节:构建从20分钟到80秒
构建是整个流水线里最耗时的一环。我做了三件事:
- 依赖缓存挂到持久化存储上,Maven/Gradle的依赖不用每次重新下载,直接命中缓存。这一步省掉的时间最多,光依赖下载就占了原来构建时间的80%。
- 多模块并行编译,以前串行编的模块改成并行,吃满机器的CPU。
- 镜像分层缓存,基础镜像不动,业务代码只打最后一层,push镜像的时候只传增量部分。
这三样做完,单次构建从20分钟压到了80秒左右,镜像推送从几分钟压到了30秒内。
4.3 并行化:能同时做的绝不排队
流水线里,安全扫描和构建是并行跑的。扫描不通过会拦截后面的部署,但不会拖慢构建本身。另外,测试阶段和代码检查也是并行的,互不等待。整体算下来,并行大约省了整条流水线1/3的时间。
下面是优化后的实际耗时明细:
| 环节 | 优化后耗时 | 关键优化点 |
|---|---|---|
| 代码检查 | 10秒 | 只查变动文件,不查全量 |
| 单元测试 | 25秒 | 并行跑,差异测试先行 |
| 编译打包 | 60秒 | 依赖缓存+多模块并行 |
| 镜像构建推送 | 30秒 | 分层缓存+增量推送 |
| 安全扫描 | 并行,不额外占时 | 边构建边扫 |
| 部署滚动更新 | 40秒 | K8s滚动,不断流量 |
| 自动健康检查 | 20秒 | 接口探活+核心链路冒烟 |
加起来,流水线全绿到生产就绪,平均3分07秒。这个数字不是我拍脑袋估的,是跑了接近一个月之后从CI系统里导出的平均值。
5. 跑起来之后踩过的坑:自动化上线不是一劳永逸
流水线从"能跑"到"稳定跑"之间,隔着一堆坑。我把踩过的有代表性的几个记录下来,你们未来大概率也会遇到。
5.1 流水线全绿,数据库却漏了脚本
第一次自动化上线成功的半个月后,有一次发版,流水线全绿,但线上查询直接报错。排查了半天,发现某个SQL迁移脚本没有执行。以前手工上线时,这一步是"记得跑一下"的,自动化之后没人记得,机器也不会替你记得。
我的解决办法是:数据库变更也纳入同一个发布单,用专门的方式管理版本化SQL脚本,流水线在部署前自动检测并执行未应用的脚本。注意,这个话题本身很复杂,微信篇幅未必装得下,但你们在做自动化时一定记得把数据库变更纳入流程,不要放在脑子或聊天记录里。
5.2 滚动更新确实不断流量,但连接没刷新
K8s滚动更新确实能做到不中断服务,但我们的网关、本地连接池、长连接这些,不会因为Pod被替换就自动重新连。第一次自动化发布之后,我们收到用户反馈"页面偶尔白屏"。
排查下来,是旧的Pod还在处理存量连接,新的Pod起来了,但入口流量还往老Pod上打,而老Pod在优雅终止期被强制干掉了。解决办法有两个:一是把优雅终止时间拉长,给存量请求足够的处理时间;二是在preStop钩子里主动把Pod从服务发现里摘掉,再等几秒才真正退出。这两步做完,白屏问题再没出现过。
5.3 权限越大,事故越大
自动化上线意味着任何能触发流水线的人,理论上都拥有直接发生产的权限。我们发生过一次乌龙:有人想跑测试环境的流水线,参数选错,把生产环境覆盖了。虽然回滚很快,但这事儿吓出了我一身冷汗。
后续做了三件事:
- 环境隔离,测试流水线和生产流水线分开,生产环境只能由特定的受保护分支触发。
- 敏感操作加二次确认,生产环境的部署,除了release阶段的人工审批,还需要操作人在群里报备。理由很简单:让团队看到谁在什么时候发了什么。
- 审计日志全留,每一次上线自动生成一份完整记录,包括谁触发的、哪个commit、哪个镜像、部署结果如何。
自动化提升的是效率,但权限和审计的能力必须跟着提升,不然效率提升反而会放大风险。
6. 上线提速这件事,真正难的不是技术
最后想聊点题外话。这套东西的技术含量,说实话不算高,CI、容器化、K8s滚动更新,每一块都有大量现成文档。真正难的是两点:
第一,团队愿不愿意为"看不见的功夫"花时间。我做标准化那两周,流水线一条没搭,看起来毫无进展,团队成员每天的活还是照旧,但上线速度一点没变。如果没有管理层的信任,"你这两天在干嘛"这句话就能让整个改造胎死腹中。
第二,每个人愿不愿意改变工作习惯。以前上线是开发者把包丢给运维就行,现在开发者要自己写Dockerfile、要关心健康检查要不要加、要理解滚动更新的机制。对一些人来说,这是额外负担;但对我而言,谁写代码,谁就该对怎么把代码跑起来负全责。这个理念贯通之后,整个团队的发布能力和质量意识都上了一个台阶。
现在每次发布,docker构建日志刷完,然后看到K8s滚动更新起新Pod、等Ready、摘老Pod,一套操作在眼前自动走完,最后钉钉弹出一条"v2.3.1发布成功,耗时3分02秒"的机器人消息。说实话,第一次跑通那天,我在屏幕前愣了挺久——同一个上线,以前是一群人折腾一整天,现在是一个人在手机上点一下确认。这个体感差异,值得任何团队去折腾一次。