基於Go語言構建的萬億級流量大資料平臺架構

七牛雲發表於2019-03-04

黨合萱
黨合萱
碩士畢業於西安電子科技大學,曾就職於阿里雲端儲存部門,主要從事儲存服務相關功能的設計與開發工作。於2016年加入七牛雲,主要負責流式計算與離線計算服務pipeline的架構和開發工作。目前pipeline承載公司每天超過千億、超過百TB的資料處理。


今天的分享主要圍繞七牛在最近一年時間裡面開發的大資料平臺進行展開,目前我們的平臺已經承載了公司核心業務的運營;關於我們的產品,主要會從一個場景展開進行介紹,當中包含了我們在設計過程中遇到的挑戰以及解決方案。也歡迎大家基於這些問題和我們展開交流與討論。

場景.產品

對於運維人員來說,在進行每日常規的線上運維時,日誌當中的一天內訪問量的波動、線上錯誤分佈、其他業務指標這些資料對於運維人員來說並非是一個透明的過程,那麼如何將這些東西做到視覺化,或是將這些資料收集起來做統一的處理分析,其實是一個比較複雜和較難實現的過程。這就是所謂的運維日誌分析,也是我們之前所提及的場景。關於我們產品解決場景的細節,在下面將會進一步進行分析。我們以Nginx-log為例對我們的Pandora產品進行敘述。

資料接入Pandora—logkit配置執行

任何資料分析的第一步都是資料接入。Pandora開發的資料接入工具logkit,可以幫助使用者將資料打入Pandora平臺內;在最開始需要下載logkit,配置並執行(圖1)

圖1
圖1

logkit工具支援多種資料來源,比如對Nginx-log、kafka資料進行採集,並打入我們的資料處理平臺當中。下面對圖 1 進行詳解,首先,我們需要檢視日誌格式,包括日誌格式的名稱。在圖 1 中,我們明確了日誌儲存的路徑以及格式。最後進入配置檔案,將需要進行配置的資訊進行配置,並指明資料需要打入存放的路徑,如果需要打到某一個訊息佇列中時,需要對金鑰進行配置並執行它,那麼此時這個資料才會採集收錄到我們的平臺當中。

日誌檢索

圖2
圖2

圖 2 所示是一個比較直觀的視覺化介面,它支援拖拽,頁面左側可以看到“資料來源”與“日誌檢索”這兩項內容,配置好的 logkit 執行之後,所有資料都會打入“資料來源”中。頁面右側則顯示了資料來源中每個欄位的名稱、格式等資訊。

圖3
圖3

圖 3 所示是“日誌檢索”的內容顯示頁面,通過“日誌檢索”我們可以清晰的檢視一些業務邏輯,在搜尋框中填入你的查詢條件,就可以進行全文檢索,當需要檢視過去某個時刻響應超過3s的所有請求,那麼通過“日誌檢索”頁面也可以清楚的查詢並顯示出來。圖 3 僅僅是展示了一個全文搜尋的狀態,在功能頁面還可以檢視相關資料分佈的柱狀圖。

日誌聚合

圖4
圖4

如圖 4 所示,打入到資料來源裡面的資料,可以通過一段SQL以每分鐘為粒度進行計算聚合。可以做聚合的內容很多,如來自某個IP的請求數量,也可以是別的一些相關操作,聚合結束之後,資料便會再次迴流到我們的資料來源當中。簡單來說,我們通過一次計算將資料重新迴流到資料來源用於下一環節的分析處理,計算、迴流的過程是可以不斷去進行級聯的,可以實現很多相對比較複雜的資料處理。

資料迴流至平臺

圖5
圖5

上面提及的資料迴流至資料來源是一種處理方式,使用者搭建自己的一套HTTP服務,資料通過HTTP的介面迴流至其自己系統內是另外一種資料處理方式。通過這種方式迴流的資料,使用者可以將分析結果在自己平臺進行沉澱,操作頁面如圖 5 所示。

實時資料展示與監控

圖6
圖6

圖 6 所示直觀展現了我們的監控頁面,監控服務需要開通之後再進行Grafana頁面配置,頁面的基本配置在我們官方文件中都有提供,使用者可以直接下載匯入。

圖7
圖7

圖 7 展示的是對 Nginx 的日誌進行分析之後的資料展示圖。左上角橙色的框(visits為0)顯示可總訪問量,右上角綠色的柱狀圖則是在過去一段時間內發生的請求數以及響應時間,右下角的餅狀圖顯示了相關使用者訪問的佔比量。這些圖的樣式及位置都可以進行配置。

