得物資料庫中介軟體平臺“彩虹橋”演進之路

得物技術發表於2022-07-19

前言

隨著得物 App 使用者開始快速增長,業務線日趨豐富,也對底層資料庫帶來了較大的壓力。各個業務線對於資料分片、讀寫分離、影子庫路由等等的需求成為了剛需,所以需要一個統一的中介軟體來支撐這些需求,得物“彩虹橋”應運而生。

在北歐神話中,彩虹橋是連結阿斯加德(Asgard)【1】和 米德加爾特(中庭/Midgard)的巨大彩虹橋。我們可以把它當作是“九界之間”的連線通道,也是進入阿斯加德的唯一穩定入口。而得物的彩虹橋是連線服務與資料庫之間的資料庫中間層處理中介軟體,可以說得物的每一筆訂單都與它息息相關。

1. 技術選型

前期我們調研了Mycat、ShardingSphere、kingshard、Atlas等開源中介軟體,綜合了適用性、優缺點、產品口碑、社群活躍度、實戰案例、擴充套件性等多個方面,最終我們選擇了ShardingSphere。準備在ShardingSphere的基礎上進行二次開發、定製一套適合得物內部環境的資料庫中介軟體。

Apache ShardingSphere 是一款開源分散式資料庫生態專案,由 JDBC、Proxy 和 Sidecar(規劃中) 3 款產品組成。其核心採用可插拔架構,通過元件擴充套件功能。對上以資料庫協議及 SQL 方式提供諸多增強功能,包括資料分片、訪問路由、資料安全等;對下原生支援 MySQL、PostgreSQL、SQL Server、Oracle 等多種資料儲存引擎。ShardingSphere 已於2020年4月16日成為 Apache 軟體基金會的頂級專案,並且在全球多個國家都有團隊在使用。

目前我們主要是ShardingSphere的Proxy模式提供服務,後續將會在JDBC&Proxy混合架構繼續探索。

2.彩虹橋目前的能力

image.png

其中白色模組為現階段以及具備的能力,綠色模組為規劃&正在做的功能。下面介紹一下幾個重點功能。注意,以下功能都是基於Proxy模式。

2.1 資料分片

資料分片指按照某個維度將存放在單一資料庫中的資料分散地存放至多個資料庫或表中以達到提升效能瓶頸以及可用性的效果。資料分片的有效手段是對關係型資料庫進行分庫和分表。分庫和分表均可以有效地避免由資料量超過可承受閾值而產生的查詢瓶頸。除此之外,分庫還能夠用於有效地分散對資料庫單點的訪問量;分表雖然無法緩解資料庫壓力,但卻能夠提供儘量將分散式事務轉化為本地事務的可能,一旦涉及到跨庫的更新操作,分散式事務往往會使問題變得複雜。使用多主多從的分片方式,可以有效地避免資料單點,從而提升資料架構的可用性。

通過分庫和分表進行資料的拆分來使得各個表的資料量保持在閾值以下,以及對流量進行疏導應對高訪問量,是應對高併發和海量資料系統的有效手段。資料分片的拆分方式又分為垂直分片和水平分片。

按照業務拆分的方式稱為垂直分片,又稱為縱向拆分,它的核心理念是專庫專用。彩虹橋主要提供的分片能力是水平分片,水平分片又稱為橫向拆分。相對於垂直分片,它不再將資料根據業務邏輯分類,而是通過某個欄位(或某幾個欄位),根據某種規則將資料分散至多個庫或表中,每個分片僅包含資料的一部分。例如:根據主鍵分片,偶數主鍵的記錄放入 0 庫(或表),奇數主鍵的記錄放入 1 庫(或表),如下圖所示。

image.png
水平分片從理論上突破了單機資料量處理的瓶頸,並且擴充套件相對自由,是資料分片的標準解決方案。

當然實際使用場景的分片規則是非常複雜的,我們提供一些內建演算法比如取模、HASH取模、自動時間段分片演算法、Inline表示式等。當內建演算法無法滿足要求時,還可以基於groovy來定製專屬的分片邏輯。

