從一個簡單的SQL查詢搞懂Sharding-Proxy核心原理

做者介紹前端

張永倫,京東數科軟件開發工程師,Apache ShardingSphere (Incubating) PPMC。2018年2月參與到ShardingSphere項目中,經歷了在Apache孵化的整個過程。比較擅長Sharding-Proxy,SQL-Parser,APM和性能測試方向。數據庫

分享的內容分爲四部分。首先,Sharding-Proxy簡介:包括Proxy在ShardingSphere中的定位,Proxy架構和特性的介紹。第二部分,SQL的一輩子:在這裏,咱們從一個簡單查詢SQL的角度,瞭解Sharding-Proxy內部的運轉流程。第三部分,核心原理:會介紹幾個不難理解,但對Sharding-Proxy很是重要的原理。最後,性能優化:對於Sharding-Proxy這種應用,它的可用性和性能是很是重要的指標,因此在這裏總結一下以前出現的比較有共性的問題。還會介紹一下目前是如何進行性能保障的。後端

1、Sharding-Proxy簡介緩存

一、Apache ShardingSphere生態圈

咱們來看一下ShardingSphere的定位。它是一個分佈式數據庫中間件組成的生態圈,之因此說它是一個生態圈,是由於它整個功能的設計是一個閉環的結構,另外也會給用戶提供多種接入方式,來方便用戶在生產當中面對不一樣的接入需求。如今你們看到的是ShardingSphere的總體架構,它的功能由五大塊組成。分別是數據分片,分佈式事務,數據庫治理,這三塊已經上線交付用戶使用。管控界面近期也由社區開發完成並捐獻給Apache基金會。多模式鏈接還在規劃和開發的進程中。底下是它的接入端,咱們爲你們提供了三款產品,分別是Sharding-JDBC,Sharding-Proxy和Sharding-Sidecar。每一款接入端都具有上面全部的功能,三款產品分別知足用戶不一樣生產場景的需求。性能優化

二、Sharding-Proxy架構

下面進入正題。Sharding-Proxy的定位是透明化的數據庫代理,它封裝了數據庫二進制協議,用於完成對異構語言的支持。目前兼容MySQL和PG,可使用任何兼容MySQL或PG協議的客戶端進行訪問,好比MySQL命令行、MySQL Workbench、Navicat等等,對DBA更加友好。bash

整個架構能夠分爲前端、後端和核心組件三部分。前端負責與客戶端進行網絡通訊,採用的是基於NIO的客戶端/服務器框架,在Windows和Mac操做系統下采用NIO 模型,Linux系統自動適配爲Epoll模型。通訊的過程當中完成對MySQL協議的編解碼。核心組件獲得解碼的MySQL命令後,開始調用Sharding-Core對SQL進行解析、路由、改寫、結果歸併等核心功能。後端與真實數據庫的交互目前藉助於Hikari鏈接池。服務器

2、SQL的一輩子微信

一、業務場景

你們能夠看到,整個ShardingSphere的功能仍是很是多的,短期內確定沒法一一講到。那麼咱們就從數據分片,這樣一個比較核心,比較基礎,又很是重要的功能點來展開跟你們分享。數據分片我以爲你們多少都會有所瞭解,基本原理就是把一個庫拆分紅多個庫,把一個表拆分紅多個表。來達到水平擴展的目的。在這個場景裏有一個數據庫,裏面只有一張表,叫t_order。因爲它的數據量很大,因此它在底層存儲的時候已經進行了拆分。按照這樣的分片策略拆分紅了兩個庫,每一個庫兩張表。Sharding-Proxy幫助用戶屏蔽掉了真實的庫表,用戶的SQL只需針對邏輯庫表就能夠了。網絡

二、MySQL協議解碼

SELECT * FROM t_order where user_id = 10 and order_id = 1;複製代碼

咱們如今看一下這個SQL是怎麼來的,它的真實面目是什麼。當鏈接Proxy的客戶端執行一個SQL的時候,咱們能夠經過Wireshark在網絡上抓到這個包。能夠看到,MySQL協議是承載在TCP協議之上的。傳輸方向是32883端口到3307端口,這個3307是 Proxy的默認端口。MySQL協議也像大多數協議同樣遵循TLV原則:架構

  • TYPE: 命令類型——Query

  • LENGTH: 消息長度——58

  • VALUE: 就是這個SQL的ASCII碼

