系統愈做愈慢、愈改愈亂?架構最佳化應該先從哪裡開始

架構最佳化不是把舊系統全部推倒重來,而是先量測真正的瓶頸、保護現有行為,再逐步整理邊界、重構熱點、改善資料層與正式環境韌性。 一個已經能運作的系統,最怕的通常不是「舊」,而是沒有人知道哪裡可以安全改、哪裡真的慢,以及改完到底有沒有變好。

系統愈做愈久,常會出現一種很熟悉的狀態:功能還能用,但每次新增需求都比以前慢;一個小修改牽動好幾個模組;查一個問題要追很多層程式;效能開始下降,團隊卻說不清楚真正瓶頸在哪裡。

這時最危險的反應,就是直接下結論:「乾脆全部重寫。」

重寫看起來很乾淨,因為新專案一開始沒有歷史包袱。但舊系統裡那些多年累積的商業規則、例外情境、資料格式、第三方整合與使用者習慣,也常常一起被藏在「看起來很亂」的程式裡。沒有先把這些行為固定下來,大重寫不是消除複雜度,而是把已知問題換成未知問題。

真正成熟的架構最佳化,順序通常相反:先找證據,再建立安全網;先處理最痛的路徑,再逐步整理整體架構。

架構最佳化真正要解決的,不只有「慢」

當一個系統開始變難維護,通常不是單一問題。

你可能同時看到三種症狀。

第一種是執行速度變慢。 API 回應時間增加、查詢愈來愈久、CPU 或記憶體壓力變高、尖峰時段開始塞車。

第二種是修改速度變慢。 一個小功能要改很多地方,工程師不敢碰某些模組,測試不足,改 A 卻壞 B。

第三種是判斷速度變慢。 團隊知道系統有問題,卻不知道該先修資料庫、快取、架構、程式碼,還是直接加機器。

這三件事常被混在一起,所以「系統變慢」最後會演變成「什麼都想一起改」。

架構最佳化真正的工作,是把它們拆開:哪些是效能問題、哪些是結構問題、哪些是可靠性問題,然後依證據決定順序。

第一步:先量測,不要靠感覺猜瓶頸

任何效能最佳化,都應該先有基準值(baseline)。

例如某個結帳流程很慢,不能只說「使用者覺得慢」,而是要知道中位數、尖峰延遲、錯誤率、吞吐量,以及 CPU、記憶體、資料庫連線或網路資源是否飽和。

常見的 p50、p95、p99 延遲,就是用來看不同百分位使用者實際遇到的速度。平均值可能看起來不差,但如果 p95 很高,就代表仍有一大群請求明顯變慢。

這一步很重要,因為工程師最容易優化「看起來最複雜」的程式,卻不一定是實際最耗時間的地方。

有時真正問題只是一個 N+1 查詢;有時是外部 API 沒有逾時;有時是首頁一次載入太多不必要資料。沒有量測,團隊很容易花好幾天改善一段只占整體延遲很小比例的程式。

所以第一個原則很簡單:沒有基準值,就還不能宣稱自己在做效能最佳化。

第二步:先把既有行為固定下來,再動舊程式

Michael Feathers 在《Working Effectively with Legacy Code》中處理的核心問題,就是「一段沒有足夠測試、又不敢亂改的程式,該怎麼安全修改」。

這時最重要的不是立刻整理程式碼,而是先建立安全網。

特徵測試(characterization test)的用途,不是先定義理想行為,而是先記錄系統現在到底怎麼運作。即使目前行為裡有一些奇怪規則,也先把它看清楚、固定下來,再決定之後要不要改。

Feathers 也提出接縫(seam)的概念:找到可以替換依賴、隔離外部服務或讓程式進入測試環境的位置。當資料庫、時間、檔案系統、第三方服務全部硬綁在一起時,測試很難寫;接縫的價值,就是先讓重要邏輯可以被獨立觀察。

這個順序很關鍵。

如果沒有測試保護,重構就很像在黑暗中搬牆壁。牆是搬了,但你不知道是不是順便拆掉了承重結構。

第三步:重新畫出真正的模組邊界

當系統愈長愈大,最常見的問題之一,是原本清楚的責任邊界慢慢漂掉。

例如商業規則直接依賴資料庫欄位、控制器裡塞進大量判斷、某個第三方服務 SDK 被散落在十幾個檔案,最後任何技術更換都變成全站工程。

Robert C. Martin 在《Clean Architecture》中提出的依賴規則(Dependency Rule),核心不是要求大家一定要畫四個同心圓,而是讓核心商業邏輯不要反過來依賴外層技術細節。

資料庫、網站框架、訊息系統、第三方 API 都會變。真正希望長期穩定的,通常是你的商業規則與使用情境。

所以實務上應該問的不是「資料夾分幾層才算乾淨架構」,而是:

如果明天換資料庫,核心規則要不要跟著大改?如果換一個付款服務,是否要修改一堆完全無關的商業邏輯?如果答案是「要」,那代表技術細節已經滲進核心。

