一次訂單系統重構實踐

  • 2020 年 11 月 21 日
  • 筆記

在我們的工作中,經常會遇到系統或模塊重構工作,今天就來聊一聊我曾經經歷過的一次系統重構經歷。

01 背景‍

重構發生的背景是,原有的系統架構採用all-in-one的方式,隨着業務的快速發展,用戶訪問量急劇上升,系統請求流量成倍增長,陸續出現了各種問題。當時的系統架構的示意圖如下

02 痛點

當時遇到的典型問題有

  • 系統模塊耦合嚴重,訪問量上漲無法快速擴容

  • 數據庫表混雜,定位不清。比如支付訂單和商品訂單在一張表,一個狀態字段代表兩種不同訂單的狀態流轉含義,經常會出現各種狀態異常單據。

  • 複雜SQL和跨表join橫行,SQL慢查多,數據庫頻頻告警

  • 無服務和領域劃分,系統和接口耦合嚴重,經常是單點出問題,全系統宕機

  • 接口響應慢,系統穩定性差,數據丟失、錯亂情況經常出現

  • 產品需求版本龐雜,業務需求場景多,業務邏輯分散,需求迭代速度慢

  • 客訴問題高發,排查問題困難,研發疲於奔命在查問題的道路上

面對着這些問題,當時擺在眼前的方案有兩個

  • 繼續按照原有系統迭代,但可能要付出更多的人力、精力來維持系統的穩定性和需求迭代速度

  • 完全重構系統,但需要投入一定的人力,並且可能會在短期影響業務的需求迭代進展

考慮到產品會長期迭代,而眼前系統已經成為巨大的瓶頸,因此決定對系統做完全的重構。

當時我被領導安排作為這個重構項目的負責人。但領導也提出了要求

  • 公司業務在快速發展中,系統重構期間,需繼續保持業務需求的迭代速度,可以適當增加人員

  • 新系統設計和規劃,需考慮到3年後可能的用戶訪問量的上漲和數據量的上漲

  • 新老系統切換期間,需要保證不影響用戶和業務方的正常使用,不出現數據的丟失和錯亂

任務既然已經確定了,接下來就是考慮如何做的問題了。

03 方案

系統重構是一個複雜的工程,而在一個業務高速發展的背景下做系統重構,無疑於給飛行中的飛機換引擎,需要考慮周全,計劃縝密,才能保證萬無一失。

針對面臨的問題和目標要求,在技術層面制定了以下幾點大的原則:

  • 採用分佈式架構設計,將各個模塊系統完全拆分出來,獨立部署迭代演進

  • 數據庫模型完全重構,原有的數據庫模型已經無法支撐新的業務需求擴張,同時配合分佈式架構的改造落地

  • 業務邏輯收歸,對涉及到的相關領域按照業務邏輯收口,統一服務接口

  • 新老數據庫雙寫,保證系統穩定性和數據不丟失

  • 新老系統並行提供服務,通過灰度控制流量切換,直至老系統下線

04 實施

需求和接口的梳理

在大目標和技術方向確定的情況下,接下來就進入到實施階段。

考慮到系統中的核心場景和瓶頸都出現在訂單模塊,因此制定了分佈分階段實施的方案,第一步核心解決訂單相關功能的重構拆分,本文也將按照訂單系統的重構拆分來展開說明。

既然是系統級重構,首先需要對業務需求和產品功能進行梳理。

好在有產品的歷史文檔,加上通過線上產品的實時模擬驗證,能夠將訂單相關的大致功能脈絡理清楚。

功能層面的需求梳理還無法滿足系統級重構的要求,需要更精確的梳理到接口級別,包括對訂單相關接口調用的上游模塊和訂單對其它下游模塊的調用,這樣才基本做到把訂單模塊的邊邊角角功能完全覆蓋。

功能需求和接口層面的整理,為數據庫表模型設計提供了大致的參考。

數據模型層面考量