Proxy解碼出邏輯SQL後,就會當即把它送給解析模塊處理。

三、SQL解析

咱們如今看到的是SQL通過解析後生成的抽象語法樹。這個語法樹是由Antlr自動生成的。

解析過程分爲詞法解析和語法解析。詞法解析器用於將SQL拆解爲不可再分的原子符號(好比select, from, t_order, 還有*, =,10),以後語法解析器將SQL轉換爲抽象語法樹。有了這個語法樹以後,經過對其遍歷,就能夠提煉出分片所需的上下文,並標記有可能須要改寫的位置。好比user_id和order_id的值要取出來,他們是分片鍵,決定路由的結果。表t_order的位置要記錄下來,改寫的時候才能找到。

四、路由

在當前的這個簡單分片規則下,真實庫的計算方式是 user_id % 2 => 10 % 2,路由到ds_0庫。同理,真實表order_id % 2 => 1 % 2,路由到t_order_1。固然,路由的功能不止這麼簡單。路由引擎支持多種分片策略,包括取模、哈希、範圍、標籤、時間等等。還支持多種分片接口,包括行表達式、內置規則、自定義類等方式。

五、改寫

爲何要改寫?上面這個面向邏輯庫與邏輯表的SQL,並不可以直接在真實的數據庫中執行,SQL改寫的做用就是把邏輯SQL改寫爲能夠在真實庫中正確執行的真實SQL。真實庫和真實表咱們以前已經知道了,因此直接把SQL改寫爲這樣。並非全部SQL的改寫都這麼簡單,好比聚合函數怎麼改寫,包含LIMIT的SQL怎麼改寫,何時須要補列,這些都是改寫引擎須要處理的事情。

六、執行

路由改寫完成了,就能夠從鏈接池裏取鏈接執行SQL了。整個執行過程是經過ShardingSphere的執行引擎完成的。執行引擎會根據路由節點的數量和單次查詢單庫容許的最大鏈接數,自動決策出是否使用流式結果集。流式結果集會最大程度減小內存的壓力。這個真實SQL最後被Proxy發送到MySQL服務器。在MySQL服務器裏通過一系列的緩存、解析、優化後,從存儲引擎裏把結果數據取出來並返回給Proxy。

七、歸併

Proxy收到結果數據後,須要對數據進行歸併處理。爲了方便說明歸併,咱們換一個SQL。這個SQL只包含一個分片鍵user_id,那麼它會被路由到ds_0庫。因爲沒有指定order_id,因此會被路由到所有的真實表t_order_0和t_order_1。兩個真實表分別存在一條知足條件的數據,那麼歸併就是將這兩條數據合併起來返回給客戶端。歸併引擎的功能很強大,咱們這個例子屬於最簡單的遍歷歸併。還有不少其餘的歸併場景:好比,排序歸併:SQL中存在order by時,須要考慮如何排序代價最小。再好比,分組歸併:在SQL中同時存在order by和group by的狀況下,須要考慮如何優化內存佔用率。

八、MySQL協議編碼



獲得了最終的歸併結果後,Proxy會把這個結果編碼成MySQL協議發送給客戶端。數據包中包含每一列的描述信息,好比數據庫名、表名、字符集、數據類型等等。


還有就是用戶可見的結果數據。MySQL命令行客戶端收到協議包後,會在終端上打印結果數據。若是使用的是JDBC客戶端,JDBC會把全部結果保存到ResultSet裏面。以上就是Sharding-Proxy的一個簡要流程。

3、核心原理

一、IO & 線程模型

核心原理部分首先介紹一下IO模型和線程模型。能夠簡單理解上面兩個是前端,下面兩個是後端。Boss Group至關於Reactor模式中的Reactor。Worker Group至關於模式中的Worker。User Executor Group用於執行MySQL命令。Sharding Execute Engine用於併發訪問數據庫。你們看到Boss Group和Worker Group應該能猜到前端用的是Netty。因此前端使用IO多路複用處理客戶端請求。後端使用的是Hikari鏈接池,同步請求數據庫。若是開啓XA事務,狀況就會比較特殊。因爲Atomiks事務管理器的資源是threadlocal的,因此一次事務的全部SQL必須所有在同一個線程中執行。在執行MySQL命令的時候會單首創建一個線程,並緩存起來。

