[原創]Sharding-Sphere之Proxy初探

 

你們好,拓海(https://github.com/tuohai666)今天爲你們分享Sharding-Sphere推出的重磅產品:Sharding-Proxy!在以前閃亮登場的Sharding-Sphere 3.0.0.M1中,首次發佈了Sharding-Proxy,這個新產品到底表現如何呢?此次但願經過幾個優化實踐,讓你們管中窺豹,從幾個細節的點可以想象出Sharding-Proxy的全貌。更詳細的MySQL協議、IO模型、Netty等議題,之後有機會再和你們專題分享。html

Sharding-Proxy簡介

Sharding-Proxy概覽

Sharding-Proxy是Sharding-Sphere的第二個產品。它定位爲透明化的數據庫代理端,提供封裝了數據庫二進制協議的服務端版本,用於完成對異構語言的支持。目前先提供MySQL版本,它可使用任何兼容MySQL協議的訪問客戶端(如:MySQL Command Client, MySQL Workbench等)操做數據,對DBA更加友好。前端

  • 對應用程序徹底透明,可直接當作MySQL使用。
  • 適用於任何兼容MySQL協議的客戶端。

與其餘兩個產品(Sharding-JDBC、Sharding-Sidecar)對比:mysql

 

Sharding-JDBCgit

Sharding-Proxygithub

Sharding-Sidecarsql

數據庫數據庫

任意後端

MySQL緩存

MySQL服務器

鏈接消耗數

異構語言

僅Java

任意

任意

性能

損耗低

損耗略高

損耗低

無中心化

靜態入口


它們既能夠獨立使用,也能夠相互配合,以不一樣的架構模型、不一樣的切入點,實現相同的功能目標,而其核心功能,如數據分片、讀寫分離、柔性事務等,都是同一套實現代碼。舉個例子,對於僅使用 Java 爲開發技術棧的場景,Sharding-JDBC 對各類 Java 的 ORM 框架支持度很是高,開發人員能夠很是便利地將數據分片能力引入到現有的系統中,並將其部署至線上環境運行,而 DBA 能夠經過部署一個 Sharding-Proxy 實例,對數據進行查詢和管理。

Sharding-Proxy架構

 

整個架構能夠分爲前端、後端和核心組件三部分來看。前端(Frontend)負責與客戶端進行網絡通訊,採用的是基於NIO的客戶端/服務器框架,在Windows和Mac操做系統下采用NIO 模型,Linux系統自動適配爲Epoll模型。通訊的過程當中完成對MySQL協議的編解碼。核心組件(Core-module)獲得解碼的MySQL命令後,開始調用Sharding-Core對SQL進行解析、改寫、路由、歸併等核心功能。後端(Backend)與真實數據庫的交互暫時藉助基於BIO的Hikari鏈接池。BIO的方式在數據庫集羣規模很大,或者一主多從的狀況下,性能會有所降低。因此將來咱們還會提供NIO的方式鏈接真實數據庫。

 

這種方式下Proxy的吞吐量將獲得極大提升,可以有效應對大規模數據庫集羣。

Sharding-Proxy功能細節

Prepared statement功能實現

我在Sharding-Sphere的第一個任務就是實現Proxy的PreparedStatement功能,聽說這是一個高大上的功能,可以預編譯SQL提升查詢速度和防止SQL注入攻擊什麼的。一次服務端預編譯,屢次查詢,下降SQL編譯開銷,提高了效率,聽起來沒毛病。然而在作完以後卻發現被坑,SQL執行效率不但沒有提升,甚至用肉眼都能看出來比原始的Statement還要慢。

先拋開Proxy不說,咱們經過wireshark抓包看看運行PreparedStatement的時候MySQL協議是如何交互的。

示例代碼以下:

 1 for (int i = 0; i < 2; i++) {  2     String sql = "SELECT * FROM t_order WHERE user_id=?";  3     try (  4         Connection connection = dataSource.getConnection();  5         PreparedStatement preparedStatement = connection.prepareStatement(sql)) {  6         preparedStatement.setInt(1, 10);  7         ResultSet resultSet = preparedStatement.executeQuery();  8         while (resultSet.next()) {  9  ... 10         } 11  } 12 }

代碼很容易理解,使用PreparedStatement執行兩次查詢操做,每次都把參數user_id設置爲10。分析抓到的包,JDBC和MySQL之間的協議消息交互以下:

 

JDBC向MySQL進行了兩次查詢(Query),MySQL返回給JDBC兩次結果(Response),第一條消息就不是咱們指望的PreparedStatement,SELECT裏面也沒有問號,說明prepare沒有生效,至少對MySQL服務來講沒有生效。對於這個問題,我想你們內心都有數,是由於jdbc的url沒有設置參數useServerPrepStmts=true,這個參數的做用是讓MySQL服務進行prepare。沒有這個參數就是讓JDBC進行prepare,MySQL徹底感知不到,是沒有什麼意義的。接下來咱們在url中加上這個參數:

jdbc:mysql://127.0.0.1:3306/demo_ds?useServerPrepStmts=true

交互過程變成了這樣:

 

初看這是一個正確的流程,第1條消息是PreparedStatement,SELECT裏也帶問號了,通知MySQL對SQL進行預編譯。第2條消息MySQL告訴JDBC準備成功。第3條消息JDBC把參數設置爲10。第4條消息MySQL返回查詢結果。然而到了第5條,JDBC怎麼又發了一遍PreparedStatement?預期應該是之後的每條查詢都只是經過ExecuteStatement傳參數的值,這樣才能達到一次預編譯屢次運行的效果。若是每次都「預編譯」,那就至關於沒有預編譯,並且相對於普通查詢,還多了兩次消息傳遞的開銷:Response(prepare ok)和ExecuteStatement(parameter = 10)。看來性能的問題就是出在這裏了。

像這樣使用PreparedStatement還不如不用,必定是哪裏搞錯了,因而拓海開始閱讀JDBC源代碼,終於發現了另外一個須要設置的參數:cachePrepStmts。咱們加上這個參數看看會不會發生奇蹟:

jdbc:mysql://127.0.0.1:3306/demo_ds?useServerPrepStmts=true&cachePrepStmts=true

果真獲得了咱們預期的消息流程,並且通過測試,速度也比普通查詢快了:

 

從第5條消息開始,每次查詢只傳參數值就能夠了,終於達到了一次編譯屢次運行的效果,MySQL的效率獲得了提升。並且因爲ExecuteStatement只傳了參數的值,消息長度上比完整的SQL短了不少,網絡IO的效率也獲得了提高。原來cachePrepStmts=true這個參數的意思是告訴JDBC緩存須要prepare的SQL,好比"SELECT * FROM t_order WHERE user_id=?"運行過一次後,下次再運行就跳過PreparedStatement,直接用ExecuteStatement設置參數值。

明白原理後,就知道該怎麼優化Proxy了。Proxy採用的是Hikari數據庫鏈接池,在初始化的時候爲其設置上面的兩個參數:

1 config.addDataSourceProperty("useServerPrepStmts", "true"); 2 config.addDataSourceProperty("cachePrepStmts", "true");

這樣就保證了Proxy和MySQL服務之間的性能。那麼Proxy和Client之間的性能如何保證呢?

Proxy在收到Client的PreparedStatement的時候,並不會把這條消息轉發給MySQL,由於SQL裏的分片鍵是問號,Proxy不知道該路由到哪一個真實數據庫。Proxy收到這條消息後只是緩存了SQL,存儲在一個StatementId到SQL的Map裏面,等收到ExecuteStatement的時候才真正請求數據庫。這個邏輯在優化前是沒問題的,由於每一次查詢都是一個新的PreparedStatement流程,ExecuteStatement會把參數類型和參數值告訴客戶端。

加上兩個參數後,消息內容發生了變化,ExecuteStatement在發送第二次的時候,消息體裏只有參數值而沒有參數類型,Proxy不知道類型就不能正確的取出值。因此Proxy須要作的優化就是在PreparedStatement開始的時候緩存參數類型。

 

完成以上優化後,Client-Proxy和Proxy-MySQL兩側的消息交互都變成了最後這張圖的流程,從第9步開始高效查詢。

Hikari鏈接池配置優化

Proxy在初始化的時候,會爲每個真實數據庫配置一個Hikari鏈接池。根據分片規則,SQL被路由到某些真實庫,經過Hikari鏈接獲得執行結果,最後Proxy對結果進行歸併返回給客戶端。那麼,數據庫鏈接池到底該設置多大?對於這個衆說紛紜的話題,今天該有一個定論了。你會驚喜的發現,這個問題不是設置「多大」,反而是應該設置「多小」!若是我說執行一個任務,串行比並行更快,是否是有點反直覺?

即便是單核CPU的計算機也能「同時」支持數百個線程。但咱們都應該知道這只不過是操做系統用「時間片」玩的一個小花招。事實上,一個CPU核心同一時刻只能執行一個線程,而後操做系統切換上下文,CPU執行另外一個線程,如此往復。一個CPU進行計算的基本規律是,順序執行任務A和任務B永遠比經過時間片「同時」執行A和B要快。一旦線程的數量超過了CPU核心的數量,再增長線程數就只會更慢,而不是更快。一個對Oracle的測試(http://www.dailymotion.com/video/x2s8uec)驗證了這個觀點。測試者把鏈接池的大小從2048逐漸下降到96,TPS從16163上升到20702,平響從110ms降低到3ms。

固然,也不是那麼簡單的讓鏈接數等於CPU數就好了,還要考慮網絡IO和磁盤IO的影響。當發生IO時,線程被阻塞,此時操做系統能夠將那個空閒的CPU核心用於服務其餘線程。因此,因爲線程老是在I/O上阻塞,咱們可讓線程(鏈接)數比CPU核心多一些,這樣可以在一樣的時間內完成更多的工做。到底應該多多少呢?PostgreSQL進行了一個benchmark測試:

 

TPS的增加速度從50個鏈接的時候開始變慢。根據這個結果,PostgreSQL給出了以下公式: connections = ((core_count * 2) + effective_spindle_count)

鏈接數 = ((核心數 * 2) + 磁盤數)。即便是32核的機器,60多個鏈接也就夠用了。因此,小夥伴們在配置Proxy數據源的時候,不要動不動就寫上幾百個鏈接,不只浪費資源,還會拖慢速度。

結果歸併優化

目前Proxy訪問真實數據庫使用的是JDBC,很快Netty + MySQL Protocol異步訪問方式也會上線,二者會並存,由用戶選擇用哪一種方法訪問。

在Proxy中使用JDBC的ResultSet會對內存形成很是大的壓力。Proxy前端對應m個client,後端又對應n個真實數據庫,後端把數據傳遞給前端client的過程當中,數據都須要通過Proxy的內存。若是數據在Proxy內存中呆的時間長了,那麼內存就可能被打滿,形成服務不可用的後果。因此,ResultSet內存效率能夠從兩個方向優化,一個是減小數據在Proxy中的停留時間,另外一個是限流。

咱們先看看優化前Proxy的內存表現。使用5個客戶端鏈接Proxy,每一個客戶端查詢出15萬條數據。結果以下圖,之後簡稱圖1。

 

能夠看到,Proxy的內存在一直增加,即時GC也回收不掉的。這是由於ResultSet會阻塞住next(),直到查詢回來的全部數據都保存到內存中。這是ResultSet默認提取數據的方式,大量佔用內存。那麼,有沒有一種方式,讓ResultSet收到一條數據就能夠當即消費呢?在Connector/J文檔(https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html)中有這樣一句話: If you are working with ResultSets that have a large number of rows or large values and cannot allocate heap space in your JVM for the memory required, you can tell the driver to stream the results back one row at a time. 若是你使用ResultSet遇到查詢結果太多,以至堆內存都裝不下的狀況,你能夠指示驅動使用流式結果集,一次返回一條數據。激活這個功能只需在建立Statement實例的時候設置一個參數:

stmt.setFetchSize(Integer.MIN_VALUE);

這樣就完成了。這樣Proxy就能夠在查詢指令後當即經過next()消費數據了,數據也能夠在下次GC的時候被清理掉。固然,Proxy在對結果作歸併的時候,也須要優化成即時歸併,而再也不是把全部數據都取出來再進行歸併,Sharding-Core提供即時歸併的接口,這裏就不詳細介紹了。下面看看優化後的效果,如下簡稱圖2。

 

數據在內存中停留時間縮短,每次GC都回收掉了數據,內存效率大幅提高。看到這裏,好像已經大功告成了,然而水還很深,請你們穿上潛水服繼續跟我探索。圖2是在最理想的狀況產生的,即Client從Proxy消費數據的速度,大於等於Proxy從MySQL消費數據的速度。

 

若是Client因爲某種緣由消費變慢了,或者乾脆不消費了,會發生什麼呢?經過測試發現,內存使用量直線拉昇,比圖1更強勁,最後將內存耗盡,Proxy被KO。下面咱們就先搞清楚爲何會發生這種現象,而後介紹對ResultSet的第2個優化:限流。下圖加上了幾個主要的緩存,SO_RCVBUF/ SO_SNDBUF是TCP緩存、ChannelOutboundBuffer是Netty寫緩存。

 

當Client阻塞的時候,它的SO_RCVBUF會被瞬間打滿,而後經過滑動窗口機制通知Proxy不要再發送數據了,同時Proxy的SO_SNDBUF也會瞬間被Netty打滿。Proxy的SO_SNDBUF滿了以後,Netty的ChannelOutboundBuffer就會像一個無底洞同樣,吞掉全部MySQL發來的數據,由於在默認狀況下ChannelOutboundBuffer是無界的。因爲有用戶(Netty)在消費,因此Proxy的SO_RCVBUF一直有空間,致使MySQL會一直髮送數據,而Netty則不停的把數據存到ChannelOutboundBuffer,直到內存耗盡。

搞清原理以後就知道,咱們的目標就是當Client阻塞的時候,Proxy再也不接收MySQL的數據。Netty經過水位參數WRITE_BUFFER_WATER_MARK來控制寫緩衝區,當buffer大小超太高水位線,咱們就控制Netty不讓再往裏面寫,當buffer大小低於低水位線的時候,才容許寫入。當ChannelOutboundBuffer滿時,Proxy的SO_RCVBUF被打滿,通知MySQL中止發送數據。因此,在這種狀況下,Proxy所消耗的內存只是ChannelOutboundBuffer高水位線的大小。

Proxy的兩種模式

在即將發佈的Sharding-Sphere 3.0.0.M2版本中,Proxy會加入兩種代理模式的配置:

MEMORY_STRICTLY: Proxy會保持一個數據庫中全部被路由到的表的鏈接,這種方式的好處是利用流式ResultSet來節省內存。

CONNECTION_STRICTLY: 代理在取出ResultSet中的全部數據後會釋放鏈接,同時,內存的消耗將會增長。

簡單能夠理解爲,若是你想消耗更小的內存,就用MEMORY_STRICTLY模式,若是你想消耗更少的鏈接,就用CONNECTION_STRICTLY模式。

MEMORY_STRICTLY的原理其實就是咱們上一節介紹的內容,優勢已經說過了。它帶來的一個反作用是,流式ResultSet須要保持對數據庫的鏈接,必須與全部路由到的真實表成功創建鏈接後,纔可以進行即時歸併,進而返回結果給客戶端。假設數據庫設置max_user_connections=80,而該庫被路由到的表是100個,那麼不管如何也不可能同時創建100個鏈接,也就沒法歸併返回結果。

CONNECTION_STRICTLY就是爲了解決以上問題而存在的。不使用流式ResultSet,內存消耗增長。但該模式不須要保持與數據庫的鏈接,每次取出ResultSet內的全量數據後便可釋放鏈接。仍是剛纔的例子max_user_connections=80,而該庫被路由到的表是100個。Proxy會先創建80個鏈接查詢數據,另外20個鏈接請求被緩存在鏈接池隊列中,隨着前面查詢的完成,這20個請求會陸續成功鏈接數據庫。

若是你對這個配置還感到迷惑,那麼記住一句話,只有當max_user_connections小於該庫可能被路由到的最大表數量時,才使用CONNECTION_STRICTLY

小結

Sharding-Sphere自2016開源以來,不斷精進、不斷髮展,被愈來愈多的企業和我的承認:在Github上收穫5000+的star,1900+forks,60+的各大公司企業使用它,爲Sharding-Sphere提供了重要的成功案例。此外,愈來愈多的企業夥伴和我的也加入到Sharding-Sphere的開源項目中,爲它的成長和發展貢獻了巨大力量。

將來,咱們將不斷優化當前的特性,精益求精;同時,你們關注的柔性事務、數據治理等更多新特性也會陸續登場。Sharding-Sidecar也將成爲雲原生的數據庫中間件!

願全部有識之士能加入咱們,一同描繪Sharding-Sidecar的新將來!

願正在閱讀的你也能助咱們一臂之力,轉載分享文章、加入關注咱們!

關於Sharding-Sphere

Sharding-Sphere是一套開源的分佈式數據庫中間件解決方案組成的生態圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar這3款相互獨立的產品組成。他們均提供標準化的數據分片、讀寫分離、柔性事務和數據治理功能,可適用於如Java同構、異構語言、容器、雲原生等各類多樣化的應用場景。

亦步亦趨,開源不易,您對咱們最大支持,就是在github上留下一個star。

項目地址:

https://github.com/sharding-sphere/sharding-sphere/

https://gitee.com/sharding-sphere/sharding-sphere/

更多信息請瀏覽官網:

http://shardingsphere.io/

相關文章
相關標籤/搜索