億級流量系統架構之如何支撐百億級資料的儲存與計算

weixin_34019929發表於2018-12-27

一、背景引入

首先簡單介紹一下專案背景,公司對合作商家提供一個付費級產品,這個商業產品背後涉及到數百人的研發團隊協作開發,包括各種業務系統來提供很多強大的業務功能,同時在整個平臺中包含了一個至關重要的核心資料產品,這個資料產品的定位是全方位支援使用者的業務經營和快速決策。

這篇文章就聊聊這個資料產品背後對應的一套大型商家資料平臺,看看這個平臺在分散式、高併發、高可用、高效能、海量資料等技術挑戰下的架構演進歷程。

因為整套系統規模過於龐大,涉及研發人員很多,持續時間很長,文章難以表述出其中各種詳細的技術細節以及方案,因此本文主要從整體架構演進的角度來闡述。

至於選擇這個商家資料平臺專案來聊架構演進過程,是因為這個平臺基本跟業務耦合度較低,不像我們負責過的C端類的電商平臺以及其他業務類平臺有那麼重的業務在裡面,文章可以專注闡述技術架構的演進,不需要牽扯太多的業務細節。

此外,這個平臺專案在筆者帶的團隊負責過的眾多專案中,相對算比較簡單的,但是前後又涉及到各種架構的演進過程,因此很適合通過文字的形式來展現出來。

二、商家資料平臺的業務流程

下面幾點,是這個資料產品最核心的業務流程:

每天從使用者使用的大量業務系統中實時的採集過來各種業務資料

接著儲存在自己的資料中心裡

然後實時的運算大量的幾百行~上千行的SQL來生成各種資料包表

最後就可以提供這些資料包表給使用者來分析。

基本上使用者在業務系統使用過程中,只要資料一有變動,立馬就反饋到各種資料包表中,使用者立馬就可以看到資料包表中的各種變化,進而快速的指導自己的決策和管理。

整個過程,大家看看下面的圖就明白了。

13894260-290c3991c80cdf1c

三、從0到1的過程中上線的最low版本

看著上面那張圖好像非常的簡單,是不是?

看整個過程,似乎資料平臺只要想個辦法把業務系統的資料採集過來,接著放在MySQL的各種表裡,直接咔嚓一下執行100多個幾百行的大SQL,然後SQL執行結果再寫到另外一些MySQL的表裡作為報表資料,接著使用者直接點選報表頁面查詢MySQL裡的報表資料,就可以了!

其實任何一個系統從0到1的過程,都是比較low的,剛開始為了快速開發出來這個資料平臺,還真的就是用了這種架構來開發,大家看下面的圖。

13894260-840bc5bda95a17e5

其實在剛開始業務量很小,請求量很小,資料量很小的時候,上面那種架構也沒啥問題,還挺簡單的。

我們直接基於自己研發的資料庫binlog採集中介軟體(這個是另外一套複雜系統了,不在本文討論的範圍裡,以後有機會可以聊聊),感知各個業務系統的資料庫中的資料變更,毫秒級同步到資料平臺自己的MySQL庫裡;

接著資料平臺裡做一些定時排程任務,每隔幾秒鐘就執行上百個複雜大SQL,計算各種報表的資料並將結果儲存到MySQL庫中;

最後使用者只要對報表重新整理一下,立馬就可以從MySQL庫裡查到最新的報表資料。

基本上在無任何技術挑戰的前提下,這套簡易架構執行的會很順暢,效果很好。然而,事情往往不是我們想的那麼簡單的,因為大家都知道國內那些網際網路巨頭公司最大的優勢和資源之一,就是有豐富以及海量的C端使用者以及B端的合作商家。

對C端使用者,任何一個網際網路巨頭推出一個新的C端產品,很可能迅速就是上億使用者量;

對B端商家,任何一個網際網路巨頭如果打B端市場,憑藉巨大的影響力以及合作資源,很可能迅速就可以聚攏數十萬,乃至上百萬的付費B端使用者。

