上節(jié)課邑狸,我們用池化技術(shù)解決了數(shù)據(jù)庫連接復(fù)用的問題,這時涤妒,你的垂直電商系統(tǒng)雖然整體架構(gòu)上沒有變化单雾,但是和數(shù)據(jù)庫交互的過程有了變化,在你的 Web 工程和數(shù)據(jù)庫之間增加了數(shù)據(jù)庫連接池,減少了頻繁創(chuàng)建連接的成本硅堆,從上節(jié)課的測試來看性能上可以提升 80%∮齑ⅲ現(xiàn)在的架構(gòu)圖如下所示:
此時,你的數(shù)據(jù)庫還是單機部署渐逃,依據(jù)一些云廠商的 Benchmark 的結(jié)果够掠,在 4 核 8G 的機器上運行 MySQL 5.7 時,大概可以支撐 500 的 TPS 和 10000 的 QPS茄菊。這時疯潭,運營負責人說正在準備雙十一活動,并且公司層面會繼續(xù)投入資金在全渠道進行推廣面殖,這無疑會引發(fā)查詢量驟然增加的問題竖哩。那么今天,我們就一起來看看當查詢請求增加時脊僚,應(yīng)該如何做主從分離來解決問題相叁。
主從讀寫分離
其實,大部分系統(tǒng)的訪問模型是讀多寫少吃挑,讀寫請求量的差距可能達到幾個數(shù)量級钝荡。
這很好理解,刷朋友圈的請求量肯定比發(fā)朋友圈的量大舶衬,淘寶上一個商品的瀏覽量也肯定遠大于它的下單量。因此赎离,我們優(yōu)先考慮數(shù)據(jù)庫如何抵抗更高的查詢請求逛犹,那么首先你需要把讀寫流量區(qū)分開,因為這樣才方便針對讀流量做單獨的擴展梁剔,這就是我們所說的主從讀寫分離虽画。
它其實是個流量分離的問題,就好比道路交通管制一樣荣病,一個四車道的大馬路劃出三個車道給領(lǐng)導(dǎo)外賓通過码撰,另外一個車道給我們使用,優(yōu)先保證領(lǐng)導(dǎo)先行个盆,就是這個道理脖岛。這個方法本身是一種常規(guī)的做法,即使在一個大的項目中颊亮,它也是一個應(yīng)對數(shù)據(jù)庫突發(fā)讀流量的有效方法柴梆。
我目前的項目中就曾出現(xiàn)過前端流量突增導(dǎo)致從庫負載過高的問題,DBA 兄弟會優(yōu)先做一個從庫擴容上去终惑,這樣對數(shù)據(jù)庫的讀流量就會落入到多個從庫上绍在,從庫的負載就降了下來,然后研發(fā)同學再考慮使用什么樣的方案將流量擋在數(shù)據(jù)庫層之上。
主從讀寫的兩個技術(shù)關(guān)鍵點
一般來說在主從讀寫分離機制中偿渡,我們將一個數(shù)據(jù)庫的數(shù)據(jù)拷貝為一份或者多份臼寄,并且寫入到其它的數(shù)據(jù)庫服務(wù)器中,原始的數(shù)據(jù)庫我們稱為主庫溜宽,主要負責數(shù)據(jù)的寫入吉拳,拷貝的目標數(shù)據(jù)庫稱為從庫,主要負責支持數(shù)據(jù)查詢坑质『衔洌可以看到,主從讀寫分離有兩個技術(shù)上的關(guān)鍵點:1. 一個是數(shù)據(jù)的拷貝涡扼,我們稱為主從復(fù)制稼跳;2. 在主從分離的情況下,我們?nèi)绾纹帘沃鲝姆蛛x帶來的訪問數(shù)據(jù)庫方式的變化吃沪,讓開發(fā)同學像是在使用單一數(shù)據(jù)庫一樣汤善。
-
主從復(fù)制我先以 MySQL 為例介紹一下主從復(fù)制。
MySQL 的主從復(fù)制是依賴于 binlog 的票彪,也就是記錄 MySQL 上的所有變化并以二進制形式保存在磁盤上二進制日志文件红淡。主從復(fù)制就是將 binlog 中的數(shù)據(jù)從主庫傳輸?shù)綇膸焐希话氵@個過程是異步的降铸,即主庫上的操作不會等待 binlog 同步的完成在旱。
主從復(fù)制的過程是這樣的:首先從庫在連接到主節(jié)點時會創(chuàng)建一個 IO 線程,用以請求主庫更新的 binlog推掸,并且把接收到的 binlog 信息寫入一個叫做 relay log 的日志文件中桶蝎,而主庫也會創(chuàng)建一個 log dump 線程來發(fā)送 binlog 給從庫;同時谅畅,從庫還會創(chuàng)建一個 SQL 線程讀取 relay log 中的內(nèi)容登渣,并且在從庫中做回放,最終實現(xiàn)主從的一致性毡泻。這是一種比較常見的主從復(fù)制方式胜茧。
在這個方案中,使用獨立的 log dump 線程是一種異步的方式仇味,可以避免對主庫的主體更新流程產(chǎn)生影響呻顽,而從庫在接收到信息后并不是寫入從庫的存儲中,是寫入一個 relay log邪铲,是避免寫入從庫實際存儲會比較耗時芬位,最終造成從庫和主庫延遲變長。
img你會發(fā)現(xiàn)带到,基于性能的考慮昧碉,主庫的寫入流程并沒有等待主從同步完成就會返回結(jié)果英染,那么在極端的情況下,比如說主庫上 binlog 還沒有來得及刷新到磁盤上就出現(xiàn)了磁盤損壞或者機器掉電被饿,就會導(dǎo)致 binlog 的丟失四康,最終造成主從數(shù)據(jù)的不一致。不過狭握,這種情況出現(xiàn)的概率很低闪金,對于互聯(lián)網(wǎng)的項目來說是可以容忍的。
做了主從復(fù)制之后论颅,我們就可以在寫入時只寫主庫哎垦,在讀數(shù)據(jù)時只讀從庫,這樣即使寫請求會鎖表或者鎖記錄恃疯,也不會影響到讀請求的執(zhí)行漏设。同時呢,在讀流量比較大的情況下今妄,我們可以部署多個從庫共同承擔讀流量郑口,這就是所說的“一主多從”部署方式,在你的垂直電商項目中就可以通過這種方式來抵御較高的并發(fā)讀流量盾鳞。另外犬性,從庫也可以當成一個備庫來使用,以避免主庫故障導(dǎo)致數(shù)據(jù)丟失腾仅。
那么你可能會說乒裆,是不是我無限制地增加從庫的數(shù)量就可以抵抗大量的并發(fā)呢?實際上并不是的推励。因為隨著從庫數(shù)量增加缸兔,從庫連接上來的 IO 線程比較多,主庫也需要創(chuàng)建同樣多的 log dump 線程來處理復(fù)制的請求吹艇,對于主庫資源消耗比較高,同時受限于主庫的網(wǎng)絡(luò)帶寬昂拂,所以在實際使用中受神,一般一個主庫最多掛 3~5 個從庫。
當然格侯,主從復(fù)制也有一些缺陷鼻听,除了帶來了部署上的復(fù)雜度,還有就是會帶來一定的主從同步的延遲联四,這種延遲有時候會對業(yè)務(wù)產(chǎn)生一定的影響撑碴,我舉個例子你就明白了。
在發(fā)微博的過程中會有些同步的操作朝墩,像是更新數(shù)據(jù)庫的操作醉拓,也有一些異步的操作,比如說將微博的信息同步給審核系統(tǒng),所以我們在更新完主庫之后亿卤,會將微博的 ID 寫入消息隊列愤兵,再由隊列處理機依據(jù) ID 在從庫中獲取微博信息再發(fā)送給審核系統(tǒng)。此時如果主從數(shù)據(jù)庫存在延遲排吴,會導(dǎo)致在從庫中獲取不到微博信息秆乳,整個流程會出現(xiàn)異常。
img第一種方案是數(shù)據(jù)的冗余钻哩。你可以在發(fā)送消息隊列時不僅僅發(fā)送微博 ID屹堰,而是發(fā)送隊列處理機需要的所有微博信息,借此避免從數(shù)據(jù)庫中重新查詢數(shù)據(jù)街氢。
第二種方案是使用緩存扯键。我可以在同步寫數(shù)據(jù)庫的同時,也把微博的數(shù)據(jù)寫入到 Memcached 緩存里面阳仔,這樣隊列處理機在獲取微博信息的時候會優(yōu)先查詢緩存忧陪,這樣也可以保證數(shù)據(jù)的一致性。
最后一種方案是查詢主庫近范。我可以在隊列處理機中不查詢從庫而改為查詢主庫嘶摊。不過,這種方式使用起來要慎重评矩,要明確查詢的量級不會很大叶堆,是在主庫的可承受范圍之內(nèi),否則會對主庫造成比較大的壓力斥杜。
另外虱颗,主從同步的延遲,是我們排查問題時很容易忽略的一個問題蔗喂。有時候我們遇到從數(shù)據(jù)庫中獲取不到信息的詭異問題時忘渔,會糾結(jié)于代碼中是否有一些邏輯會把之前寫入的內(nèi)容刪除,但是你又會發(fā)現(xiàn)缰儿,過了一段時間再去查詢時又可以讀到數(shù)據(jù)了畦粮,這基本上就是主從延遲在作怪。所以乖阵,一般我們會把從庫落后的時間作為一個重點的數(shù)據(jù)庫指標做監(jiān)控和報警宣赔,正常的時間是在毫秒級別,一旦落后的時間達到了秒級別就需要告警了瞪浸。
如何訪問數(shù)據(jù)
庫我們已經(jīng)使用主從復(fù)制的技術(shù)將數(shù)據(jù)復(fù)制到了多個節(jié)點儒将,也實現(xiàn)了數(shù)據(jù)庫讀寫的分離,這時对蒲,對于數(shù)據(jù)庫的使用方式發(fā)生了變化钩蚊。以前只需要使用一個數(shù)據(jù)庫地址就好了贡翘,現(xiàn)在需要使用一個主庫地址和多個從庫地址,并且需要區(qū)分寫入操作和查詢操作两疚,如果結(jié)合下一節(jié)課中要講解的內(nèi)容“分庫分表”床估,復(fù)雜度會提升更多。為了降低實現(xiàn)的復(fù)雜度诱渤,業(yè)界涌現(xiàn)了很多數(shù)據(jù)庫中間件來解決數(shù)據(jù)庫的訪問問題丐巫,這些中間件可以分為兩類。
第一類以淘寶的 TDDL( Taobao Distributed Data Layer)為代表勺美,以代碼形式內(nèi)嵌運行在應(yīng)用程序內(nèi)部递胧。你可以把它看成是一種數(shù)據(jù)源的代理,它的配置管理著多個數(shù)據(jù)源赡茸,每個數(shù)據(jù)源對應(yīng)一個數(shù)據(jù)庫缎脾,可能是主庫,可能是從庫占卧。當有一個數(shù)據(jù)庫請求時遗菠,中間件將 SQL 語句發(fā)給某一個指定的數(shù)據(jù)源來處理,然后將處理結(jié)果返回华蜒。
這一類中間件的優(yōu)點是簡單易用辙纬,沒有多余的部署成本,因為它是植入到應(yīng)用程序內(nèi)部叭喜,與應(yīng)用程序一同運行的贺拣,所以比較適合運維能力較弱的小團隊使用;缺點是缺乏多語言的支持捂蕴,目前業(yè)界這一類的主流方案除了 TDDL譬涡,還有早期的網(wǎng)易 DDB,它們都是 Java 語言開發(fā)的啥辨,無法支持其他的語言涡匀。另外,版本升級也依賴使用方更新溉知,比較困難渊跋。
另一類是單獨部署的代理層方案,這一類方案代表比較多着倾,如早期阿里巴巴開源的 Cobar,基于 Cobar 開發(fā)出來的 Mycat燕少,360 開源的 Atlas卡者,美團開源的基于 Atlas 開發(fā)的 DBProxy 等等。
這一類中間件部署在獨立的服務(wù)器上客们,業(yè)務(wù)代碼如同在使用單一數(shù)據(jù)庫一樣使用它崇决,實際上它內(nèi)部管理著很多的數(shù)據(jù)源材诽,當有數(shù)據(jù)庫請求時,它會對 SQL 語句做必要的改寫恒傻,然后發(fā)往指定的數(shù)據(jù)源脸侥。
它一般使用標準的 MySQL 通信協(xié)議,所以可以很好地支持多語言盈厘。由于它是獨立部署的睁枕,所以也比較方便進行維護升級,比較適合有一定運維能力的大中型團隊使用沸手。它的缺陷是所有的 SQL 語句都需要跨兩次網(wǎng)絡(luò):從應(yīng)用到代理層和從代理層到數(shù)據(jù)源外遇,所以在性能上會有一些損耗。
這些中間件契吉,對你而言跳仿,可能并不陌生,但是我想讓你注意到是捐晶,在使用任何中間件的時候一定要保證對于中間件有足夠深入的了解菲语,否則一旦出了問題沒法快速地解決就悲劇了。
我之前的一個項目中惑灵,一直使用自研的一個組件來實現(xiàn)分庫分表山上,后來發(fā)現(xiàn)這套組件有一定幾率會產(chǎn)生對數(shù)據(jù)庫多余的連接,于是團隊討論后決定替換成 Sharding-JDBC泣棋。原本以為是一次簡單的組件切換胶哲,結(jié)果上線后發(fā)現(xiàn)兩個問題:一是因為使用姿勢不對,會偶發(fā)地出現(xiàn)分庫分表不生效導(dǎo)致掃描所有庫表的情況潭辈,二是偶發(fā)地出現(xiàn)查詢延時達到秒級別鸯屿。由于對 Sharding-JDBC 沒有足夠了解,這兩個問題我們都沒有很快解決把敢,后來不得已只能切回原來的組件寄摆,在找到問題之后再進行切換。
其實修赞,我們可以把主從復(fù)制引申為存儲節(jié)點之間互相復(fù)制存儲數(shù)據(jù)的技術(shù)婶恼,它可以實現(xiàn)數(shù)據(jù)的冗余,以達到備份和提升橫向擴展能力的作用柏副。在使用主從復(fù)制這個技術(shù)點時勾邦,你一般會考慮兩個問題:
主從的一致性和寫入性能的權(quán)衡,如果你要保證所有從節(jié)點都寫入成功割择,那么寫入性能一定會受影響眷篇;如果你只寫入主節(jié)點就返回成功,那么從節(jié)點就有可能出現(xiàn)數(shù)據(jù)同步失敗的情況荔泳,從而造成主從不一致蕉饼,而在互聯(lián)網(wǎng)的項目中虐杯,我們一般會優(yōu)先考慮性能而不是數(shù)據(jù)的強一致性。
我們采用的很多組件都會使用到這個技術(shù)昧港,比如擎椰,Redis 也是通過主從復(fù)制實現(xiàn)讀寫分離;Elasticsearch 中存儲的索引分片也可以被復(fù)制到多個節(jié)點中创肥;寫入到 HDFS 中文件也會被復(fù)制到多個 DataNode 中达舒。只是不同的組件對于復(fù)制的一致性、延遲要求不同瓤的,采用的方案也不同休弃。但是這種設(shè)計的思想是通用的,是你需要了解的,這樣你在學習其他存儲組件的時候就能夠觸類旁通了。