億級(jí)流量系統(tǒng)架構(gòu)之如何設(shè)計(jì)承載百億流量的高性能架構(gòu)
我們面對(duì)的是日益增多和復(fù)雜的各種業(yè)務(wù)系統(tǒng),我們面對(duì)的是不斷增加的系統(tǒng)用戶,我們面對(duì)的是即將迎來(lái)每天百億級(jí)的高并發(fā)流量。
給大家先說(shuō)下當(dāng)時(shí)的系統(tǒng)部署情況,數(shù)據(jù)庫(kù)那塊一共部署了8主8從,也就是16臺(tái)數(shù)據(jù)庫(kù)服務(wù)器,每個(gè)庫(kù)都是部署在獨(dú)立的數(shù)據(jù)庫(kù)服務(wù)器上的,而且全部用的是物理機(jī),機(jī)器的配置,如果沒(méi)記錯(cuò)的話,應(yīng)該是32核+128G+SSD固態(tài)硬盤。
為啥要搞這么多物理機(jī),而且全部都是高配置呢?不知道大家發(fā)現(xiàn)沒(méi)有,目前為止,我們最大的依賴就是MySQL!
之前給大家解釋過(guò),在當(dāng)時(shí)的背景下,我們要對(duì)涌入的億級(jí)海量數(shù)據(jù),實(shí)時(shí)的運(yùn)行數(shù)百個(gè)復(fù)雜度為幾百行到上千行的大SQL,幾秒鐘就要出分析結(jié)果。
這個(gè)是沒(méi)有任何一個(gè)開(kāi)源系統(tǒng)可以做到的,Storm不行,Spark Streaming也不行,因此必須得基于MySQL純自研一套數(shù)據(jù)平臺(tái)架構(gòu)出來(lái),支撐這個(gè)需求場(chǎng)景。
所以,只有MySQL是可以支撐如此復(fù)雜的SQL語(yǔ)句完美運(yùn)行的,因此我們?cè)谠缙诒仨殗?yán)重依賴于MySQL作為數(shù)據(jù)的存儲(chǔ)和計(jì)算,將源源不斷涌入的數(shù)據(jù)放在MySQL中進(jìn)行存儲(chǔ),接著基于數(shù)據(jù)分片計(jì)算的架構(gòu)來(lái)高性能的運(yùn)行復(fù)雜大SQL基于MySQL來(lái)進(jìn)行計(jì)算。
所以大家就知道了,MySQL目前為止是這套系統(tǒng)的命脈。在當(dāng)時(shí)的場(chǎng)景下,每臺(tái)數(shù)據(jù)庫(kù)服務(wù)器都要抗住每秒2000左右的并發(fā)請(qǐng)求,高峰期的CPU負(fù)載、IO負(fù)載其實(shí)都非常高,而且主庫(kù)和從庫(kù)的延遲在高峰期已經(jīng)有點(diǎn)嚴(yán)重,會(huì)達(dá)到秒級(jí)了。
在我們的生產(chǎn)系統(tǒng)的實(shí)際線上運(yùn)行情況下,單臺(tái)MySQL數(shù)據(jù)庫(kù)服務(wù)器,我們一般是不會(huì)讓他的高峰期并發(fā)請(qǐng)求超過(guò)2000/s的,因?yàn)橐坏┻_(dá)到每秒幾千的請(qǐng)求,根據(jù)當(dāng)時(shí)線上的資源負(fù)載情況來(lái)看,很可能MySQL服務(wù)器負(fù)載過(guò)高會(huì)宕機(jī)。
所以此時(shí)就有一個(gè)很尷尬的問(wèn)題了,假如說(shuō)每天億級(jí)流量的場(chǎng)景下,需要用8主8從這么多高配置的數(shù)據(jù)庫(kù)服務(wù)器來(lái)抗,那如果是幾十億流量呢?甚至如果是百億流量呢?難道不停的增加更多的高配置機(jī)器嗎?
要知道,這種高配置的數(shù)據(jù)庫(kù)服務(wù)器,如果是物理機(jī)的話,是非常昂貴的!
之前給大家簡(jiǎn)單介紹過(guò)項(xiàng)目背景,這整套大型系統(tǒng)組成的商業(yè)級(jí)平臺(tái),涉及到N多個(gè)系統(tǒng),這個(gè)數(shù)據(jù)產(chǎn)品只是一個(gè)子產(chǎn)品而已,不可能為了這么一個(gè)產(chǎn)品,投入大量的預(yù)算通過(guò)不停的砸高配置的機(jī)器來(lái)?yè)巫「叩牟l(fā)寫(xiě)入。
我們必須用技術(shù)的手段來(lái)重構(gòu)系統(tǒng)架構(gòu),盡量用有限的機(jī)器資源,通過(guò)最優(yōu)秀的架構(gòu)來(lái)抗住超高的并發(fā)寫(xiě)入壓力!
計(jì)算與存儲(chǔ)分離的架構(gòu)
這個(gè)架構(gòu)里的致命問(wèn)題之一,就是數(shù)據(jù)的存儲(chǔ)和計(jì)算混在了一個(gè)地方,都在同一個(gè)MySQL庫(kù)里!
大家想想,在一個(gè)單表里放上千萬(wàn)數(shù)據(jù),然后你每次運(yùn)行一個(gè)復(fù)雜SQL的時(shí)候,SQL里都是通過(guò)索引定位到表中他要計(jì)算的那個(gè)數(shù)據(jù)分片。這樣搞合適嗎?
答案顯然是否定的!因?yàn)楸砝锏臄?shù)據(jù)量很大,但是你每次實(shí)際SQL運(yùn)算只要對(duì)其中很小很小的一部分?jǐn)?shù)據(jù)計(jì)算就可以了,實(shí)際上我們?cè)谏a(chǎn)環(huán)境中實(shí)踐過(guò)后發(fā)現(xiàn),如果你在一個(gè)大表運(yùn)行一個(gè)復(fù)雜SQL,哪怕通過(guò)各種索引保證定位到的數(shù)據(jù)量很少,因?yàn)楸頂?shù)據(jù)量過(guò)大,也是會(huì)導(dǎo)致性能直線下降的。
因此第一件事情,先將數(shù)據(jù)的存儲(chǔ)和計(jì)算這兩件事情拆開(kāi)。
我們當(dāng)時(shí)的思路如下:
數(shù)據(jù)直接寫(xiě)入一個(gè)存儲(chǔ),僅僅只是簡(jiǎn)單的寫(xiě)入即可
然后在計(jì)算的時(shí)候從數(shù)據(jù)存儲(chǔ)中提取你需要的那個(gè)數(shù)據(jù)分片里的可能就一兩千條數(shù)據(jù),寫(xiě)入另外一個(gè)專用于計(jì)算的臨時(shí)表中,那個(gè)臨時(shí)表內(nèi)就這一兩千條數(shù)據(jù)
然后運(yùn)行你的各種復(fù)雜SQL即可。
bingo!一旦將數(shù)據(jù)存儲(chǔ)和計(jì)算兩個(gè)事情拆開(kāi),架構(gòu)里可以發(fā)揮的空間就大多了。
首先你的數(shù)據(jù)存儲(chǔ)只要支撐高并發(fā)的寫(xiě)入,日百億流量的話,高峰每秒并發(fā)會(huì)達(dá)到幾十萬(wàn),撐住這就可以了。然后支持計(jì)算引擎通過(guò)簡(jiǎn)單的操作從數(shù)據(jù)存儲(chǔ)里提取少量數(shù)據(jù)就OK。
太好了,這個(gè)數(shù)據(jù)存儲(chǔ)就可以PASS掉MySQL了,就這點(diǎn)兒需求,你還用MySQL干什么?兄弟!
當(dāng)時(shí)我們經(jīng)過(guò)充分的技術(shù)調(diào)研和選型之后,選擇了公司自研的分布式KV存儲(chǔ)系統(tǒng),這套KV存儲(chǔ)系統(tǒng)是完全分布式的,高可用,高性能,輕量級(jí),支持海量數(shù)據(jù),而且之前經(jīng)歷過(guò)公司線上流量的百億級(jí)請(qǐng)求量的考驗(yàn),絕對(duì)沒(méi)問(wèn)題。主要支持高并發(fā)的寫(xiě)入數(shù)據(jù)以及簡(jiǎn)單的查詢操作,完全符合我們的需求。
這里給大家提一句,其實(shí)業(yè)內(nèi)很多類似場(chǎng)景會(huì)選擇hbase,所以大家如果沒(méi)有公司自研的優(yōu)秀kv存儲(chǔ)的話,可以用選用hbase也是沒(méi)問(wèn)題的,只不過(guò)hbase有可能生產(chǎn)環(huán)境會(huì)有點(diǎn)坑,需要大家對(duì)hbase非常精通,合理避坑和優(yōu)化。
輕量級(jí)的分布式kv系統(tǒng),一般設(shè)計(jì)理念都是支持一些簡(jiǎn)單的kv操作,大量的依托于內(nèi)存緩存熱數(shù)據(jù)來(lái)支持高并發(fā)的寫(xiě)入和讀取,因?yàn)椴恍枰С諱ySQL里的那些事務(wù)啊、復(fù)雜SQL啊之類的重量級(jí)的機(jī)制。
因此在同等的機(jī)器資源條件下,kv存儲(chǔ)對(duì)高并發(fā)的支撐能力至少是MySQL的數(shù)倍甚至數(shù)十倍。
就好比說(shuō),大家應(yīng)該都用過(guò)Redis,Redis普通配置的單機(jī)器撐個(gè)每秒幾萬(wàn)并發(fā)都是ok的,其實(shí)就是這個(gè)道理,他非常的輕量級(jí),轉(zhuǎn)為高并發(fā)而生。
然后,我們還是可以基于MySQL中的一些臨時(shí)表來(lái)存放kv存儲(chǔ)中提取出來(lái)的數(shù)據(jù)分片,利用MySQL對(duì)復(fù)雜SQL語(yǔ)法的支持來(lái)進(jìn)行計(jì)算就可以了。也就是說(shuō),我們?cè)谶@個(gè)架構(gòu)里,把kv系統(tǒng)作為存儲(chǔ),把MySQL用做少量數(shù)據(jù)的計(jì)算。
此時(shí)我們?cè)谙到y(tǒng)架構(gòu)中引入了分布式kv系統(tǒng)來(lái)作為我們的數(shù)據(jù)存儲(chǔ),每天的海量數(shù)據(jù)都存放在這里就可以了,然后我們的Slave計(jì)算引擎每次計(jì)算,都是根據(jù)那個(gè)數(shù)據(jù)分片從kv存儲(chǔ)中提取對(duì)應(yīng)的數(shù)據(jù)出來(lái)放入MySQL內(nèi)的一個(gè)臨時(shí)表,接著就是對(duì)那個(gè)臨時(shí)表內(nèi)的一兩千條數(shù)據(jù)分片運(yùn)行各種復(fù)雜SQL進(jìn)行計(jì)算即可。

