Java架構-億級流量系統架構之如何支撐百億級數據的存儲與計算

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。
https://blog.csdn.net/Coco_Wditm/article/details/84638472

本文聊一下筆者幾年前所帶的團隊負責的多個項目中的其中一個,用這個項目來聊聊一個億級流量系統架構演進的過程。

一、背景引入

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

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

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

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

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

二、商家數據平臺的業務流程

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

每天從用戶使用的大量業務系統中實時的採集過來各種業務數據

接着存儲在自己的數據中心裏

然後實時的運算大量的幾百行~上千行的SQL來生成各種數據報表

最後就可以提供這些數據報表給用戶來分析。

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

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

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

看着上面那張圖好像非常的簡單,是不是?
1
看整個過程,似乎數據平臺只要想個辦法把業務系統的數據採集過來,接着放在MySQL的各種表裏,直接咔嚓一下運行100多個幾百行的大SQL,然後SQL運行結果再寫到另外一些MySQL的表裏作爲報表數據,接着用戶直接點擊報表頁面查詢MySQL裏的報表數據,就可以了!

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

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

我們直接基於自己研發的數據庫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服務器壓力越來越大。

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

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

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

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

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

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

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

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語句跑起來效率還是不錯的,秒級出結果是可以做到的。

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

九、讀寫分離降低數據庫服務器的負載

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

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

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

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


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

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

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

在數據庫binlog採集中間件採集的過程中,要將數據的變更切割爲一個一個的滑動時間窗口,每個滑動時間窗口爲幾秒鐘,對每個窗口內的數據打上那個窗口的標籤

同時需要維護一份滑動時間窗口的索引數據,包括每個分片的數據在哪個窗口裏,每個窗口的數據的一些具體的索引信息和狀態

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

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

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

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

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


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

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

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

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

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

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

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

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

十二、階段性總結

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

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

而且,因爲做了讀寫分離,因此每個數據庫服務器的CPU負載和IO負載都不會在高峯期打滿,避免數據庫服務器的負載過高。

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

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

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

最後實時與離線的計算結果在同一個MySQL數據庫中融合,此時用戶如果對業務系統做出操作,實時數據報表在幾秒後就會刷新,如果要看昨天以前的數據可以隨時選擇時間範圍查看即可,暫時性是滿足了業務的需求。

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

十三、下一階段的展望

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

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

十四、上篇文章的答疑

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

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

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

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

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

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

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

希望此文能幫到大家的同時,也聽聽大家的觀點。歡迎留言討論,加關注,分享你的高見!持續更新!

我本人邀約各大BATJ架構大牛共創Java高級架構交流社區羣,(羣號:673043639)致力於免費提供Java架構行業交流平臺,通過這個平臺讓大家相互學習成長,提高技術,讓自己的水平進階一個檔次,成功通往Java架構技術大牛或架構師發展。

爲什麼某些人會一直比你優秀,是因爲他本身就很優秀還一直在持續努力變得更優秀,而你是不是還在滿足於現狀內心在竊喜!

合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章