因此,很不幸,接下來的一兩年內,這套系統將要面臨業務的高速增長帶來的巨大技術挑戰和壓力。

四、海量資料儲存和計算的技術挑戰

其實跟很多大型系統遇到的第一個技術挑戰一樣,這套系統遇到的第一個大問題,就是海量資料的儲存。

你一個系統剛開始上線也許就幾十個商家用,接著隨著你們產品的銷售持續大力推廣,可能幾個月內就會聚攏起來十萬級別的使用者。

這些使用者每天都會大量的使用你提供的產品,進而每天都會產生大量的資料,大家可以想象一下,在數十萬規模的商家使用者使用場景下,每天你新增的資料量大概會是幾千萬條資料,記住,這可是每天新增的資料!這將會給上面你看到的那個很low的架構帶來巨大的壓力。

如果你在負責上面那套系統,結果慢慢的發現,每天都要湧入MySQL幾千萬條資料,這種現象是令人感到崩潰的,因為你的MySQL中的單表資料量會迅速膨脹,很快就會達到單表幾億條資料,甚至是數十億條資料,然後你對那些怪獸一樣的大表執行幾百行乃至上千行的SQL?其中包含了N層巢狀查詢以及N個各種多表連線?

我跟你打賭,如果你願意試一下,你會發現你的資料平臺系統直接卡死,因為一個大SQL可能都要幾個小時才能跑完。然後MySQL的cpu負載壓力直接100%,弄不好就把MySQL資料庫伺服器給搞當機了。

所以這就是第一個技術挑戰,資料量越來越大,SQL跑的越來越慢,MySQL伺服器壓力越來越大。

我們當時而言,已經看到了業務的快速增長,因此絕對要先業務一步來重構系統架構,不能讓上述情況發生,第一次架構重構,勢在必行!

五、離線計算與實時計算的拆分

其實在幾年前我們做這個專案的時候,大資料技術已經在國內開始運用的不錯了,而且尤其在一些大型網際網路公司內,我們基本上都運用大資料技術支撐過很多生產環境的專案了,在大資料這塊技術的經驗積累,也是足夠的。

針對這個資料產品的需求,我們完全可以做到,將昨天以及昨天以前的資料都放在大資料儲存中,進行離線儲存和離線計算,然後只有今天的資料是實時的採集的。

因此在這種技術挑戰下,第一次架構重構的核心要義,就是將離線計算與實時計算進行拆分。

13894260-9f8b85a9ed3ca3ed

大家看上面那張圖,新的架構之下,分為了離線與實時兩條計算鏈路。

一條是離線計算鏈路:每天凌晨,我們將業務系統MySQL庫中的昨天以前的資料,作為離線資料匯入Hadoop HDFS中進行離線儲存,然後凌晨就基於Hive / Spark對離線儲存中的資料進行離線計算。

如果有同學不清楚大資料的知識,可以參加我之前寫的一篇文章:《兄弟,用大白話告訴你小白都能聽懂的Hadoop架構原理》。Hadoop與Spark作為世界上最優秀、運用最廣泛的大資料技術,天然適合PB級海量資料的分散式儲存和分散式計算。

在離線計算鏈路全面採用大資料相關技術來支撐過後,完美解決了海量資料的儲存,哪怕你一天進來上億條資料都沒事,分散式儲存可以隨時擴容,同時基於分散式計算技術天然適合海量資料的離線計算。

即使是每天凌晨耗費幾個小時將昨天以前的資料完成計算,這個也沒事,因為凌晨一般是沒人看這個資料的,所以主要在人家早上8點上班以前,完成資料計算就可以了。

另外一條是實時計算鏈路:每天零點過後,當天最新的資料變更,全部還是走之前的老路子,秒級同步業務庫的資料到資料平臺儲存中,接著就是資料平臺系統定時執行大量的SQL進行計算。同時在每天零點的時候,還會從資料平臺的儲存中清理掉昨天的資料,僅僅保留當天一天的資料而已。

