← 回作品列表
系統工程 · 2026.05 – 08 · 根因診斷、優先序推動、實測驗收 · 上線中

系統效能治理

三條拖了一年以上的效能問題,用資料庫證據逐條定位根因、排出優化順序、推動工程執行,全部以實測前後對比驗收。

三組效能前後對比:問卷查詢 153 秒降到 7 秒、結帳 SQL 3,000ms 降到 0.13ms、批量列印 9.3 分降到 1.3 分。
角色
根因診斷、優先序推動、實測驗收
年份
2026.05 – 08
狀態
上線中
技術
Azure Query Store, T-SQL, MongoDB, Azure CLI

先說清楚我的角色

分工圖:工程師寫程式;我負責根因、優先序與實測驗收

程式碼是團隊的工程師寫的,不是我。

我做的是另外三件事:把根因查出來、決定先修哪一條、以及在上線後用實測數字證明真的修好了。這個案子值得寫下來的原因也在這裡——它是一個沒有工程背景的 PM,在一個沒有資深 DBA 的公司裡,怎麼用證據推動技術決策。

問題背景

CPU 飆高的時間軸,旁邊漂著三個互相競爭的假設:新框架?流量?互卡?

系統升級到 .NET 9 之後,資料庫 CPU 開始間歇性飆高,結帳偶發逾時。這種問題最麻煩的地方是每個人都有一套說法:可能是新框架、可能是流量成長、可能是結帳在互相鎖住。

而且其中兩條線已經被優化過了——問卷查詢在 2024 年 12 月和 2025 年 10 月各做過一次,都有改善、都上線了、也都沒有真正解決。

我要回答的其實是一個很簡單的問題:到底是哪幾支查詢在吃 CPU? 不是猜,是查。

具體實作

一、先證明那個大家都相信的原因是錯的

三層獨立證據都是 0:程式碼無交易、即時無阻塞、七天無鎖等待

當時最主流的假設是「結帳互卡」——兩張訂單同時進來互相鎖住。這個假設很合理,也很難反駁,因為「它偶爾才發生」永遠可以解釋任何反證。

我用三層證據把它排除掉:

  1. 程式碼:grep 整條結帳路徑,BeginTransaction 出現 0 次。沒有交易就沒有互卡的條件。
  2. 資料庫即時狀態:查當下的阻塞情形,0 個 blocking、0 個鎖等待。
  3. 歷史紀錄:Query Store 拉七天,結帳相關的鎖等待累計為 0。

三層都指向同一件事:架構上不會發生。證明一個東西不存在,比找出一個東西存在還難,所以我不用「我測不到」當結論,而是用三種互相獨立的方法各查一次。

排除掉這個之後,真正的兇手才浮出來。

二、影響程度的證據,用客服的痛、不用我的碼表

六起客服事件標在 CPU 時間軸上,全部落在飆高的時段裡

「結帳很慢」要慢到什麼程度才值得動手術?我一開始想用自己實測的秒數當證據,後來丟掉了——我在辦公室按碼表量到的數字,只代表那一刻、那條網路、那台機器,說服不了任何人,也承擔不起「動付款路徑」這種決策的重量。

改用的做法:把一段期間內客服實際收到的「結帳有問題」事件逐筆撈出來,對到資料庫 CPU 的時間軸上。六起客訴,起起都落在 CPU 飆高的時段裡。這種證據有兩個好處:它是客人真實的痛,不是我的體感;而且事件時間點與資源指標對得上,因果的方向就立得住。

挑證據跟挑查法一樣重要——你要的不是「我覺得慢」,是「客人在痛、而且痛的時間跟系統指標吻合」。

三、逐條定位根因

三條根因與修法對照:JSON 全文掃描、OR 走不到索引、批次列印

問卷查詢(153 秒):舊做法是 WHERE Reply LIKE '%分數%',每查一次就把整篇評論文字掃過一遍。分數本來是埋在 JSON 裡的,讀取時才現算。這不是查詢寫法的問題,是資料結構的問題——所以前兩次「加 view 欄位」「加快取」都只能治標。

修法是把分數在寫入時就算好、拆成獨立欄位與資料表,用 dual-write 加 backfill 分階段切換。這是唯一真正動到底層結構的一次,也是唯一根治的一次。

結帳 payload SQL(3,000ms):LINQ 寫成 Where(w => w.OrdersId == X || w.AddOrderId == Y)。這個 OR 讓資料庫走不到任何一個索引,只能全表掃描。修法很小——拆成兩個各自走索引的查詢再合併——效果是 3,000ms 變成 0.13ms。

