1. 達達系統架構升級經驗總結
1.1. 概述
- 達達是全國領先的最后三公里物流配送平台。達達業務主要包含兩部分:商家發單,配送員接單配送。
- 達達的業務規模增長極大,在1年左右的時間從零增長到每天近百萬單,給后端帶來極大的訪問壓力。壓力主要分為兩類:讀壓力、寫壓力。讀壓力來源於配送員在APP中搶單,高頻刷新查詢周圍的訂單,每天訪問量幾億次,高峰期QPS高達數千次/秒。寫壓力來源於商家發單、達達接單、取貨、完成等操作。達達業務讀的壓力遠大於寫壓力,讀請求量約是寫請求量的30倍以上。
1.2. 初始架構
- 隨着業務的發展,訪問量的極速增長,上述的方案很快不能滿足性能需求。每次請求的響應時間越來越長,比如配送員在app中刷新周圍訂單,響應時間從最初的500毫秒增加到了2秒以上。業務高峰期,系統甚至出現過宕機,一些商家和配送員甚至因此而懷疑我們的服務質量。在這生死存亡的關鍵時刻,通過監控,我們發現高期峰MySQL CPU使用率已接近80%,磁盤IO使用率接近90%,Slow Query從每天1百條上升到1萬條,而且一天比一天嚴重。數據庫儼然已成為瓶頸,我們必須得快速做架構升級。
1.3. 讀寫分離
- 實現讀寫分離后,數據庫的壓力減少了許多,CPU使用率和IO使用率都降到了5%內,Slow Query也趨近於0。主從同步、讀寫分離給我們主要帶來如下兩個好處:
- 減輕了主庫(寫)壓力:達達的業務主要來源於讀操作,做讀寫分離后,讀壓力轉移到了從庫,主庫的壓力減小了數十倍。
- 從庫(讀)可水平擴展(加從庫機器):因系統壓力主要是讀請求,而從庫又可水平擴展,當從庫壓力太時,可直接添加從庫機器,緩解讀請求壓力。
1.4. 主從延遲
- 當然,沒有一個方案是萬能的。讀寫分離,暫時解決了MySQL壓力問題,同時也帶來了新的挑戰。業務高峰期,商家發完訂單,在我的訂單列表中卻看不到當發的訂單(典型的read after write);系統內部偶爾也會出現一些查詢不到數據的異常。通過監控,我們發現,業務高峰期MySQL可能會出現主從延遲,極端情況,主從延遲高達10秒。
- 那如何監控主從同步狀態?在從庫機器上,執行show slave status,查看Seconds_Behind_Master值,代表主從同步從庫落后主庫的時間,單位為秒,若同從同步無延遲,這個值為0。MySQL主從延遲一個重要的原因之一是主從復制是單線程串行執行。
- 那如何為避免或解決主從延遲?我們做了如下一些優化:
- 優化MySQL參數,比如增大innodb_buffer_pool_size,讓更多操作在MySQL內存中完成,減少磁盤操作。
- 使用高性能CPU主機。
- 數據庫使用物理主機,避免使用虛擬雲主機,提升IO性能。
- 使用SSD磁盤,提升IO性能。SSD的隨機IO性能約是SATA硬盤的10倍。
- 業務代碼優化,將實時性要求高的某些操作,使用主庫做讀操作。
1.5. 數據庫拆分
-
同時,業務越來越復雜,多個應用系統使用同一個數據庫,其中一個很小的非核心功能出現Slow query,常常影響主庫上的其它核心業務功能。
-
這時,主庫成為了性能瓶頸,我們意識到,必需得再一次做架構升級,將主庫做拆分,一方面以提升性能,另一方面減少系統間的相互影響,以提升系統穩定性。這一次,我們將系統按業務進行了垂直拆分。如下圖所示,將最初龐大的數據庫按業務拆分成不同的業務數據庫,每個系統僅訪問對應業務的數據庫,避免或減少跨庫訪問。
-
垂直分庫過程,也遇到不少挑戰,最大的挑戰是:不能跨庫join,同時需要對現有代碼重構。單庫時,可以簡單的使用join關聯表查詢;拆庫后,拆分后的數據庫在不同的實例上,就不能跨庫使用join了。比如在CRM系統中,需要通過商家名查詢某個商家的所有訂單,在垂直分庫前,可以join商家和訂單表做查詢,分庫后,則要重構代碼,先通過商家名查詢商家id,再通過商家Id查詢訂單表。
-
垂直分庫過程中的經驗教訓,使我們制定了SQL最佳實踐,其中一條便是程序中禁用或少用join,而應該在程序中組裝數據,讓SQL更簡單。一方面為以后進一步垂直拆分業務做准備,另一方面也避免了MySQL中join的性能較低的問題。
1.6. 水平分庫
- 讀寫分離,通過從庫水平擴展,解決了讀壓力;垂直分庫通過按業務拆分主庫,緩存了寫壓力,但系統依然存在以下隱患:
- 單表數據量越來越大。如訂單表,單表記錄數很快將過億,超出MySQL的極限,影響讀寫性能。
- 核心業務庫的寫壓力越來越大,已不能再進一次垂直拆分,MySQL 主庫不具備水平擴展的能力。
-
水平分庫面臨的第一個問題是,按什么邏輯進行拆分。一種方案是按城市拆分,一個城市的所有數據在一個數據庫中;另一種方案是按訂單ID平均拆分數據。按城市拆分的優點是數據聚合度比較高,做聚合查詢比較簡單,實現也相對簡單,缺點是數據分布不均勻,某些城市的數據量極大,產生熱點,而這些熱點以后可能還要被迫再次拆分。
-
按訂單ID拆分則正相反,優點是數據分布均勻,不會出現一個數據庫數據極大或極小的情況,缺點是數據太分散,不利於做聚合查詢。比如,按訂單ID拆分后,一個商家的訂單可能分布在不同的數據庫中,查詢一個商家的所有訂單,可能需要查詢多個數據庫。針對這種情況,一種解決方案是將需要聚合查詢的數據做冗余表,冗余的表不做拆分,同時在業務開發過程中,減少聚合查詢。
-
最終從架構上,我們將系統分為三層:
- 應用層:即各類業務應用系統。
- 數據訪問層:統一的數據訪問接口,對上層應用層屏蔽讀寫分庫、分庫、緩存等技術細節。
- 數據層:對DB數據進行分片,並可動態的添加shard分片。
-
水平分庫的技術關鍵點在於數據訪問層的設計,數據訪問層主要包含三部分:
- ID生成器:生成每張表的主鍵
- 數據源路由:將每次DB操作路由到不同的shard數據源上
- 緩存: 采用Redis實現數據的緩存,提升性能
-
ID生成器是整個水平分庫的核心,它決定了如何拆分數據,以及查詢存儲-檢索數據。ID需要跨庫全局唯一,否則會引發業務層的沖突。此外,ID必須是數字且升序,這主要是考慮到升序的ID能保證MySQL的性能。同時,ID生成器必須非常穩定,因為任何故障都會影響所有的數據庫操作。
-
ID的生成策略借鑒了Instagram的ID生成算法。具體方案如下:
1.7. 總結
- 前期為了快速滿足業務需求,我們采用簡單高效的方案,如使用雲服務、應用服務直接訪問單點DB;后期隨着系統壓力增大,性能和穩定性逐漸納入考慮范圍,而DB最容易出現性能瓶頸,我們采用讀寫分離、垂直分庫、水平分庫等方案。面對高性能和高穩定性,架構升級需要盡可能超前完成,否則,系統隨時可能出現系統響應變慢甚至宕機的情況。
1.8. 流程梳理
從一開始單個數據庫 -> 到讀寫分離 -> 解決主從延遲 -> 數據庫業務拆分 -> 數據水平拆分
- 這個升級流程很多地方都能看到,大同小異,但是都沒有明確的數據層面的參考,從這個流程我感觸最深的就是,系統的壓力真的大部分來自數據庫,對服務層系統,做到能水平擴展,系統就已經可以承受很大的壓力了
參考 https://blog.csdn.net/czbing308722240/article/details/52350219#commentBox