實時計算鏈路最大的改變,就是僅僅在資料平臺的本地儲存中保留當天一天的資料而已,這樣就大幅度降低了要放在MySQL中的資料量了。

舉個例子:比如一天就幾千萬條資料放在MySQL裡,那麼單表資料量被維持在了千萬的級別上,此時如果對SQL對應索引以及優化到極致之後,勉強還是可以在幾十秒內完成所有報表的計算。

六、持續增長的資料量和計算壓力

但是如果僅僅只是做到上面的架構,還是隻能暫時性的緩解系統架構的壓力,因為業務還在加速狂飆,繼續增長。

你老是期望單日的資料量在千萬級別,怎麼可能?業務是不會給你這個機會的。很快就可以預見到單日資料量將會達到幾億,甚至十億的級別。

如果一旦單日資料量達到了數十億的級別,單表資料量上億,你再怎麼優化SQL效能,有無法保證100多個幾百行的複雜SQL可以快速的執行完畢了。

到時候又會回到最初的問題,SQL計算過慢會導致資料平臺核心系統卡死,甚至給MySQL伺服器過大壓力,CPU 100%負載後當機。

而且此外還有另外一個問題,那就是單個MySQL資料庫伺服器的儲存容量是有限的,如果一旦單日資料量達到甚至超過了單臺MySQL資料庫伺服器的儲存極限,那麼此時也會導致單臺MySQL資料庫無法容納所有的資料了,這也是一個很大的問題!

第二次架構重構,勢在必行!

七、大資料領域的實時計算技術的缺陷

在幾年前做這個專案的背景下,當時可供選擇的大資料領域的實時計算技術,主要還是Storm,算是比較成熟的一個技術,另外就是Spark生態裡的Spark Streaming。當時可沒有什麼現在較火的Flink、Druid等技術。

在仔細調研了一番過後發現,根本沒有任何一個大資料領域的實時計算技術可以支撐這個需求。

因為Storm是不支援SQL的,而且即使勉強你讓他支援了,他的SQL支援也會很弱,完全不可能執行幾百行甚至上千行的複雜SQL在這種流式計算引擎上的執行。

Spark Streaming也是同理,當時功能還是比較弱小的,雖然可以支援簡單SQL的執行,但是完全無法支援這種複雜SQL的精準運算。

因此很不幸的是,在當時的技術背景下,遇到的這個實時資料運算的痛點,沒有任何開源的技術是可以解決的。必須得自己根據業務的具體場景,從0開始定製開發自己的一套資料平臺系統架構。

八、分庫分表解決資料擴容問題

首先我們要先解決第一個痛點,就是一旦單臺資料庫伺服器無法儲存下當日的資料,該怎麼辦?

第一個首選的方案當然就是分庫分表了。我們需要將一個庫拆分為多庫,不用的庫放在不同的資料庫伺服器上,同時每個庫裡放多張表。

採用這套分庫分表架構之後,可以做到每個資料庫伺服器放一部分的資料,而且隨著資料量日益增長,可以不斷地增加更多的資料庫伺服器來容納更多的資料,做到按需擴容。

同時,每個庫裡單表分為多表,這樣可以保證單表資料量不會太大,控制單表的資料量在幾百萬的量級,基本上效能優化到極致的SQL語句跑起來效率還是不錯的,秒級出結果是可以做到的。

同樣,給大家來一張圖,大家直觀的感受一下:

13894260-f1c3f7a6ff066e11

九、讀寫分離降低資料庫伺服器的負載

此時分庫分表之後,又面臨著另外一個問題,就是現在如果對每個資料庫伺服器又是寫入又是讀取的話,會導致資料庫伺服器的CPU負載和IO負載非常的高!

為什麼這麼說呢?因為在此時寫資料庫的每秒併發已經達到幾千了,同時還頻繁的執行那種超大SQL來查詢資料,資料庫伺服器的CPU運算會極其的繁忙。

