大型網站面臨大流量、高併發、巨量資料,或是使用者規模直線上升,在重要的業務資料仍仰賴RDB(如MySQL),然後"單一資料庫"即便有索引也會有效率變低的問題,RDB常見的效能瓶頸只要有以下兩個:
1.大量併發讀/寫操作,致使單庫難以負荷
2.單表資料量過大,導致索引效率降低
這時候常見做法即是DB Sharding(分庫 & 分表)改造
個人心得:
如近期於軟體測試實務 — 業界成功案例於高校實踐II書中提及的木桶理論(Cannikn Law或Buckets Effect),亦被稱為木桶原理、短版效應,指的是系統處在最短的那塊木板時,整體系統的容量就是木板位置的水位,就一般系統流量結構來說,整個系統最短的木板就是DB的TPS(概念圖如下),所以DB其實在完整的系統設計中是非常重要的一塊,瞭解DB在達到效能瓶頸時,可以如何調教改善,就變得很重要!!!而在DevOps觀點來說即是如何配合建立合適的基礎設施
一個交易請求次序:入口CDN →Ingress(LB) →應用層(AP) →快取(Cache) →最後到DB完成交易(概念圖如下)
5.1.1 資料庫讀/寫分離
一般企業建構網際網路專案一般資料起始都是儲存在"單一資料庫",但是隨著使用者規模提升,TPS/QPS越來越低
所以生產環境會考慮以讀/寫分離狀態(一主一從,或一主多從),由Master負責寫入,而Slave負責允許讀取相關操作,並由二八法則來看,80%資料庫流量都是讀取操作上,剩餘20%則是寫入
特別需要注意的是Master若有TPS較高狀況,會與Slave之間資料同步存在延遲,這種狀況則可以在資料寫入Master前先寫到快取中,以避免Slave讀取不到資料
此時資料庫設計:
資料庫讀/寫分離
5.1.2 資料庫垂直分庫
在完成前述讀寫分離的資料庫設計後,仍可能因使用規模上升而遇到瓶頸
屆時可以進一步進行"垂直分庫"即企業依據"業務",將單庫中的資料表拆分到不同業務資料庫中,實現分而治之的資料管理
但是書中筆者(原文簡體書出版自2020年)"單一業務資料"的MySQ經驗為例:
當MySQL單表資料量超過500萬列時,即會對讀取操作造成瓶頸(即便有索引)
(但是寫入則是依序進行的,通常不會造成瓶頸,但是讀取就會有上限問題)
所以此時垂直分庫基礎上,就要再考慮下一步水平分表改造
此時資料庫設計:
資料庫讀/寫分離 + 資料庫垂直分庫(業務)
個人心得:
類似於Monolithic到Microservice的應用系統設計上的演變,上面這個"500萬列"的數據是建構在作者當時的時空背景的Monolithic,包含當年版本的MySQL,硬碟/CPU/MEM等資源支撐的I/O,足夠好的Table與索引設計,數字隨著時間軟硬體更迭一定還會持續拉高
5.1.3 資料庫水平分庫與水平分表
水平分表:
單庫中單個業務表拆分為n個[邏輯相關]的業務子表,例如:
user_tab(使用者表)拆分為=>
user_tab_0000、user_tab_0001、user_tab_0002...
不同子表儲存不同區間資料,對外形成一個整體,這也就是Sharding操作
水平分庫:
跟前面水平分表概念類似,當Master的TPS過高,也可以針對垂直分庫後的單一業務庫進行水平化,例如將水平分表後的業務子表分拆放在n個[邏輯相關]的業務子庫中
db(原本單庫)拆分為=>
db_0000、db_0001、db_0002…
這個就相對更為複雜,通常要仰賴專門的Sharding的中介軟體負責資料的路由處理
以上不論分庫或是分表都是充分利用分散式的優點提升資料庫讀/寫效能來應對高併發的效能瓶頸
但是資料量與日俱增後仍然可能會達到效能閥值,DBA則只需再次對現有業務庫與業務表進行水平擴展即可
此時資料庫設計:
資料庫讀/寫分離 + 資料庫垂直分庫(業務) + 水平分表 + 水平分庫
個人心得:
該書原文是2020出版的簡體書後翻譯,現今雲端時代還有更多的DB架構設計考量,例如DB建立仰賴單一Cloud平台提供?或是僅僅建構在Cloud提供機台,而企業自己在上面架設DB?讀/寫分離的Slave是否跨雲跨地?等各種排列組合
接著在疊上分庫分表,其架構更加複雜詭譎
5.1.4 MySQL Sharding與MySQL Cluster的區別
MySQL Cluster也就是叢集模式優勢上僅僅擴展資料的並行處理能力,但是使用與維護成本較高(實行複雜且當前生產環境使用者少)
而MySQL Sharding也就是分散式模式則是成熟且實惠的方案,不僅提升並行處理能力,也解決檢索瓶頸,實屬當前最好的選擇
5.2 Sharding中介軟體
執行分庫分表後,開發人員首當其衝面對兩個問題:
1.SQL語句中的Shard Key(路由條件)
2.根據定義的Shard Key進行存取路由
滿足以上兩個條件的演算法或規則
才能讓SQL抵達準確定位具體業務庫的和業務表
目前多數都仰賴成熟的Sharding中介軟體
額外一提的是避免使用那些Sharding軟體是把路由邏輯程式寫在持久層中的
因為耦合度過高,往後DBA又要進行庫表的擴展時,開發者就要調整持久層中的程式邏輯
5.2.1 常見的Sharding中介軟體對比
結構上分為兩種類型:
1.根據Proxy架構,不侷限特定資料庫,容易客製
2.應用整合架構,捨棄通用性,但是直連資料庫,讀/寫效能比前者高出10%~20%
該書中後面以整合架構的Sharding中介軟體為例做介紹,早期較成熟的只有淘寶的TDDL(Taobao Distributed Data Layer),但由於社群活躍度低、技術文章資料匱乏、部分功能開源但核心功能封閉不開放等因素來說並非完美
其他尚有Proxy架構的Cobar,MyCat等
另外屬於應用整合架構的TDDL與Shark
後面作者以Shark為例講解
5.2.2 Shark簡介
Shark是為Apache License 2.0開源協定的分散式MySQL分庫分表中介軟體
Shark倚仗於SpringJDBC、Druid之上採用應用整合架構
放棄通用性來換取更好執行效能
提供不同企業、業務過億級別的SQL讀/寫服務,具體有以下優點:
- 完善技術文件支援
- 動態資料來源的無縫切換
- 豐富、靈活的分散式路由演算法支援
- 非Proxy架構,應用直連資料庫,降低周邊系統依賴所帶來的當機風險
- 業務零修改,設定簡單
- 執行效能高、穩定
- 提供多機SequenceID的API支援,用以解決多機SequenceID難題
- 預設支援已ZooKeeper、Redis 3.X Cluster作為集中是資源配置中心
- 以Velocity樣板引擎渲染內容,支援SQL語句獨立設定與動態拼接,與業務邏輯程式碼解偶
- 提供內建驗證葉面,方便開發、測試人員對執行後的SQL驗證
- 提供自動產生設定檔的API支援,降低設定資訊出錯率
5.2.3 Shark的架構模型
Sharding中介軟體最核心的基礎功能:
以Shard Key為條件動態切換覆寫替換原本SQL中的全域表名
例如"SELECT * FROM tab WHERE uid = 1" 替換後變成
"SELECT * FROM tab_0001 WHERE uid = 1"
另外特別注意Shark內並沒有實作DBConnectionPool
所以開發者可以任意切換DBConnectionPool產品
另外Shark是基於Spring的AbstractRoutingDataSource類別,透過AOP方式採用Druid的Sqlparser來對SQL解析,於此基礎上達到讀/寫分離、資料路由等操作
以上反映了Shark原始碼註定是簡單的、健壯、易讀、易維護
並且核心僅專注在Sharding
它牌Sharding中介軟體則包了DBConnectionPool、動態資料來源、通用性支援、多種類型RDBMS或NoSQL支援等等造成過度臃腫
書中提及根據官方發佈的Shark Benchmark結果來看,Shark執行SQL的效率很高,損耗可以達到忽略不計的程度!!!
5.2.4 使用Shark實作分庫分表後的資料路由任務
這邊書中主要提及Shark元件引入,但是可以看到已經N年沒更新了
實作細節參數有的沒的就不太贅述,也許現今也有更好的替代
5.2.5 分庫分表後所帶來的影響
從上面講述DB與Table的架構演變,從單庫單表到
「垂直分庫、水平分表」或著「水平分庫、水平分表」
將會有以下四個比較棘手的問題要盡早考慮與規劃:
- ACID特性如何保證
- 多表間的關聯查詢如何進行
- 無法繼續使用FK約束
- 無法繼續使用如Oracle提供的Sequence或著MySQL提供的AUTO_INCREMENT產生的全域唯一和連續性ID
並且實作分庫分表後,對於SQL語句都會有一個無法撼動的「軍規」就是更傾向簡單化、輕量化、避免使用多表聯集語句,拆成多條單表查詢語句,多次查詢
有可能會有人問道:多表聯集查詢效能不如多次單表查詢快嗎?
書中提及從測試結果來看「多次單表查詢」並不比「多表聯集查詢」慢多少
而為了擴展性、易讀性、維護性,犧牲一點效能又怎樣?
單表查詢得到的優勢主要有以下兩個:
1.語句簡單易於理解、維護、擴展
2.快取利用率高
5.2.6 全域唯一SequenceID解決方案
單點環境(就是單庫單表)來說這個SequenceID是很簡單的
但是在多庫甚至多表就很難確保
通常開發者可以利用UUID、實體機器IP位置、隨機值、時間戳等方式來產生唯一ID,用以兼顧ID的連續性與唯一性
而Shark透過底層的Zookeeper作為申請SequenceID的儲存系統
只要透過Shark的API取得兼顧連續性與唯一性的SequenceID
後面書中講解Shark這方面DDL等的設定就不贅述
但是可以特別注意到SequenceID的上限問題
另外也有企業採用"ID產生器"這段應用獨立佈署在AP與DB之間,但是就增加了維護成本與網路開銷
5.2.7 基於Solr滿足多維度的複雜條件查詢
Solr作為Apache旗下的一個Java編寫的子專案,用以擴展Lucene的搜尋引擎,通常佈署在Web容器中,提供一下列HTTP API介面供開發者呼叫使其向伺服器送出Document對索引進行建立修改與刪除
主要應用場景有二:
- MySQL的like模糊查詢,這在資料庫是不會進行索引的,而全表掃描,Solr即補足這塊
- 資料庫分庫分表後,要維持原本的查詢維度依舊,同時滿足多維度複雜條件查詢需要,則可以透過Solr這類搜尋引擎
5.2.8 關於分散式事務
所為事務(其實就是台灣講的交易)指的是程式多操作情況下要滿足上下文的影響一致性
常見三種分散式系統中的一至性協定:
- 2PC(2 Phase Commit Protocol,i.e. 2PC)
- 3PC
- Paxos協定
書中僅是提及上述,並沒有深入講解
提醒到分散式事務的實施成本太高,達成強一致性的三散式事務來保證資料一致性非常困難
例如2PC為例,事務涉及多次節點間網路通訊,另外事務時間,資源鎖定時間延長,導致資源等待時間變長
能夠不導入分散是事務還是盡量不要導入
如果有此需要則可以仰賴訊息中介軟體保證資料最終一致性方案
5.3 資料庫的HA方案
以MySQL為例,DBA進行Master/Slave建置即是HA的一種表現
另一個則是無狀態應用系統可以因為了提升並行處理能力採用的叢集佈署
不論是主從模式還是叢集佈署都可以用以避免單一節點的故障(i.e. SPOF)
以WebServer來說佈署叢集,則存取層可能使用反向代理像是Nginx+Tomcat組合,由Nginx負責分發平衡以及Failover
資料庫也需要機制保證主從的切換,如Master當機後,Slave能開發讀/寫許可權
目前業界有三個成熟方案:
- 根據配置中心實作主從切換
- 根據Keepalived實作主從切換
- 根據MHA實作主從切換
5.3.1 根據配置中心實現主從切換
主從切換常見模式有二:
- 監控預警發出警告後,運維人員手動修改資料來源資訊
- Master實例發生故障後自動切換到Slave上
採用上述1的方式則基於配置中心是一個不錯的選擇
Shark則提供了基於ZooKeeper的配置中心客戶端,資料來源的相關資訊統一設定在ZooKeeper中
後面書中就是許多Shark的Spring配置範例(還是xml格式的...超舊...)
5.3.2 根據Keepalived實現主從切換
若有支援Keepalived,會在主從機台上設置VIP位置
配置後Master與Slave會互送心跳信號
當Master出現異常,Master上的Keepalived會選擇「自殺」
之後Slave會接管VIP的請求,將寫入請求落在Slave庫上
5.3.3 保障主從切換過程中的資料一致性
在Master實例當機後,Slave變成新的Master時
主從資料肯定不一致了,所以要考慮主從資料的一致性 — 主從同步的問題上
這邊以微信搶紅包的案例,流量不是特別大狀況下
採用主從同步之間可以顯示開啟半同步複製:
上述半同步複製可理解微主從間的強制資料同步
當事務送到Master後,Master等待Slave的回應,待Slave回應收到Binlog(二進為日誌)後,Master才會回應慶求方已經完成了事務
以上不適合峰值流量較大的場景下,會對TPS造成影響
考量到Master當機的狀況,為了避免手動比對Binlog來確保主從資料的一致性
可以使用MySQL提供的GTID(全域事務ID)特性來保持主從之間的資料最終一致性
5.4 訂單業務冗餘表需求
當資料庫實施分庫分表後,訂單業務查詢需求會變得複雜
最初以甚麼樣的維度進行寫入(例如訂單的買家或是賣家)
最後只能夠透過這樣的維度查詢(以買家維度查詢,或是賣家維度查詢)
為了因應特殊業務需求就會對資料進行冗餘儲存
例如同時維護兩張訂單表
一張賣家訂單表:t_order_seller(seller_id,buyer_id,order_id)
一張買家訂單表:t_order_buyer(buyer_id,seller_id,order_id)
即可同時滿足買賣雙方的查詢需求
5.4.1 冗餘表的實作方案
同時對t_order_seller與t_order_buyer兩張表插入訂單資料會有兩種形式:
- 資料同步寫入
- 資料非同步寫入
1.資料同步寫入:
複雜度低,但是缺點是寫入時間增加1倍,若對系統有嚴格的TPS要求則要考慮以下非同步操作
2.資料非同步寫入:
當服務成功將資料寫入到第一張表後,透過非同步模型將資料寫入到第二張
因為第二張表的資料寫入是非同步的,服務不用等待其結果,所以系統效能提升,但連帶的複雜度增加(如下圖)
在上述兩中雙寫入(不論同步或非同步)
由於沒有可靠事務機制保證資料雙寫入的一致性
在DB的TPS較高狀況下,冗餘表極可能出現資料不一致
此時又涉及到到底先寫入哪一張表好?
書中作者認為優先寫入t_order_buyer,以客戶角度推動訂單狀態流轉優先
5.4.2 資料最終一致性方案
前面有提及到資料庫實施分庫分表後,資料一致性難以得到保證,對於強調一致性的場景,要麼是使用強一致性的分散式事務,要麼是使用最終一致性方案
但是分散式的與生俱來複雜性與低效性,所以在能接受短期不一致的業務場景,則會建議使用最終一致性方案
如上範例的最終一致性方案 — 線上檢測補償,在資料寫入t_order_buyer後,立即寫入一份到MQ內
接著成功寫入到t_order_seller表後,再把相同訊息也寫入到MQ內
消費端接收到第一條訊息後,若單位時間內沒有消費到第二條訊息,就認定資料已經不一致了,即進行資料補償作業
後面作者提供範例程式片段就不補充
另一個最終一致性方案 — 阿里開源的Canal
Canal會偽裝成MySQL的Slave節點,向Master發動dump請求,Master接收到後會發送Binlog供Canal解析,即可完成資料的增量同步