騰訊Flink實踐:實時計算平臺Oceanus建設歷程

2019年4月1-2日,Flink Forward 2019 San Francisco會議在舊金山召開。Flink Forward會議邀請了來自Google, Uber, Netflix和Alibaba等公司在實時計算領域的頂尖專家和一線實踐者,深入討論了Flink社區的最新進展和發展趨勢,以及Flink在業界的應用實踐。隨着近年來對Flink技術的廣泛應用以及對Flink社區的活躍貢獻,騰訊也受邀參加了會議並以主題Developing and Operating Real-Time Applications at Tencent介紹了騰訊大數據在實時計算平臺建設上的工作。

一、背景介紹

image

近年來,實時計算在騰訊得到了越來越廣泛的應用。在騰訊內部,實時計算應用主要分爲以下四類:

  • ETL:ETL應該是目前實時計算最普遍的應用場景。例如在TDBank的數據鏈路中,TDSort讀取消息緩存系統Tube中的消息,通過流數據處理系統將消息隊列中的數據進行實時分揀,並落地到HDFS接口機集羣,並將最終分揀後的數據由加載到TDW中。

  • 監控系統:隨着服務數量和機器規模的不斷增長,線上環境日益複雜,對監控和報警系統也提出了更高的要求。監控系統需要能夠對產品和服務進行多維度的監控,對指標數據進行實時的聚合和分析,並支持方便靈活的報警規則設置。

  • 實時BI:實時的業務報表對產品運營有着非常大的幫助,能夠幫助我們的運營人員實時掌握產品數據,及時制定運營策略,通過更好的時效性獲取競爭優勢。

  • 在線學習:實時計算目前在推薦、廣告和搜索等產品中也有着十分廣泛的應用。一般來說,用戶興趣會在多個時間維度上持續變化。通過對用戶行爲進行實時檢測,我們能夠及時獲取用戶的當前興趣並提供更精準的用戶行爲預測。

目前騰訊的實時計算的規模已經十分龐大。數據平臺部實時計算團隊每天需要處理超過了17萬億條數據,其中每秒接入的數據峯值達到了2.1億條。

二、Oceanus簡介

image

