數(shù)據(jù)庫優(yōu)化方案:查詢請求增加時,如何做主從分離
圖片
當(dāng)前數(shù)據(jù)庫仍為單機(jī)部署,根據(jù)一些云廠商的基準(zhǔn)測試結(jié)果,在4核8GB的機(jī)器上運行MySQL 5.7時,大概可以支撐500TPS和10000QPS。運營負(fù)責(zé)人表示正在準(zhǔn)備雙十一活動,并且公司層面會繼續(xù)加大在全渠道的推廣投入,這無疑會引發(fā)查詢量的大幅增加。今天我們將討論如何通過主從分離來解決查詢請求量激增的問題。
主從讀寫分離
大部分系統(tǒng)的訪問模型是讀多寫少,讀寫請求量的差距通??梢赃_(dá)到幾個數(shù)量級。這一點很容易理解,比如刷朋友圈的請求量肯定比發(fā)朋友圈的要大,淘寶商品的瀏覽量通常也遠(yuǎn)大于下單量。因此,我們的首要任務(wù)是讓數(shù)據(jù)庫能夠應(yīng)對更高的查詢請求。為了實現(xiàn)這一點,首先需要將讀寫流量區(qū)分開來,因為只有這樣,才能對讀流量進(jìn)行單獨擴(kuò)展,這就是我們所說的主從讀寫分離。
其實,這本質(zhì)上是一個流量分離的問題,就像道路交通管制一樣,我們將一個四車道的大馬路劃出三個車道供領(lǐng)導(dǎo)外賓使用,剩下一個車道供普通車輛行駛,優(yōu)先保證領(lǐng)導(dǎo)先行,原理類似。主從讀寫分離是一個常規(guī)的做法,在面對數(shù)據(jù)庫突發(fā)讀流量時也是一種有效的應(yīng)對策略。在我目前的項目中,曾出現(xiàn)過前端流量激增導(dǎo)致從庫負(fù)載過高的情況,這時DBA同事會優(yōu)先做從庫擴(kuò)容,分擔(dān)讀流量,將負(fù)載分散到多個從庫上,減輕了從庫的壓力,接下來研發(fā)團(tuán)隊則會考慮其他方案來進(jìn)一步優(yōu)化數(shù)據(jù)庫層的流量處理。
主從讀寫的兩個技術(shù)關(guān)鍵點
一般來說,在主從讀寫分離機(jī)制中,我們將一個數(shù)據(jù)庫的數(shù)據(jù)拷貝為一份或多份,并將其寫入到其他數(shù)據(jù)庫服務(wù)器中。原始數(shù)據(jù)庫被稱為主庫,主要負(fù)責(zé)數(shù)據(jù)的寫入;而拷貝的目標(biāo)數(shù)據(jù)庫稱為從庫,主要負(fù)責(zé)支持?jǐn)?shù)據(jù)查詢。
可以看到,主從讀寫分離有兩個技術(shù)上的關(guān)鍵點:
- 數(shù)據(jù)的拷貝,也就是主從復(fù)制。
- 如何屏蔽主從分離帶來的訪問數(shù)據(jù)庫方式的變化,使得開發(fā)人員在使用時,感覺像是在操作單一數(shù)據(jù)庫一樣。
1. 主從復(fù)制
我先以 MySQL 為例介紹一下主從復(fù)制。
MySQL 的主從復(fù)制依賴于 binlog,即將 MySQL 上的所有變化記錄下來,并以二進(jìn)制形式保存在磁盤上的二進(jìn)制日志文件中。主從復(fù)制的過程是將 binlog 中的變化從主庫傳輸?shù)綇膸?,通常這個過程是異步的,也就是說,主庫上的操作不會等待 binlog 同步完成。
主從復(fù)制的具體過程如下:首先,從庫在連接到主庫時,會創(chuàng)建一個 IO 線程,用于請求主庫更新的 binlog,并將接收到的 binlog 內(nèi)容寫入到一個名為 relay log 的日志文件中;與此同時,主庫會創(chuàng)建一個 log dump 線程,負(fù)責(zé)將 binlog 發(fā)送給從庫。然后,從庫還會創(chuàng)建一個 SQL 線程,讀取 relay log 中的內(nèi)容,并在從庫中進(jìn)行回放,最終實現(xiàn)主從一致性。
這種主從復(fù)制方式是比較常見的。在此方案中,使用獨立的 log dump 線程以異步的方式進(jìn)行數(shù)據(jù)傳輸,可以避免影響主庫的主體更新流程。并且從庫接收到信息后,并不是直接寫入從庫的存儲,而是寫入 relay log,這樣可以避免直接寫入存儲帶來的性能開銷,從而避免主從延遲過長。
圖片
你會發(fā)現(xiàn),基于性能考慮,主庫的寫入流程并不會等待主從同步完成后才返回結(jié)果。因此,在極端情況下,比如主庫上的 binlog 還沒有來得及刷新到磁盤上,就發(fā)生磁盤損壞或機(jī)器掉電的情況,就可能導(dǎo)致 binlog 丟失,從而造成主從數(shù)據(jù)的不一致。不過,這種情況出現(xiàn)的概率較低,對于大多數(shù)互聯(lián)網(wǎng)項目來說是可以容忍的。
在實現(xiàn)主從復(fù)制后,我們可以在寫入時只操作主庫,而在讀數(shù)據(jù)時只讀取從庫。這樣,即使寫請求會鎖表或鎖記錄,也不會影響讀請求的執(zhí)行。同時,在讀流量較大的情況下,可以部署多個從庫共同承擔(dān)讀流量,這就是所謂的“一主多從”部署方式。比如在你的垂直電商項目中,可以通過這種方式來應(yīng)對較高的并發(fā)讀流量。此外,從庫還可以作為備庫使用,避免主庫故障導(dǎo)致數(shù)據(jù)丟失。
那么,你可能會問,是否可以通過無限增加從庫的數(shù)量來抵抗大量的并發(fā)請求呢?實際上,并不是這樣。隨著從庫數(shù)量的增加,連接到每個從庫的 IO 線程也會增多,主庫需要創(chuàng)建更多的 log dump 線程來處理這些復(fù)制請求,導(dǎo)致主庫的資源消耗增加。而且,由于受限于主庫的網(wǎng)絡(luò)帶寬,實際上一個主庫最多只能連接 3~5 個從庫。
解決這個問題的思路有很多,核心思想就是盡量避免從庫查詢數(shù)據(jù)。以剛才的例子為基礎(chǔ),我有三種解決方案:
第一種方案:數(shù)據(jù)冗余
你可以在發(fā)送消息隊列時,不僅僅發(fā)送微博 ID,而是將隊列處理機(jī)需要的所有微博信息一并發(fā)送。這樣就避免了從數(shù)據(jù)庫中重新查詢數(shù)據(jù)。
第二種方案:使用緩存
在同步寫入數(shù)據(jù)庫的同時,將微博數(shù)據(jù)也寫入到 Memcached 等緩存中。這樣隊列處理機(jī)在獲取微博信息時,會優(yōu)先查詢緩存,從而確保數(shù)據(jù)的一致性。
第三種方案:查詢主庫
隊列處理機(jī)可以選擇查詢主庫,而不是從庫。不過,這種方式需要謹(jǐn)慎使用,必須確保查詢量級不會過大,能夠在主庫的承受范圍內(nèi),否則可能對主庫造成過大的壓力。
在這三種方案中,我通常會優(yōu)先考慮第一種方案,因為它相對簡單,雖然可能會導(dǎo)致單條消息較大,增加消息發(fā)送的帶寬和時間,但其簡潔性和可控性較高。緩存方案適合用于新增數(shù)據(jù)的場景,但在更新數(shù)據(jù)時,可能會引發(fā)數(shù)據(jù)不一致的問題。例如,如果兩個線程同時更新緩存,可能會導(dǎo)致緩存中的數(shù)據(jù)與數(shù)據(jù)庫中的數(shù)據(jù)不一致。查詢主庫的方案,我會盡量避免使用,除非沒有其他選擇。原因是如果為隊列處理機(jī)提供查詢主庫的接口,很難保證團(tuán)隊中的其他成員不會濫用該接口,導(dǎo)致主庫承受過多的讀請求,最終影響系統(tǒng)的穩(wěn)定性。
因此,選擇哪種方案,還是要根據(jù)實際的項目需求和系統(tǒng)架構(gòu)來決定
另外,主從同步的延遲,是我們排查問題時很容易忽略的一個問題。有時候我們遇到從數(shù)據(jù)庫中獲取不到信息的詭異問題時,會糾結(jié)于代碼中是否有一些邏輯會把之前寫入的內(nèi)容刪除,但是你又會發(fā)現(xiàn),過了一段時間再去查詢時又可以讀到數(shù)據(jù)了,這基本上就是主從延遲在作怪。所以,一般我們會把從庫落后的時間作為一個重點的數(shù)據(jù)庫指標(biāo)做監(jiān)控和報警,正常的時間是在毫秒級別,一旦落后的時間達(dá)到了秒級別就需要告警了。
2. 如何訪問數(shù)據(jù)庫
我們已經(jīng)通過主從復(fù)制技術(shù)將數(shù)據(jù)復(fù)制到多個節(jié)點,并實現(xiàn)了數(shù)據(jù)庫的讀寫分離。此時,數(shù)據(jù)庫的使用方式發(fā)生了變化:過去只需要使用一個數(shù)據(jù)庫地址,現(xiàn)在需要配置主庫地址和多個從庫地址,同時區(qū)分寫入操作和查詢操作。如果再結(jié)合“分庫分表”的技術(shù),復(fù)雜度會進(jìn)一步增加。為了降低實現(xiàn)的復(fù)雜度,業(yè)界涌現(xiàn)了很多數(shù)據(jù)庫中間件來解決數(shù)據(jù)庫的訪問問題,這些中間件大致可以分為兩類。
第一類:內(nèi)嵌式數(shù)據(jù)庫中間件
這一類中間件以淘寶的 TDDL(Taobao Distributed Data Layer)為代表,它以代碼形式內(nèi)嵌在應(yīng)用程序內(nèi)部??梢园阉醋魇且环N數(shù)據(jù)源代理,配置管理多個數(shù)據(jù)源,每個數(shù)據(jù)源對應(yīng)一個數(shù)據(jù)庫,可能是主庫,也可能是從庫。
當(dāng)有數(shù)據(jù)庫請求時,中間件將 SQL 語句發(fā)給某個指定的數(shù)據(jù)源處理,并返回結(jié)果。這類中間件的優(yōu)點是簡單易用,沒有額外的部署成本,因為它直接植入到應(yīng)用程序內(nèi)部,與應(yīng)用程序一起運行,適合運維能力較弱的小團(tuán)隊使用。缺點是缺乏多語言支持,目前主流的方案如 TDDL 和早期的網(wǎng)易 DDB 都是基于 Java 開發(fā)的,無法支持其他語言。此外,版本升級依賴使用方更新,管理起來較為困難。
第二類:獨立部署的代理層中間件
這一類中間件包括早期阿里巴巴開源的 Cobar、基于 Cobar 開發(fā)的 Mycat、360 開源的 Atlas、美團(tuán)開源的 DBProxy 等等。這些中間件部署在獨立的服務(wù)器上,業(yè)務(wù)代碼像使用單一數(shù)據(jù)庫一樣使用它,但它內(nèi)部管理著多個數(shù)據(jù)源。當(dāng)有數(shù)據(jù)庫請求時,代理層會對 SQL 語句進(jìn)行必要的改寫,并將其發(fā)送到指定的數(shù)據(jù)源。這類中間件使用標(biāo)準(zhǔn)的 MySQL 通信協(xié)議,因此能很好地支持多語言。而且,因為它是獨立部署的,升級和維護(hù)也較為方便,適合有一定運維能力的大中型團(tuán)隊使用。它的缺點是所有 SQL 請求都需要跨越兩次網(wǎng)絡(luò):從應(yīng)用到代理層,再從代理層到數(shù)據(jù)源,因此在性能上會有所損耗。
圖片
這些中間件對你來說可能并不陌生,但我想強調(diào)的是,在使用任何中間件時,一定要對它有足夠深入的了解。否則,一旦遇到問題,無法快速解決的話,后果可能會很嚴(yán)重。舉個例子,我之前有一個項目中,團(tuán)隊一直使用自研組件來實現(xiàn)分庫分表,后來發(fā)現(xiàn)這套組件偶爾會產(chǎn)生多余的數(shù)據(jù)庫連接。于是,團(tuán)隊討論后決定將其替換為 Sharding-JDBC。我們原本以為這只是一次簡單的組件切換,結(jié)果上線后遇到了兩個問題:一是因為使用方式不當(dāng),偶爾會出現(xiàn)分庫分表不生效的情況,導(dǎo)致掃描所有庫表;二是偶爾出現(xiàn)查詢延時達(dá)到秒級別。由于當(dāng)時對 Sharding-JDBC 的了解不夠深入,這兩個問題沒能很快解決。最后,我們只得切回原來的組件,待找到問題后再進(jìn)行切換。