image.png

2.2 讀寫分離

面對日益增加的系統訪問量,資料庫的吞吐量面臨著巨大瓶頸。對於同一時刻有大量併發讀操作和較少寫操作型別的應用系統來說,將資料庫拆分為主庫和從庫,主庫負責處理事務性的增刪改操作,從庫負責處理查詢操作,能夠有效的避免由資料更新導致的行鎖,使得整個系統的查詢效能得到極大的改善。通過一主多從的配置方式,可以將查詢請求均勻地分散到多個資料副本,能夠進一步地提升系統的處理能力。

與將資料根據分片鍵打散至各個資料節點的水平分片不同,讀寫分離則是根據 SQL 語義的分析,將讀操作和寫操作分別路由至主庫與從庫。

image.png

這裡配置的方式比較簡單,給目標主庫繫結一個或多個從庫、設定對應的負載均衡演算法即可。

這裡的實現方式就是通過SQL解析,把查詢語句路由到對應的從庫即可,但是在一些對主從同步延遲比較敏感的場景,可能需要強制走主庫,這裡我們也提供一個API(原理就是SQL Hint),讓上游可以指定某些模組讀強制走主,還有相關全域性配置可以讓事務內所有讀請求全部走主。

2.3 影子庫壓測

在基於微服務的分散式應用架構下,業務需要多個服務是通過一系列的服務、中介軟體的呼叫來完成,所以單個服務的壓力測試已無法代表真實場景。在測試環境中,如果重新搭建一整套與生產環境類似的壓測環境,成本過高,並且往往無法模擬線上環境的複雜度以及流量。因此,業內通常選擇全鏈路壓測的方式,即在生產環境進行壓測,這樣所獲得的測試結果能夠準確地反應系統真實容量和效能水平。

全鏈路壓測是一項複雜而龐大的工作。需要各個微服務、中介軟體之間配合與調整,以應對不同流量以及壓測標識的透傳。通常會搭建一整套壓測平臺以適用不同測試計劃。在資料庫層面需要做好資料隔離,為了保證生產資料的可靠性與完整性,需要將壓測產生的資料路由到壓測環境資料庫,防止壓測資料對生產資料庫中真實資料造成汙染。這就要求業務應用在執行 SQL 前,能夠根據透傳的壓測標識,做好資料分類,將相應的 SQL 路由到與之對應的資料來源。

這裡配置的方式類似讀寫分離,也是給目標主庫繫結一個影子庫,當SQL攜帶了影子標就會被路由到影子庫。

2.4 限流&熔斷

當DB的壓力超過自身水位線時,會導致DB發生故障。當我們預估出某個維度的水位線後,可以配置對於的限流規則,拒絕掉超過本身水位線以外的請求來保護DB。讓系統儘可能跑在最大吞吐量的同時保證系統整體的穩定性。維度方面我們支援DB、Table、SQL以及DML型別。

image.png
彩虹橋下面連線了上百個RDS例項,每個RDS例項都有可能出現各種故障,當單個例項出現故障會影響到整個邏輯庫,會迅速造成阻塞誘發雪崩效應。所以我們需要一種快速失敗的機制來防止雪崩,目前我們是支援DB例項級別的熔斷,基於獲取連線&SQL執行2種行為,以及執行時間跟失敗比例來實現熔斷,以達到在DB故障時快速失敗的效果。

image.png

2.5 流量糾偏

在雙活架構下,彩虹橋作為資料庫的代理層,可以保證雙活架構下流量切換過程的流量做兜底攔截,保證資料一致性。原理就是基於SQL Hint攜帶的userId與機房規則做匹配,攔截不屬於當前機房的流量。

3. 基於ShardingSphere我們做了哪些改造

