系統升級到 .NET 9 之後,資料庫 CPU 開始間歇性飆高,結帳偶爾逾時。團隊很快提出幾種合理解釋:可能是新框架、流量成長,也可能是結帳彼此鎖住。更棘手的是,其中兩條效能問題已經拖了一年以上,各自優化過一到兩次;每次都有改善,卻始終沒有根治。
我要先回答一個更具體的問題:到底是哪幾支查詢在吃 CPU? 我用資料庫證據逐條定位根因,再把 21 項優化依風險與效益排成三波,交由工程團隊執行;每一波都以前後實測驗收,再決定是否往下走。
MY ROLE
程式碼由團隊的工程師負責,我沒有把他們的成果算在自己身上。 我的責任是找出根因、排定修復順序,並在上線後用實測證明問題確實改善。這個案例記錄的是:一位沒有工程背景的 PM,如何在沒有資深 DBA 的團隊裡,建立一套足以推動技術決策的證據。
CHALLENGE 01
當時最主流的假設是「結帳互卡」:兩張訂單同時進來,彼此鎖住。它合理,也很難反駁,因為任何反證都可以用「只是剛好沒發生」解釋。我因此不用單次測試下結論,而是找了三層互相獨立的證據:
BeginTransaction 出現 0 次。沒有交易就沒有互卡的條件。三層結果都不支持互卡假設。要排除一個原因,不能只說「我測不到」;必須用不同來源的證據交叉確認。先把最受相信的假設排除,團隊才願意把注意力移向真正消耗 CPU 的查詢。
CHALLENGE 02
我一開始想用自己的秒表證明「結帳很慢」,最後把這份數據丟掉。 辦公室裡量到的秒數,只代表當下的網路與設備,不足以支撐「修改付款路徑」這種高風險決策。
我改為逐筆整理一段期間內客服收到的結帳異常事件,再對照資料庫 CPU 時間軸。六起客訴全都落在 CPU 飆高的時段。這份證據一端是客人的真實問題,另一端是系統指標;兩者在時間上吻合,至少足以證明這不是辦公室裡的主觀體感。
挑證據跟挑查法一樣重要。我要證明的不是「我覺得慢」,而是「客人遇到問題的時間,確實與系統指標吻合」。
ROOT CAUSES
問卷查詢(153 秒):瓶頸不在 SQL 寫法,而在資料結構。 舊做法用 WHERE Reply LIKE '%分數%',每次查詢都要掃過完整評論文字,因為分數埋在 JSON 裡,直到讀取時才計算。這也解釋了前兩次「加 view 欄位」與「加快取」為何只能治標。這次改成寫入時計算,將分數拆成獨立欄位與資料表,再用 dual-write 與 backfill 分階段切換,才真正處理到底層問題。
結帳 payload SQL(3,000ms):一個 OR 造成兩萬倍差距。 LINQ 寫成 Where(w => w.OrdersId == X || w.AddOrderId == Y),讓資料庫無法使用任一索引,只能全表掃描。修正本身很小:拆成兩個能各自走索引的查詢,再合併結果。執行時間從 3,000ms 降到 0.13ms。
批量列印物流單(9.3 分):驗收直接用真實的列印紀錄。 比對 Mongo 的列印 log,改版前 721 次、改版後 486 次真實列印,中位數從 4.8 秒降到 1.1 秒。
CHALLENGE 03
完整問題清單有 21 項,但一次全改,結果就無法歸因。 系統即使變快,也分不出哪項修正真正有效、哪項只是同時上線。我因此按風險與預期效益分成三波;每波上線後先量測,確認效果再推下一波。
結帳這條動到付款路徑,另外走了完整的測試矩陣:五條付款路徑(網頁綁卡、網頁 LinePay、網頁重新付款、App LinePay、App 綠界)逐一驗過才上正式站。
RESULTS
| 項目 | 改善前 | 改善後 |
|---|---|---|
| 問卷查詢最慢單次 | 153 秒 | 7 秒 |
| 問卷查詢月總 CPU | 推估約 44,000 CPU 秒 | 39.8 CPU 秒(實測) |
| 結帳 payload SQL | 約 3,000ms | 0.13ms |
| 批量列印(最慢一次) | 9.3 分 | 1.3 分 |
| 單筆列印中位數 | 4.8 秒 | 1.1 秒 |
| 結帳逾時 | 偶發 | 0 |
上線後實測 8 筆綁卡結帳,平均 5.06 秒、零逾時。修法前同類測試會直接撞上 30 秒逾時。
每個數字都標明來源:效能數據來自 Azure Query Store,列印數據來自 Mongo 列印 log。唯一的推估值是「改善前月總 CPU」,因為原始紀錄已超過 30 天保留期。我在報告裡將它明確標為推估,只用來判斷量級,不與實測值混在一起。
AUTOMATION
專案收尾後,我把這半年累積的診斷方法寫成規則,交給一支常駐的監控機器人。 資料庫 CPU 告警一進來,它就會抓出當下最耗 CPU 的查詢、完成初步診斷,再把分析報告寄到我的信箱。
其中最重要的一條規則,是我自己踩過的坑:先看 CPU 消耗,不要只看執行時間。 查詢跑得久,可能是在等鎖或等 IO,它可能是受害者,不是兇手;真正需要優先處理的是 CPU 時間高的查詢。把這個判斷寫進機器人後,下一次問題發生時,診斷不必等我到場才開始。對我來說,這才是效能治理:不只修掉眼前的查詢,也讓之後的排查更快啟動。
ENCORE
兩個月後,「結帳慢」再次被回報。如果直接沿用上次的答案,認定又是 SQL 寫法,就會走錯方向。重新拆解後,這次是兩個不同原因疊加:結帳中繼頁面寫死了一段固定秒數的空等,同時資料庫統計值過期,又放大了 IO 的延遲。
症狀會重複,根因不一定相同。 上一次的結論,只能成為下一次排查的假設之一。
LESSONS
STACK
Azure Query Store、T-SQL、SQL Statistics、MongoDB、Azure CLI、Application Insights。診斷方法與判斷規則已沉澱為常駐監控機器人與內部 playbook。