因此我們將MySQL做了讀寫分離的部署,每個主資料庫伺服器都掛了多個從資料庫伺服器,寫只能寫入主庫,查可以從從庫來查。

大家一起來看看下面這張圖:

13894260-212828260fa0e5a5

十、自研的滑動視窗動態計算引擎

但是光是做到這一點還是不夠的,因為其實在生產環境發現,哪怕單表資料量限制在了幾百萬的級別,你執行幾百個幾百行復雜SQL,也要幾十秒甚至幾分鐘的時間,這個時效性對付費級的產品已經有點無法接受,產品提出的極致效能要求是,秒級!

因此對上述系統架構,我們再次做了架構的優化,在資料平臺中嵌入了自己純自研的滑動視窗計算引擎,核心思想如下:

在資料庫binlog採集中介軟體採集的過程中,要將資料的變更切割為一個一個的滑動時間視窗,每個滑動時間視窗為幾秒鐘,對每個視窗內的資料打上那個視窗的標籤

同時需要維護一份滑動時間視窗的索引資料,包括每個分片的資料在哪個視窗裡,每個視窗的資料的一些具體的索引資訊和狀態

接著資料平臺中的核心計算引擎,不再是每隔幾十秒就執行大量SQL對當天所有的資料全部計算一遍了,而是對一個接一個的滑動時間視窗,根據視窗標籤提取出那個視窗內的資料進行計算,計算的僅僅是最近一個滑動時間視窗內的資料

接著對這個滑動時間視窗內的資料,可能最多就千條左右吧,執行所有的複雜SQL計算出這個滑動時間視窗內的報表資料,然後將這個視窗資料計算出的結果,與之前計算出來的其他視窗內的計算結果進行合併,最後放入MySQL中的報表內

此外,這裡需要考慮到一系列的生產級機制,包括滑動時間視窗如果計算失敗怎麼辦?如果一個滑動時間視窗計算過慢怎麼辦?滑動視窗計算過程中系統當機瞭如何在重啟之後自動恢復計算?等等

通過這套滑動視窗的計算引擎,我們直接將系統計算效能提升了幾十倍,基本上每個滑動視窗的資料只要幾秒鐘就可以完成全部報表的計算,相當於一下子把最終呈現給使用者的實時資料的時效性提升到了幾秒鐘,而不是幾十秒。

同樣,大家看看下面的圖。

13894260-8f909ddab8541786

十一、離線計算鏈路的效能優化

實時計算鏈路的效能問題通過自研滑動視窗計算引擎來解決了,但是離線計算鏈路此時又出現了效能問題。。。

因為每天凌晨從業務庫中離線匯入的是歷史全量資料,接著需要在凌晨針對百億量級的全量資料,執行很多複雜的上千行復雜SQL來進行運算,當資料量達到百億之後,這個過程耗時很長,有時候要從凌晨一直計算到上午。

關鍵問題就在於,離線計算鏈路,每天都是匯入全量資料來進行計算,這就很坑了。

之所以這麼做,是因為從業務庫同步資料時,每天都涉及到資料的更新操作,而hadoop裡的資料是沒法跟業務庫那樣來進行更新的,因此最開始都是每天匯入全量歷史資料,作為一個最新快照來進行全量計算。

在這裡,我們對離線計算鏈路進行了優化,主要就是全量計算轉增量計算:每天資料在匯入hadoop之後,都會針對資料的業務時間戳來分析和提取出來每天變更過的增量資料,將這些增量資料放入獨立的增量資料表中。

同時需要根據具體的業務需求,自動分析資料計算的基礎血緣關係,有可能增量資料需要與部分全量資料混合才能完成計算,此時可能會提取部分全量歷史資料,合併完成計算。計算完成之後,將計算結果與歷史計算結果進行合併。

在完成這個全量計算轉增量計算的過程之後,離線計算鏈路在凌晨基本上百億級別的資料量,只要對昨天的增量資料花費一兩個小時完成計算之後,就可以完成離線計算的全部任務,效能相較於全量計算提升至少十倍以上。