雖然ShardingSphere Proxy本身其實已經足夠強大,但是針對得物內部環境,還是存在一些缺陷和功能缺失,主要分為以下幾點:

  • 易用性
    a.分片、讀寫分離等規則配置檔案過於複雜,對於業務開發不夠友好
    b.規則動態變更完全依賴配置中心,缺失完善的變更流程
    c.連線池治理能力不完善
    d.Hint方式不夠友好,需要業務寫RAL語句
    e.自定義分片演算法需要釋出
    f.SQL相容性
  • 穩定性
    a.多叢集治理功能缺失
    b.邏輯庫之間的隔離性缺失
    c.限流熔斷元件缺失
    d.資料來源、規則動態變更有損
    e.雙活架構下流量糾偏功能的缺失
    f.釋出有損
  • 可觀測性
    a.SQL Trace功能不完善
    b.監控指標不夠全面
    c.SQL洞察能力缺失
  • 效能
    由於多了一次網路轉發,單條SQL的RT比直連會上浮2~3ms

為了解決以上問題,我們做了以下改造與優化。

3.1 易用性提升

針對資料來源、規則的配置、變更、審計等一系列操作,整合到管控臺進行統一操作。通過圖形化的方式降低了分片、讀寫分離等規則配置檔案的複雜度,並且加上一系列校驗來規避了一部分因為配置檔案錯誤導致的低階錯誤。其次加上了審計功能,保障了配置動態變更的安全性和可控性。

管控臺新增了連線池治理,基於RDS連線數、Proxy節點數、掛載資料來源數量等因素自動計算出一個安全合理的連線池大小。

新增Client,針對Hint做一系列適配,比如影子標傳遞、雙活架構使用者id傳遞、trace傳遞、強制路由等等。使用方只需要呼叫Client中的API,即可在發出SQL階段自動改寫成Proxy可以識別的Hint增強語句。

管控臺新增了叢集治理功能:由於我們部署了多套Proxy叢集,為了最大程度的保障故障時的爆炸半徑,我們按照業務域對Proxy叢集進行了劃分,儘量保證統一業務域下面的邏輯庫流量進入同一套叢集。

新增了Groovy來支援自定義分片演算法,在後臺配置好分片邏輯後稽核通過即可生效,無需Proxy發版

3.2 穩定性提升

3.2.1 Proxy多叢集治理

(1)背景
隨著Proxy承載的業務域越來越多,如果所有的流量都通過負載均衡路由到Proxy節點,當一個庫出現問題時,可能會導致整個叢集癱瘓,爆炸範圍不可控。而且由於DB的連線數資源有限,這就導致Proxy的節點無法大規模橫向擴充套件。

(2)解決方案
為了不同業務域之間的隔離性,我們部署了多套Proxy叢集,並且通過管控臺維護各個邏輯庫與叢集之間的關係。保證同一業務域下面的邏輯庫流量進入同一套叢集。並且在某個叢集發生故障時,可把故障叢集中的邏輯庫迅速、無損的動態切換至備用叢集,儘可能減少Proxy本身故障給業務帶來的損失。

而針對連線數治理方面,Proxy在初始化連線池的時候會判斷一下當前邏輯庫是否在當前叢集,如果不在把最小連線數配置設定成最小,如果在則按照正常配置載入,並在叢集切換前後做好目標叢集的預熱以及原叢集的回收。這樣可以極大程度上緩解DB連線池資源對Proxy節點的橫向擴充套件。

(3)實現原理
上游應用引入Rainbow(自研連線池)後,在連線池初始化之前會根據邏輯庫讀取當前庫所在叢集,並動態把Proxy的域名替換成其所在叢集的域名。同時還會新增對叢集配置的監聽,這樣在管控臺切換叢集操作後,Rainbow會根據切換後的叢集域名建立一個新的連線池,然後替換掉老的連線池,老的連線池也會進行延時優雅關閉,整個過程對上游應用無感知。

(4)架構圖

image.png

3.2.2 Proxy工作執行緒池隔離

(1)背景
開源版本的Proxy,所有邏輯庫共用一個執行緒池,當單個邏輯庫發生阻塞的情況,會耗盡執行緒池資源,導致其他邏輯庫也跟著受影響。

