
寫這篇文章的時候,我剛通宵處理完一個P0級(最高級別)的線上事故,天剛亮,煙灰缸是滿的🚬。
事故的原因,說出來你可能不信,不是什麼伺服器宕機,也不是什麼駭客攻擊,就因為我們package.json裡的一個依賴項,少寫了一個小小的^(脫字符號)。
這個小小的失誤,導致我們給客戶A的數據計算模組,在一次平平無奇的依賴更新後,全線崩潰。而我們,直到客戶的業務方打電話來投訴,才發現問題。
等我們回滾、修復、安撫客戶,已經是7個小時後。按照合同的SLA(服務等級協議),我們公司需要為這次長時間的服務中斷,賠付客戶十萬塊。
老闆在事故復盤會上,倒沒說什麼重話,只是默默地把合同複印件放在了桌上。
今天,我不想抱怨什麼,只想把這個價值 十萬塊 的教訓,原原本本地分享出來,希望能給所有前端、乃至所有工程師,敲響一個警鐘。
我們先來復盤一下事故的現場。
我們有一個給客戶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"
    // ...
  }
}
注意看,express和lodash前面,都有一個^符號,而我們的@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.6、1.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/[email protected]這個包...
“Error: Package '1.3.5' not found.”
流水線崩潰了。一個本該5分鐘完成的文案更新,導致了整個服務7個小時的宕機😖。
事故復盤會上,我們所有人都沉默了。我們復盤的,不是誰的鍋,而是我們對依賴管理這個最基礎的認知,出了多大的偏差。
^ (Caret) 和 ~ (Tilde) 不是選填,而是必填
^ (脫字符) :^1.3.5 意味著 1.x.x (x >= 5)。這是最推薦的寫法。它允許我們自動享受到所有 非破壞性 的小版本和補丁更新(比如1.3.6, 1.4.0),這也是npm install默認的行為。~ (波浪號) :~1.3.5 意味著 1.3.x (x >= 5)。它只允許補丁更新,不允許小版本更新。太保守了,一般不推薦。1.3.5 意味著鎖死。除非你是react或vue這種需要和生態強綁定的宿主,否則,永遠不要在你的業務專案裡這麼幹!我們團隊現在強制規定,所有package.json裡的依賴,必須、必須、必須使用^。
關於lock文件
我們以前對lock文件(pnpm-lock.yaml, package-lock.json)的理解太淺了,以為它只是個緩存。
現在我才明白,package.json裡的^1.3.5,只是在定義一個規則。
而pnpm-lock.yaml,才是基於這個規則,去計算出的最終答案。
lock文件,才是保證你同事、你電腦、CI伺服器,能安裝一模一樣的依賴樹的唯一路徑。它必須被提交到Git。
依賴更新,是一個主動的行為,不是被動的
我們以前太天真了,以為只要依賴發了新版,我們就該自動用上。
這次事故,讓我們明白:依賴更新,是一個嚴肅的、需要主動管理和測試的行為。
我們現在的流程是:

pnpm update --interactive:pnpm會列出所有可以安全更新的包(基於^規則)。pnpm-lock.yaml文件,作為一個單獨的PR提交,並寫清楚更新了哪些核心依賴。staging環境,用這個新的lock文件,跑一遍完整的E2E(端到端)測試。這十萬塊,是技術Leader(我)的失職,也是我們整個團隊,為基礎不牢付出的最昂貴的一筆學費。
一個小小的^,背後是整個npm生態的依賴管理的核心。
分享出來,不是為了博眼球,是真的希望大家能回去檢查一下自己的package.json。
看看你的依賴前面,那个小小的^,它還在嗎?😠