天天看點

别光看NB的Github開源項目,你得參考他們去設計自己的架構

作者:石杉的架構筆記

一、背景引入

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

這篇文章就聊聊這個資料産品背後對應的一套大型商家資料平台,看看這個平台在分布式、高并發、高可用、高性能、海量資料等技術挑戰下的架構演進曆程。

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

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

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

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

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

  • 每天從使用者使用的大量業務系統中實時的采集過來各種業務資料
  • 接着存儲在自己的資料中心裡
  • 然後實時的運算大量的幾百行~上千行的SQL來生成各種資料報表
  • 最後就可以提供這些資料報表給使用者來分析。

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

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

别光看NB的Github開源項目,你得參考他們去設計自己的架構

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

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

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

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

别光看NB的Github開源項目,你得參考他們去設計自己的架構

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

我們直接基于自己研發的資料庫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伺服器壓力越來越大。

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

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

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

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

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

别光看NB的Github開源項目,你得參考他們去設計自己的架構

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

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

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

即使是每天淩晨耗費幾個小時将昨天以前的資料完成計算,這個也沒事,因為淩晨一般是沒人看這個資料的,是以主要在人家早上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語句跑起來效率還是不錯的,秒級出結果是可以做到的。

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

别光看NB的Github開源項目,你得參考他們去設計自己的架構

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

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

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

是以我們将MySQL做了讀寫分離的部署,每個主資料庫伺服器都挂了多個從資料庫伺服器,寫隻能寫入主庫,查可以從從庫來查。

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

别光看NB的Github開源項目,你得參考他們去設計自己的架構

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

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

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

  • 在資料庫binlog采集中間件采集的過程中,要将資料的變更切割為一個一個的滑動時間視窗,每個滑動時間視窗為幾秒鐘,對每個視窗内的資料打上那個視窗的标簽
  • 同時需要維護一份滑動時間視窗的索引資料,包括每個分片的資料在哪個視窗裡,每個視窗的資料的一些具體的索引資訊和狀态
  • 接着資料平台中的核心計算引擎,不再是每隔幾十秒就運作大量SQL對當天所有的資料全部計算一遍了,而是對一個接一個的滑動時間視窗,根據視窗标簽提取出那個視窗内的資料進行計算,計算的僅僅是最近一個滑動時間視窗内的資料
  • 接着對這個滑動時間視窗内的資料,可能最多就千條左右吧,運作所有的複雜SQL計算出這個滑動時間視窗内的報表資料,然後将這個視窗資料計算出的結果,與之前計算出來的其他視窗内的計算結果進行合并,最後放入MySQL中的報表内
  • 此外,這裡需要考慮到一系列的生産級機制,包括滑動時間視窗如果計算失敗怎麼辦?如果一個滑動時間視窗計算過慢怎麼辦?滑動視窗計算過程中系統當機了如何在重新開機之後自動恢複計算?等等

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

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

别光看NB的Github開源項目,你得參考他們去設計自己的架構

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

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

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

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

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

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

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

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

别光看NB的Github開源項目,你得參考他們去設計自己的架構

十二、階段性總結

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

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

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

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

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

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

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

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

十三、下一階段的展望

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

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

十四、上篇文章的答疑

之前釋出過一篇文章,寫了一個分布式鎖的高并發優化的文章,具體參見:為什麼公司規定所有接口都必須加上分布式鎖,你知道嗎?收到了大家很多的提問,其實最終都是一個問題:

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

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

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

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

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

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

繼續閱讀