二、流式歸併

下面介紹兩個早期開發時遇到的問題,實現起來不難,可是原理值得研究一下。這是我在本身電腦上測試的一個場景,使用5個JDBC客戶端鏈接Proxy,每一個客戶端查詢出15萬條數據。能夠看到,Proxy的內存在一直增加,即便GC也回收不掉。這是由於在收到所有15萬條數據以前,JDBC ResultSet的get()方法是阻塞的。簡單說,就是JDBC在沒收到全量結果以前,是不讓用戶讀數據的,這是ResultSet的默認提取數據方式。會致使Proxy緩存大量臨時數據。那麼,有沒有一種辦法,能讓ResultSet一收到結果就當即給用戶消費呢?

從Connector/J文檔上能夠找到兩種解決方法:


一種是流式結果集:只要把Statement的FetchSize設置成這個值,JDBC就會使用流式ResultSet處理返回結果,讓用戶可以一邊接收數據一邊消費數據,而不須要所有接收完再消費。

另外一種方式是基於遊標的流式結果集:設置JDBC參數useCursorFetch=true,表示要使用遊標。設置FetchSize,指示每次返回數據的行數。

這種方式效率很低,要謹慎使用。經過抓包能夠發現,客戶端每次讀取數據都會向服務端發送Request Fetch Data這個請求,在網絡上的時間開銷是很是大的。

Proxy採用的是第一種方案,能夠看到內存已經恢復正常了。流式結果集是流式歸併的前提條件,流式歸併還要知足SQL條件和鏈接數條件,比較複雜,這裏就不詳細介紹了。這個內存使用效果是在最理想的狀況下產生的,也就是客戶端從Proxy消費數據的速度,大於等於Proxy從MySQL消費數據的速度。

三、限流


若是客戶端因爲某種緣由消費變慢了,或者乾脆不消費了,會發生什麼呢?經過測試發現,內存使用量直線飆升,比剛纔那張圖還誇張。咱們來研究下爲何會產生這種現象。這裏加上了幾個主要的緩存,SO_RCVBUF和SO_SNDBUF,他們是TCP的緩存。

ChannelOutboundBuffer是Netty寫緩存。在結果數據回傳的過程當中,若是Client阻塞,那麼他的SO_RCVBUF會瞬間被數據填滿,觸發TCP的滑動窗口去通知Proxy不要再發送數據了。與此同時,數據就會積壓到Proxy端,因此Proxy端的SO_SNDBUF也同時被填滿了。Proxy的SO_SNDBUF滿了以後,Netty的ChannelOutboundBuffer就會像一個無底洞同樣,吞掉全部MySQL發來的數據,由於在默認狀況下ChannelOutboundBuffer是無界的。因爲Netty在消費,因此Proxy的SO_RCVBUF一直是空的,致使MySQL能夠一直把數據發送過來,而Netty則不停的把數據存到ChannelOutboundBuffer,直到內存耗盡。

找到根本緣由以後,咱們須要作的就是當Client阻塞的時候,不讓Proxy再接收MySQL的數據。Netty經過水位參數來控制寫緩衝區,當buffer大小超太高水位線,咱們就控制Netty再也不往裏面寫,當buffer大小低於低水位線的時候,才容許寫入。設置完水位線後,當ChannelOutboundBuffer滿時,Proxy的SO_RCVBUF天然也滿了,觸發TCP滑動窗口通知MySQL中止發送數據。在這種狀況下,Proxy所消耗的內存只是ChannelOutboundBuffer高水位線的大小。咱們的目的就達到了。

四、分佈式鏈路追蹤系統


這個很是炫酷的界面來自Skywalking。他監控到了Sharding-Proxy中,執行一條SQL的完整調用鏈,並且對Proxy的代碼沒有任何侵入。他是怎麼作到的呢?說道對代碼沒有侵入,你們首先想到的可能就是鉤子。沒錯Skywalking就是使用的Instrument Agent實現的自動探針。目前已經支持的探針達幾十種,涵蓋了不少主流應用。若是把SkyWalking部署在一個真正的業務系統上,你看到的調用鏈會更加豐富。核心原理部分就介紹到這裏。