13894260-1e1276263205ecaa

十二、階段性總結

到此為止,就是這套系統在最初一段時間做出來的一套架構,不算太複雜,還有很多缺陷,不完美,但是在當時的業務背景下效果相當的不錯。

在這套架構對應的早期業務背景下,每天新增資料大概是億級左右,但是分庫分表之後,單表資料量在百萬級別,單臺資料庫伺服器的高峰期寫入壓力在2000/s,查詢壓力在100/s,資料庫叢集承載的總高峰寫入壓力在1萬/s,查詢壓力在500/s,有需要還可以隨時擴容更多的資料庫伺服器,承載更多的資料量,更高的寫入併發與查詢併發。

而且,因為做了讀寫分離,因此每個資料庫伺服器的CPU負載和IO負載都不會在高峰期打滿,避免資料庫伺服器的負載過高。

而基於滑動時間視窗的自研計算引擎,可以保證當天更新的實時資料主要幾秒鐘就可以完成一個微批次的計算,反饋到使用者看到的資料包表中。

同時這套引擎自行管理著計算的狀態與日誌,如果出現某個視窗的計算失敗、系統當機、計算超時,等各種異常的情況,這個套引擎可以自動重試與恢復。

此外,昨天以前的海量資料都是走Hadoop與Spark生態的離線儲存與計算。經過效能優化之後,每天凌晨花費一兩個小時,算好昨天以前所有的資料即可。

最後實時與離線的計算結果在同一個MySQL資料庫中融合,此時使用者如果對業務系統做出操作,實時資料包表在幾秒後就會重新整理,如果要看昨天以前的資料可以隨時選擇時間範圍檢視即可,暫時性是滿足了業務的需求。

早期的幾個月裡,日增上億資料,離線與實時兩條鏈路中的整體資料量級達到了百億級別,無論是儲存擴容,還是高效計算,這套架構基本是撐住了。

十三、下一階段的展望

這個大型系統架構演進實踐是一個系列的文章,將會包含很多篇文章,因為一個大型的系統架構演進的過程,會持續很長時間,做出很多次的架構升級與重構,不斷的解決日益增長的技術挑戰,最終完美的抗住海量資料、高併發、高效能、高可用等場景。

下一篇文章會說說下一步是如何將資料平臺系統重構為一套高可用高容錯的分散式系統架構的,來解決單點故障、單系統CPU負載過高、自動故障轉移、自動資料容錯等相關的問題。包括之後還會有多篇文章涉及到我們自研的更加複雜的支撐高併發、高可用、高效能、海量資料的平臺架構。

十四、上篇文章的答疑

上一篇文章寫了一個分散式鎖的高併發優化的文章,具體參見:《每秒上千訂單場景下的分散式鎖高併發優化實踐》。收到了大家很多的提問,其實最終都是一個問題:

針對那篇文章裡的用分散式鎖的分段加鎖的方式,解決庫存超賣問題,那如果一個分段的庫存不滿足要購買的數量,怎麼辦?

第一,我當時文章裡提了一句,可能沒寫太詳細,如果一個分段庫存不足,要鎖其他的分段,進行合併扣減,如果你做分段加鎖,那就是這樣的,很麻煩。

如果大家去看看Java 8裡的LongAdder的原始碼,他的分段加鎖的優化,也是如此的麻煩,要做段遷移。

第二,我在那篇文章裡反覆強調了一下,不要對號入座,因為實際的電商庫存超賣問題,有很多其他的技術手段,我們就用的是其他的方案,不是這個方案,以後有機會給大家專門講如何解決電商庫存超賣問題。

那篇文章僅僅是用那個例作為一個業務案例而已,闡述一下分散式鎖的併發問題,以及高併發的優化手段,方便大家來理解那個意思,僅此而已。

第三,最後再強調一下,大家關注分段加鎖的思想就好,切記不要對號入座,不要關注過多在庫存超賣業務上了。

相關文章