維運與可靠性練習題 (Practice - Ops and Reliability)
Related Concepts
- 12-Ops-and-Reliability/01-Dealing-With-Contention
- 12-Ops-and-Reliability/02-Overload-Protection
- 12-Ops-and-Reliability/03-Reliable-Delivery
- 12-Ops-and-Reliability/04-Observability
| 概念 | 預設做法 | 關鍵點 |
|---|---|---|
| Race condition 根因 | 讀寫不原子,讀與寫之間有時間窗 | 規模放大窗口,需同步機制 |
| Atomicity 不夠 | 只保證 transaction 內部 | 擋不住別人同時讀到一樣的資料 |
| 高/低 contention | 高→Pessimistic Lock;低→OCC | OCC 用自然欄位當版本(避 ABA) |
| 跨 DB 協調 | 強一致→2PC;韌性→Saga;簡單→Distributed Lock | 盡量塞進同一 DB,分散式是最後手段 |
| 過載不保護 | 雪崩——所有請求都失敗 | 10 倍流量也該服務 30% |
| 四種限流 | Fixed/Sliding/Token/Leaky | 面試預設 Token Bucket |
| Rate vs Concurrency | 速率(每秒) vs 同時處理中的數量 | concurrency 更貼近資源 |
| Load shed vs 背壓 | 直接丟 vs 放慢不丟 | 背壓需上游配合(TCP/HTTP-2) |
| 無超時危害 | 執行緒/連線池耗盡 → 級聯故障 | Read 超時依 P99 調校 |
| 重試前提 | 暫時性故障 + 冪等 + 退避加抖動 | 4xx/業務錯誤/非冪等不重試 |
| at-least-once 安全 | 冪等 Consumer + 去重表 | 等效 exactly-once,複雜度低 |
| 熔斷器三狀態 | Closed → Open → Half-Open | Half-Open 探針成功逐步放量 |
| 三支柱 | Metrics 狀態 / Logs 細節 / Traces 路徑 | 排查序:告警→定位→確診 |
| 告警原則 | 對症狀(用戶有感),不對原因 | 對原因 → 告警疲勞 |
| SLI/SLO/SLA | 指標 / 內部目標 / 對外合約 | SLA < SLO 留緩衝;SLO 不設 100% |
Part 1:Dealing with Contention
Q1(Recall):什麼是 contention?race condition 的根本原因是什麼?
Contention 是多個 process 同時搶同一個資源,沒協調好就 race condition(超賣、重複扣款)。
根因是讀取與寫入不是原子的——在「讀當前狀態」和「依狀態做更新」之間有一個時間窗(記憶體微秒、網路毫秒),那個窗裡一切都可能變。
Q2(Recall):為什麼光有 Atomicity(transaction)擋不住超賣?
Transaction 只保證自身內部全部成功或全部失敗,擋不住別的 transaction 同時讀到一樣的資料。
Terry 和 Bohr 可各自開 transaction、都讀到 available_seats >= 1、各自 UPDATE,兩個都成功 → 1 個座位賣 2 張票。需要額外的協調機制。
Q3(Recall):Pessimistic Locking 和 Optimistic Concurrency Control 各自適合什麼 contention 場景?
- Pessimistic(悲觀鎖):假設衝突一定發生,事先取 lock(
SELECT ... FOR UPDATE)。適合高 contention。 - OCC(樂觀並發):假設衝突罕見,衝突後才偵測(compare-and-swap,用版本號)。適合低 contention——無衝突時零 locking overhead,只有真衝突才重試。
Q4(Recall):預設的 isolation level 能擋住門票 race 嗎?要用哪一級才行,代價是什麼?
不行。READ COMMITTED(PostgreSQL 預設)/ REPEATABLE READ(MySQL 預設)都允許門票 race。
必須用 SERIALIZABLE:DB 自動偵測衝突、中止其中一個。代價是比明確 lock 貴得多——要追蹤所有讀寫偵測衝突,abort 又浪費要重做的工作。確切知道要協調哪些資源時,FOR UPDATE 更精準有效率。
Q5(Recall):OCC 的版本號應該選什麼欄位?為什麼?
用「每次更新都會變的自然欄位」即可,不必獨立版本欄位:演唱會用 available_seats、競標用當前最高出價、轉帳用帳戶餘額。
但要選永遠單調遞增的欄位(如 review_count)以避免 ABA 問題——值 A→B→又回 A 時,compare-and-swap 看到「還是 A」會以為沒變、漏掉中間狀態。
Q6(Application):銀行帳戶已 shard 到不同 DB,跨 shard 轉帳要 A 扣 B 加同時成功。2PC 和 Saga 怎麼選?
面試先講 Saga(韌性),面試官要求嚴格一致才提 2PC。
- Saga:拆成獨立步驟、每步立刻 commit,失敗時用補償交易撤銷。代價是執行過程暫時不一致(eventual consistency),但避開 2PC 的脆弱性。
- 2PC:coordinator 管 prepare/commit 兩階段,強一致但昂貴又脆弱——持鎖跨網路、coordinator 崩潰需從持久化 log 恢復、任一 DB 變慢整筆卡住。
Q7(Application):活動訂票系統怎麼避免「用戶填完付款才發現座位被搶」?
用座位預留機制(Distributed Lock 的延伸應用):選座立刻進「已預留」中間狀態,配 10 分鐘 timeout。
這把 contention 窗口從整個購票流程(5 分鐘)縮到只剩預留那一步(毫秒)。同理 Uber 設司機 pending_request、電商把商品 hold 進購物車。
Q8(Analysis):Taylor Swift 開唱搶票——所有人搶同一場演唱會的 row,為什麼一般擴展策略全失效?怎麼解?
需求集中單一點,sharding 沒用(沒法把一場演唱會拆成多 DB)、load balancing 沒用(server 最終都打同一 row)、read replica 沒用(瓶頸在寫入)。
第一策略:質疑能不能改變問題本身(10 件一樣的拆 10 場拍賣;追蹤數不需即時一致)。真需強一致 → queue-based serialization:把該資源的所有請求丟進專用 queue,單一 worker 循序處理,徹底消除 contention(取捨是延遲)。
Q9(Analysis):Pessimistic locking 怎麼避免 deadlock?
Deadlock 根因是兩個 transaction 以不同順序取得 lock(A 先鎖 Terry 再鎖 Bohr;B 反過來 → 互等)。
標準解法:有序加鎖(ordered locking)——永遠按一致的確定性 key(user ID / primary key)排序取得 lock,不管業務流程。所有 transaction 同序 → 沒有循環等待。後備網是 transaction timeout + DB 內建自動 deadlock 偵測。
Part 2:Overload Protection
Q10(Recall):系統過載若無任何保護機制會發生什麼?「雪崩」的關鍵災難點是什麼?
會雪崩:請求堆積在佇列 → 延遲上升 → 執行緒池耗盡 → 記憶體膨脹 → GC 瘋狂 → DB 連線池塞滿 → 整個系統停止回應。
關鍵災難點:不只是超出容量的那部分失敗,而是「所有」請求都失敗——10% 的超量流量可以打掛 100% 的服務。設計良好的系統,10 倍流量下應還能服務 30%。
Q11(Recall):四種 rate limiting 演算法各有什麼特性?面試預設選哪個?
| 演算法 | 特性 |
|---|---|
| Fixed Window | 最簡單,但有邊界突發(跨視窗 2 倍量) |
| Sliding Window | 邊界精確,但存每筆時間戳記憶體較高 |
| Token Bucket | 允許突發、最符合真實場景 |
| Leaky Bucket | 強制平滑、不允許突發(嚴格保護下游 API) |
面試預設 Token Bucket:閒置時累積令牌、突發時一次放行,最貼近真實流量。
Q12(Recall):Rate limiting 和 Concurrency limiting 管的維度有什麼不同?
- Rate limiting 管速率(每秒幾個請求)。
- Concurrency limiting 管同時在處理中的請求數量。
每個請求 500ms、系統 100 條執行緒 → 安全並發上限 100。rate limit 設「每秒 200」但每請求要 1 秒,同時就有 200 個在跑遠超執行緒池 → 雪崩。Concurrency limiting 更直接對應系統資源,常與 rate limiting 一起用。
Q13(Recall):被 rate limit 擋下時,伺服器該回什麼?為什麼?
回 429 Too Many Requests,並用 header(Retry-After、X-RateLimit-*)告知何時可重試。
這讓客戶端能配合退避(backoff),而不是盲目重試加重伺服器壓力。
Q14(Application):分散式部署下 rate limiting 為什麼會失效?怎麼修?
多台伺服器各自計數——客戶端把請求分散打到不同機器就能繞過限制。
解法:用集中式計數器(通常 Redis),INCR + EXPIRE 或 Lua script 原子操作保證無 race condition。延遲代價通常 < 1ms。滑動視窗常用 Redis Sorted Set(ZREMRANGEBYSCORE → ZCARD → ZADD → EXPIRE)。
Q15(Application):視訊轉碼服務遇到流量尖峰,怎麼用過載保護工具設計?
用 Queue-based Load Leveling + Auto-scaling:
- 上傳後任務進 SQS / Kafka,佇列吸收尖峰,後端只看到自己能承受的平滑流量。
- 依佇列深度觸發 auto-scaling(>100 加一台、<10 持續 10 分鐘縮一台)。
轉碼是非同步任務,用戶拿到「已收到,處理中」完全合理(不像付款後等 30 秒不合理)。
Q16(Analysis):Load shedding 和 backpressure 都處理過載,差別是什麼?各自的前提?
- Load shedding:系統主動丟棄部分請求(讓 30% 快速失敗,好過 100% 慢死)。承壓那一側自己做決定。
- Backpressure:不丟棄任何東西,讓下游發訊號告訴上游「慢一點」,整條鏈路協同減速。
Backpressure 更溫和,但需要上游配合(支援背壓訊號,如 TCP receive window、gRPC/HTTP-2 flow control),不是所有情況都能用。
Q17(Analysis):Bulkhead 的粒度該怎麼決定?太細或太粗各有什麼問題?
依「故障影響的相關性」劃分:把「如果它掛掉,你希望不影響哪些功能」的依賴隔離成獨立 bulkhead(支付掛掉不該影響商品瀏覽)。
- 太細 → 管理成本高、資源浪費(一池閒著另一池排隊)。
- 太粗 → 隔離沒意義(局部過載仍蔓延成全面崩潰)。
從 3~5 個 bulkhead 起步,再依實際故障模式調整。
Part 3:Reliable Delivery
Q18(Recall):為什麼說分散式系統裡「故障是必然」?可靠性設計的目標是什麼?
每個依賴都有獨立故障機率,相乘再乘上每天數百萬請求,統計上故障必然發生(5 個 0.999 串起來 → 0.995,× 百萬請求 → 每天數千次失敗)。
目標不是「零故障」,而是讓系統在故障時優雅降級、自動恢復、不放大災情。
Q19(Recall):不設超時為什麼會導致級聯故障(cascading failure)?
對方卡住不回應 → 你的連線懸著不釋放 → 大量發生 → 執行緒池 / 連線池耗盡 → 你自己也停止回應 → 上游再被你拖垮。
一個依賴的故障透過資源耗盡把你一起拉下去,這就是級聯故障的標準路徑。設了超時就能主動放棄、釋放執行緒、走降級邏輯。
Q20(Recall):哪些情況該重試、哪些不該重試?
- 該重試(暫時性故障):網路超時 /
ECONNRESET、5xx(503/502)、DB 連線池暫時耗盡。 - 不該重試:4xx 客戶端錯誤(請求本身有問題)、業務邏輯錯誤(「庫存不足」是正確回應)、非冪等操作(未做冪等保護前,重試會建兩筆)。
上限通常 3~5 次,且必須搭配退避——不加退避的重試等於在過載服務上繼續倒垃圾。
Q21(Recall):哪些 HTTP 方法天然冪等?POST 怎麼變冪等?
GET / PUT / DELETE 天然冪等(讀取不改狀態;PUT 完整替換重複無影響;DELETE 第二次系統狀態不變)。POST 不冪等(建兩次 = 兩筆訂單)。
POST 用冪等鍵(Idempotency Key):客戶端首次帶唯一 ID、重試帶相同 ID;伺服器處理前查有沒處理過——有則回之前結果、沒有則執行並存下。Stripe 是範本。
Q22(Recall):退避加抖動(backoff with jitter)解決的是什麼問題?
解決驚群效應(thundering herd):100 個客戶端同時失敗 → 1ms 後一起重試 → 又全失敗 → 循環把岌岌可危的服務徹底壓垮。
指數退避(每次等待加倍)給系統喘息,但所有客戶端曲線相同仍會同步脈衝;加**抖動(隨機性)**把脈衝攤平成平滑流量。Full Jitter(uniform(0, min(base*2^n, max)))實務最常推薦。
Q23(Application):訊息佇列常保證 at-least-once,同一則訊息可能被消費多次。怎麼讓系統仍正確工作?
Consumer 必須冪等,標準解法是去重表(deduplication table):在同一個 DB 事務裡先查去重表 WHERE message_id = ?,已存在則跳過直接 commit offset;不存在則處理訊息 + INSERT 去重表(同事務)。
業界主流是 at-least-once 送達 + 冪等 Consumer = 等效 exactly-once,比真正的 exactly-once(需分散式交易)複雜度低得多。
Q24(Application):設計呼叫外部支付 API 的可靠性策略,用一段話涵蓋多道防線。
「設 3 秒讀取超時(正常 1 秒內完成,3 秒提供緩衝又不讓卡住的依賴佔資源)。對 5xx 做指數退避重試(最多 3 次,加 jitter 防驚群)。每個支付請求帶冪等鍵確保重試不重複扣款。若失敗率 30 秒內超過 50%,熔斷器打開直接回『支付暫時不可用』,而不是讓用戶等到超時。」——涵蓋超時、重試、退避加抖動、冪等性、熔斷器。
Q25(Analysis):熔斷器的三個狀態如何轉換?Half-Open 探針成功後要注意什麼?
- Closed:正常轉發、監控失敗率;失敗率超閾值 → 轉 Open。
- Open:直接走降級不打下游(fail fast);等冷卻時間 → 轉 Half-Open。
- Half-Open:放一個探針請求;成功 → 回 Closed,失敗 → 回 Open。
注意:探針成功後不要一次放開全流量,應逐步放量(ramp-up),防止突然的全流量把剛恢復的服務又打垮。
Q26(Analysis):Failover 和 Fallback 有什麼差別?同步複製 vs 非同步複製在 failover 時的取捨?
Failover:找一個健康的同類替代(切到健康節點 / 新 Primary)。Fallback:同類也沒了,用較簡陋但能用的替代撐過去(serve stale、預設值、部分降級)。
複製取捨:同步複製——Primary 等 Replica 確認才回,failover 不丟資料但寫入延遲更高(支付類);非同步複製——延遲低但 failover 可能遺失 replication lag 期間未複製的寫入(社群動態類)。
Part 4:Observability
Q27(Recall):可觀測性的三支柱各回答什麼問題?資料形式是什麼?
- Metrics(指標):系統現在狀態如何?趨勢? → 數值 + 時序,做告警/趨勢。
- Logs(日誌):某個特定事件到底發生了什麼? → 文字事件,做 debug 確診。
- Traces(追蹤):一個請求走了哪些路徑、每步多久? → DAG(跨服務鏈路),找瓶頸。
三者互補:Metrics 說「有問題」、Traces 說「問題在哪個服務哪一步」、Logs 說「那一步具體錯了什麼」。
Q28(Recall):三種 Metric 類型各代表什麼?P99 為什麼必須用 Histogram 而非平均值?
- Counter:只單調遞增,增長速率才有意義(請求總數)。
- Gauge:可上下浮動,某時間點即時狀態(記憶體、佇列深度)。
- Histogram:把數值分桶統計分佈,是算百分位數的基礎。
P99 用平均值會被大量快請求拉低、掩蓋尾端慢請求。Histogram 把延遲丟進桶裡取第 99 百分位,才反映「最差體驗」的真實數字。
Q29(Recall):四個黃金信號(Google SRE)是哪四個?
| 信號 | 問題 | 要點 |
|---|---|---|
| Latency | 請求要多久? | 要分開成功 vs 失敗(快速失敗 ≠ 緩慢成功) |
| Traffic | 每秒多少請求? | 系統負載基準(RPS) |
| Errors | 失敗比例? | 區分顯性(HTTP 500)與隱性(回 200 內容錯) |
| Saturation | 還剩多少餘裕? | CPU/記憶體/磁碟,越接近上限越脆弱 |
Q30(Recall):SLI、SLO、SLA 各是什麼?為什麼 SLA 通常比 SLO 寬鬆?
- SLI(Indicator):衡量品質的具體指標(成功請求比例、P99 延遲)。
- SLO(Objective):對 SLI 設的內部目標值(成功率 ≥ 99.9%)。
- SLA(Agreement):對外部客戶的合約,通常較寬鬆(承諾 99.5%)。
SLA 比 SLO 寬鬆是刻意的:內部 SLO 99.9% 先觸發預警,讓你在違反對外 SLA 99.5% 之前就有時間修。
Q31(Application):告警應該對「原因」還是「症狀」設?舉例說明錯誤與正確做法。
應對症狀告警(用戶有沒有感受到問題),不對原因。
- 錯誤(對原因):
cpu_usage > 0.8就告警——CPU 高不一定代表用戶有感 → 告警太多 → alert fatigue → 工程師無視 → 真事故被淹沒。 - 正確(對症狀):
5xx 錯誤率 > 1%(用戶正在受影響)、P99 延遲 > 1 秒。
Q32(Application):結構化日誌為什麼比純文字好?每條 Log 為什麼要帶 trace_id?
純文字 "User 123 created order 456" 難以程式解析、過濾、聚合;JSON 結構化日誌每個欄位都能查詢。
trace_id 是讓 Logs 與 Traces 互通的橋樑:在 Trace 上找到慢請求 → 拿它的 trace_id → 直接撈出該請求在所有服務的 Log。沒這欄位三支柱就斷掉。另外 ERROR/WARN 應永遠 100% 保留、INFO 可取樣 10~20%。
Q33(Analysis):實際線上排查一個「P99 延遲飆高」的問題,三支柱要用什麼順序?
Metrics 告警 → Traces 定位 → Logs 確診:
- Metrics 告警:「P99 從 200ms 升到 2 秒」——知道有問題,但不知為什麼。
- Traces 定位:「這些慢請求都卡在 Order Service 的 DB Query」——縮小到哪個服務哪步。
- Logs 確診:「大量 'slow query: 1.8s' warning」——具體錯了什麼(SQL 沒用到索引)。
Q34(Analysis):為什麼不能把 user_id、request_id 這種高基數標籤塞進 Prometheus metric label?
Metrics 的每個 label 值組合都是一條獨立時序。把 user_id、request_id 這種高基數值放進 label,會讓時序數量爆炸、TSDB 記憶體炸掉。
原則:高基數資訊放 Logs/Traces(它們本來就逐事件記錄),Metrics 的 label 只放低基數維度(method、endpoint、status_code、region)。
| 問題 | 解法 |
|---|---|
| Race condition 根因 | 讀寫不原子,讀與寫之間有時間窗 → 需同步機制 |
| Atomicity 不夠 | 只保證 transaction 內部,擋不住別人同時讀 |
| 高 contention | Pessimistic Locking(FOR UPDATE) |
| 低 contention | OCC(自然欄位當版本,選單調遞增避 ABA) |
| 預設 isolation 擋超賣 | 不行,要 SERIALIZABLE(代價貴 + 重試) |
| 跨 DB 強一致 | 2PC(昂貴脆弱,最後手段) |
| 跨 DB 要韌性 | Saga(補償交易,暫時不一致) |
| 面向用戶競爭流程 | Distributed Lock + 預留機制(縮短 contention 窗口) |
| Hot partition | 先改變問題;真需強一致用 queue-based serialization 單 worker |
| 過載不保護 | 雪崩——所有請求都失敗 |
| 限流演算法 | 預設 Token Bucket;嚴格保護下游用 Leaky Bucket |
| Rate vs Concurrency | 速率(每秒) vs 同時處理中(更貼近資源) |
| 分散式限流 | 集中式 Redis 計數器(INCR+EXPIRE / Lua 原子) |
| 被限流回什麼 | 429 + Retry-After,讓客戶端退避 |
| 削峰 | Queue-based Load Leveling(非同步任務)+ Auto-scaling |
| Auto-scaling 節奏 | P99 最準;擴容快、縮容慢防振盪 |
| Load shed vs Backpressure | 直接丟 vs 放慢不丟(背壓需上游配合) |
| Bulkhead 粒度 | 依故障相關性,3~5 個起步 |
| 無超時 | 執行緒/連線池耗盡 → 級聯故障;Read 超時依 P99 |
| 重試前提 | 暫時性故障 + 冪等 + 退避加抖動;4xx/業務錯誤不重試 |
| POST 重複扣款 | 冪等鍵(Stripe 範本) |
| at-least-once 安全 | 冪等 Consumer + 去重表 = 等效 exactly-once |
| 驚群效應 | 指數退避加抖動(Full Jitter) |
| 熔斷器三狀態 | Closed → Open → Half-Open(探針成功逐步放量) |
| Failover vs Fallback | 找健康同類 vs 用簡陋替代撐過去 |
| 同步 vs 非同步複製 | 同步不丟資料但慢;非同步快但可能丟 lag 期資料 |
| 三支柱 | Metrics 狀態 / Logs 細節 / Traces 路徑 |
| 排查順序 | Metrics 告警 → Traces 定位 → Logs 確診 |
| P99 | 用 Histogram,不用平均值 |
| 四個黃金信號 | Latency / Traffic / Errors / Saturation |
| 告警原則 | 對症狀(用戶有感),不對原因 → 避免告警疲勞 |
| SLI/SLO/SLA | 指標 / 內部目標 / 對外合約;SLA < SLO 留緩衝;不設 100% |
| Error Budget | 99.9% → 每月約 43 分鐘,決定發布快慢 |
| 高基數標籤 | 放 Logs/Traces,別放 Metrics label |