[原創]Sharding-Sphere之Proxy初探

来源:https://www.cnblogs.com/tuohai666/archive/2018/09/06/9596049.html
-Advertisement-
Play Games

Sharding-Proxy是Sharding-Sphere的第二個產品。它定位為透明化的資料庫代理端,提供封裝了資料庫二進位協議的服務端版本,用於完成對異構語言的支持。目前先提供MySQL版本,它可以使用任何相容MySQL協議的訪問客戶端(如:MySQL Command Client, MySQL... ...


 

大家好,拓海(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/


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. 框架以外依賴包引入 1.1. Pomelo.EntityFrameworkCore.MySql 1.2. Pomelo.EntityFrameworkCore.MySql.Design 2. 衝突包的檢查和排除(自己遇到的出問題的地方) 2.1. 排除-Mysql.Data.Entity,具體 ...
  • How to avoid WPF TreeView SelectedItemChanged being called twice Very often, we need to execute some code in SelectedItemChanged depending on the sele ...
  • 來看看8月份最後一個周F#和.NET最新相關信息. https://www.yuque.com/rock/fsharp-weekly/35 這次我們多聊聊.NET相關的東西, 看看.NET的健康生態. 新聞 隨著Core的穩定發佈,.NET Standard 2.1也更新發佈, WebSharper ...
  • shell數組 數組介紹 數組就是各種數據類型的元素按一定順序排列的集合。 數組就是把有限個元素變數或數據用一個名字命名,然後用編號區分它們的變數的集合。這個名字稱為數組名,編號稱為數組下標。組成數組的各個變數稱為數組的分量,也稱為數組的元素,有時也稱為下標變數。 數組定義與增刪改查 方法1:arr... ...
  • 重啟系統後按'e'鍵,進入編輯模式,在'UTF -8'後空格輸入'rd.break'後,按快捷鍵'Ctrl+X'進入新界面進行編輯,代碼如下: switch_root:/# mount -o remount,ru /sysroot/ switch_root:/#mount switch_root:/ ...
  • 第1章 Rsync基本概述 1.1 什麼是Rsync rsync是一款開源,快速,多功能的可實現增量的本地或遠程的數據鏡像同步備份的優秀工具。適用於多個平臺。從軟體名稱可以看出來是遠程同步的意思(remote sync)可實現全量備份與增量備份,因此非常適合用於架構集中式備份或異地備份等應用。 1. ...
  • 首先聲明,我的文章不配圖,就靠文字描述,然後自己體會,摸著石頭體驗吧! 從今天開始玩Linux,Ubuntu16.04據說是比較穩定的,界面友好,類似與Windows界面,也有Linux的命令終端,用起來有一種想要起飛的感覺,O(∩_∩)O哈哈~ 1、首先說安裝步驟,就是怎麼將Ubuntu16.04 ...
  • Windows 下安裝drozer(Windows 10),連接手機(紅米note4X) 首先下載drozer(http://mwr.to/drozer)。 紅米手機開發者模式 遇到第一個問題,紅米手機開發者模式。好久不用這個了,發現找不到。網上搜索一波, 在設置-我的設備-全部參數,位置,點擊MI ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...