好的架構不是層數很多,而是變動能被控制在合理範圍內。

第四步:模組不要只分得細,還要藏得住複雜度

John Ousterhout 在《A Philosophy of Software Design》中提出「深模組(deep module)」的觀念。

一個好的模組,應該用相對簡單的介面,包住大量內部複雜度。相反地,如果你建立了很多類別、服務與介面,但每一層只是把參數原封不動傳給下一層,使用者仍然必須理解整條呼叫鏈,那只是把複雜度切碎,沒有真的隱藏它。

這類「淺模組」會讓系統看起來很有架構,實際上修改成本卻很高。

另一個常見問題是資訊洩漏:同一個設計決策被散落在很多地方。例如某個價格計算規則、快取鍵格式或資料欄位邏輯,在不同模組各寫一份。只要規則一改,就必須到處找。

架構最佳化不是把檔案切得愈小愈好,而是讓每一個重要知識盡量有清楚的歸屬。

第五步:用小步重構取代「一次整理乾淨」

Martin Fowler 對重構(refactoring)的定義非常精準:改變軟體內部結構,讓它更容易理解與修改,同時不改變可觀察行為。

這和「重寫」最大的差異,就是步伐大小。

重構通常是一連串很小的變化:抽出函式、拆開過長條件、移動方法、消除重複、把混在一起的責任分開。每做一小步,就重新跑測試,確認行為沒有漂移。

如果改了三天,整個系統一直不能執行,最後才一次測試,那比較像重新改造,不是安全的小步重構。

這裡還有一個重要原則:結構調整與功能改變盡量分開。

先把「讓程式更容易改」這件事做好,再做真正的功能或效能變更。兩種變化混在一起,一旦出錯,就很難判斷問題來自哪裡。

第六步:效能問題要處理真正的熱點,不是先加基礎設施

當安全網與結構比較清楚後,才進入真正的效能處理。

常見順序應該由便宜、直接的改善開始。

先看演算法是否有明顯浪費,再看是不是重複呼叫外部服務、資料庫查詢次數太多、一次抓了不必要的大量資料。這些問題如果沒處理,就算加快取、加伺服器,也只是用更多資源掩蓋原本的浪費。

快取也不是萬靈丹。每一個快取都應該回答兩個問題:哪些資料值得存?什麼事件發生時要失效?沒有失效規則的快取,很容易把速度問題變成資料不一致問題。

訊息佇列也一樣。適合移出即時請求的工作,例如寄信、產生報表或耗時背景處理,可以放到非同步流程;但如果只是因為架構看起來「比較大型」,就先加入佇列,反而會多出重試、監控、順序與一致性問題。

真正的最佳化,是用證據證明新增的複雜度值得。

第七步:資料層常常才是真正瓶頸

Martin Kleppmann 在《Designing Data-Intensive Applications》中反覆討論可靠性、可擴充性與可維護性的取捨。對實際系統來說,資料層往往比應用程式碼更容易成為瓶頸。

幾個很常見的問題包括:每筆資料再查一次造成 N+1、該有索引的查詢沒有索引、列表沒有分頁、一次抓出太多欄位,以及深度分頁隨資料量增加愈來愈慢。

這些問題最有效的診斷方式,不是只看 ORM 寫法,而是看真正送到資料庫的查詢,以及查詢計畫。

索引也不是愈多愈好。索引可以加快特定讀取,但也會增加寫入成本與儲存負擔。真正該做的是根據實際存取模式決定,而不是看到慢就一律加索引。

資料一致性也是架構的一部分。當系統開始加入快取、副本、背景工作與多個資料來源,讀到舊資料、重複處理與交易衝突都可能出現。速度提升不能建立在「偶爾算錯也沒關係」的假設上。

第八步:正式環境的「慢」,常常來自失敗中的依賴

開發環境跑得快,不代表正式環境就穩。

Michael Nygard 在《Release It!》關注的正是這個落差:系統在正常狀態測試通過,上線後卻可能因為某個外部依賴變慢,拖垮整條請求。

例如一個第三方服務平常 200 毫秒回應,某天突然卡住 30 秒。如果系統沒有明確逾時,大量請求就會一起等,最後連線池、執行緒或工作程序被耗光。

因此正式環境的架構最佳化,還要考慮逾時、斷路器(circuit breaker)、隔艙(bulkhead)、重試退避與監控。

重試尤其容易被誤用。當對方服務已經過載,如果每一個失敗請求立刻連續重試,等於在對方最脆弱的時候加更多流量。好的重試需要上限、退避與隨機抖動,而不是無腦再試。

為什麼「全部重寫」通常不是第一個答案?

不是因為重寫永遠錯,而是因為它的風險很容易被低估。