大家看上面的圖,此時(shí)通過(guò)這一步計(jì)算與存儲(chǔ)架構(gòu)的分離,我們選用了適合支撐高并發(fā)的kv集群來(lái)抗住每天百億級(jí)的流量寫(xiě)入。然后基于MySQL作為臨時(shí)表放入少量數(shù)據(jù)來(lái)進(jìn)行運(yùn)算。這一個(gè)步驟就直接把高并發(fā)請(qǐng)求可以妥妥的抗住了。
而且分布式kv存儲(chǔ)本來(lái)就可以按需擴(kuò)容,如果并發(fā)越來(lái)越高,只要擴(kuò)容增加機(jī)器就可以了。此時(shí),就完成了架構(gòu)的一個(gè)關(guān)鍵的重構(gòu)步驟。
自研純內(nèi)存SQL計(jì)算引擎
下一步,我們就要對(duì)架構(gòu)追求極致!因?yàn)榇藭r(shí)我們面臨的一個(gè)痛點(diǎn)就在于說(shuō),其實(shí)僅僅只是將MySQL作為一個(gè)臨時(shí)表來(lái)計(jì)算了,主要就是用他的復(fù)雜SQL語(yǔ)法的支持。
但是問(wèn)題是,對(duì)MySQL的并發(fā)量雖然大幅度降低了,可是還并不算太低。因?yàn)榇罅康臄?shù)據(jù)分片要計(jì)算,還是需要頻繁的讀寫(xiě)MySQL。
此外,每次從kv存儲(chǔ)里提取出來(lái)了數(shù)據(jù),還得放到MySQL的臨時(shí)表里,還得發(fā)送SQL去MySQL里運(yùn)算,這還是多了幾個(gè)步驟的時(shí)間開(kāi)銷。
因?yàn)楫?dāng)時(shí)面臨的另外一個(gè)問(wèn)題是,每天請(qǐng)求量大,意味著數(shù)據(jù)量大,數(shù)據(jù)量大意味著時(shí)間分片的計(jì)算任務(wù)負(fù)載還是較重。
總是這么依賴MySQL,還要額外維護(hù)一大堆的各種臨時(shí)表,可能多達(dá)幾百個(gè)臨時(shí)表,你要維護(hù),要注意他的表結(jié)構(gòu)的修改,還有分庫(kù)分表的一些運(yùn)維操作,這一切都讓依賴MySQL這個(gè)事兒顯得那么的多余和麻煩。
因此,我們做出決定,為了讓架構(gòu)的維護(hù)性更高,而且將性能優(yōu)化到極致,我們要自己研發(fā)純內(nèi)存的SQL計(jì)算引擎。
其實(shí)如果你要自研一個(gè)可以支持MySQL那么復(fù)雜SQL語(yǔ)法的內(nèi)存SQL計(jì)算引擎,還是有點(diǎn)難度和麻煩的。但是在我們仔細(xì)研究了業(yè)務(wù)需要的那幾百個(gè)SQL之后,發(fā)現(xiàn)其實(shí)問(wèn)題沒(méi)那么的復(fù)雜。
因?yàn)槠鋵?shí)一般的數(shù)據(jù)分析類的SQL,主要就是一些常見(jiàn)的功能,沒(méi)有那么多的怪、難、偏的SQL語(yǔ)法。
因此我們將線上的SQL都分析過(guò)一遍之后,就針對(duì)性的研發(fā)出了僅僅支持特定少數(shù)語(yǔ)法的SQL引擎,包括了嵌套查詢組件、多表關(guān)聯(lián)組件、分組聚合組件、多字段排序組件、少數(shù)幾個(gè)常用函數(shù),等等。
接著就將系統(tǒng)徹底重構(gòu)為不再依賴MySQL,每次從kv存儲(chǔ)中提取一個(gè)數(shù)據(jù)分片之后,直接放入內(nèi)存中,然后用我們自研的SQL計(jì)算引擎來(lái)在純內(nèi)存里針對(duì)一個(gè)數(shù)據(jù)分片執(zhí)行各種復(fù)雜的SQL。
這個(gè)純內(nèi)存操作的性能,那就不用多說(shuō)了,大家應(yīng)該都能想象到了,基本上純內(nèi)存的SQL執(zhí)行,都是毫秒級(jí)的,基本上一個(gè)時(shí)間分片的運(yùn)算全部降低到毫秒級(jí)了。性能進(jìn)一步得到了大幅度的提升,而且從此不再依賴MySQL了,不需要維護(hù)復(fù)雜的分庫(kù)分表等等東西。

