就因为package.json里少了个^号,我们公司赔了客户十万块

一个^号引发的十万事故
写这篇文章的时候,我刚通宵处理完一个P0级(最高级别)的线上事故,天刚亮,烟灰缸是满的🚬。

事故的原因,说出来你可能不信,不是什么服务器宕机,也不是什么黑客攻击,就因为我们package.json里的一个依赖项,少写了一个小小的^(脱字符号) 。

这个小小的失误,导致我们给客户A的数据计算模块,在一次平平无奇的依赖更新后,全线崩溃。而我们,直到客户的业务方打电话来投诉,才发现问题。

等我们回滚、修复、安抚客户,已经是7个小时后。按照合同的SLA(服务等级协议),我们公司需要为这次长时间的服务中断,赔付客户十万块

老板在事故复盘会上,倒没说什么重话,只是默默地把合同复印件放在了桌上。

满脸写着无奈.gif

今天,我不想抱怨什么,只想把这个价值 十万块 的教训,原原本本地分享出来,希望能给所有前端、乃至所有工程师,敲响一个警钟。


事故是怎么发生的?

我们先来复盘一下事故的现场。

我们有一个给客户A定制的Node.js数据处理服务。它依赖了我们内部的另一个核心工具库@internal/core

在项目的package.json里,依赖是这么写的:

{
  "name": "customer-a-service",
  "dependencies": {
    "@internal/core": "1.3.5",
    "express": "^4.18.2",
    "lodash": "^4.17.21"
    // ...
  }
}

注意看,expresslodash前面,都有一个^符号,而我们的@internal/core没有

这个^代表什么?它告诉npm/pnpm/yarn:“我希望安装1.x.x版本里,大于等于1.3.5最新版本。”

而没有^,代表什么?它代表:我安装1.3.5这一个版本,锁死它,不许变。

问题就出在这里。

上周,core库的同事,修复了一个严重的性能Bug,发布了1.3.6版本,并且在公司群里通知了所有人。

我们组里负责这个项目的同学,看到了通知,也很负责任。他想:core库升级了,我也得跟着升。

于是,他看了看package.json,发现项目里用的是1.3.5。他以为,只要他去core库的仓库,把1.3.5这个tag删掉,然后把1.3.6的tag打上去,CI/CD在下次部署时,重新pnpm install,就会自动拉取到最新的代码。

他错了!

最致命的锁死版本

因为我们的依赖写的是"1.3.5",而不是"^1.3.5",所以我们的pnpm-lock.yaml文件里,**把这个依赖的解析规则,彻底锁死在了1.3.5**。

无论core库的同事怎么发布1.3.61.3.7,甚至2.0.0...

只要我们不去手动修改package.json,我们的CI/CD流水线,在执行pnpm install时,永远、永远,都只会去寻找那个被写死的1.3.5版本。

然后,灾难发生了。

core库的同事,在发布1.3.6后,为了保持仓库整洁,就把1.3.5那个旧的git tag删掉了

然后,客户A的项目,某天下午需要做一个常规的文案更新,触发了部署流水线。

流水线执行到pnpm install时,pnpm拿着lock文件,忠实地去找@internal/core@1.3.5这个包...

“Error: Package '1.3.5' not found.”

流水线崩溃了。一个本该5分钟完成的文案更新,导致了整个服务7个小时的宕机😖

十万块换来的血泪教训

事故复盘会上,我们所有人都沉默了。我们复盘的,不是谁的锅,而是我们对依赖管理这个最基础的认知,出了多大的偏差。

^ (Caret) 和 ~ (Tilde) 不是选填,而是必填

  • ^ (脱字符) :^1.3.5 意味着 1.x.x (x >= 5)。这是最推荐的写法。它允许我们自动享受到所有 非破坏性 的小版本和补丁更新(比如1.3.61.4.0),这也是npm install默认的行为。

  • ~ (波浪号) :~1.3.5 意味着 1.3.x (x >= 5)。它只允许补丁更新,不允许小版本更新。太保守了,一般不推荐。

  • (啥也不写) :1.3.5 意味着锁死。除非你是reactvue这种需要和生态强绑定的宿主,否则,永远不要在你的业务项目里这么干!

我们团队现在强制规定,所有package.json里的依赖,必须、必须、必须使用^

关于lock文件

我们以前对lock文件(pnpm-lock.yamlpackage-lock.json)的理解太浅了,以为它只是个缓存。

现在我才明白,package.json里的^1.3.5,只是在定义一个规则。

而pnpm-lock.yaml,才是基于这个规则,去计算出的最终答案。

lock文件,才是保证你同事、你电脑、CI服务器,能安装一模一样的依赖树的唯一路径。它必须被提交到Git

依赖更新,是一个主动的行为,不是被动的

我们以前太天真了,以为只要依赖发了新版,我们就该自动用上。

这次事故,让我们明白:依赖更新,是一个严肃的、需要主动管理和测试的行为。

我们现在的流程是:

image.png
  1. 使用pnpm update --interactivepnpm会列出所有可以安全更新的包(基于^规则)。

  2. 本地测试:在本地跑一遍完整的测试用例,确保没问题。

  3. 提交PR:把更新后的pnpm-lock.yaml文件,作为一个单独的PR提交,并写清楚更新了哪些核心依赖。

  4. CI/CD验证:让CI/CD在staging环境,用这个新的lock文件,跑一遍完整的E2E(端到端)测试。

这十万块,是技术Leader(我)的失职,也是我们整个团队,为基础不牢付出的最昂贵的一笔学费。

一个小小的^,背后是整个npm生态的依赖管理的核心。

分享出来,不是为了博眼球,是真的希望大家能回去检查一下自己的package.json

看看你的依赖前面,那个小小的^,它还在吗?😠

作者:ErpanOmer

链接:https://juejin.cn/post/7568418604812632073

“分享、点赞、在看” 支持一波👍
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值