數據庫拆分實戰
前言
對遺留系統的微服務化改造,從整體上來說,整個過程包含兩個部分:一,通過某一種方法論將系統進行微服務劃分,比如 DDD 倡導的限界上下文劃分方法。根據系統的特點和運行狀態,又分爲具體的兩種實施策略,絞殺者模式和修繕模式。二,數據庫的拆分,只有在數據層面也拆分開,才能真正達到服務化的目的。具體也可以分爲,與業務服務拆分同時進行,或者等業務服務拆分後再單獨進行兩種策略。
似曾相識的步驟
如果不考慮在拆庫的同時引入新功能,拆庫其實也是一種重構。Martin Fowler 在**《Refacotring》**中強調數據庫具有高度的耦合性,數據庫重構存在相當的難度。不過好在還有另一本權威著作來爲此背書,那就是**《Refactoring Databases》**。
來看看這本書提到的數據庫重構步驟:
-
Verify that a database refactoring is appropriate
-
Choose the most appropriate database refactoring
-
Deprecate the original database schema
-
Test before, during, and after
-
Modify the database schema
-
Migrate the source data
-
Refactor external access program(s)
-
Run your regression tests
-
Version control your work
-
Announce the refactoring
-
What you have learned
是不是和代碼的重構似曾相識,分析 -> 測試 -> 修改 -> 測試……
同時也看看我們的數據庫拆分實踐是否能和這些步驟有所呼應。
背景介紹
我們曾經對某客戶企業的系統做服務化改造。根據其組織架構和系統特點,最終採取了先服務拆分,再數據庫拆分的演進路線。
到服務化改造基本完成時,系統邏輯結構如下圖所示:
右邊的圖完全就是《Refactoring Databases》裏說的 Multi-Application Database。
接下來就是數據庫的重構了,也是本文的重點。
分析在前
系統數據庫採用 MySQL,由於之前是一個大單體,所有的數據都存在一個數據庫裏。隨着業務的增長,單庫雖然已經使用了頂級的硬件,性能仍顯不足。所以不管從架構上,還是性能上,拆庫都迫在眉睫。這也就回答了 Verify that a database refactoring is appropriate 的問題。
數據庫重構相對於代碼重構畢竟影響更廣,風險更大。直接採用 XP 的模式風險太大,必要的分析必不可少,整個過程力求一次正確。
首先必須瞭解數據庫的全貌,經過一番溝通梳理出架構圖如下:
主庫,歷史庫,歸檔庫之間可以互相遷移數據,遷移代碼是完全自研的,支持單個訂單的一系列數據遷移,也支持批量的訂單遷移。
業務服務的遷移歷經一年多的時間,單體也進化成了十幾個微服務,要想一次性把數據庫全部拆開不太現實,風險也不可控。最好的方式是找出當前數據庫的瓶頸,先將業務上訪問量最大的部分拆開。經過調研,決定先將數據庫一分爲二,先將發貨單拆出去,類似於修繕模式,訂單及其他數據先保留。這也呼應了 Choose the most apporiate database refactoring,所以設想拆分後的數據庫應該如下圖所示:
1. 業務代碼
1.1 發貨單服務的數據庫配置
1.2 所有類似 join 查詢的級聯操作,主要集中在頁面查詢,導出,報表等。(寫入操作在微服務拆分時基本已經修改)
2. 數據
2.1 新建發貨單數據庫,schema 和用戶
2.2 已有的發貨單相關的數據遷移至新數據庫
3. 遷移代碼
3.1 源庫和目標庫都要支持多源配置,現在是一分二,將來會更多
3.2 遷移邏輯,要保證數據一致性。即一個訂單的數據要麼全在主庫,要麼全在歷史庫或歸檔庫
4. 監控和 BI
4.1 多源配置
4.2 監控邏輯修改
中間過程
通知上下游
這一點非常重要,在一個涉及多部門的系統上做數據庫遷移。有些影響絕不是靠自己就能考慮周全的,在做所有遷移之前就要通知各方評估各自的影響和改動週期。
業務代碼的修改
- 測試先行
重構最重要的一點是不改變程序的外在表現。對於數據庫重構來說,只需要保證對外暴露的 API 在特定輸入下,輸出是一致的。
在這個點上,測試是比較容易寫的。自動化測試和人工測試同時展開,以黑盒集成測試爲主。在每個 API 修改之前先根據現有結果編寫測試,同時 QA 記錄輸入,輸出,注意各種邊界情況的測試。在這個階段基本忽略現有代碼邏輯的正確性,先保證拆庫前後 API 的行爲一致。
當然,這是理想的情況,在真正開始做以後,就會發現情況並不是這麼簡單。
- 業務代碼修改
指導思想是將級聯查詢修改爲 API 調用補齊數據。然而這裏面有一個特殊情況,當遇到 join,groupBy,有 where 條件,再加上分頁的場景,API 調用補齊數據的方式就不能很好的處理。說說當時的幾種處理辦法:
-
非批量的查詢,通過 API 補齊數據。
-
批量查詢,但是級聯的數據不在過濾條件中,通過 API 補齊數據。根據性能和調用頻率考慮加緩存。
-
批量查詢,級聯數據在過濾條件中,沒有分頁(隱含的意思是數據量小),通過 API 先拿到數據,在內存中處理。
-
批量查詢,有過濾,有分頁。跟業務溝通是否能在查詢結果中刪除級聯的數據,如果不行,是否能在過濾條件中刪除級聯的數據。
實際操作下來,發現其實業務上並沒有設想的那麼難。首先只有個別 API 存在這種情況,其次這些 API 的一些字段可能是一些歷史原因造成的,刪除對現有的業務影響並不大。
當然如果最終無法在業務上達成一致,那就要考慮在報表庫和數據湖層面做聚合了,方法總是有的。
數據遷移
- 開發過程
過程中有三種方法:
- 同一個物理庫,保持相同的 schema,不同的用戶通過授權不同的表,達到邏輯劃分的目的。例如爲發貨單服務新建一個數據庫用戶,只把發貨單相關的表授權給它訪問。當前用戶收回訪問發貨單表的權限。
優點:簡單易操作,開發過程無需做數據遷移。
缺點:邏輯劃分畢竟不能完全模擬真實的生產環境。例如有些表是多個服務共享的,開發時只能多個用戶同時授權。如果業務代碼修改不徹底,就會出現一個服務寫入,其他服務讀取的情況。一旦上了生產,表做了物理隔離,就會造成讀取不到數據的事故。grant select,insert,update,delete on existing_schema.existing_table to 'new_user'@'%';
- 同一個物理庫,不同的 schema,可以保持相同的用戶,這樣修改較少。
優點:幾乎可以模擬生產數據庫,業務代碼好排查,畢竟新加的 schema 在之前的業務代碼裏沒有,很容易測試發現問題。
缺點:需要將部分表遷移至新 schema。如果是 MySQL,在不同 schema 之間遷移表還是比較容易的。例如:alter table existing_schema.existing_table rename new_schema.existing_table;
- 不同的物理庫,schema 是否修改取決於當前名稱是否表意。
優點:完全模擬生產數據庫
缺點:不同物理庫之間要做數據遷移
回頭看,有條件的情況下第三種方法最爲保險。第二種方法性價比最高。
- 生產數據遷移過程
由於是線上運行系統,版本上線窗口時間有限,不可能在上線當晚執行全部的數據遷移,所以必須提前做,這樣數據質量也有保證。
-
利用 MySQL 的主從機制來同步,需要注意的是,在發貨單主庫(上線之前是主庫的從庫之一)需要打開
--log-slave-updates
,否則無法再接一個從庫。 -
利用第三方數據庫同步工具,這類工具常常會帶有 UI,相對比較友好。
這樣在上線前就可以不斷檢查數據遷移的質量,上線當晚只需要很短時間的停機,甚至不停機。上線後兩個主庫都包含了很多彼此的歷史數據,可以不急於刪除,以防需要回滾。
主備庫的遷移代碼修改
之前要遷移的表都在一個數據庫裏,遷移可以用一個事務來保證同時成功或失敗。現在分佈在兩個庫裏,只能通過最終一致性來保證。
像以往的 AP 系統的處理方法,事件表加消息隊列,訂單的遷移觸發發貨單的遷移。實際修改過程中還碰到很多具體問題,發了兩次消息才最終達成一致,不過這些都是細節了。
這裏需要提醒的是,遷移程序的數據庫信息最好都是可配置的,以防上線過程中數據庫地址、schema、用戶名等臨時變更。
監控和 BI
這些在當時的上下文下優先級偏低,在第一次上線時,對於不能及時調整的,都先做了屏蔽處理,不過處理的思路和上面類似。
以上的這些步驟基本上和《Refactoring Databases》中提到的如下步驟不謀而合。Test before, during, and afterModify the database schemaMigrate the source dataRefactor external access program(s)Run your regression testsVersion control your workAnnounce the refactoring
上線
因爲上線之前發貨單的數據庫一直在同步主庫的數據,並且上線過程中同步仍然保持,理論上上線可以做到不停機。但是如果存在高併發的情況,主從 binlog 同步延遲大,很可能會造成部分髒數據,保險起見短暫的停機上線比較安全。
其實上面提到的問題,理論上是新老兩個版本同時在線上運行造成的。其中一個典型的例子就是灰度發佈,但是灰度發佈往往在數據上是隔離的,唯一要考慮的是配置項能不能區分開,因爲當前微服務往往會從_配置服務器_中取配置。如果配置項中不支持灰度實例配置項,就要特別注意。
這裏也有兩種方法可以選擇:
-
新版本讀取另一個變量。例如老版本讀取 DB_URL,新版本讀取 DB_URL_NEW。通過一份配置,多個配置項名稱的方式區分開。
-
如果是容器化部署的話,容器的環境變量通常優先級更高,在容器層設置變量覆蓋掉 Configuration server 中的變量。
第二種方法會導致配置項分散,所以優先選擇第一種。
另外,上線之後生產環境的測試必不可少。測試環境再如何測試也不能百分百保證生產環境一定沒問題。條件允許的情況下,多測一些修改過的地方和系統的關鍵功能。
總結
回顧整個拆庫流程,整體的策略還是對的。先找到數據庫的瓶頸,把一部分拆分出去,梳理清楚整個流程,之後進一步的細分,就水到渠成了。
但是數據庫重構和代碼重構有相似之處,也有不同之處。
相似之處在於修改的過程中基本的思路是一致的,測試 -> 修改 -> 測試,小步快跑,反覆迭代。
不同之處在於拆庫還依賴於硬件的基礎設施,這就更要求測試環境儘量去模擬生產環境。總結下來,整個過程出了兩個問題都是沒有完全模擬生產環境導致的:
-
測試環境通過不同用戶授權的形式做邏輯劃分,導致有一張表存在一個服務寫入,其他服務讀取的情況沒有在測試環境發現。
-
測試環境的 schema 和生成環境的名稱不一致,導致漏掉了歷史庫遷移程序的修改。
好在這兩個問題都及時發現,並很快糾正了過來。
在實際中,可能每個拆庫的場景都不盡相同,沒有絕對適用的流程方法,需要因地制宜,靈活操作。
最後,不管是業務代碼的修改還是數據庫的修改,最怕的是有些場景沒想到。一旦想到了,解決的辦法總是有的。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/6SiH6trc7L6_SucYRhNQbA