這套架構(gòu)上線之后,徹底消除了對(duì)MySQL的依賴,理論上,無(wú)論多大的流量過(guò)來(lái),都可以通過(guò)立馬擴(kuò)容kv集群以及擴(kuò)容Slave計(jì)算集群來(lái)解決,不需要依賴MySQL的分庫(kù)分表、幾百?gòu)埮R時(shí)表等比較耗費(fèi)人力、麻煩而且坑爹的方案了。而且這種純內(nèi)存的計(jì)算架構(gòu)直接把計(jì)算性能提升到了毫秒級(jí)。
而且消除對(duì)MySQL的依賴有另外一個(gè)好處,數(shù)據(jù)庫(kù)的機(jī)器總是要高配置的,但是Slave機(jī)器主要4核8G的普通虛擬機(jī)就夠了,分布式系統(tǒng)的本質(zhì)就是盡量利用大量的廉價(jià)普通機(jī)器就可以完成高效的存儲(chǔ)和計(jì)算。
因此在百億流量的負(fù)載之下,我們Slave機(jī)器部署了幾十臺(tái)機(jī)器就足夠了,那總比你部署幾十臺(tái)昂貴的高配置MySQL物理機(jī)來(lái)的劃算多了!
MQ削峰以及流量控制
其實(shí)如果對(duì)高并發(fā)架構(gòu)稍微了解點(diǎn)的同學(xué)都會(huì)發(fā)現(xiàn),這個(gè)系統(tǒng)的架構(gòu)中,針對(duì)高并發(fā)的寫(xiě)入這塊,還有一個(gè)比較關(guān)鍵的組件要加入,就是MQ。
因?yàn)槲覀內(nèi)绻麘?yīng)對(duì)的是高并發(fā)的非實(shí)時(shí)響應(yīng)的寫(xiě)入請(qǐng)求的話,完全可以使用MQ中間件先抗住海量的請(qǐng)求,接著做一個(gè)中間的流量分發(fā)系統(tǒng),將流量異步轉(zhuǎn)發(fā)到kv存儲(chǔ)中去,同時(shí)這個(gè)流量分發(fā)系統(tǒng)可以對(duì)高并發(fā)流量進(jìn)行控制。
比如說(shuō)如果瞬時(shí)高并發(fā)的寫(xiě)入真的導(dǎo)致后臺(tái)系統(tǒng)壓力過(guò)大,那么就可以由流量分發(fā)系統(tǒng)自動(dòng)根據(jù)我們?cè)O(shè)定的閾值進(jìn)行流量控制,避免高并發(fā)的壓力打垮后臺(tái)系統(tǒng)。
而且在這個(gè)流控系統(tǒng)中,我們其實(shí)還做了很多的細(xì)節(jié)性的優(yōu)化,比如說(shuō)數(shù)據(jù)校驗(yàn)、過(guò)濾無(wú)效數(shù)據(jù)、切分?jǐn)?shù)據(jù)分片、數(shù)據(jù)同步的冪等機(jī)制、100%保證數(shù)據(jù)落地到kv集群的機(jī)制保障,等等。