一行寫法的差別,兩萬倍。

批量列印物流單(9.3 分):驗收方式是直接比對 Mongo 的列印紀錄,改版前 721 次、改版後 486 次真實列印,中位數從 4.8 秒降到 1.1 秒。

四、拆成分波清單,不要一次全上

21 項優化分成三波,每波之間隔著一道「量測」關卡

找出來的問題不只三條——完整清單有 21 項。但一次全改等於沒有辦法歸因:改了二十件事之後系統變快了,你永遠不知道是哪一件起了作用、哪一件其實是白做的。我把 21 項按風險與預期效益排成三波,每一波上線後量一次,確認因果關係成立再推下一波。

結帳這條動到付款路徑,另外走了完整的測試矩陣:五條付款路徑(網頁綁卡、網頁 LinePay、網頁重新付款、App LinePay、App 綠界)逐一驗過才上正式站。

結果

成果數字:問卷 22 倍、結帳約 20,000 倍、列印 7.2 倍、逾時歸零

項目改善前改善後
問卷查詢最慢單次153 秒7 秒
問卷查詢月總 CPU推估約 44,000 CPU 秒39.8 CPU 秒(實測)
結帳 payload SQL約 3,000ms0.13ms
批量列印(最慢一次)9.3 分1.3 分
單筆列印中位數4.8 秒1.1 秒
結帳逾時偶發0

上線後實測 8 筆綁卡結帳,平均 5.06 秒、零逾時。修法前同類測試會直接撞上 30 秒逾時。

數據源全部標明:效能數字來自 Azure Query Store,列印數字來自 Mongo 的列印 log。唯一的推估值是「改善前月總 CPU」,因為那筆原始紀錄已經超過 30 天保留期被清掉了——我在報告裡也是這樣寫的,標成推估、只供量級參考。

把判斷寫進自動化

監控機器人流程:CPU 告警、自動抓兇手、初步診斷、寄出報告

專案收尾之後我做了一件事:把這半年學會的「怎麼判斷誰是兇手」寫成規則,交給一支常駐的監控機器人——資料庫 CPU 告警一進來,它自動抓出當下最吃 CPU 的查詢、跑完初步診斷、寄一封分析報告到我信箱。

其中最重要的一條判斷規則,是我自己踩過的坑:看 CPU 消耗,不要看執行時間。 一支查詢跑得久,可能只是在等鎖、等 IO,它是受害者不是兇手;真正的兇手是 CPU 時間高的那支。這條規則寫進機器人之後,等於把「我的判斷力」變成一個不用我在場也會執行的東西——這是我對「效能治理」的定義:不是修好幾支查詢,是讓下一次問題出現時,診斷自動開始。

同樣的症狀,不一樣的兇手

同一個「結帳很慢」,五月與八月是兩組完全不同的根因

兩個月後「結帳慢」又被回報了一次。如果我直接拿上次的答案抄——「應該又是 SQL 寫法」——就錯了。這次逐步拆解後發現是完全不同的兩件事疊在一起:結帳的中繼頁面裡寫死了固定秒數的空等,加上資料庫統計值過期放大了 IO 的尾巴。

症狀會重複,根因不會忠誠。 每一次都要重新查,上一次的結論只是這一次的假設之一。

學到的東西

三條守則:優化過不等於解決、標明可信度、定位問題本身就是產出

  • 「已經優化過了」不等於「已經解決了」。 前兩次優化都是真的有效、也都是當時可行的選擇;只是真正的瓶頸在資料結構,而動資料結構風險高、估要一個月以上,所以一直被留著。把「為什麼這次才根治」寫清楚,比宣告成果重要。
  • 報告要標可信度。 我在給老闆的報告裡把每個數字分成「實測」和「推估」,並附上口徑說明。這件事沒有讓報告變弱,反而讓實測的那些數字更有份量。
  • 不要獨佔功勞,但也不要低估自己的貢獻。 程式是工程師寫的。但這三條線在被診斷出來之前已經躺了一年以上,沒有人知道該從哪裡開始。定位問題本身就是產出。

技術架構

五個證據來源:Query Store、T-SQL、Mongo 列印 log、Azure CLI、App Insights

Azure Query Store、T-SQL、SQL Statistics、MongoDB、Azure CLI、Application Insights。

診斷方法與判斷規則已沉澱為常駐監控機器人與內部 playbook。

下一則作品

看更多 作品 →