(2)解決方案
我們這裡採用了基於執行緒池的隔離方案,引入了獨佔&共享執行緒池的概念,優先使用邏輯庫獨佔執行緒池,當獨佔執行緒池出現排隊情況再使用共享執行緒池,共享執行緒池達到一定負載後會在一個時間視窗內強制路由到獨佔執行緒池,在保障隔離性的前提下,最大化利用執行緒資源。

3.2.3 流控與熔斷

(1)背景
開源版本的Proxy缺少對庫、表、SQL等維度的流控,當短時間內爆發超過系統水位的流量時,很可能就會擊垮DB導致線上故障,而且缺少針對DB快速失敗的機制,當DB發生故障(比如CPU 100%)無法快速失敗的情況下,會迅速造成阻塞誘發雪崩效應。

(2)解決方案
新增了各個維度(DB、table、SQL、語句型別)的限流,各個庫可以根據預估的水位線以及業務需要配置一個合理的閾值,最大程度保護DB。同時我們也引入DB例項的熔斷策略,在某個例項出現故障時可快速失敗。在分庫場景下,可最大程度減少對其他分片的影響,進一步縮小了故障的爆炸半徑,提升彩虹橋整體的穩定性。

(3)實現原理
流控跟熔斷都是基於sentinel來實現的,在管控臺配置對應的規則即可。

3.2.4 無損釋出

(1)背景
在前期,Proxy每次釋出或者重啟的時候,都會收到上游應用SQL執行失敗的一些報警,主要的原因是因為上游與Proxy之間是長連線,這時如果有連線正在執行SQL,那麼就會被強制斷開導致SQL執行失敗,一定程度上對上游的業務造成損失。

(2)解決方案
釋出系統配合自研連線池Rainbow,在Proxy節點發布或重啟之前,會通知Rainbow連線池優雅關閉應用於需要重啟&釋出的Proxy節點之間的連線,在Proxy流量跌0後再執行重啟&釋出。

3.3 可觀測性

3.3.1 執行時指標

(1)背景
開源版本對於Proxy執行時的監控指標很少,導致無法觀測到Proxy上面每個庫執行的詳細狀態。

(2)解決方案
在Proxy各個執行階段加了埋點,新增了以下指標、並繪製了對應的監控大盤。

  • 庫&表級別的QPS、RT、error、慢SQL指標
  • Proxy-DB連線池的各項連線數指標
  • 流控熔斷指標
  • 執行緒池活躍執行緒數、佇列大小指標

(3)效果圖

image.png

image.png

image.png

3.3.2 全鏈路追蹤

(1)背景
開源版本的trace只支援Proxy內部執行階段的鏈路追蹤,無法和上游串聯,導致排障效率低下。

(2)解決方案
主要通過RAL、SQL註釋2種方式傳遞trace資訊,實現了上游到Proxy的全鏈路追蹤。

(3)實現原理
我們首先想到的方案就是通過SQL註釋方式傳遞trace資訊,但是在真正投產之後卻發現了一些問題,在上游使用了prepare模式(useServerPrepStmts=true&cachePrepStmts=true)時,Proxy會快取statmentId和SQL,而trace每次都不一樣,這樣會導致儲存快取無限增長最終導致OOM。

image.png

所以通過SQL註釋方式傳遞trace資訊只適用於非prepare場景。於是我們又新增了一種方案就是在每次SQL執行之前傳送一條RAL語句來傳遞trace資訊,並在Proxy中快取channelId與trace資訊的對應關係,由於單個channel所有的SQL都是序列執行,加上channelId數量可控,不會無限膨脹。但是這種方案相對於每次SQL執行之前都有一次RAL語句的執行,對效能的影響還是比較大的。從監控上看下來每次SQL執行的RT會上浮2-3ms(網路傳輸),對於一些鏈路較長的介面來說還是挺致命的。

(4)總結
綜合下來2種方案其實都有比較明顯的缺點,針對這個問題之前ShardingSphere的小夥伴來過我們得物進行過一次深度溝通,亮哥給出了一個比較可行的方案,就是通過虛擬列傳輸trace資訊,但是需要上游對SQL進行改寫,這可能會增加上游應用的負擔,目前這塊我們還沒有開始做。