公司的MQ集群天然都支撐過(guò)大流量寫(xiě)入以及高并發(fā)請(qǐng)求,因此MQ集群那個(gè)層面抗住高并發(fā)并不是什么問(wèn)題,再高的并發(fā)按需擴(kuò)容就可以了,然后我們自己的流控系統(tǒng)也是集群部署的,線上采用的是4核8G的虛擬機(jī),因?yàn)檫@個(gè)機(jī)器不需要太高的配置。
流控系統(tǒng),基本線上我們一般保持在每臺(tái)機(jī)器承載每秒小三千左右的并發(fā)請(qǐng)求,百億流量場(chǎng)景下,高峰每秒并發(fā)在每秒小幾十萬(wàn)的級(jí)別,因此這個(gè)流控集群部署到幾十臺(tái)機(jī)器就足夠了。
而公司的kv集群也是天然支撐過(guò)大流量高并發(fā)寫(xiě)入的,因此kv集群按需擴(kuò)容,抗住高并發(fā)帶流量的寫(xiě)入也不是什么問(wèn)題,而且這里其實(shí)我們因?yàn)樵谧陨砑軜?gòu)層面做了大量的優(yōu)化(存儲(chǔ)與計(jì)算分離的關(guān)鍵點(diǎn)),因此kv集群的定位基本就是online storage,一個(gè)在線存儲(chǔ)罷了。
通過(guò)合理、巧妙的設(shè)計(jì)key以及value的數(shù)據(jù)類型,使得我們對(duì)kv集群的讀寫(xiě)請(qǐng)求都是優(yōu)化成最最簡(jiǎn)單的key-value的讀寫(xiě)操作,天然保證高并發(fā)讀寫(xiě)是沒(méi)問(wèn)題的。
另外稍微給大家一點(diǎn)點(diǎn)的劇透,后面講到全鏈路99.99%高可用架構(gòu)的時(shí)候,這個(gè)流控集群會(huì)發(fā)揮巨大的作用,他是承上啟下的一個(gè)效果,前置的MQ集群故障的高可用保障,以及后置的KV集群故障的高可用保障,都是依靠流控集群來(lái)實(shí)現(xiàn)的。
數(shù)據(jù)的動(dòng)靜分離架構(gòu)
在完成上述重構(gòu)之后,我們又對(duì)核心的自研內(nèi)存SQL計(jì)算引擎做了進(jìn)一步的優(yōu)化。因?yàn)閷?shí)際生產(chǎn)環(huán)境運(yùn)行過(guò)程中,我們發(fā)現(xiàn)了一個(gè)問(wèn)題:就是每次如果Slave節(jié)點(diǎn)都是對(duì)一個(gè)數(shù)據(jù)分片提取相關(guān)聯(lián)的各種數(shù)據(jù)出來(lái)然后進(jìn)行計(jì)算,其實(shí)是沒(méi)必要的!
給大家舉個(gè)例子,如果你的SQL要對(duì)一些表進(jìn)行關(guān)聯(lián)計(jì)算,里面涉及到了一些大部分時(shí)候靜態(tài)不變的數(shù)據(jù),那些表的數(shù)據(jù)一般很少改變,因此沒(méi)必要每次都走網(wǎng)絡(luò)請(qǐng)求從kv存儲(chǔ)里提取那部分?jǐn)?shù)據(jù)。
我們其實(shí)完全可以在Slave節(jié)點(diǎn)對(duì)這種靜態(tài)數(shù)據(jù)做個(gè)輕量級(jí)的cache,然后只有數(shù)據(jù)分片里對(duì)應(yīng)的動(dòng)態(tài)改變的數(shù)據(jù)才從kv存儲(chǔ)來(lái)提取數(shù)據(jù)。
通過(guò)這個(gè)數(shù)據(jù)的動(dòng)靜分離架構(gòu),我們基本上把Slave節(jié)點(diǎn)對(duì)kv集群的網(wǎng)絡(luò)請(qǐng)求降低到了最少,性能提升到了最高。大家看下面的圖。

