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