架構設計

圖8
圖8

圖 8 所示展示了Pandora的業務架構。資料通過Portal/Logkit/SDK/API可以匯入我們的平臺,進入訊息佇列當中,訊息佇列當中的資料可以經過計算反覆在計算任務和訊息佇列之間進行流動,當然,這些資料也可以直接匯出。匯出後的資料經過下游系統(日誌檢索/時序資料等)處理最終可以生成資料包表,以上就是資料的整個流向。

Pipeline設計目標及技術選型

每個系統在最初設計時都會擬定設計目標以及相應的需要解決的問題。下面先講一下我們的設計目標,首先這個系統必須支援資料快速接入、高吞吐量、低延遲;其次作為一個雲服務,它必須支援海量使用者併發訪問以及必須支援海量訊息佇列;要提供實時計算與離線計算的框架滿足計算需求;最終它必須是視覺化的操作滿足使用者操作需求。在設計目標提出之後,我們要對選型進行規劃,我們需要選擇具備高吞吐量的儲存系統,當然目前七牛的儲存系統無疑是最滿足需求的;其次我們需要強大靈活的大資料處理引擎;最後開發人員必須保證最終設計的產品是可以快速迭代開發的。基於這些要求,我們很輕易選擇了相應的技術支撐,使用Kafka來滿足我們的對海量訊息佇列設計的需求;使用Spark作為計算引擎;語言選型上則選用我們底蘊積澱深厚的Golang,最終,在確定這幾種技術選型之後,我們便開始搭建系統。

圖9
圖9

圖 9 所示,是我們Pipeline的整體架構設計,它負責pandora中資料的接入和處理。資料通過Logkit等方式匯入到資料接入層,也就是apiserver。通過apiserver的資料會進入到訊息佇列裡面,之後通過計算引擎的讀取和回寫操作,最終匯入到下游系統中(LogDB/TSDB/HTTP/七牛雲端儲存)我們今天著重關注綠色箭頭指引的資料流方向,會提及裡面相關的重點進行詳解。在整個資料流流動過程中,有幾個因素可能會決定這個系統的效率,比如穩定性、效能等。所以我將從使用者到訊息佇列,經過計算任務再返回到訊息佇列,最終匯出資料這整個過程來講解。

資料接入層

圖10
圖10

圖 10 所示顯示的是資料接入層。資料通過apiserver匯入,排程器用來管理一些使用者訊息佇列的源資料,其中包括資料以何種形式寫入到訊息佇列當中去。logkit這個工具之所以放在這裡,不是因為資料會通過apisever流向logkit最終再流向訊息佇列,而是因為它可以採集各種形式的資料,在這裡我們用它採集系統審計日誌與監控資訊。它很容易進行管理和配置。

容器化

圖11
圖11

在最開始設計這個系統時,擴容是一個比較困擾我們的問題。因為接入的基本是內部使用者,接入速度比較快,所以一週之內需要擴容至少一到兩次,這在運維上是一個比較重的負擔。之後我們採用了容器的方案,因為整個資料接入層是一個無狀態的元件,所以我們將它容器化,使用我們的容器雲產品解決。如圖 11 所示,每一個pod中,我們都將apisever與logkit佈局在一起,通過監控資料,我們將每個容器包括這個叢集整體的資訊全部都彙總在了這個排程器當中。排程器裡面承載著整個叢集負載及資源總量這些資訊,可以及時根據這些資訊動態的實現擴容縮容。

資料寫入優化

圖12
圖12

圖 12 所示是對資料寫入進行優化的過程。第一代資料寫入流程,採用了序列的方式進行,資料匯入之後是一行一行進行解析,全部解析之後再將資料寫入到訊息佇列當中,但是這種方式的處理效率是非常低效的。所以我們利用go語言的特性,採用了line channel,資料來源源不斷進入channel,然後會在channel下游起多個parser,並行的對資料進行解析。也就是說我們利用channel將處理變成了併發的過程,最終提高了CPU的利用率,降低了使用者響應的延遲率,大大優化了效能。

計算

圖13
圖13

如圖 13 所示,我們的計算基於Spark 實現,提供了一個比較簡單的SQL,對使用者遮蔽了底層細節。

匯出優化

圖14
圖14

