【架構必備】讀寫分離的方方面面及主要設計
一. 問題&分析
在讀多寫少的互聯網業務場景,往往“讀”性能會成為第一個瓶頸。
隨著業務的發展,數據庫負載越來越高,逐漸成為系統的瓶頸。面對“讀”性能瓶頸,大致有以下幾種解題思路:
- 提升 DB 配置從而獲取更高的性能。使用更 NX 的機器,升級 DB 的 CPU、內存、磁盤等;
- 使用更多的 DB 來分擔讀壓力。對 DB 進行“拆分”,一個 DB 實例負責數據寫入,一組 DB 實例負責數據查詢,也就是常說的讀寫分離;
- 將 讀 壓力轉移到其他存儲引擎。比如引入讀性能更高的 Cache,讓 Cache 擋在 DB 前面,降低落到 DB 上的請求量;
上面三種方案各具千秋,但性價比最高的仍舊是“讀寫分離方案”:
- 方案一,升級硬件資源,簡單粗暴,主要是金錢上的考量;另外,硬件是存在天花板的,金錢不能解決一切問題;
- 方案三,緩存是提升讀性能的一大殺器,追求性能的同時需要做很多事,比如調整邏輯代碼、進行一致性保障、增加運維成本等,同時也為系統引入更多的復雜性。落地效果非常不錯。但有時,殺雞焉用牛刀?
- 方案二,中規中矩介于兩者之間,無需過量的金錢投入,也無需過早的引入太多復雜性
二. 讀寫分離
以數據庫部署架構為基礎,對數據操作進行分離,主節點主要處理寫請求,從節點主要處理讀請求。
通過引入多個副本來分散讀請求,從而實現 讀請求 的水平擴展。主副本 與 從副本 間的數據一致就是通過 “復制” 來完成。
讀寫分離架構有幾個非常重要的概念:
- 主副本。也稱主節點,可以接受 讀&寫請求;
- 從副本。也稱從節點,只能處理 讀請求;
- 復制。主副本 與 從副本 間 基于“復制”技術實現數據同步;
- 路由。基于路由規則將請求分發至整個集群(主副本 + 從副本)
以最常見的MySQL主從架構為例:
通過擴展 Slave 可以實現 讀請求 的水平擴展,核心流程如下:
- 應用將寫請求路由到 Master 節點;
- Master 節點完成寫入后(事務提交),將變更寫入到 Binlog;
- Slave 節點從 Master 節點獲取 Binlog,并執行變更(涉及中繼日志和并發復制),保持與 Master 數據一致;
- 應用將讀請求路由到 Slave 節點,從 Slave 中獲取數據;
備注:Slave 過多會加重 Master 的壓力,可通過多級復制或分區進行解決。
讀寫分離架構可以方便的對讀請求進行擴展,看似美好,但需要解決兩個問題:
- 數據同步。如何保障主從副本間的數據一致性,通常情況下由存儲引擎的“復制”機制來保障;
- 請求路由。何時操作主副本,何時操作從副本,如何在多個副本間做負載均衡?
1、復制模式
復制模式主要解決數據同步問題。通常比想象中的復雜,在此只對 單主復制 進行介紹,對于多主復制,由于過于復雜,并不在討論范圍。
(1)同步復制
同步復制架構如圖所示:
核心流程:
- 主節點處理完請求后,將復制信息同步到所有節點;
- 待所有節點返回后,再向用戶返回最終結果;
特點:
- 優點
- 強一致性保障,應用寫入成功后,從節點與主節點間就達成了一致,不存在數據延時問題
- 缺點
- 影響寫入性能。寫性能 = Master + Max(Slave)
- 影響可用性。某個 Slave 異常,直接影響寫入,導致寫入流程被中斷
常見應用場景:
- 在實際開發中,該方案很少使用,特別是在 CAP 最終一致性思想的影響下
- TiDB 等 NewSQL 內部通過一致性協議保障 強一致,基于 NRW 理論保障可用性,這塊非常復雜,不在討論范圍
(2)異步復制
異步復制架構如下:
核心流程:
- 主節點處理完請求后,直接返回處理結果;
- 從節點通過異步方式從主節點獲取信息;
特點
- 優點:寫入性能好
- 缺點:存在數據丟失風險
應用場景
- 數據安全性要求低,性能要求高的場景,如 日志記錄 等
- 極端情況下的“飲鴆止渴”,如 秒殺、大促場景
(3)半同步復制
半同步復制是同步復制和異步復制的結合體,架構如下:
核心流程
- 主節點處理完請求后,對部分節點進行同步復制,等待其復制完成后,在向應用返回最終的處理結果;
- 其他剩余節點進行異步復制
特點
- 在性能和一致性間做平衡
應用場景
- 滿足大多數業務場景。
2、路由模式
路由模式,主要決定請求如何分發到眾多的數據庫節點。
(1)應用路由
在應用層使用代碼對請求進行分發,整體架構如下:
核心流程:
- 為每個數據庫節點構建一個DataSource,結合 ORM 框架,構建不同的 DAO
- 應用代碼根據業務場景,調用不同的 DAO 實現,完成讀寫操作
- 多個從節點 DataSource 可以封裝為 一個聚合 DAO,內嵌負載均衡算法,在不同的 從庫 間進行路由
特點:
- 優點:簡單,使用編碼實現,掌控力最強
- 缺點:對系統存在極大的侵入性,需要修改大量的邏輯代碼
場景:
- 存在于老的項目或需要極致掌控力的場景
(2)智能數據源
將多個 DataSource 封裝為一個具有路由功能的 SmartDataSource,整體架構如下:
核心設計如下:
- 每一個數據節點對應一個 DataSource
- 將多個 DataSource 封裝為一個 SmartDataSource
- SmartDataSource 自動解析 SQL,根據 SQL 類型自動完成請求路由
- 應用程序僅于 SmartDataSource 進行通信
特點:
- 優點:對程序沒有侵入性,無需調整代碼,只需替換底層 DataSource 即可
- 缺點:集群情況下,需要配置中心進行統一協調
場景:
- 特別使用于高性能場景
(3)Proxy 路由
將 SmartDataSource 核心功能抽取到單獨的服務,整體架構如下:
核心設計:
- 將 SmartDataSource 的職能抽取到單獨的服務
- 向下管理多個數據庫節點
- 向上暴露標準的 jdbc 接口,供應用程序使用
特點:
- 優點。便于管理,所有的管理動作全部收口到 Proxy 層;
- 缺點。增加一層網絡開銷,對性能有一定的影響;
場景:
- 使用于管理場景
通常情況下,會在配置中心的基礎上,綜合使用智能路由和Proxy路由兩種模式:
智能路由。用于應用程序,追求極致的性能;
Proxy路由。用于數據庫管理,追求管理的便利性;
配置中心。為智能路由和Proxy路由提供統一的配置信息。
三、延時挑戰
應用程序集成讀寫分離后,最主要的挑戰便是:復制延時。所以,在系統設計時,需要對特定場景進行特殊處理。
1、更新場景,強制切主
對于更新場景,為了避免 主從延時導致的 寫覆蓋問題,通常使用強制切主策略。
寫覆蓋的根源,見下圖:
由于存在主從延時,所加載的 聚合根 不一定是最新的數據,因此,后續的修改 和 保存,都是在過期數據上執行,導致寫丟失。
備注:樂觀鎖保護下,不會出現寫丟失情況;
面對這種場景,最簡單的策略便是:強制切主。具體流程如下:
直接從 Mater 進行加載,避免 Slave 查詢到過期數據。
SmartDataSource 和 Proxy 都提供了強制切主的設置方式,在此不做過多介紹。
2、根據 version 進行智能路由
如果下游能拿到最新版本的 version,便可以根據 version 智能的獲取數據。
以領域事件場景為例,問題描述如下:
核心流程如下:
- 業務完成后,將變更更新至 DB,Master 更新完成后,直接返回處理結果;
- Slave 啟動異步同步,但完成時間不可控;
- 業務發送 領域事件 至 Topic;
- 下游業務監聽消息后,從 Slave 查詢數據,如果Slave 尚未同步完成,則出現獲取不到或獲取過期數據的問題
針對這個場景,可以引入 version 進行數據驗證,基于 Version 的流程如下:
核心流程如下:
- 業務完成后,將業務變更和version變更更新至 DB,Master 更新完成后,直接返回處理結果;
- Slave 啟動異步同步,但完成時間不可控;
- 領域事件包含當前的最新 version,將其發送至 Topic;
- 下游業務監聽消息后,先從 Slave 獲取數據,并比對兩者的 version
- 如果大于等于 msg 中的 version,則直接使用;
- 否則 從 Master 中進行加載,然后執行業務邏輯
備注:步驟4 中 version 管理應該封裝在服務接口,對外提供統一的帶 version 參數的接口;
時間戳是一種特殊的version,可以使用數據表的 update_time 作為 version。
3、讀己之寫
讀己之寫,簡單說就是:保存完數據后,理解讀取數據。
由于復制延時的存在,通常無法立即讀取剛寫入的數據,問題流程如下:
核心流程:
- 業務基于 Master 完成操作直接返回,異步并將變更復制到從節點
- UI跳轉至下一個頁面,該頁面會讀取最新數據(詳情頁、列表頁)
- 由于存在主從復制延時,可能無法獲取最新數據
(1)主動延時
最簡單的解法便是,在完成數據更新操作后,UI 主動sleep幾秒,然后在進行下一步操作。
整體流程如下:
- 在跳轉新頁面前,增加 loading 頁,主動等待主從完成同步
- 在系統壓力大時,仍舊無法從根源上解決該問題
- 由于其簡單性,在項目中也大量使用
(2)UI 動態添加
主動等待對用戶存在一定的傷害,可以使用動態添加方案提升用戶體驗。
核心點包括:
- 更新請求處理完成后,直接返回最新的數據,包括新增數據或修改后的數據;
- 前端獲取數據后,直接在 UI 上進行操作,如將其 append 到 Table 中 或 直接渲染 詳情頁;
(3)智能切主
UI主動添加只是一種障眼法,用戶刷新頁面,仍舊可能看不到最新數據,可以試試強制切主
根據規則,決定是否強制切主,如下:
- 根據業務場景,“我的 xxx” 強制切主,其他請求 默認走 Slave
- 時間間隔,請求時攜帶時間戳或版本,對請求進行切主判斷
四、小結
簡單回顧,本文概要介紹了“讀寫分離”的方方面面,主要設計
- 讀寫分離是提升系統讀性能的重要手段
- 落地讀寫分離,需要解決復制 和 路由 技術問題
- 由于復制延時的存在,對特殊的業務場景進行治理