3.3.3 SQL洞察

(1)背景
目前Proxy雖然有列印邏輯SQL和物理SQL的日誌,但是由於生產請求量較大,開啟日誌會對IO有較大挑戰。所以目前我們生產環境還是關閉的狀態。而且這個日誌也無法與上游串聯,對於排障的幫助也是比較有限。

(2)解決方案
目前也只有個大概的思路,還沒有完善的方案,要達到的效果就是收集所有Proxy執行的邏輯SQL、物理SQL以及上游資訊,包括JDBCDatabaseCommunicationEngine以外的一些SQL(比如TCL等等),並通過管控臺實時查詢。最終的效果類似阿里雲RDS的收費服務-SQL洞察

3.4 bug修復

由於歷史原因,我們是基於Apache ShardingSphere 5.0.0-alpha上做的二次開發,在實際使用的過程中遇到了很多bug,大部分都給官方提了issue,並且在彩虹橋版本上做了修復,當然ShardingSphere社群的小夥伴也給與了很多幫助和修復的思路。

3.5 JDBC&Proxy混合架構

(1)背景
上面4項內容大多數針對Proxy模組或上游連線池模組的改造,但是還是有一些問題是Proxy模式暫時無法解決的,比如前面提到的SQL相容性和效能問題。還有就是如果整個Proxy叢集全部當機情況下我們沒有一個兜底機制。所以Proxy模式並不適用於所有場景。在Apache ShardingSphere的官方文件可以看到這樣一段內容:
image.png

於是我們準備在JDBC&Proxy混合架構上做了進一步探索。

(2)解決方案
在管控臺實現對邏輯庫的模式配置,並通過自研連線池Rainbow感知並根據不同模式啟動不同型別的資料來源。並且可以在後臺切換模式後無損動態調整,這樣對於使用者來說是完全無感知的。對SQL效能、相容性要求比較高的應用可以調整為JDBC模式。同時當Proxy所有叢集癱瘓時也有個兜底的方案,不至於全站崩潰。

(3)實現原理
Rainbow連線池啟動的時候會查詢當前邏輯庫對應的模式,如果是Proxy模式則直接連線Proxy來啟動連線池,如果是JBDC則根據該邏輯庫的資料來源配置以及分片&讀寫分離&影子庫等等規則來載入JDBC模式的資料來源,對應的DataSource為GovernanceShardingSphereDataSource。並且會監聽這個模式配置,當模式發生變化後會動態無損替換當前連線池。具體的無損替換方案與前面提到的叢集切換類似。同時需要解決的還有監控問題和連線池資源的管理,Proxy切換至JDBC模式後指標的暴露由Proxy節點變成了上游節點,對應的大盤也需要做對應的融合,連線池治理這塊也使用了新的計算模式做了對應的適配。

4. 目前的困惑

由於歷史原因,我們是基於Apache ShardingSphere 5.0.0-alpha上做的二次開發,目前社群最新的版本是5.1.2-release,從5.0.0-alpha~5.1.2-release做了大量的優化跟bug修復,但是我們沒有很好的辦法將社群的程式碼合併到我們的內部程式碼,因為無法確定社群開源版本更改的內容,會不會對現有業務產生影響,短時間內也無法享受社群帶來的紅利。同時我們也在尋找一種方式將我們做的一些優化後續合併到社群中,也算是一種反哺社群。為中國開源做一份貢獻~

5. 寫在最後

ShardingShpere的原始碼非常優秀,很多地方的設計非常的巧妙,模組劃分的也很清晰。但總體的程式碼非常龐大,剛開始閱讀起來還是非常吃力的。雖然文章的大部分是在指出目前開源版本的問題,但再優秀的產品也不可能適用所有場景。今年年初ShardingSphere團隊的大牛們包括亮哥也來到了我們得物總部和我們做了一次線下交流,給我們分享了很多幹貨,我們也提出了一些我們現在遇到的一些問題,亮哥也給出了非常有用的思路和指導。非常期待後續可以再來一次線下交流。

*文/陳浩
@得物技術公眾號

相關文章