資料流入整個系統中,在系統中不管是做計算還是儲存,這些經過處理的資料如果需要發揮作用,都要流入到下游系統中,所以“匯出資料”這個過程起到的是一個連線上下游,承上啟下的作用。圖 14 是這個系統的總架構圖,因為當時並未對匯出服務做細粒度的任務切分,並且單臺server也處理不了過大的使用者任務,所以在高峰期時,會導致延遲增大,基於此,我們經過一個月的開發最終推出了一個全新的版本。

圖15
圖15

如圖 15 所示,是經過改進後的整體架構圖。圖的頂層是我們的master,用它來控制所有任務的排程管理。所有任務都是經由排程器轉發給master,由master來評估每一臺機器上的負載,之後再根據機器本身的一些狀態(CPU使用率、網路頻寬、執行任務的情況)去做相應的排程,除此之外我們還將任務做了更細粒度的切分。

排程方法的設計首要考慮到的就是面向資源,其次需要充分利用異構機器,並且能滿足自動調整。面向資源大家都能夠理解,充分利用異構的機器,是因為我們機器規格眾多,所能解決的任務強度不一致,我們需要充分利用該機器的資源,而不能讓其在處理任務時,有"機器資源"不足或浪費的情況發生;至於自動調整,就是可以保證在面對使用者量突增或者突減這種突發情況發生時,我們具備自動調整任務分佈的能力,其最終的目的也是為了充分利用資源。

任務分配

圖16
圖16

圖 16 是任務分配的過程圖。假設最初任務(T1-T7)都相對均勻的分佈在三臺機器上,此時又有另外兩個任務(T8-T9)進入,那麼我們就需要尋找一些相對比較空閒的機器(S1 或 S2)優先將這兩個任務分配給他們。這只是針對一個相對比較均衡的情況做的一個調整。

自動調整

圖17
圖17

圖18
圖18

當然也會有不均衡的情況產生(圖 17-18)那麼此時就需要我們去做一些自動調整,比如有一個使用者刪除了其很多工,那麼此時的S1與S2相對S3會比較空閒,那麼此時我們就需要通過server向master上報心跳,這個內容包括對資源的佔用以及任務的分佈情況,根據結果對比較空閒的機器做一個調整,保持一個相對平衡的狀態。

水平擴充套件

圖19
圖19

圖 19 是進行水平擴充套件時會產生的一個問題。所有機器目前都處於一個比較繁忙的狀態,此時如果過來一個新的任務(T13),但是前12個任務已經全部分佈在這三臺機器上面處理,騰不出空閒的機器處理新增任務,那麼此時就會需要對機器進行擴容。

圖20
圖20

如圖 20 所示,在前三臺機器都處於“忙碌”狀態時,我們需要新增server4,一旦啟動S4,它會向master彙報心跳,然後master就會感知到這個任務的存在以及S4的存在,重新對整個資源分佈使用情況做一次評估,將T13分配到比較空閒的S4上,甚至可以將在S1、S2、S3等待處理的任務分配到S4上。

資源隔離

圖21
圖21

實際上,不單單是對任務進行自動調整均衡分擔機器處理壓力是非常重要的,對於一些比較特殊的任務,如何保證這個使用者流量在突增時不會影響到其他相對較小的使用者,或是當資料匯出到雲端儲存進行壓縮時(壓縮的過程非常耗費CPU資源)如何保證它不會影響其他任務,這些都是我們需要處理的問題。針對這些問題我們提出了資源隔離的概念(圖 21)將機器和任務進行隔離,提供排程組(排程組中是相近的一組機器或者是一類任務)功能,通過對他們物理上的隔離,達到相互之間互不影響,並對資源進行充分利用。

master高可用

圖22
圖22

圖23
圖23

綜上我們可以看出我們的系統是一對多的狀態(一個master對多個server)那麼在這種情況下,如何解決在出現單點故障時仍然保證服務的高可用。如圖22到圖23所示,是我們設計的一個核心所在,我們可以在圖中看到最底端是一個zookeeper叢集,我們通過對一個臨時檔案的建立來模擬一個鎖,多臺機器可以同時去搶佔這把鎖,搶佔成功的master會成為一個主master,沒有搶佔成功的則會作為一個備份,在平時會空閒,但一旦S1丟鎖,master2就會搶佔鎖,接過整個排程任務以及一些叢集管理任務,這就是master高可用思路。

Server高可用

圖24
圖24