舊系統常有大量沒有被文件記錄的規則。只看程式碼結構,會覺得很多地方「很醜」;但其中可能藏著多年修出來的例外處理。重寫時最容易遺漏的,往往就是這些看起來不起眼、實際上有人正在依賴的行為。

另一個問題是雙軌成本。舊系統仍要維護,新系統同時開發,兩邊都要處理需求。重寫期間市場不會停止,最後常出現新系統還沒完成,舊系統又已經多了幾個月的新功能。

比較安全的方法通常是:先挑最痛、最有證據的熱點,把行為固定下來,再逐步抽離邊界、改善資料層或替換元件。

如果真的需要重寫,也應該是因為已經有足夠證據證明局部改善成本高於替換,而不是單純因為「看舊程式不順眼」。

網站、外掛與線上課程系統,也可以用同一套思路

這套方法不只適用大型 SaaS。

一個經營多年的網站,也可能逐漸累積外掛、客製程式、追蹤碼、會員邏輯、金流整合、課程系統與自動化。當網站開始變慢或難維護,最容易做的就是亂停外掛、換主機、換佈景主題,甚至整站重做。

但如果真正瓶頸是一支慢查詢,換主機未必有效;如果問題是多個外掛重複做同一件事,換佈景主題也解決不了;如果會員與訂單邏輯沒有測試,整站搬家反而可能增加風險。

比較合理的順序仍然是:先看量測資料,再確認重要流程能不能被驗證,接著處理重複與耦合,最後才決定是否需要更大的架構調整。

同樣地,如果你的問題其實是「專案一直加東西、一直做不完」,可以延伸閱讀 37signals 如何用少做一點,反而更快做出好產品。範圍失控與架構失控,很多時候會互相放大。

一套實用的架構最佳化判斷順序

如果你的系統現在已經慢、亂、難改,可以先照這個順序判斷:

先量測:哪一條流程真的慢?慢多少?瓶頸在哪裡?

再保護:重要行為有沒有測試?哪些舊邏輯目前沒人敢動?

再整理邊界:商業規則是不是被框架、資料庫或第三方服務綁死?

再重構熱點:先讓最常改、最常出問題的地方變得容易理解與修改。

再處理效能:演算法、查詢、快取、非同步、擴充,依量測結果由便宜到昂貴處理。

最後補韌性:逾時、重試、斷路、監控與效能預算,避免今天優化完、幾個月後又退回原點。

這個順序的重點,是每一步都能驗證,而不是一次賭一個大改版。

架構最佳化的終點,不是「漂亮」,而是更容易安全地改

好的架構不一定有最多層、最多模式,也不一定看起來最先進。

真正有價值的架構,會讓團隊更快回答幾個問題:這個改動會影響哪裡?有測試可以保護嗎?瓶頸有數據嗎?如果改錯能快速回復嗎?下次再遇到同類問題,會不會比這次更容易處理?

如果每次改動都需要全團隊開會、每個模組都可能被波及、每次效能問題都靠猜,那才是架構正在拖慢產品。

架構最佳化不是一次性的「整理大掃除」,而是把系統重新變成可觀察、可測試、可修改、可驗證的狀態。

常見問題

系統很慢,第一步應該先看程式碼嗎?

通常不是。先建立可比較的效能基準,確認真正慢的是哪個流程,以及時間花在哪裡。沒有量測就直接改程式,很容易優化到不重要的地方。

什麼情況才適合整套重寫?

當既有系統的核心限制已經無法透過合理的漸進改善處理,而且團隊對現有行為、遷移範圍、資料轉換與雙軌成本都有足夠掌握時,重寫才比較有討論價值。它應該是經過證據比較後的選項,不是第一反應。

沒有測試的舊系統,可以直接開始重構嗎?

風險很高。比較安全的做法是先為要修改的關鍵路徑建立特徵測試,先固定目前行為,再小步調整。測試不必一開始覆蓋整套系統,但至少要保護這次會碰到的地方。

Clean Architecture 是不是一定要分很多層?

不是。真正重要的是依賴方向與責任邊界,而不是資料夾數量。為了看起來符合架構圖而增加大量沒有價值的中介層,反而可能提高複雜度。

換更快的主機,可以解決架構問題嗎?

只能解決部分資源不足問題。如果真正瓶頸是低效率查詢、重複運算、外部服務等待或模組耦合,單純升級主機可能只是延後問題再次出現。先量測再決定,通常比較省錢。

參考資料

Michael Feathers. Working Effectively with Legacy Code.
Robert C. Martin. Clean Architecture.
John Ousterhout. A Philosophy of Software Design.
Martin Fowler. Refactoring: Improving the Design of Existing Code. MartinFowler.com: “Refactoring” and “Legacy Seam.”
Martin Kleppmann. Designing Data-Intensive Applications.
Michael T. Nygard. Release It!.
David Thomas and Andrew Hunt. The Pragmatic Programmer.

元氣站長
元氣站長

經營網路事業,賺取持續收入!
網路賺錢修煉,成就理想人生!