為什麼維運是隱形稅賦#

Instagram 共同創辦人兼 CTO Mike Krieger: 「你所增加的每一項技術,在數學上都保證了隨時間推移一定會出錯。 到了某個時間點,維運這些技術會耗盡整個團隊的精力。」

Instagram 在 2010 年上線到 2012 年被 Facebook 收購時,使用者已成長到 4,000 萬,當時團隊卻只有 13 個人,用戶/員工比例超過 300 萬比 1。 他們的關鍵心法之一就是 最小化維運負擔:拒絕在生產環境中使用未經驗證的新技術,固守 PostgreSQL、Memcache、Redis 這些穩定、易理解的選項。

系統成本不是在上線那一刻停止累積,而是「才剛開始累積」。 每天花在維運、修 Bug、教新人的時間,都是無法投入其他高槓桿活動的機會成本。


1. 擁抱維運的簡單性(Embrace Operational Simplicity)#

Steve Jobs 談 iPod 設計:「人們遇到問題時,往往停在第一個複雜的解決方案。但如果你像剝洋蔥般持續深入,你常常能找到非常優雅且簡單的解法——大多數人只是沒有投入夠多時間和精力到那一步。」

Instagram 的前身 Burbn 是一個結合打卡、互動、照片分享的 App,做了一年後因為「太複雜、太雜亂」被砍光,只保留照片、留言與點讚——這就是 Instagram 的誕生。

反例:Pinterest 早期過度複雜#

兩年內成長到每月百億 PV 的過程中,Pinterest 三人工程團隊資料/快取層曾同時使用 七種技術:MySQL、Cassandra、Membase、Memcache、Redis、Elastic Search、MongoDB。 過度複雜的架構帶來四種成本:

  • 專業知識被切割分散到多個系統
  • 單點故障增加:每個元件都是一個潛在故障點
  • 新人學習曲線陡峭
  • 工具與抽象的投入被稀釋:沒有任何一個系統能被好好支援

最終他們簡化到只用 MySQL、Memcache、Redis、Solr,並透過「水平擴展同類元件」而非「引入新元件」來支撐後續 4 倍以上的成長。

落地原則#

  • 新語言:副專案實驗 OK,但放上正式環境前先問:團隊熟嗎?好招人嗎?
  • 資料儲存:新潮 NoSQL 承諾解決 MySQL/PostgreSQL 的痛,但維運成本是否真的更低?
  • 重用 vs. 自造:與其各場景挑「最合適工具」,不如想想「多元件帶來的維運複雜度」是否大於標準化的收益
  • 分散式 vs. 單機:在引入叢集前先確認資料是否真的大到需要

2. 建構「快速失敗(Fail Fast)」的系統#

許多工程師誤以為「Robustness(健壯)」=「永不崩潰」,因此把錯誤吞掉或預設值帶過。 這反而導致軟體 「緩慢失敗(Fail Slowly)」,Bug 難以追蹤。

Jim Shore 在《Fail Fast》一文:「Fail Fast 看起來會讓你的軟體更脆弱,但實際上會讓它更健壯——Bug 更容易被發現與修復,所以更少進到生產環境。」

常見的 Fail Fast 技巧#

  1. 啟動時崩潰:遇到設定錯誤直接停止,而非帶著錯誤參數執行
  2. 嚴格驗證輸入:在資料被消費前就攔截錯誤
  3. 拋出例外:當關鍵資料結構損壞(如 Iterator 失效)時立即拋出
  4. 使用斷言(Assert):在複雜邏輯前後確保關鍵不變量成立
  5. 向上拋出無法處理的外部錯誤:不要吞掉(Swallow)
  6. 越早告警越好:偵測到不一致狀態就通知工程師

真實事故案例#

作者親身經歷的一個 Bug:Web 請求 timeout 後,沒有正確重置 MySQL 連線;下一個請求重用了同一條連線,回傳結果竟是上一個請求的內容。 如果一開始就採用「連線使用前 assert 乾淨」這類 Fail Fast 設計,原本一週的災難只需幾分鐘就能定位。

Fail Fast 不等於把錯誤直接拋到使用者眼前。可採用混合策略: 元件內部 Fail Fast,但搭配全域 Exception Handler 紀錄錯誤、優雅降級給使用者,再用儀表板依錯誤頻率排序,讓工程師優先處理高頻問題。


3. 無情自動化機械式任務(Relentlessly Automate)#

工程師常面臨判斷:「手動執行的總時間」 vs.「自動化的前期成本」。 我們會自動化得太少,常見原因:

  • 沒時間:迫於期限,犧牲長期利益換取短期交付
  • 公地悲劇(Tragedy of Commons):手動工作分散在多人輪值上,沒有單一人有足夠動機解決
  • 工具不熟:缺乏 Shell Script 或系統整合的技能——但這正是練越多越快的技能
  • 低估頻率:誤以為這項任務不會常做
  • 低估時間複利:每次省 10 秒 × 每天 10 次 = 一年快一個工作日