爲了提高用戶流計算任務持續集成和持續發佈的效率,實時計算團隊從2017年開始圍繞Flink打造了Oceanus (http://data.qq.com),一個集開發、測試、部署和運維於一體的一站式可視化實時計算平臺。Oceanus集成了應用管理、計算引擎和資源管理等功能,提供了三種不同的應用開發方式,包括畫布,SQL和Jar,來滿足不同用戶的開發需求,同時通過日誌、監控、運維等周邊服務打通了應用的整個生命週期。

Oceanus還研發了Oceanus-ML來提高在線學習任務的開發效率。Oeanus-ML提供端到端的在線機器學習,涵蓋數據接入,數據處理,特徵工程,算法訓練,模型評估,模型部署整個機器學習流程。通過Oceanus-ML,用戶可以方便地利用完備的數據處理函數,豐富的在線學習算法來構建自己的在線學習任務,輕鬆地完成模型訓練和評估,同時可以一鍵部署模型,管理模型的整個生命週期。

在完成作業開發之後,用戶可以通過Oceanus對作業進行測試、配置和部署。Oceanus爲用戶程序提供了一系列的工具來協助作業測試。用戶既可以使用Oceanus提供的一鍵生成功能產生測試數據,也可以自己向Oceanus上傳自己的的測試數據,通過對比預期結果和實際結果來驗證應用邏輯的正確性。Oceanus依託騰訊內部的資源調度系統Gaia來進行資源管理和作業部署。用戶可以通過Oceanus配置作業所需要的CPU和內存資源,並指定作業需要部署的集羣。當用戶完成配置之後,Oceanus會向Gaia申請對應的資源並將作業提交到Gaia上運行。

Oceanus對Flink作業運行時的多個運行指標進行採集,包括Task Manger的內存,I/O和GC等。通過這些豐富的運行指標,用戶能夠很好的瞭解應用運行的情況,並在出現異常時能協助用戶及時的定位問題。運維人員則可以通過這些採集到的指標,設置報警策略並實現精細化的運營。

image

大部分Oceanus的用戶可以使用畫布方便的構建他們的實時計算應用。Oceanus提供了常見的流計算算子。在開發實時計算應用時,用戶將需要的算子拖拽到畫布上,配置這些算子的屬性並將這些算子連接,這樣就構建好了一個流計算應用。這種構建方式十分簡單,不需要用戶瞭解底層實現的細節,也不需要掌握SQL等語言的語法,使得用戶能夠專注於業務邏輯。

image

Oceanus同樣也提供了SQL和Jar的方式來開發實時計算作業。在使用SQL和Jar進行開發時,一個比較麻煩的地方就是作業的配置問題。例如SQL腳本沒有提供任何方式來允許用戶進行作業資源配置。儘管Flink的DataStream API爲用戶提供了接口來修改併發度和資源等配置,但爲了靈活修改這些配置,用戶常常需要自己通過外部配置文件的進行處理。爲了用戶能夠在使用SQL和Jar進行開發時也能方便的進行作業配置,Oceanus會首先對用戶提交的SQL腳本和JAR包進行解析和編譯,生成作業執行的JobGraph,並可視化在頁面上。用戶之後可以根據可視化的JobGraph來進行作業配置。通過提供可視化的配置方式,用戶作業開發的效率可以得到極大的提高。

配置好的JobGraph之後將被提交到Yarn集羣上執行。爲了能夠更準確的獲取更多的作業信息,Oceanus放棄了Flink默認的提交方式,而通過增強過的ClusterClient接口來提交作業。通過ClusterClient,Oceanus可以獲得作業的JobID, Yarn Application ID等信息,並利用這些信息來提供作業的生命週期管理。

image

Oceanus對正在運行的作業採集了大量的指標,通過這些指標來監控作業運行情況,並在發生故障時定位原因。爲了提高運維效率,我們根據長期積累的經驗對運行指標進行了篩選,並對Flink UI進行了重構來合理展示這些指標。

每個task輸入和輸出隊列的使用率是在實際生產中非常有用的運行指標。通常來說,當一個task的輸入隊列被佔滿,而輸出隊列爲空時,說明這個task的上游的數據產生速度已經超過了這個task的處理能力,導致了這個task的輸入出現了堆積。當一個作業中出現這樣的task時,我們就需要通過性能優化或者增加併發度的方式來提高這個task的處理性能。

輸入和輸出的TPS也是在作業運行中的關鍵指標。通常來說,一個task的輸出TPS和輸入TPS之間的比例並不會隨着併發度的變化而變化。我們利用這個性質來確定作業運行時的併發度。當確定作業併發度時,我們首先將所有task的併發度設置爲1並啓動作業。此時這個作業顯然是無法處理上游的數據的,因此大部分task的單機處理能力會被打滿,其輸入和輸出TPS可以達到最大值。根據需要的TPS和單機最大TPS,我們可以估算出每個task的併發度,並重新啓動。之後根據前面提到的輸入輸出隊列的使用率,我們對作業併發度進行一定的調整來去除作業中的性能瓶頸。一般通過幾次調整之後,我們就可以得到較爲理想的作業併發度配置。

在後面,我們希望能夠實現自動化腳本或者優化器來簡化作業併發的配置。不過這仍然是一個非常有挑戰的工作。一個主要的難點在於窗口算子的處理。很多窗口算子在平時較爲空閒,但在窗口觸發時會一下子發送大量的結果。在那一瞬間,如果窗口算子的併發度不夠就會出現一定的結果延遲。如何平衡窗口算子在空閒和觸發時的併發度目前看來仍然需要很多的trade-off。

當一個task的最大和最小TPS之間出現較大的差值時,一般就意味着作業中出現了負載傾斜。負載傾斜會對作業的性能造成較大的影響,同時也很難通過增加併發度的方式來提高性能。爲了減少負載傾斜對作業性能的影響,我們引入了Local Keyed Streams。相關工作將在後面的第三部分進行介紹。

image

在即將發佈的新版Oceanus中,我們還對TaskExecutor的線程信息進行了採集。這些線程信息能夠很好地幫助用戶定位發生的問題。例如checkpoint可能會由於多種多樣的原因而超時。當用戶實現的source function在被IO或者網絡堵塞時並沒有釋放checkpoint鎖,那麼正在執行的checkpoint可能就會由於無法及時獲取鎖而超時。用戶也有可能實現了一個堵塞的checkpoint函數,由於較慢的HDFS寫入或者其他原因而導致checkpoint超時。通過觀察線程信息,我們就可以容易的知道checkpoint超時的原因。

這些採集的線程信息也能對程序的性能優化提供很多幫助。一般而言,當一個task線程的cpu使用率達到100%時,就說明這個task的執行並沒有受到加鎖,I/O或者網絡等操作的影響。在上圖中,我們展示了一個Word Count程序的Task Executor的線程信息。在Word Count程序,我們有一個source task在持續不斷的發送word,還有一個map task對出現的word進行計數。可以看到,在Task Executor中,map線程的cpu使用率幾乎達到了100%,這說明其的執行是沒有太大問題的。而source線程的cpu使用率僅僅只有80%,這說明其的性能受到了影響。觀察線程堆棧,我們可以發現source線程時常會堵塞在數據的發送上。這是很好理解的,因爲每產生一個word,map線程都需要比source線程執行更多的指令。也就是說,map線程的數據處理能力比source線程的生產能力要低。爲了提高這個Word Count程序的性能,我們就需要保證map線程的數目比source線程的數目多一點。

image

三、Flink內核改進

除了在Oceanus上提供方便強大的接口和工具之外,我們還對Flink內核進行了大量的改進來提高其可用性和可靠性。這些改進主要包括以下幾個部分:

  • 作業管理相關:我們對Flink的作業管理的改進主要以提高作業執行的可靠性爲主,包括對分佈式環境下的leader選舉的重構和無需作業重啓的Job Master恢復機制等。同時,我們也正在研究和開發細粒度恢復機制來減少發生故障時需要重啓的task數目。

  • 資源調度相關:我們對Flink的資源調度,特別是在Yarn集羣上的資源調度進行了重構,以提供更好的資源使用率。同時,我們也正在研究如何使用分佈式和異步的資源調度框架來提高超大併發度的作業的資源調度效率。

  • 可用性相關:我們在Flink中提供了多個算子,包括local keyby, incremental windows, dim join等。這些算子能夠很好的提高用戶開發程序的效率和程序執行的性能。

3.1 分佈式Leader選舉

image

Flink的master負責資源申請、任務調度、checkpoint協調,並響應用戶請求。在任何時刻,一個集羣中都只可以有一個master節點可以在工作狀態。當集羣中出現多個master節點時,就需要通過leader選舉確定工作的master節點。

在分佈式環境下進行Leader選舉是分佈式系統中的一個經典問題。目前Flink依賴Zookeeper進行leader選舉,並將當選的leader的信息保存在Zookeeper上以實現服務發現。但在複雜的集羣環境中,Flink當前的實現並不能很好的保證leader選舉和發佈的正確性。

如上圖左側所示,當JM1獲得leader之後,其需要在Zookeeper發佈其地址以供其他節點來發現自己。但如果在其發佈地址之前,JM1發生了Full GC,那麼集羣就可以陷入混亂之中。其長時間的GC可能會導致其丟失leader以及和Yarn之間的心跳連接。此時一個新的master節點, JM2, 可能會被Yarn拉起。JM2在獲得leader之後會將其地址發佈在集羣中。當如果此時JM1從Full GC中恢復過來,並繼續執行之前的代碼,將其地址發佈在集羣中,那麼JM1的地址將會覆蓋JM2的地址導致集羣混亂。

另一個由於leader選舉導致的常見問題是checkpoint的併發訪問。當一個master丟失leader節點之後,其需要立即停止其所有正在進行的工作並退出。但是如果此時舊master的Checkpoint Coordinator正在完成checkpoint,那麼退出方法將無法獲取到鎖而執行。此時,在已經丟失了leader的情況下,舊master仍然有機會完成一個新的checkpoint。而此時,新master卻會從一個較舊的checkpoint進行恢復。目前Flink使用了許多tricky的方法來保證多個master節點對checkpoint的併發訪問不會導致作業無法從故障中恢復,但這些方法也導致我們目前無法對失敗的checkpoint進行有效的髒數據清理。

image

爲了上述問題,我們對Flink的leader選舉和發佈進行了重構。我們要求每個master節點在競爭leader時都創建一個EMPHEMERAL和SEQUENTIAL的latch節點。之後所有master節點會檢查latch目錄下所有的節點,序列號最小的那個節點將會被選舉爲leader。

Zookeeper的實現保證了創建的latch節點的序列號是遞增的。所以如果一個master節點被選爲leader之後,只要它的latch節點仍然存在,就意味着它的序列號仍然是所有master節點中最小的,它仍然是集羣中的leader。從而我們就可以通過檢查一個master的latch節點是否存在來判斷這個master是否已經丟失leader。通過將leader地址的發佈以及對checkpoint的修改等更新操作和對latch節點的檢查放置在一個Zookeeper事務中,我們可以保證只有保有leader的master節點纔可以對作業執行狀態進行修改。

3.2 無需作業重啓的master恢復機制

image

Master節點會由於多種不同的原因而發生故障。目前在master重啓時,Flink會重啓所有正在執行的task,重新開始執行作業。在Zookeeper連接出現抖動時,集羣中所有task都會重啓,對HDFS, Zookeeper和YARN這些集羣基本組件帶來較大的壓力,使得集羣環境進一步惡化。

爲了減少master恢復的開銷,我們實現了無需作業重啓的master恢復機制。首先,我們使用Zookeeper和心跳等手段來對master的狀態進行監控。當master發生故障時,我們立即拉起一個新的master。新master在啓動時,並不會像第一次執行時那樣申請資源並調度任務,而是會進入到reconcile階段,等待task的彙報。

在另一邊,task executor在丟失了和master節點的連接之後,也不會立即殺死這個master負責的task。相反,它將等待一段時間來發現新master的地址。如果在這段時間內發現了新master的地址,那麼task executor將把其執行的task的信息彙報給新master。

新master通過task executor彙報上來的信息來重建其execution graph和slot pool。當所有task完成彙報,並且所有task在master恢復的這段時間內沒有出現故障,那麼master就可以直接切換作業狀態到running,並繼續作業的執行。如果有task未能在規定時間內彙報,或者有task在這段時間內發生故障,那麼master將切換到failover狀態並通過重啓恢復執行。

3.3 細粒度資源分配

image

目前Oceanus依賴YARN來進行資源申請和任務調度。但現有Flink在YARN上資源分配的實現有着較大的問題,對作業可靠性帶來了一定的風險。

在現在Flink的實現中,每個task executor都有着一定數目的slot。這些slot的數目是在task executor啓動時根據配置得到的。當爲任務分配資源時,task會按照可用slot的數目分配到空閒的task executor上,一個task佔據一個slot。在這個過程中,Flink並不會考慮task實際使用的資源量以及task executor剩餘可用的資源量。

這種資源分配的方式是十分危險的,會導致task executor向YARN申請的資源量和實際task使用的資源量不匹配。在集羣資源緊張的時候,由於YARN會殺死那些超用資源的container,作業就會進入不斷重啓的狀態之中。

這種資源分配的方式也會導致較嚴重的資源浪費。在實際中每個算子所需的資源使用量是不同的。有的算子需要較多的CPU資源,而有的算子需要較少的內存資源。由於現在的配置中所有task executor具有相同的slot數目,所有slot都具有相同的資源,因此導致較爲嚴重的資源碎片,無法充分利用集羣資源。

爲了避免由於資源分配導致的不穩定,我們修改了Flink在YARN上的資源申請協議。我們不再使用靜態的slot配置,而是根據task申請動態的創建和銷燬slot。首先,我們要求用戶能夠爲每個operator設置其所需的資源量。這樣我們就可以根據slot中執行的operator來得到每個slot所需的資源量。當Master節點請求一個slot時,我們遍歷所有的task executor並在空餘資源量能夠滿足slot請求的task executor上創建一個新的slot提供給master節點。當這個slot中的task完成執行之後,這個slot也將被刪除並將其資源歸還給task executor。這種動態的slot申請方式可以使得Flink的資源利用率極大的提高。

3.4 Local Keyed Streams

現實中,很多數據具有冪律分佈。在處理這類數據時,作業執行性能就會由於負載傾斜而急劇下降。

image

以WordCount程序作爲示例。爲了統計每個出現word的次數,我們需要將每個word送到對應的aggregator上進行統計。當有部分word出現的次數遠遠超過其他word時,那麼將只有少數的幾個aggregator在執行,而其他的aggregator將空閒。當我們增加更多的aggregator時,因爲絕大部分word仍然只會被髮送到少數那幾個aggregator上,程序性能也不會得到任何提高。

爲了解決負載傾斜的問題,我們提供了Local Keyby算子,允許用戶在task本地對數據流進行劃分。劃分得到的Local keyed streams和一般的Keyed streams是類似的。用戶可以通過RuntimeContext訪問keyed state,也可以在數據流上執行窗口操作。利用Local keyed streams,我們就可以在數據發送一端就進行本地的預聚合,統計一定時間段內word在當前task出現的次數。這些預聚合的結果然後被髮送給下游,通過合併得到最終的結果。

image

但Local keyed streams的數據劃分和分發和keyed streams不同。在keyed streams中,數據流會劃分成多個key group,每個task都會負責一部分key group的處理。每個task之間的key group是沒有任何交集的。而由於local keyed streams是在task本地對數據流進行劃分,因此每個task上的key group range都是key group全集。即如果數據流總共有3個key group,那麼每個task的local key group range都爲[1, 3]。

當併發度改變時,這些local key group將按照數據均勻分給新的task。例如當task併發度從3變爲2時,那麼第一個task將分配到5個local key group,而第二個task將被分配到4個。在這種情況下,同一個task將會被分配到多個具有相同id的local key group。這些具有相同id的local key group將會被合併起來。當合並完成之後,所有task上的local key group range將仍然是[1, 3]。對於Reducing State, Aggregating State以及List State來說,它們的合併是比較簡單的。而對於Value State, MapState和Folding State等類型的數據而說,則需要用戶提供自定義的合併函數來實現local key group的合併。

由於在一定時間段內發送給下游的數據量不過超過上游的併發度,下游的負載傾斜可以有效緩解。同時由於數據在上游一般沒有較爲嚴重的傾斜,程序性能不會由於負載傾斜而嚴重降低。我們測試了WordCount程序在不同數據傾斜程度下的吞吐。可以看到,在沒有使用local keyed streams的情況下,程序性能隨着傾斜程度而迅速下降,而使用local keyed streams之後,程序性能幾乎不受影響。

3.5 可用性提升

image

爲了方便用戶開發畫布和SQL程序,我們實現了超過30個的Table API和SQL函數。用戶可以利用這些內置函數極大地提高實時計算應用的開發效率。此外,我們也對數據流和外部維表的join進行了大量優化,並補充了Flink還未支持的Top N功能。我們還提供了incremental window功能,允許用戶能夠在窗口未觸發時得到窗口的當前結果。Incremental window在多個應用場景中有着廣泛的應用。例如用戶可以利用incremental window統計活躍用戶數目在一天內的增長情況。

四、後續工作

我們後續的工作主要包括以下幾個方面:

  • 我們將繼續研究提高Flink任務調度效率的方法。目前Flink使用單線程模型執行任務調度。在作業併發度較高的情況下,Flink的任務調度效率較低。我們將嘗試使用分佈式和異步的任務調度模型來提高任務調度效率。

  • 我們還將繼續研究批流融合的checkpoint機制。目前Flink基於chandy-lamport算法來爲流作業進行checkpoint,而使用upstream restart的方式來將批作業從故障中恢復。我們可以將兩者結合起來,提供一種統一的checkpoint機制,使得在流作業的恢復可以利用緩存的中間結果來減少所需重啓的task數目,而在批作業中,通過對長時間運行的任務進行checkpoint來避免在發生故障時從頭開始重新執行。

  • 我們還將在制定執行計劃時考慮數據在空間和時間維度上劃分。例如在系統資源不足以支持對數據流式處理時,我們可以將數據在時間維度上進行劃分,依次對劃分好的數據進行處理。

  • 騰訊大數據產品矩陣即將發佈的SuperSQL項目,利用Flink的計算能力來滿足跨數據中心,跨數據源的聯合分析需求。它可以做到:數據源SQL下推,避免集羣帶寬資源浪費;單DC內CBO(基於代價優化),生成最優的執行計劃;跨DC CBO,根據DC負載和資源選擇最佳DC執行計算,從而獲得更好的資源利用和更快的查詢性能。

原文鏈接:https://mp.weixin.qq.com/s/tyq6ZdwsgiuXYGi-VR_6KA

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