1. 程式人生 > >[原創]Sharding-Sphere之Proxy初探

[原創]Sharding-Sphere之Proxy初探

利用 兩種模式 概覽 map benchmark 高水位 內存耗盡 tint 連接

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

Sharding-Proxy簡介

Sharding-Proxy概覽

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

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

技術分享圖片

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

Sharding-JDBC

Sharding-Proxy

Sharding-Sidecar

數據庫

任意

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/

[原創]Sharding-Sphere之Proxy初探