4、性能優化

一、代碼

我在這裏總結了一下常見的性能問題,有想提交代碼的同窗能夠重點留意一下,避免出現相同的問題。

第一類是代碼類問題。第一個例子:

你們看一下這個函數,有什麼問題嗎?入參若是是LinkedList,可能會產生什麼後果?LinkedList是鏈表,若是用下標get,時間複雜度是O(n)。循環n次,整個時間複雜度是O(n方)。換成for each遍歷遍歷就能夠了。

這個以前有社區的同窗測試過,50000個元素,執行時間相差1000多倍。

第二個例子:

若是把Properties當作整個系統的全局變量來使用,系統中大量併發調用getProperty()。Properties是Hashtable的子類,Hashtable的讀寫都是同步的。因此併發讀會被串行處理。使用其餘數據保存全局變量,好比ConcurrentHashMap。

二、額外非預期SQL

第二類是額外非預期SQL。第一個例子:

最近有一個PR,在每次執行SQL的時候調用了JDBC 的這個接口提取用戶名。JDBC會利用select user()這個SQL,到數據庫去查詢用戶名。致使在不知情的狀況下,你認爲執行了一個SQL,實際上執行了兩個。優化方式就是把UserName緩存。性能能夠提高32%。

第二個例子:

另一個非預期SQL問題與咱們以前使用的流式ResultSet有關。

這是我從JDBC 5.1.0的Release Notes上截的圖。每建立一個流式ResultSet,JDBC都會設置一次網絡超時時間。致使網絡上有大量的 SET net_write_timeout 請求。


解決方式是手動把這個參數的值設爲0。通過測試,性能提高33%。可見額外SQL對性能影響很是大,你們必定要注意。

三、IO & 系統調用

第三類涉及到比較底層的IO和系統調用。

Netty中發送數據分爲兩步:write()和flush()。Write()把數據寫入netty的緩存中,flush()把數據發送出去。以前的實現是,每次從MySQL收到一條結果數據,就馬上flush一次把它發送到客戶端。這樣的用法效率很低。諾曼在《Netty In Action》裏提到要儘可能減小flush()的調用次數,由於系統調用的代價很是高。

其實不止系統調用代價高,網絡利用率也很低。咱們能夠算一下,TCP每次發送數據,各類包頭加起來要54字節(MAC頭:14字節 + IP頭:20字節 + TCP頭:20字節),可能比你真正傳輸的數據都多。頻繁的flush,單位時間內傳輸的有效數據確定會變小。優化方式是多條結果調用一次flush(),性能提高達到50%。

四、全路由

最後一類是全路由問題。(這張圖是京東數科內部的鏈路監控工具SGM,是對接京東白條現場的截圖)

原本預期路由到單個節點的SQL,被路由到了n個節點。通常是由解析問題引發的。致使響應時間指數級增加。定位這類問題可使用Java性能監控工具定位,例如JMC、JProfiler。或者APM工具SkyWalking、SGM等等。利用他們跟蹤路由相關的函數,檢查調用次數是否正確。

五、測試

爲了可以即時發現性能問題,咱們會對每一個合併的PR進行性能測試,把問題定位在最小的範圍內。


好比此次構建,性能明顯降低,經過構建序號就能找到Github的提交記錄。

與性能測試不一樣,壓力測試側重於真實業務,天天進行長時間的壓測,保證當天代碼的穩定性。

目前天天壓測10小時左右,像內存泄漏這種問題就可以測出來。這個壓測的頁面之後會放到官網上。你們提完PR後,次日就能夠看到效果。

Sharding-Proxy在經歷了一年多的磨練後已經走向成熟,開始逐漸被部署到生產環境使用。歡迎感興趣的小夥伴試用,使用過程當中有什麼問題能夠加微信羣討論。若是你們有什麼想法、意見和建議,也歡迎留言與咱們交流,更歡迎加入到ShardingSphere的開源項目中。

相關文章
相關標籤/搜索