那个熬了三个通宵的上线夜
凌晨三点的杭州未来科技城,写字楼的中央空调早就停了,只有星点科技那间朝南的办公室里还亮着灯。烟灰缸里堆了快二十个烟头,运维工程师老张第无数次把凉透的咖啡倒掉,屏幕上的商家后台报错页面红得刺眼——就在两个小时前,他们刚上线的V1.0版本SaaS(Software as a Service,软件即服务)客户管理系统,让37个付费商家多扣了近2.3万的优惠券金额,客户投诉的电话已经把老板的手机打爆了。
<!-- 纸书:本章核心场景引入,通过真实上线事故引出CI/CD价值 --> <!-- 电子版:侧边栏【星点科技小档案】:成立于2022年,核心团队仅3人(后端开发1人、前端开发1人、测试1人),运维工作由创始人兼管,2023年拿到500万天使轮融资,主打面向中小商家的轻量SaaS客户管理系统,核心功能包括客户管理、订单 tracking、优惠券发放 --> <!-- Markdown:标签:上线事故、手动部署痛点、CI/CD价值、通俗类比 -->
三天前,老板就在部门群里发了最后通牒:“双十一之前必须上线,错过这波流量,咱们的钱撑不过明年春天。”整个团队忙了整整一个月,开发小周连续两周熬到凌晨,把优惠券、会员等级、支付回调三个核心模块赶了出来;测试阿琳熬了三个通宵,写了200多个测试用例,把能测的场景都测了一遍;老张更是提前一周就把服务器环境搭好,把上线 checklist 列了整整三页纸,连“上线后先刷10个订单测试”这种细节都写进去了。所有人都觉得这次上线稳了,直到上线前半小时,意外还是发生了。
“周哥,你刚才改的优惠券逻辑怎么没同步给我?我这边测的支付场景里,满100减30的券和满200减50的券叠加的时候,金额算错了。”阿琳抱着笔记本电脑冲到开发工位,声音里带着明显的焦虑。 小周正盯着屏幕改最后一行代码,头也不抬地回:“我刚改了个边界bug,就是优惠券金额超过订单50%的时候自动抵扣上限的问题,本地跑得好好的,你重新测下呗。” “你改代码能不能提前说一声?我这边的测试用例还没覆盖到这个场景,而且你上次提交的代码里还没同步最新的支付SDK,我测的环境里是最新的SDK,你本地的是旧的,能一样吗?”阿琳的语气已经带了点火气。 “我本地没问题啊,你怎么不说是你环境的问题?”小周也来了脾气,手指在键盘上敲得噼里啪啦响。 两个人吵得不可开交的时候,老张端着泡面从运维室走过来,手里还攥着那张写了三页的上线 checklist:“行了行了,都别吵了,还有二十分钟就要上线了,先把代码合到主分支,我这边重新打包。” “老张,我刚提交的代码你千万别打进去,还没测呢!”小周突然喊了一声,手里还攥着还没按下的“提交”按钮。 “知道知道,我先打上周测试通过的版本,你代码测完了再合过来。”老张摆摆手,坐回了自己的工位。
<!-- 电子版:侧边栏【老张的吐槽:手动部署的“七宗罪”】1. 代码全靠人传,版本乱得像一团毛线 2. 环境全靠人搭,开发、测试、生产三个样 3. 测试全靠人跑,漏测是家常便饭 4. 打包全靠人敲,漏文件、版本错是常事 5. 部署全靠人点,手滑点错按钮的事故每年都有 6. 日志全靠人记,出问题找不到原因 7. 锅全靠人甩,出了问题没人负责 -->
然而忙中出错是人的本能。老张打包的时候接了个老板的电话,问上线进度,挂了电话之后才发现,自己刚才打包的时候,不小心把小周上周提交的旧版本代码打了进去——小周上周改优惠券逻辑的时候,把抵扣上限设成了订单金额的30%,昨天刚改成50%,结果这个旧版本包一进去,生产环境的优惠券直接变成了满100最多减30,不管叠加多少张券都是这个数。更要命的是,小周提交代码的时候忘了备注改了什么东西,只写了“fix bug”,老张根本没注意到版本的问题,阿琳的测试用例里也没有“叠加3张以上优惠券”的场景,三个人就这么把带着bug的版本推到了线上。
上线后十分钟,第一个商家的投诉电话就打了过来:“我们系统里的订单,客户付100块只扣了70块,优惠券怎么算的?”小周先慌了,赶紧登后台看代码,翻了十分钟没找到问题;阿琳赶紧跑测试用例,测了三遍也没发现问题;老张查服务器日志,看了半天也没看到代码更新的记录。三个人在会议室里吵得面红耳赤,老板冲进来的时候,屏幕上已经跳出来17个投诉订单。 “到底是代码的问题还是打包的问题?我怎么跟客户交代?”老板的声音都在抖。 “我代码本地跑得好好的,肯定是打包的时候出问题了!”小周先甩锅。 “我的测试用例里没有叠加3张券的场景,而且开发提交代码的时候没同步SDK,我怎么测?”阿琳也不甘示弱。 “你们提交的代码乱七八糟,版本都不统一,我打包的时候怎么知道哪个是对的?”老张也急了,他熬了三个通宵,本来答应周末带女儿去参加幼儿园的亲子运动会,结果这次上线直接泡了汤。 最后还是小周一行行翻代码,花了两个小时才找到问题:旧版本包里还留着上周的优惠券上限逻辑,加上没同步的SDK版本,导致叠加计算的时候直接跳过了上限校验。等他们把bug改完,重新打包部署的时候,天已经快亮了,37个投诉商家,总共多扣了2.3万,为了安抚客户,不仅退了钱,还送了三个月的免费高级会员,额外损失了1.7万,加上浪费的人力,这次上线总损失超过了6万。
老张拖着疲惫的身子回到家的时候,女儿已经去上学了,餐桌上放着女儿给他画的画:画着一个穿红披风的超人爸爸,旁边歪歪扭扭地写着“希望爸爸下次早点回来”。老张看着画,鼻子一酸,这三个通宵他掉了快半斤头发,眼角的血丝都快连成一片了,他搞了这么多年运维,每次上线都要熬通宵,每次出问题都要背锅,难道就没有办法解决吗?
他打开技术社区,想找找有没有什么自动化的部署方案,翻了十几页帖子,突然看到一个大标题:“从月更到日更:我们是如何用CI/CD把上线时间从8小时压缩到10分钟的”。他点进去,正文里的一行字突然戳中了他:“CI/CD(Continuous Integration/Continuous Deployment,持续集成/持续部署)不是什么高大上的黑科技,它就是软件行业的全自动奶茶机,把原本分散的各个环节串起来,自动执行,自动检查,从根源上消灭‘人忘了、人搞错、人没同步’的问题。”
全自动奶茶机?老张突然笑了,他上周刚带女儿去买过奶茶,那个扫码点单之后自动出餐的机器,他确实见过。他想起以前在大厂的时候,团队200多号人,每个月要发几十个版本,从来没熬过通宵,出问题了直接看流水线日志,哪一步出错一目了然,那时候他用的就是CI/CD啊,怎么到了小团队,反而忘了这个东西?
<!-- 纸书:图1-1 传统手动部署流程,每个环节均需人工介入,出错概率随环节增加呈指数级上升 --> <!-- 电子版:可点击查看高清流程图 --> ```mermaid graph TD A[开发本地改代码] --> B[手动提交到代码仓库] B --> C[测试手动拉取代码] C --> D[测试环境手动部署] D --> E[测试用例人工执行] E -->|测试不通过| A E -->|测试通过| F[运维手动打包] F --> G[运维手动上传到服务器] G --> H[运维手动执行部署脚本] H --> I[生产环境人工验证] I -->|验证不通过| J[人工排查问题] J --> A I -->|验证通过| K[上线完成] style A fill:#ffcccc style B fill:#ffcccc style C fill:#ffcccc style D fill:#ffcccc style E fill:#ffcccc style F fill:#ffcccc style G fill:#ffcccc style H fill:#ffcccc style I fill:#ffcccc style J fill:#ffcccc ```
他点进那篇帖子的正文,仔仔细细看了起来。作者是个初创团队的运维,情况和星点科技几乎一模一样:团队10个人,每次上线都要三方凑时间,至少耽误一天,每个月上线4次,一年就是48个工作日,加上出问题修复的时间,一年至少浪费20万。后来他们搭了一套最简单的CI/CD流水线,提交代码自动跑测试,测试通过自动打包,一键就能部署到测试环境,上线前只需要人工确认一下,现在每个月可以上线十几次,再也没熬过通宵,出问题的概率降了90%。
老张算了算星点科技的上线成本:每次上线要凑开发、测试、运维三个人的时间,至少耽误一天,三个人一天的人力成本就是6000块,一个月上线4次,一年就是28.8万;加上这次事故的6万损失,还有之前几次小事故的损失,一年在上线这件事上浪费的钱快40万了,这还不算耽误的营收。而CI/CD的成本呢?作者在帖子里说,他们用的全是开源工具,GitLab做代码仓库,Jenkins做流水线引擎,Docker做环境打包,Ansible做部署,总共花了3天时间搭好,除了服务器成本,一分钱没花。
他接着往下看,作者用奶茶店的例子解释CI/CD,和老张想的几乎一模一样:“在奶茶机出现之前,街边奶茶店的工作流程是:点单员接单之后手写小票给后厨,后厨按小票做奶茶,做完了喊服务员端给客人。如果点单员改了优惠规则(比如第二杯半价),没告诉后厨,后厨还是按原价算,服务员也没核对,客人拿到账单的时候自然会炸。而全自动奶茶机把所有环节串在了一起:你点单之后,系统自动核对优惠规则、自动按配方配比、自动出餐,每一步都有日志记录,哪一步出了问题直接调机器参数就行,根本不会出现‘规则改了没人知道’的荒唐事。CI/CD就是软件行业的‘全自动奶茶机’,它把写代码、测代码、打包、部署这些原本分散的环节全部串起来,自动执行、自动检查,从根源上消灭‘人忘了、人搞错、人没同步’的问题。”
老张越看越兴奋,他想起这次事故的根因:代码版本混乱,小周改代码的时候没pull最新的主分支,导致提交的代码里漏了SDK的更新;环境不一致,开发本地、测试环境、生产环境的依赖版本不一样,旧版本的pandas库有优惠券计算的bug,本地和测试都没问题,生产就炸了;流程没有标准,没有提交代码必须跑测试的规则,没有打包必须校验版本的规则,没有上线前的自动检查清单,全靠人记,一忙就错;出了问题没有日志可查,不知道哪一步改了什么,只能互相甩锅。这些问题,不就是CI/CD要解决的核心问题吗?
他翻到帖子的最后,作者列了一套适合10人以下小团队的极简CI/CD方案,第一步先实现“提交代码自动跑单元测试”,解决代码质量和版本统一的问题;第二步实现“测试通过自动打包”,解决环境一致和打包出错的问题;第三步实现“一键部署到测试环境”,解决测试环境和生产环境一致的问题;等团队再大一点,再加自动部署到生产的环节。整个方案不需要复杂的配置,花一两天就能搭好,完全适合星点科技现在的团队规模。
<!-- 纸书:图1-2 CI/CD自动化流水线流程,除上线确认环节外全部自动执行,大幅降低人工出错概率 --> <!-- 电子版:可点击查看高清流程图 --> ```mermaid graph TD A[开发提交代码到仓库] --> B[流水线自动触发构建] B --> C[自动运行单元测试] C -->|测试不通过| D[自动通知开发修改] D --> A C -->|测试通过| E[自动打包生成版本包] E --> F[自动部署到测试环境] F --> G[自动运行集成测试] G -->|测试不通过| D G -->|测试通过| H[等待人工确认上线] H --> I[自动部署到生产环境] I --> J[自动运行线上健康检查] J -->|检查不通过| K[自动回滚到上一个版本] J -->|检查通过| L[上线完成,自动发送通知] style A fill:#ccffcc style B fill:#ccffcc style C fill:#ccffcc style D fill:#ccffcc style E fill:#ccffcc
老张把帖子末尾的极简方案复制到自己的加密笔记里,指尖因为长时间敲键盘已经微微发僵。他走到窗边,看着楼下凌晨三点的路灯把沥青路染成暖黄色,突然想起三个月前那次差点让公司破产的会员积分事故——那次事故和这次的优惠券事故如出一辙,只是后果更严重。
那是星点科技成立半年的时候,小周花了三周时间做了会员积分体系:用户下单1元积1分,1000积分可以抵10块钱。上线前阿琳测了三遍,老张也提前搭好了生产环境,所有人都觉得万无一失。结果上线后不到两个小时,客服就炸了:所有用户下单都送了1000积分,不管订单金额是多少,甚至0.01元的订单都送了1000积分。一查才知道,小周改代码的时候忘了pull最新的主分支,自己本地还是旧版本的代码,旧版本里没有“订单金额满1元才积分”的校验逻辑,他直接把自己的本地代码提交了上去,覆盖了阿琳刚更新的校验规则。那时候他们还没有CI/CD,没有自动跑测试的规则,也没有代码合并的检查,小周提交代码之后没人核对,老张打包的时候也没看版本日志,直接把带着bug的版本推到了线上。等发现的时候,已经多送了几十万积分,相当于几万块的现金,公司账户里的钱差点不够发工资。最后还是老张连夜写了个脚本,把所有异常积分都收回来,给用户发了道歉短信,送了一个月的会员,才没闹到工商局。那次事故之后,团队开了三天会,定了各种规则,比如提交代码必须备注改了什么东西,上线前必须三个人都签字确认,结果规则写了满满三页,下次上线还是照样忘。
老张叹了口气,又想起上个月的支付回调事故:生产环境的Python版本是3.7,有个第三方支付SDK的bug,回调金额会四舍五入,开发本地用的是3.8,测试环境用的是3.9,都没有这个bug,所以测了三次都没问题,一上生产就出问题,导致几十个订单支付成功但是订单状态没更新,用户投诉到支付平台,差点被封号。那次他们花了整整两天时间排查问题,最后才发现是Python版本的问题,老张当时还特意把所有环境的Python版本都统一成了3.9,结果这次优惠券事故里,开发本地用的是3.9,测试环境用的是3.9,生产环境的Docker镜像里却打包了旧的3.7版本的依赖包,还是因为打包的时候没人核对版本导致的。
他把这三次事故的原因列在笔记本上: 1. 代码版本混乱:提交代码不pull最新分支、不备注改动内容,导致旧版本bug被带到生产环境 2. 环境不一致:开发、测试、生产环境的依赖、版本、配置不统一,本地和测试没问题,生产就炸 3. 流程无约束:没有自动检查规则,全靠人记,一忙就忘,漏测、漏改、漏核对是常事 4. 无日志可查:出问题不知道哪一步改了什么,只能互相甩锅,排查问题全靠人肉翻代码 5. 上线成本极高:每次上线要凑三个人,至少耽误1.5天,一个月上线4次,一年浪费近20个工作日
他掏出计算器,按着团队三个人的工资算:后端小周月薪2万,测试阿琳月薪1.8万,他自己月薪2.2万,按每月22个工作日算,平均每人每天的人力成本是(20000+18000+22000)/22≈2736元。每次上线需要三个人凑1.5天,单次上线的人力成本就是2736*3*1.5≈12324元,一年上线48次,光是上线的人力成本就是12324*48≈59万,加上三次事故的直接损失6万、间接损失3万,还有耽误的业务机会损失,一年在上线这件事上浪费的钱快70万——这相当于他们半个月的营收。
而搭这套CI/CD的成本呢?帖子作者说全是开源工具,GitLab他们已经在用,Jenkins、Docker、Ansible全是免费的,只需要3天时间搭好,除了每个月几百块的服务器成本,一分钱额外的钱都不用花。搭好之后,每次上线只需要10分钟,不用凑三个人,老张自己点一下确认就行,一年能省出至少15个工作日,光人力成本就能省4万多,还不算事故损失和业务机会损失,这笔账怎么算都划算。
<!-- 电子版:侧边栏【星点科技上线事故档案】 | 时间 | 事故原因 | 直接损失 | 间接损失 | |------|----------|----------|----------| | 2023年6月 | 代码版本混乱,旧版本逻辑被带到生产 | 0元(连夜修复) | 耽误3天业务上线,损失约2万 | | 2023年8月 | 环境不一致,生产Python版本有SDK bug | 1.2万(支付平台罚款+用户赔付) | 耽误2天修复,损失约1.8万 | | 2023年10月 | 无自动检查,优惠券逻辑bug未测出 | 3.9万(用户退款+会员赠送) | 团队熬3个通宵,损失约2.1万 | -->
他接着往下看帖子的正文,作者用奶茶店的例子解释CI/CD,他越看越觉得贴切,干脆把整个对应关系记在了笔记本上: > 传统手动部署 = 街边传统奶茶店 > 顾客点单 = 开发提交代码 > 点单员手写小票给后厨 = 开发手动提交代码到仓库,备注改了啥全靠记 > 后厨按小票做奶茶 = 测试手动拉代码、手动部署到测试环境、手动跑测试用例 > 服务员核对小票和奶茶 = 运维手动打包、核对版本、部署到生产 > 顾客拿到错的账单 = 上线出bug,客户投诉 > 老板追责 = 开发、测试、运维互相甩锅 > 全自动奶茶机 = CI/CD流水线 > 顾客扫码点单 = 开发提交代码到代码仓库 > 系统自动核对最新菜单和优惠规则 = 流水线自动拉取最新代码、检查代码规范 > 自动核对原料有效期、配比是否正确 = 自动运行单元测试、集成测试、安全扫描 > 自动按标准配方制作奶茶 = 测试通过自动打包成标准化镜像 > 自动放到取餐台让服务员核对 = 自动部署到测试环境,自动跑测试用例 > 服务员确认没问题叫号 = 产品/运维手动确认上线 > 顾客取走奶茶 = 自动部署到生产环境 > 自动核对订单和出餐是否一致 = 自动跑线上健康检查,有问题立刻自动回滚
“原来CI/CD就是这么回事啊。”老张忍不住笑出了声,他上周刚带女儿去买过奶茶,那个扫码点单之后不到1分钟就出餐的机器,他当时还觉得神奇,没想到居然和软件部署的逻辑一模一样。他以前在大厂的时候,团队200多号人,每个月要发几十个版本,从来没熬过通宵,出问题了直接看流水线日志,哪一步出错一目了然,哪次提交的代码有问题直接就能查到,那时候用的就是CI/CD,怎么到了小团队,反而忘了这个好东西?
他翻到帖子里的成本对比部分,作者列了非常具体的数字:他们团队8个人,之前每次上线要凑4个人(2个开发、1个测试、1个运维),至少耽误1天,一个月上线4次,一年就是16个工作日,人力成本近10万,加上之前几次上线事故的损失,一年在上线这件事上浪费近20万。搭好CI/CD之后,每次上线只需要运维点一下确认,10分钟就能搞定,一年省出至少12个工作日,加上事故损失全部省掉,一年至少省15万,搭流水线的成本只有3天的人力成本,不到5000块,不到一个月就回本了。
<!-- 纸书:表1-1 手动部