一文淺談“讀寫分離”技術
讀寫分離,作為一種常用的數據庫訪問優(yōu)化手段,得到廣泛的應用。本文嘗試從讀寫分離的技術實現、適用場景及典型產品等角度,闡述這一技術的整體現狀。
1. 讀寫分離:概述
1).何為讀寫分離
讀寫分離,從字面理解就是將對數據庫的讀操作與寫操作分離的一種優(yōu)化手段。其最早起源于互聯網快速發(fā)展時期,面對海量用戶訪問問題,通過這一技術來解決數據庫性能瓶頸問題。目前已經成為非常常見的一種數據庫訪問優(yōu)化技術。
2).讀寫分離好處
提高訪問性能通過引入讀寫分離技術,將之前集中于單點的訪問壓力,分散到更多節(jié)點。即可利用更多的資源,支撐業(yè)務系統(tǒng),可有效提升整體訪問性能。
提高穩(wěn)定性通過將讀取與寫入操作的分離,可有效規(guī)避由于異常操作所帶來的風險。常見如一個大查詢語句,因訪問數據規(guī)模巨大占用大量CPU資源。通過承載端分離,可避免影響更為重要的寫入操作。
提高資源利用率為了更好地保護數據,數據庫系統(tǒng)通常采用多副本技術冗余保護數據,但其備用副本如無法提供業(yè)務訪問,將是一種資源浪費,而讀寫分離可有效利用只讀副本,提升整體資源利用率。
提高可用性通過引入更多節(jié)點來承載讀寫操作,結合負載均衡與高可用探查技術,可避免單點故障引發(fā)可用性問題。
提高訪問效率通過利用不同節(jié)點分別承載讀取與寫入,還可緩解因為鎖帶來的爭用問題,提高單節(jié)點的訪問效率。
更大優(yōu)化空間針對讀取操作的特殊性,可通過分離后的獨立資源采取特有的優(yōu)化技術,進一步提升訪問效率。
2. 讀寫分離:技術實現
1).用方案
目前業(yè)界流行的讀寫分離方案,通常都是基于上述主從模式的數據庫架構,通過引入數據訪問代理層,來實現訪問動作的讀寫分離。引入數據訪問代理的好處是源程序不需要做任何改動就可以實現讀寫分離,壞處是由于多了一層中間件做中轉代理,性能上會有所下降,數據訪問代理也容易成為性能瓶頸,并且還存在一定維護成本。還有另一種方式,是將數據訪問代理層前置到應用側,通過SDK方式與應用集成在一起,可避免獨立一層所帶來的性能損耗和維護成本高的問題。但這種方式對開發(fā)語言有一定要求,存在適用性問題。
2).技術要點
讀寫分離功能的好與不好,主要是在易用性和靈活度問題。前者是關心如何讓業(yè)務開發(fā)像操作單個主庫一樣,無需過多關注主從讀寫分離的細節(jié),只需要做好相應讀寫配置后,就無需考慮寫主讀從的細節(jié)。后者是解決用戶多變的業(yè)務場景和拓撲變化,并可實現自動適應。這其中是需要解決一系列技術問題,如下面這些常見的問題。
? 判斷讀寫操作
如何判斷讀寫操作,是讀寫分離面臨的首要問題。判斷方式可大致分為自動和手動兩種,前者是通過顯式的方式由用戶來指定;后者則是自動進行判斷,用戶無需關心。這兩種判斷方式往往是互補的,可配合來使用。下面是常見判斷邏輯及處理:
- 基于不同端口連接
該實現方式就讀寫分離功能而言不是太好,因為此方式與應用自己實現沒有明顯差別,只是將直接連接不同數據庫的邏輯變成了連接中間件服務器的不同端口,并沒有對應用系統(tǒng)開發(fā)帶來實質性的簡化工作。
- 基于SQL匹配
采用正則表達式匹配是比較容易實現的方案,可以無需應用的修改,只需要在中間件添加正則匹配的規(guī)則,即可將讀、寫分發(fā)的邏輯在中間件完成。讀寫分離的效果,取決于中間件的正則匹配規(guī)則的編寫質量。
- 基于Hint
應用系統(tǒng)發(fā)送SQL時,可以添加Hint,顯示的告訴中間件想要將該SQL發(fā)送到何處。中間件解析特定規(guī)則的Hint,即可實現對帶有不同Hint的語句分發(fā)到不同的數據庫節(jié)點。
- 基于語法解析
當中間件獲取到應用發(fā)送的SQL字符串時,對其進行完整的語法解析,可以最大程度的獲取SQL字符串中的信息,例如類型、操作對象等?;谡Z法的判斷,就能夠自動針對不同語句類型進行讀寫分發(fā),可以最大限度的減少應用的適配工作。
使用語法解析是相對來說較為友好的方式,無需開發(fā)人員感知即可實現讀寫操作分離。但這其中存在難點,就是如何準確判斷出只讀操作存在一定困難,例如使用函數、存儲過程、觸發(fā)器或諸如“SELECT ... FOR UPDATE”類的操作。此時,是需要引入輔助機制進行判斷,可采取配置名單方式來輔助分析;或者通過Hint、API的方式強制指定走寫庫或讀庫。除此之外,還有些命令也需要規(guī)范是否可在備庫執(zhí)行,如COPY、SHOW、SET、BEGIN...END等。
? 如何處理事務
事務類操作,往往意味著數據變化,在讀寫分離中如何處理呢?通常有兩種思路,一種是簡單粗暴方式,將所有事務及關聯操作全部發(fā)送到主機;一種是更為精確的處理,即分析事務內的語句序列,將事務中先寫后讀的對象進行關聯,一起發(fā)送到主機,確保數據正確,而把和寫操作無關的讀操作,進行拆分,發(fā)送到備機執(zhí)行。后一種處理方式能最大限度的利用讀寫分離,當然需要解決對象前后關系這一問題。
? 解決主備延遲
基于副本方式的延遲是常見的,也是讀寫分離在設計之初就需考慮的問題。其通常的處理思路可以有多種:
- 強制讀寫走主庫
這類解決方案最簡單粗暴,也是實際工作中最常用的方案。通過對主備節(jié)點延遲情況的判斷,來決定如何是走主庫還是備庫。通??蓪⒀舆t判斷封裝在中間層,前端應用可不感知,只需配置延遲閾值即可,當超過這一閾值就自動走主庫。如下次訪問時延遲低于閾值,可重新走備庫。當然,這一方式無疑會加大對主庫的壓力。
- 輪轉和重試備庫
當在備庫讀取不到最新數據時,另一種思路多讀取幾次或者嘗試讀取其他備庫。這里面的核心是對讀取最新數據的判斷,通常需要在應用開發(fā)時有所考慮才可。同時還需要制定退化方案,在何種情況下退化到讀取主庫。
- 結合緩存解決
如延遲是常態(tài),很難短期內解決,通過引入緩存可達到立竿見影的效果。其原理是在數據寫入主庫時,同步或異步寫入緩存,應用讀取時優(yōu)先讀取緩存,失效時才讀取數據庫。這種方案因引入緩存組件稍顯復雜,需解決緩存與數據庫同步更新及失效問題;同時對應用側有一定影響,需感知到緩存。比較好的處理方式是都封裝在中間層,通過它來統(tǒng)一處理訪問邏輯。
數據庫優(yōu)化
最后一種就是盡量避免出現延遲,常見對數據庫有些可優(yōu)化的措施。例如盡量減少在主節(jié)點上執(zhí)行大事務操作、減少主庫索引進而減小寫入開銷、主備庫采用不同存儲引擎提升效率等等。當然這些方案只能起到一定作用,無法完全避免延遲問題。
? 靈活負載策略
針對多個讀庫,讀寫分離組件還需提供靈活的負載均衡策略,常見的如隨機、輪詢、權重等等。這其中有幾個特殊情況需要考慮:
- QoS
不同讀庫的服務能力有所差異下,其能提供的服務保障不同,需在讀寫分離中提供例如權重的配置,進行干預。當然,更好的方式是提供服務質量評估機制,可根據各讀庫的服務能力進行分配。
- 位置感知
針對多AZ、多Region的情況,不同讀庫承載的角色不同,有的只作為備選主庫不承擔讀、有的作為遠程災備等,因此在讀寫分離中希望能感知到這些信息,有所區(qū)別對待。往往可通過設置標簽的方式解決,根據不同標簽設置不同策略。
? 解決讀一致性
在讀寫分離中,當存在多個讀庫下,會因為延遲不同,出現讀取不一致的情況。即路由到不同的讀庫,讀取的數據鮮活度不同。這對于前端應用會造成一定困擾,解決的方法可采用會話粘性的策略,針對同一會話路由到同一讀庫,避免出現讀不一致。
? 拓撲結構感知
如果讀寫分離訪問的數據集群拓撲發(fā)生變化,例如主備發(fā)生切換,寫操作要到新的主庫;亦或是增加了備庫數量,流量可以打到新備庫等,這些都是需要讀寫分離組件感知到底層數據庫拓撲的變化。這里的難點在于幾個方面:
- 準確感知變化
當出現網絡等原因,底層發(fā)生變化,可能讀寫分離組件沒有探查到;或者探查本身就出現問題,沒有發(fā)生變化而誤認為發(fā)生變化。此時就會出現兩張拓撲結構,一個實際結構,一是讀寫分離組件感知到的結構。這一問題,一方面可通過引入共識機制,增加多方判斷解決;一方面也可通過與高可用組件互動減少誤判。
- 感知時效問題
當發(fā)生拓撲變化后,從發(fā)生變化到被讀寫分離組件感知是需要時間的,過短會導致數據庫探查壓力大;過長會影響整體恢復時間,這其中需要有個取舍。建議將這一能力開放給用戶,由用戶根據自身業(yè)務進行決策。同時也可與高可用組件互動,將拓撲變化信息盡快推送到讀寫分離組件,變被動探查為主動感知,提高時效性。
- 人為干預能力
除因故障等原因發(fā)生的拓撲變化外,有時還需人工干預讀寫分離。如發(fā)生機器維護、數據庫升級等情況下,可提前通過人工手段,從拓撲結構中摘除相關節(jié)點,做到更加平順。
? 個性化訴求
除了上述要點外,還有些用戶個性化的需求。如某個數據庫用戶的訪問只走主庫,某類應用的訪問只走主庫等,這類需求比較分散,比較好的處理方式是提供一定的腳本擴展能力,類似lua擴展Nginx的方式。
3. 讀寫分離:最佳實踐
1).數據庫優(yōu)化手段對比
讀寫分離技術,是一種有效的數據庫訪問優(yōu)化手段,但不是唯一。隨著業(yè)務增長,達到一定規(guī)模后,提升數據庫承載能力可以有多種方式,從大的分類來看可分為業(yè)務層優(yōu)化、架構層優(yōu)化、訪問層優(yōu)化與數據庫優(yōu)化幾個方面。
業(yè)務層-垂直拆分最為徹底的優(yōu)化手段,在業(yè)務層就做了拆分,投入較高,但取得效果往往也比較可觀。
架構層-緩存/搜索通過引入緩存、搜索等技術,減輕對數據庫壓力,讓數據庫專注于有價值操作。這種方式需要一定改造工作量,取得收益取決于業(yè)務對數據的要求而定。
訪問層-讀寫分離簡單快速的優(yōu)化方式,可快速提升性能,針對部分場景效果明顯。
訪問層-分庫分表分庫分表方式,原理上是采取“大化小”的策略,但對于SQL兼容性有較高要求,會存在一定業(yè)務改造工作量。預期收益效果看規(guī)模和業(yè)務對數據要求而定。
數據庫-垂直拆分對現有數據庫根據業(yè)務進行拆分,難易程度及投入成本取決于之前架構設計,難點在于拆分后的數據交互。預期收益不很明確。
數據庫-垂直擴展對數據庫升級是快速見效的措施,對應用幾乎無影響,但需一定的成本投入及升級所需的中斷服務的時間。取得收益存在上限瓶頸,預期中等。
數據庫-水平擴展對分庫分表類似,但通常初始投入較大,對應用存在一定侵入性。
從上述對比可見,讀寫分離,可以說是對應用侵入最小,也最容易實現的優(yōu)化手段。相對投入不到,就可取得一定效果。特別是對于大量讀請求和少量寫請求的業(yè)務場景,會有不錯的效果。
2).讀寫分離適用場景
讀寫分離是一種簡單有效的優(yōu)化方式,但不是萬能,其有著明顯的適用場景特征。
- 讀多寫少
當單機數據庫不能支持業(yè)務的讀寫規(guī)模,就可以考慮讀寫分離。但需要考慮兩者的比例,如果寫操作比例大于讀操作,那么大量寫操作都在主庫進行,讀寫分離達不到預期降低主庫壓力的作用。一般來說,兩者讀寫比越大,效果越好。當然還需考慮寫規(guī)模不能也不能高于單機數據庫支持規(guī)模。
- 讀有限擴展
針對承載讀的規(guī)模超大的情況,也需慎重。通過讀寫分離是可以實現一定程度讀操作的橫向擴展,但不是無限的,受限于數據庫復制的效率與成本,其存在擴展上限。對于大規(guī)模的可綜合考慮緩存、數據拆分等多種手段。
- 允許延遲
針對主備方式難免存在延遲,因此對于延遲很敏感的操作不適于此方案。
- 非復雜查詢
采用讀寫分離能在一定程度上解決查詢效率問題,但針對復雜查詢試圖通過這一方式去解決不是一個好的思路。這類訴求建議通過搜索引擎、OLAP等技術去解決。
4. 讀寫分離:典型產品
業(yè)內有很多讀寫分離方案,一類是采用中間件思路開發(fā),以開源產品為主;一類是數據庫產品,內置讀寫分離功能。下面簡單介紹下主要的產品:
1).MySQL-Proxy
MySQL-Proxy是MySQL官方提供的MySQL中間件服務。MySQL-Proxy實際上是在客戶端請求與MySQLServer之間建立了一個連接池。所有客戶端請求都是發(fā)向MySQL-Proxy,然后經由MySQL-Proxy進行相應的分析,判斷出是讀操作還是寫操作,分發(fā)至對應的MySQLServer上。對于多節(jié)點Slave集群,也可以起做到負載均衡的效果。
# ./mysql-proxy --daemon --log-level=debug --user=mysql --keepalive --log-file=/var/log/mysql-proxy.log --plugins="proxy" --proxy-backend-addresses="192.168.1.5:3306" --proxy-read-only-backend-addresses="192.168.1.6:3306" --proxy-lua-script="/root/soft/mysql-proxy/rw-splitting.lua" --plugins=admin --admin-username="admin" --admin-password="admin" --admin-lua-script="/root/soft/mysql-proxy/lib/mysql-proxy/lua/admin.lua"
其中proxy-backend-addresses是master服務器,proxy-read-only-backend-addresses是slave服務器。
2).Apache ShardingSphere
Apache ShardingSphere 是一款開源的數據庫中間件產品,并在Apache基金會畢業(yè),可以說是非常成熟的開源項目。其產品內置了豐富的功能,包括讀寫分離能力,具體包括:
- 支持動態(tài)、靜態(tài)讀寫分離能力,支持自動拓撲感知與人工設定。
- 支持多種數據庫(如MySQL、PG、openGauss等)及多種架構(如MySQL 主從、MGR等)
- 支持多端接入(Driver、Proxy),可滿足低時延場景
- 支持語法解析自動判斷或 Hint 方式手工指定
- 支持包括熔斷等能力的人工干預手段,可適應多種場景
- 支持類SQL的管理配置方式,支持熱加載配置
支持豐富的負載均衡算法,如下圖
3).MyCAT
MyCAT 是一款開源的數據庫中間件產品。讀寫分離功能通過配置文件完成,如下
balance,讀寫分離策略
- 0,不開啟讀寫分離機制,所有讀操作發(fā)到當前可用writeHost上
- 1,全部readHost與stand by writeHost參與select語句負載均衡
- 2,所有讀操作都隨機在writeHost、readhost上分發(fā)
- 3,所有讀請求隨機分發(fā)到readhost
writeType,寫模式
- 0,所有的操作發(fā)送到配置的第一個writehost
- 1,隨機發(fā)送到配置的所有writehost
- 2,不執(zhí)行寫操作
switchType,切換模式
- -1,表示不自動切換
- 1,默認值,表示自動切換
- 2,基于MySQL主從同步的狀態(tài)決定是否切換
- 3,基于MySQL galary cluster的切換機制
- 4).阿里云-RDS數據庫代理(以RDS PG為例)
數據庫代理是阿里云數據庫RDS提供的一款安全、穩(wěn)定、高性能,對應用完全透明的數據庫中間層服務。數據庫代理是位于數據庫服務端和應用服務端之間的網絡代理服務,代理服務端代替應用服務端數據庫發(fā)送和接受所有數據庫請求,進而可以在代理服務層上實現比如讀寫分離、連接池、端對端加密、防閃斷等附加功能。通過數據庫代理用戶只需要通過一個鏈接地址即可實現讀寫分離架構,讀寫屬性和只讀屬性的多樣化選擇滿足了不同業(yè)務場景。
數據庫代理支持了PostgreSQL的協(xié)議,并且具備對用戶的請求連接認證權限的能力。代理中的路由策略是核心:可以通過hint中指定固定的實例節(jié)點來轉發(fā)流量;也能夠將事務內寫操作之前的讀請求轉發(fā)到只讀實例,降低主實例負載;路由策略還可以根據用戶自定義的只讀模式或讀寫模式對請求進行不同的分發(fā)執(zhí)行。健康巡檢模塊周期性感知讀寫分離架構的拓撲變化情況,在實例節(jié)點不健康或者超過延遲閾值,會自動把讀請求路由到其他的只讀實例上。
5).OceanBase
OceanBase 數據庫天然支持讀寫分離的功能,即通過 OBProxy 代理服務和修改 OBServer 的配置即可實現業(yè)務的讀寫分離策略。OceanBase 數據庫在讀取數據時,提供了兩種一致性級別:強一致性和弱一致性。
強一致性是指請求路由給主副本讀取最新數據;
弱一致性是指請求優(yōu)先路由給備副本,不要求讀取最新數據。
通過應用側為執(zhí)行的 SQL 添加 SQL Hint 來顯性開啟弱一致性讀就可以實現基于注釋的讀寫分離功能,同時也衍生出如下三種常用的讀寫分離策略:
6).KunlunBase
KunlunBase是一個開源、高性能的分布式關系數據庫,支持混合負載、PB級數據量管理并提供毫秒延遲的新一代數據庫解決方案。
KunlunBase 的讀寫分離在計算層的遠程查詢優(yōu)化器內實現的,當用戶的SQL同時滿足如下條件:
- 當前SQL類型為select;
- SQL中不包含用戶自定義函數,除非當前事務為只讀事務;
- 如果不在事務中(autocommit=on),則允許讀寫分離;
- 如果語句在顯式事務中,則要滿足:
- 如果在只讀事務中,則允許讀寫分離;
- 如果在讀寫事務中,則該事務未更新過數據;
遠程查詢優(yōu)化器就會將相應的SQL 執(zhí)行計劃下發(fā)到從備機的節(jié)點上執(zhí)行。KunlunServer 會根據以下規(guī)則選擇發(fā)送select語句到目標存儲集群的哪個備機節(jié)點:
- 根據節(jié)點權重值選擇 (ro_weight)
- 根據網絡延遲(ping)
- 根據主從副本的數據一致性延遲(latency)