適合自動化的任務#

  • 程式碼/系統行為驗證
  • 資料提取、轉換、摘要
  • 偵測錯誤率異常
  • 建置與部署
  • 資料庫快照與還原
  • 定期批次計算
  • 重啟 Web 服務
  • 風格 Linter
  • 訓練機器學習模型
  • 帳號/使用者資料管理
  • 增減服務群組中的伺服器

自動化「機械」 vs. 自動化「決策」#

Facebook 前基礎設施工程總監 Bobby Johnson 把自動化分成兩種:

  • 機械式自動化:執行一連串步驟,相對直接、可測試
  • 決策式自動化:要在發生狀況時自我修復、做出正確判斷——困難很多

「我們最糟的幾次大故障,都是來自決策式自動化跑出失控行為——它們很少被好好測試,因為照定義就是在不尋常的情境下執行。」

Facebook 早期管理上千個 MySQL shard,「機械式」部分(搬移 shard 的命令)已自動化,但「決策式」部分(要把哪些 shard 搬到哪)仍由一位工程師手動執行多年,直到後來才推出 MySQL Pool Scanner 進行自動再平衡。

先把所有機械式任務的低垂果實摘完,再投資決策自動化。


4. 讓批次流程具備冪等性(Idempotence)#

冪等性:指一個操作無論執行一次還是多次,產生的結果都是一樣的。

當自動化規模變大,部分腳本因網路波動或外部服務暫時失敗的機率也會變大。 若批次流程冪等,就能放心地「重試(Retry)」——不擔心產生重複資料或副作用。

範例:每週使用者行為計數#

  • 非冪等做法:直接掃 log、把每筆事件加進週計數器。若中途失敗,重跑會把某些事件多算一次
  • 冪等做法:先以「日」為單位算出當日計數,再加總七天得到週計數。重跑只是把當日值覆蓋一次,不會雙重計算

當完全冪等做不到時,至少做到「可重入(Reentrant)」:被中斷後再執行能順利完成。 讓每個任務「要不全部成功,要不全部失敗」。

額外好處:把不常跑的流程也常跑#

Dropbox 前工程師 Rajiv Eranki 建議:把「每月才跑一次」的腳本,設成每天乾跑(Dry Run)。 這樣假設出問題,當週就會被發現,而不是月底警報響起時手忙腳亂。

把每 5 到 10 分鐘的系統檢查改成每 60 秒檢查、僅在「連續失敗」才告警,也能過濾掉許多瞬時網路抖動。


5. 快速回應與復原(Respond and Recover Quickly)#

任何足夠複雜的系統都會故障。 與其投資無限資源追求「零故障」,不如投資「快速復原的能力」。

Netflix 的 Chaos Monkey#

Netflix 工程師主動寫了一隻會在生產環境隨機殺掉自家服務的工具 Chaos Monkey。 理由是:「對抗重大意外的最好辦法,就是經常發生小意外。」 他們把 Chaos Monkey 設定在週間工作時段觸發故障,方便工程師在辦公室即時處理,而非在半夜被叫醒。 當 AWS 出現大規模事故時,Netflix 影響甚微;其他公司如 Airbnb、Reddit、Foursquare、Quora 卻 down 了好幾個小時。

各家災難演習#

  • Google:每年舉辦 DiRT(Disaster Recovery Testing),模擬地震、停電、整個資料中心或辦公室下線,驗證團隊溝通與系統 failover
  • Dropbox:主動對生產環境施加額外負載,提早觸發瓶頸,再關掉模擬流量從容調查

用「腳本」決策關鍵時刻#

舊金山 49 人隊前教練 Bill Walsh 在《The Score Takes Care of Itself》提倡「Scripting for Success」: 預先擬好開賽前 20-25 個進攻 if-then 流程。 因為比賽現場觀眾吼叫、計時器倒數、情緒緊繃——這時候靠臨場判斷往往最糟。 把決策提前到平靜時刻完成,是高槓桿做法。

工程上該預演的「What if」#

  • 重大 Bug 被部署上線——多快能回滾?
  • 資料庫掛了——多快能切換並恢復資料?
  • 流量暴增——能擴展或卸載多快?
  • 測試/預備環境壞了——能多快重建?
  • 客戶通報緊急問題——通知到工程師需要多久?

也適用於工程以外的議題#

  • 主管在重要會議上突然反對產品計畫——你準備好應對問題了嗎?
  • 關鍵成員生病、離職——知識怎麼分享?
  • 用戶反彈新功能——立場是什麼、多快能回應?
  • 專案延誤——多早能偵測?怎麼補救?

重點摘要(Key Takeaways)#

  • 先做最簡單的事:簡單的系統易於理解、擴展與維護
  • 快速失敗以定位錯誤源頭:別吞錯、別延後失敗
  • 優先自動化機械式任務:決策自動化很難正確,謹慎為之
  • 追求冪等性與可重入性:讓腳本可以被安全地重複執行與重試
  • 預演失敗情境:對「能快速復原」建立信心,才能在開發時更大膽前行