通過對已有產品功能和接口的分析,分析清楚訂單模塊提供的核心能力應該有哪些,和其它模塊的邊界是怎樣的,外部對訂單模塊的複雜調用需求有哪些,基於這幾點設計新的數據庫模型。這裏面有幾個關鍵的考慮:

  • 大數據量的解決方案:分表。考慮到訂單數據量過大,原有的單一訂單主表已經無法滿足需求,因此將訂單主表按照用戶ID取模的方式分64張表,按照單表5000w數據的測算,基本可以支撐未來3年內數據量的增長。按照用戶維度的分表方案,在單個用戶的訂單查詢場景下,通過數據庫單表就可以完成。但除了按照用戶維度的查詢,還有按照時間、地域等維度的訂單查詢需求,考慮到繼續按照其它維度建立相應的分表方案太過冗餘,因此決定對其它的查詢能力通過ES構建搜索索引提供。

  • 主鍵生成策略:分佈式ID自增。訂單表的主鍵,原來採用的是數據庫自增策略,分表後已不再適合,借鑒twitter的snowflake方案,設計了分佈式的ID自增方案。

  • 跨表查詢的解決方案:服務層聚合。原有的代碼中,有大量的跨表查詢,容易導致複雜SQL出現,嚴重影響數據庫性能。在新的數據庫表結構下,將表的職責劃分清楚後,不再允許新的跨表查詢,涉及到跨表查詢的需求,通過在代碼層面拆分成單表查詢再聚合的方式,解除跨表查詢帶來的問題。

  • 新老模型雙寫:為了保障系統的穩定性和不停機灰度流量驗證,設計開關來實現對新老模型進行雙寫,因此還需要將新老模型的相關表整理好對應關係,如表字段枚舉值不同帶來的映射等等。新老模型雙寫採用的方案也是通過程序處理,而非binlog等方式,主要考慮是為了處理的靈活性和設置開關用於切換的可控性。

數據庫模型設計完成,接下來需要考慮到訂單模塊的架構設計方案。

架構方案設計

根據對於需求的整理和理解、接口的梳理以及表模型的整理,大致可以確定的系統架構示意圖如下

這裏面有幾點需要說明:

首先,考慮到歷史版本App無法強制要求所有用戶升級,因此需要在Nginx中將老版本的接口做重定向,轉發到新設計的接口服務層對應的接口上。

其次,對接口服務層做了拆分,因產品有不同的展現形態,包括App、Web管理後台等,因不同用戶角色也有多個不同的App,因此設計接口服務層,將相關的用戶鑒權、數據加解密等統一收歸到這一層處理。

第三,設計業務邏輯層,將訂單相關的業務邏輯抽象到業務邏輯層,對外提供聚合封裝的訂單服務能力,如訂單詳情服務,訂單列表服務等。業務邏輯層需要調用訂單領域層的服務,還可能會調用到其它模塊的領域層服務做聚合,例如訂單詳情頁除了展現訂單的信息,還有商品相關信息、支付相關信息、配送相關信息,這些信息基本都在業務邏輯層做聚合處理。

第四,領域服務層,核心是本領域內數據庫表的操作封裝,這一層基本只做單個表的增刪改查。

最後,將訂單相關的庫從原有的單一庫中拆分出來,建立訂單庫。實際上訂單系統又分了多個領域,也可根據實際情況將訂單相關的單一庫再做拆分細化。

以上的設計只是一個改造完後的方案。但真正在實施重構的時候,為了保障線上系統可以不停機切換,又分別作了相關的開關設計用於過渡階段的驗證。

階段一的過渡方案架構示意圖如下:

在階段一,有以下兩點設計

  • 在接口服務層all-in-one-app應用中,設計開關,可以控制all-in-one-app應用調用新的接口服務層,或繼續走原有的直接訪問數據庫的邏輯。一旦出現新服務、新的庫表模型有問題,通過開關直接切換回原有的調用鏈路中。

  • 在領域服務層如oder-domain1-service、order-domain2-service、other-domain-service等應用中,設計開關,實現對原all-in-one庫和訂單庫的讀、寫開關。

第一階段上線後,正常的流程實現是

1、通過nginx將老的訂單相關接口,轉發到新的訂單接口服務層應用的相關接口,實現流量切換。

2、將all-in-one-app應用中的調用開關打開,切換到調用新的拆分過的相關業務邏輯層。

3、在領域服務層,將對all-in-one庫實現讀、寫,而對訂單庫實現只寫不讀。

這個階段主要驗證了整個服務和接口調用鏈路正常。當相關鏈路或環節出現問題,也可以通過關閉對應開關快速切換回原有方案。

階段二的架構示意圖如下

經過階段一的驗證,基本可以保證整個接口鏈路層面的邏輯正確,外部的調用方不再感知接下來的改動變化。

階段二的核心是在內部的數據層面做驗證,保證落在新模型中的數據是正確無誤的。

