1、背景
MySQL是OPPO使用最廣泛的關聯式資料庫,不同程式語言的微服務都是通過MySQL官方的SDK直連真實的資料庫例項。這種最傳統的使用方式,會給業務開發和資料庫運維帶來一系列影響效率和穩定性的問題。
- 不合理的資料庫訪問,比如不恰當的連線池設定、高危SQL無攔截、無限流和熔斷等治理能力
- 無彈性伸縮能力,單機資料庫效能或者容量不足時,擴容非常繁瑣低效
- 缺乏常用的功能特性,不支援讀寫分離、影子庫表、單元化架構、資料庫加密
- 不支援跨語言,並且與應用服務強耦合,編碼和升級都非常困難
以上問題,我們講通過CDAS來解決單MySQL帶來的一系列問題。
2、CDAS產品簡介
MySQL在併發能力、穩定性、高可用方面久經考驗,是絕大多數網聯網產品首選的OLTP場景儲存元件,但單機MySQL在超高併發、海量儲存、OLAP能力上先天不足,因此當資料到達一定量級後一般採用分庫分表的方式來水平擴充套件MySQL,提升系統的整體處理能力。
CDAS基於分片理念設計,目的是解決MySQL單機效能瓶頸,對外提供超過併發、海量儲存、支援HTAP的MySQL服務。在一組MySQL叢集前搭建一套高可用的代理 + 計算叢集,提供分片能力、自動化的彈性伸縮能力、讀寫分離、影子庫、資料加密儲存能力,為使用者提供一體化的產品。
CDAS Proxy使用Java語言開發,基於開源產品Apache ShardingSphere Proxy改造而來,並新增了許多高階特性來支援內部業務。我們的開發理念是依託開源的成熟產品來搭建服務,從社群汲取營養的同時也把發現的問題、特性回饋給社群,積極參與、和社群共同發展。
2.1 CDAS產品特點
(1)穩定性
CDAS進行了大量的基準測試、調優,實現了特殊的動態佇列 + 執行緒池模型來保證Proxy的總體併發程度不會由於執行緒數過多導致上下文切換頻繁從而效能下降,同時Proxy在長時間高負載的場景下不會產生Full GC導致業務中斷,目前正式環境已有多個QPS:5000+的業務場景使用了CDAS,表現穩定。
(2)高度可擴充套件
我們建議業務在申請分片表時預估未來3~5年的資料總量來設定分片總數,前期可申請1~2個資料庫,資料量變大後擴容。
理論上可擴充套件性和分片總數相關,最大可擴充套件分片數相等的資料庫例項,最大可擴充套件到250TB,同時Proxy叢集也支援線上擴容。
(3)平臺化運維
由雲平臺統一運維、部署,MySQL和Proxy都支援自動化流程申請和變更,自動接入後設資料管理系統。
(4)相容MySQL語法
支援絕大多數MySQL語法。
- 路由至單資料分片100%相容
- 支援分頁、去重、排序、分組、聚合、關聯等常見查詢場景
(5)功能豐富
- 多種分片演算法
- 自增分散式ID
- 讀寫分離,可設定主從同步容忍閾值
- 影子庫
- HTAP
- 完善的監控資訊:審計日誌、慢日誌、鑑權
3.核心設計
CDAS的核心目標是解決使用者海量資料儲存、訪問問題,聚焦到分片場景中,主要體現在解析、路由、重寫、聚合等邏輯 。
3.1 核心架構
站在高處看Proxy核心,主要分為連線接入模組、I/O多路複用模組、解析路由執行引擎3個大的部分,左側可以看到執行緒模型也分為3塊,實際涉及執行緒資源的還有其他子邏輯,如並行執行執行緒池等,接下來將圍繞多個模型和執行流程瞭解核心架構的細節。
3.2 執行緒模型
核心入口層基於Netty實現,從整體上看是一個典型的Netty使用場景,核心圖從可以看到主要分為3個部分。
(1)Boss Thread
負責Accept connection,也就是接受、建立連線,客戶端一般都會使用連線池技術,因此建立連線的請求不會太多,單執行緒即可處理
(2)I/O Threads
即Netty EventLoopGroup ,負責編解碼、發起auth認證,基於Epoll事件驅動 I/O多路複用,一個執行緒可處理多個連線,共CPU核心數 * 2個執行緒。
(3)Worker Threads
負責執行、回寫資料等核心流程。
Worker執行緒整體是同步的,其中Parser\Router\Rewriter\RateLimter是純CPU計算,Executor Engine、ResultManager底層基於JDBC規範,JDBC目前對外暴露的是一個同步的呼叫模型,在資料庫未響應前執行緒狀態為blocked,因此Worker執行緒池的配置決定了Proxy整體的併發能力。
執行緒資源在作業系統中是相對昂貴的資源,在CPU核心數固定的情況下,執行緒數量過大會耗費大量記憶體和導致上下文頻繁切換,降低效能。我們在壓測過程中額外關注了執行緒數對服務的影響,找到了一個合適的計算公式。
Math.min(cpuNum * X, maxThreadsThreshold)
- X:預設75,可配置
- maxThreadsThreshold:預設800,可配置
同時我們自研了DynamicBlockingQueue,在佇列積壓任務到一定閾值後提前建立新執行緒執行任務。
3.3 連線模型
在接受客戶端連線後,Proxy內部會為每個客戶端連線維護1個Backend Connection的邏輯連線。Backend Connection的connections列表儲存執行過程中用到的真實連線,請求執行完畢或事務結束後清空connections列表,釋放物理連線。
同時,Proxy內部使用連線池技術來儲存物理連線,減少連線建立時間並控制總的連線數量。
3.4 事務模型
接下來聊一下在事務場景中的執行流程。
3.4.1 單庫事務
假設有以下事務場景:
begin;
select * from t_order where order_id = 1;
update t_order set gmt_modify = now() where order_id = 1;
update t_order_item set cnt = 2 where order_id = 1;
commit/rollback;
事務過程中一共會和資料庫順序互動5次。
語句1:begin時Proxy並不知道後面要執行什麼語句,無法路由到RDS,只是在連線上記錄了一個begin狀態
語句2:執行帶分片鍵的select,還是以16分片為例,最終語句會路由到t_order_1這張表中,獲取connection_db1,先執行begin,再執行select,並放入邏輯連線的連線列表中
connection_db1為t_order_1表所在資料庫上的連線
語句3、語句4:路由到t_order_1,t_order_item_1,和語句2路由到相同DB,複用connection_db1執行
語句5:單純的commit或rollback語句,取出當前邏輯連線的所有RDS連線,挨個執行commit/rollback
在事務執行過程中,是否會產生分散式事務完全由使用者SQL語句來控制,如果事務執行過程中不會切換資料庫,則會退化成單純的RDS transaction,保持完整的ACID特性,如果事務執行過程中出現路由到多個DB的情況,則會產生分散式事務。
3.4.2 分散式事務
目前分散式事務的解決方案主要有以下3種
(1)最終一致
業務發起的最終一致性方案,如Seata\TCC等,業務有感,多用於跨服務呼叫場景,如訂單和庫存系統,某一環提交失敗需特定邏輯來整體回滾,不適合Proxy場景
(2)強一致XA
多用於跨服務呼叫場景,目前存在效能不佳,協調器單點\Recovery鎖佔用等問題,不適合高併發場景使用
(3)廠商提供分散式事務
分散式資料庫廠商如OceanBase本身會對資料進行分片,事務執行過程中操作了多個分散式節點從而引入分散式事務。針對單個分散式庫內的事務保證ACID,Proxy後端基於MySQL協議,無法在多個RDS間實現這類事務。
(4)現狀
CDAS Proxy並沒有提供XA來保證跨RDS例項的強一致性,也沒有在內部支援最終一致性。如果觸發多庫事務則分開提交,有部分提交成功、部分失敗的風險,因此建議業務方在設計事務時儘量不再RDS叢集內部產生跨庫事務。
3.5 分片模型
以4庫16分片場景為例闡述CDAS分片方案和傳統方案的區別
傳統方案每個DB中的分片名都是從字尾0開始的,每個庫中有相同的分片數量。
有以下劣勢:
- 熱點分片處理困難
- 無法針對分片進行遷移
- 分片名無意義,無法自解釋
針對以上問題,我們採用了類似範圍分片的思路來優化了分片名。
分片名是唯一的,位於不同DB中的分片名互不相同,這種設計方式與Redis/MongoDB中不同slot位於不同例項上很相似,有利於擴充套件。
t_order_1為熱點分片或DB1壓力過大時遷移t_order_1到新的DB5中:
當然這種遷移方式並不適用於所有場景,如表t_order、t_order_item的分片演算法完全相同,則他們一定落到同一個DB中,如果遷移過程中只遷移其中一張表勢必會導致不必要的分散式事務,因此遷移時會將繫結關係的表一併遷移。
3.6 執行流程
以最簡單的場景舉例,邏輯表:t_order分16片,分片鍵:order_idselect * from t_order where order_id=10
(1)解析
Worker執行緒拿到這條語句後首先要做的是解析SQL語句,基於開源產品Antlr實現語法樹解析。
{type:select,table:t_order,condition: order_id=10}
(2)路由
在獲取到分片表名和條件後,根據表名獲取分片規則並計算分片order_id mod 16 => 10 mod 16 => 10
得到真實的table:t_order_10
(3)重寫
重寫流程會將邏輯表名替換為真實表名,並計算真實位於哪個DB中。
select * from t_order_10 where order_id=10
(4)限流
限流功能可以控制某類SQL的瞬時併發度,目前僅對部分場景進行了限流,如OLAP請求。
(5)執行
語句將會發往真實的資料庫中執行,目前OLTP請求使用MySQL官方Connector/J傳送,OLAP請求使用ClickHouse官方SDK傳送,並且AP流量不會路由+重寫,因為在整體設計上ClickHouse叢集中的分散式表名和邏輯表名是相同的
(6)聚合結果
聚合的主要場景是查詢請求被路由到了超過一個真實表的場景,如
select * from t_order where order_id in (10,11)
這種情況一條SQL最終到DB層面會是兩次子請求
select * from t_order_10 where order_id in (10,11)
select * from t_order_11 where order_id in (10,11)
聚合時,通過JDBC API遍歷兩個子請求的ResultSet,在記憶體中新建一個邏輯ResultSet,當邏輯ResultSet被遍歷時會觸發子ResultSet被遍歷,最終將邏輯ResultSet編碼在MySQL協議包發往客戶端。
如果SQL語句中包含order by,聚合時會在記憶體裡面進行歸併。記憶體裡面排序需要將所有行資料載入到記憶體中排序,記憶體壓力會很大,因此雖然有排序能力,我們還是不建議在請求分裂多個子請求的場景中使用order by語法。
3.7 HTAP
HTAP的是指混合OLTP(Online Transactional Processing)和 OLAP(Online Analytical Processing),在資料庫中同時支援線上事務請求和後臺分析請求。
MySQL是一個典型的OLTP型資料庫,不太適合OLAP型業務,因為分析語句一般需要執行復雜的聚合和查詢大量資料,這可能會影響其在OLTP上的穩定性。同時OLAP資料庫一般在儲存方式上採用列存來最大化壓縮效率、空間佔用,而MySQL採用行存,一般情況下須先將整行記錄查詢出來再過濾出某些列,在執行分析請求時會有明顯的IO讀放大,效率不高。
CDAS通過DTS(資料傳輸)服務將MySQL各分片資料傳輸並聚合到ClickHouse(分析型列存資料庫),形成統一的資料檢視。AP流量到達Proxy後會被轉發到ClickHouse,資料響應後再以MySQL資料包的形式返回給客戶端,通過這種方式,CDAS實現了基於MySQL協議的HTAP能力。
4.部署架構
部署拓撲圖如下:
現代架構中業務方對高可用的要求越來越高,目前大多數重要的服務都會採用多機房主備或互為主備的方案來部署,一般採用雙機房或雙中心化方案,保證其中一個區域掛掉後仍然能對外服務。
在設計之初CDAS考慮到作為L7層的資料流量入口擁有極高的高可用要求,因此允許使用者申請雙機房Proxy叢集,保證其中1個機房掛掉後另1個機房依然可以承接流量對外服務,實現機房級的高可用。
業務系統通過L4負載均衡來和Proxy建立連線,L4和Proxy緊密協調保證優雅下線以及新例項發現,並感知Proxy節點的健康狀態,及時下線不可用節點。同時由L4來做同機房優先路由、流量轉發,Proxy後的所有MySQL叢集都採用semi-sync的高可用架構模式。
這種部署架構使CDAS具備了機房級災備的能力。
5.效能測試
在測試過程中我們尤其關注了效能方面的損耗,並對Proxy代理、直連RDS進行了對比測試,這樣就能直觀的得到效能損耗程度。
5.1 壓測工具
內部壓測平臺(內部基於JMeter)
5.2 壓測方式
Java SDK + 連線池方式壓測
主要分為兩組
- 壓測Proxy,Proxy連線2個RDS叢集
- 壓測單個RDS叢集,作為Proxy效能對比的基準
壓測語句:
參考SysBench的資料庫壓測語句,由於分片場景一般不會直接使用主鍵ID分片,且主鍵ID在InnoDB中查詢效率高於二級索引,因此新增sharding_k列進行分片測試,更符合實際的應用場景。
CREATE TABLE `sbctest_x` (
`id` bigint(11) NOT NULL,
`sharding_k` bigint(11) NOT NULL,
`k` int(11) DEFAULT NOT NULL,
`name` varchar(100) NOT NULL,
`ts` timestamp NULL DEFAULT NULL,
`dt` datetime DEFAULT NULL,
`c` char(100) DEFAULT NULL,
`f` float DEFAULT NULL,
`t` tinyint(4) DEFAULT NULL,
`s` smallint(6) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `sharding_k_idx` (`sharding_k`),
KEY `k_idx` (`k`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
壓測SQL
#QPS 非事務的簡單查詢
SELECT COUNT(1) FROM $table WHERE sharding_k=?
#TPS 包含 1條查詢,4條變更語句
BEGIN;
SELECT c FROM $table WHERE sharding_k=?
UPDATE $table SET k=k+1 WHERE sharding_k=?
UPDATE $table SET c=?,ts=?,dt=? WHERE sharding_k=?
DELETE FROM $table WHERE sharding_k=?
INSERT INTO $table (id,sharding_k,k,name,ts,dt,c,pad,f,t,s) VALUES(?,?,?,?,?,?,?,?,?,?,?)
COMMIT;
5.3 壓測報告
以8C16G規格為例:
SSD\100分片\單片250W行\資料量大於InnoDB buffer pool
(1)QPS場景
Proxy QPS只有RDS QPS的1/2,壓測過程中觀察到RT約為2倍
QPS場景主要是大量非事務查詢的場景,Proxy的效能損耗約1/2,QPS壓測過程中我們觀察到Proxy後的RDS例項CPU使用率較低,於是將Proxy的CPU規格提升1倍後,Proxy QPS立即提升1倍,接近直連RDS的效能。
(2)TPS場景
由於TPS涉及事務提交、log等操作,I/O頻繁,瓶頸也主要在I/O,即使是SSD磁碟效能也都不高。
Proxy後掛載了2個RDS例項,TPS在各併發使用者數中都明顯高於直連單個RDS,且在併發使用者數增加後可達到其2倍,效能損耗幾乎可忽略。
(3)結論
如果業務場景中非事務查詢的請求佔絕大多數,且RDS的CPU利用率較高,建議選擇Proxy的規格時CPU要高於RDS的規格
如果業務場景中事務執行請求較多,Proxy不會成為效能瓶頸,規格可和RDS保持一致
6.案例
目前支援了多個線上業務,擁有完善的指標展示、報警機制,我們例舉個業務的控制檯介面及監控圖。
Proxy列表:
單個節點TPS:
單個節點QPS:
在多個產品的使用過程中CDAS表現穩定,我們將會繼續往更大流量的場景推廣,服務更多的業務方。
7.總結與展望
CDAS為資料分片而生,構建基於MySQL分片場景的內部標準,通過MySQL協議實現跨語言可訪問,統一使用方式,簡化使用成本。
未來我們將持續專注於CDAS的效能提升和功能補充,主要體現在下面2個方面:
1.ShardingSphere社群已計劃將Proxy的資料庫驅動從MySQL Connnector/J 替換為事件驅動元件vertx-mysql-client(一款Reactive MySQL Client),基於Netty實現和MySQL互動I/O多路複用、事件驅動
替換後worker執行緒池執行語句時將從blocking等待變為事件驅動,少量的執行緒即可支援大量的併發,我們將持續關注社群的開發進度、積極參與。
2.CDAS和MySQL互動過程中會自動解碼Rows資料包,產生大量的物件並且都是朝生夕滅的,導致GC壓力增大,同時編解碼也會耗費大量CPU時間。
Rows資料包在非解密等特殊場景外都可以基於Netty ByteBuf實現零拷貝傳送,這是最佳的方案。我們計劃在開源版本替換為vertx-mysql-client後對這部分邏輯進行優化,使絕大多數的請求的響應時間能達到接近4層負載均衡的效能。
作者簡介
jianliu OPPO高階後端工程師
目前主要專注於資料庫代理、註冊中心、雲原生相關技術曾就職於折800、京東商城
獲取更多精彩內容,請掃碼關注[OPPO數智技術]公眾號