階段性總結(jié)
這套架構(gòu)到此為止,基本上就演進(jìn)的比較不錯(cuò)了,因?yàn)槌卟l(fā)寫(xiě)入、極速高性能計(jì)算、按需任意擴(kuò)容,等各種特性都可以支持到了,基本上從寫(xiě)入到計(jì)算,這兩個(gè)步驟,是沒(méi)什么太大的瓶頸了。
而且通過(guò)自研內(nèi)存SQL計(jì)算引擎的方案,將我們的實(shí)時(shí)計(jì)算性能提升到了毫秒級(jí)的標(biāo)準(zhǔn),基本已經(jīng)達(dá)到極致。
下一步展望
下一步,我們就要看看這個(gè)架構(gòu)中的左側(cè),還有一個(gè)MySQL呢!
首先是實(shí)時(shí)計(jì)算鏈路和離線計(jì)算鏈路,都會(huì)導(dǎo)入大量的計(jì)算結(jié)果到那個(gè)MySQL中。
其次面向數(shù)十萬(wàn)甚至上百萬(wàn)的B端商家時(shí),如果是實(shí)時(shí)展示數(shù)據(jù)分析結(jié)果的話,一般頁(yè)面上會(huì)有定時(shí)的JS腳本,每隔幾秒鐘就會(huì)發(fā)送請(qǐng)求過(guò)來(lái)加載最新的數(shù)據(jù)計(jì)算結(jié)果。
因此實(shí)際上那個(gè)專門面向終端用戶的MySQL也會(huì)承受極大的數(shù)據(jù)量的壓力,高并發(fā)寫(xiě)入的壓力以及高并發(fā)查詢的壓力。