server高可用,我們也是採用類似的思路。我們將master視作一個高可用節點,每一個server都需要向master彙報心跳,心跳的內容包含了機器本身的存活以及相應任務的執行情況。如圖 24 所示,master一旦感知到S3當機,那麼此時就會將S3上執行的兩個任務(T5-T6)都調走,並且它會認為S1與S2是相對比較合適的選擇,並且會將這兩個任務調去相應的server上,這樣就完成了server的高可用目標。

系統級水平擴充套件

圖25
圖25

圖26
圖26

最開始有提及我們的整個訊息佇列是使用kafka實現的,kafka其實也是有上限的,在最開始我們也是採用了kafka單個叢集(圖 25)後來發現,一旦業務量上來,訊息佇列資料一旦多到一定程度,系統會發生雪崩。所以我們對單個叢集做了一個擴充套件(圖 26)將單個kafka叢集直接拆成多個叢集,讓每一個kafka叢集都保持一個相對比較小的規模,這樣效能方面就會得到很大的提升,圖 26 所示就是經過擴充套件後的情況,由三個kafka提供的資訊會彙總到我們的排程器上,排程器通過壓力或者是訊息佇列的數量,對使用者新建立的任務以及新的資料來源進行分配,分配至合適的kafka叢集中。

上下游協議優化

圖27
圖27

在實踐中還是會出現上下游之間效能較低的情況。在最開始,我們採用Json來做上下游的資料傳輸,可是在日誌檢索時暴露出的問題就是,這樣做對網路消耗很大,於是我們決定採用Protobuf進行上下游資料傳輸。圖 27 展示了使用Json與Protobuf時,從序列化、反序列化角度進行對比的資料結果展示,從圖中可以看出,使用Protobuf消耗的時間都更短,尤其在反序列化時,它的CPU消耗降低了將近一個數量級。因此,採用這種方式,不管從叢集計算資源利用還是從網路頻寬提升上都將效率提升了數倍。

流水線處理

圖28
圖28

至於對流水線的處理,最開始的設計其實是一個序列的操作,匯出服務從訊息佇列當中拉取資料,經過處理之後做一個推送,持續這樣的工作過程,其中處理操作是很快的,但是拉取和推送相對就很慢,這樣的一個過程,執行效率其實是很低的,並且由於每種操作的處理時間不一樣,有的快有的慢,就導致在監控圖上監察到網路的趨勢圖是時高時低的,也就導致了利用率的降低。鑑於此,我們優化了流水線操作,採用並行化操作(圖 28)結果顯示,這樣做的結果是推送和拉取效率都會比上面一種方式高。

Golang GC

我們的整個語言選型是採用Golang的,而它是一個帶GC的語言,實際上出現的情況還是很多的,系統當中會有1/10的時間是不幹活而在進行垃圾回收的。因此在程式碼層面我們做了一些改進,一個是sync.Pool的使用能夠降低垃圾回收的頻率;其次是重用物件,將一個物件儘可能重複使用,這樣一來,每一次GC的量就會減小。之後我們對Golang進行了版本升級,升級至1.8版本後我們再檢視了一下GC的耗時,發現提升了將近兩個數量級。這就是程式碼層面的優化。

有限資源假設

最後敘述一下我們關於資源假設方面進行的一個優化,也就是要建立起一個有限資源假設的概念。前段時間由於資料接入量比較大,我們需要自己進行運維,突發接入的客戶,會使系統輕易就被跑滿,此時我們會想辦法加機器或者是說在排程上去做一些調整和優化,但是這樣終究不是一個長久的辦法。於是我們會在剛開始去做一個資源有限假設,就是要在最開始評估出來資源有限情況下我們該如何去做。比如需要提前預估出10M頻寬所能對應多少使用者的任務,這個必須是有一個資料存在的,並且在這個基礎上我們需要做一個資源的預估和叢集資源的規劃。根據這個預估的資料的情況,去劃定一個水位標準,超過水位標準之後,再考慮是不是需要進行擴容。客戶這邊也需要溝通清楚我們現有的處理能力,這樣,才能保證整個叢集/服務是處於一個比較健康的狀態。

成果

上面我們提及了我們的架構實現以及整體的一個優化,目前的成果就是:我們支撐了萬億級的資料點,並且每天可以處理幾百TB的資料,以及支援海量使用者。我們的系統目前保持很低的延遲,較高的處理效率;由於我們實現了自動化運維,所以人力成本也大大減少,我們的期望就是可以在寫程式碼時不被運維事情干擾;至於可用性,目前已經達到3個9(99.9%)的成效。

相關文章