這一階段沒有特別多的開發工作,主要操作是

1、在訂單領域層相關應用中,將對all-in-one庫的寫開關保留,讀開關關閉

2、在訂單領域層相關應用中,將對訂單庫的讀寫開關同時打開

這時整個調用鏈路和數據鏈路已經完全實現了走新的接口服務和新的數據庫表。再通過產品功能層面驗證數據展現和產品流程是否正確,輔助老庫相關數據做對照,基本能夠驗證整個系統的重構的正確與否。

這個階段如果相關鏈路或環節出現問題,可以繼續通過開關的控制切回到原有的調用鏈路和數據鏈路。

階段二驗證通過後,後續還需要做一些收尾工作,包括去除雙寫代碼、去除代碼中的開關及歷史代碼邏輯等等。

項目重構實施

整個架構方案確定後,接下來的重點是制定重構項目的計劃,鎖定相關資源,確定重構項目的各個裡程碑節點。

項目計劃的制定,不僅僅是關注訂單模塊本身的改造開發,還包括識別相關資源方和調用方,推動項目排期和落地。

在大部分的程序員認知中,只要自己系統沒有大問題,都不願意做相關的改動,畢竟任何一點改造都會額外的工作量,也會對系統的穩定性有着未知的影響。另外業務方也可能會對重構有排斥,這時就需要搞定關鍵人物,將改造的利弊陳述清楚,有時甚至需要上升到更高的層級去推動。最終能夠和相關方達成一致,確定改造的時間計劃,提前鎖定對應的開發、測試資源,保障整個重構的順利進行。

開發階段的任務既包括重構相關的接口改造開發,還需要考慮新老庫表模型切換所做的兼容,包括新老庫表數據遷移兼容、消息隊列兼容、緩存兼容等。

在開發完成後,需要做新老庫表數據遷移的模擬演練,以驗證老的表數據導入到新庫表後,流程和展現不會出現問題。

系統級的重構改動,不可或缺的是全流程的測試驗證。

為了保證測試的充分性,當時我們採取了以下幾點關鍵措施:

1、通過已經沉澱和新增加的接口自動化用例,對大部分接口的響應和返回值做多次的跑批驗證

2、通過測試人員不斷的交叉測試,對可能遺漏的業務場景驗證

3、通過將線上的流量複製重放,對新的接口進行邏輯驗證

4、通過預發環境的流量灰度,對全流程的業務做模擬驗證

系統重構在開發測試完成後,面臨的另外一個重要問題是上線。

首先,制定詳細的上線計劃,將上線步驟事項按照先後順序全部羅列出來

其次,每個上線步驟事項需要預估出操作的時間並明確責任人

第三,對任一環節可能出現的問題,提出假設並給出解決預案,防止上線中途出現問題因慌亂導致可能出現的異常。

最後,統一指揮,有序切換流量做灰度驗證,保障整體流程正常。

上線過程中,藉助已有的監控系統,用於觀察系統、服務、接口等各項數據指標的變化情況,判斷上線中的每個環節是否有異常。

上線完成後,通過逐步的控制灰度流量佔比,驗證流程和數據是否正常,進而驗證整個重構是否成功完成。

在訂單模塊重構的過程中,其它模塊也在改造和推動中,經過接近半年左右的時間,基本完成了對原有all-in-one服務的完全拆分重構。

整個架構也在後續的迭代中不斷進行着新的重構和演進,包括在接口層前置設計網關接入層、訂單服務的細化拆分、ES對查詢場景的替換改造、業務邏輯層的中台化演進等。

05 總結

總結在整個重構過程中的幾個關鍵步驟

  • 分析目前系統的問題點,找到最重要最優先要突破的點

  • 確定重構所要達成的目標、方向及限制條件

  • 確定重構涉及到的核心技術方案及可行性

  • 梳理重構所涉及到的需求、場景及相關上下游依賴方

  • 設計明確和完善的技術方案

  • 制定詳細的項目計劃,鎖定資源和里程碑節點並推進

  • 全流程的測試驗證

  • 詳細完備的上線計劃

  • 不可或缺的灰度驗證

系統重構是一件耗時耗力的工作,但同時也是對自身綜合能力的一個巨大挑戰和鍛煉,期間會遇到各種各樣新的問題。但正是通過這些真實的實戰,在不斷的重構中發現自身的能力瓶頸,去學習和成長。

如果你也有相關經歷和想法,也歡迎與我交流。