Flink架構,源碼及debug

工作中用Flink做批量和流式處理有段時間了,感覺只看Flink文檔是對Flink ProgramRuntime的細節描述不是很多, 程序員還是看代碼最簡單和有效。所以想寫點東西,記錄一下,如果能對別人有所幫助,善莫大焉。

        說一下我的工作,在一個項目裏我們在Flink-SQL基礎上構建了一個SQL Engine, 使懂SQL非技術人員能夠使用SQL代替程序員直接實現Application, 然後在此基礎上在加上一些拖拽的界面,使不懂SQL非技術人員 利用拖拽實現批量或流式數據處理的Application 。 公司的數據源多樣且龐大,發佈渠道也很豐富, 我們在SQL Engine 裏實現了各種各樣的Table Source (數據源) , Table Sink (數據發佈)和 UDF (計算器), 公司裏有很多十分懂業務專業分析員,如果他們真的可以簡簡單單,託託拽拽的操作大數據,建立計算模型,然後快速上線和發佈,這樣的產品應該前景廣闊。

        可是後臺並非說起來這麼簡單,SQL使用不善,難以達到業務想要的效果,數據量一上來各種問題會出現,後端需要大量的優化工作, 比如 數據傾斜, 是最常發生的事情。SQL基本上是一個Join Language。用戶經常會將一個大數據源和一個小數據源做Inner Join, 如果大數據源的數據項很大部分都使用極少數的幾個join key, 就很容易出現數據傾斜。現實傾斜的或不均衡的,比如國際資本>80%以用美元計價,世界人口50%屬於某兩個國家, 財富主要有20%的人擁有, 等等 。 Flink 如果把SQL join 執行成Hash Join, 最後的結果是無論你實現分配了多少個TaskSlots, 如果80%的數據都跑到某一個TaskSlot裏,緩慢運行直至將個這Slot的資源耗盡,整個job失敗。這種情況最好是將小數據集廣播給所有的下游通道, 大數據集按原始的分片並行,這樣的join因分配均衡而快速。然而標準SQL裏沒有辦法指定joinhint , Flink sql也不支持這個,只能通過debug flink 來看看哪裏能做一些改變解決這個問題。我們在最後一章,從Flink client , flink optimizer, flink run-time (job manager, task manager) 一步一步的 在源碼裏設置斷點, debug, 將數據流過一遍,看看有哪些方案可以將這個小數據集合廣播起來。

       爲了使本文讀起來流暢一些, 我先通過幾個章節大概介紹一下Flink 。本文關心架構, 所以不會涉及很多關於API的東西(比如Flink streaming 的windowing, watermark, Dataset, DataStream, 及SQL的API等, 網上應該有很多關於這些的文章)。只是想大概梳理一個Flink的架構,使架構對應到源碼結構裏, 瞭解一下Flink的 Graph metadata, 高可靠性的設計,不同cluster環境裏 depoloyment的實現等, 最後利用IntelliJ IDEA 通過一個小例子帶大家debug一下Flink 。如果對flink的架構有較好的理解(比如主要類及metadata),就比較容易在準確的地方設置斷點,debug Flink代碼將更有效率,從而解決問題就會更有效率, 這就是本文的目的。大概瞭解一下框架,但並不會面面俱到。當如果你需要深入瞭解一下Flink某方面的細節, 本文能夠告訴你入口在哪裏,或者通過對架構瞭解過程中得到的common sense , 再加上一點想象力, 你或許直接能夠得到解決問題的方案, 然後再通過閱讀源碼及調試來加以驗證。 

1.  Flink的架構簡介

1.1  Flink 分佈式運行環境(官方圖)

(圖-1 Flink Runtime 來自:https://ci.apache.org/projects/flink/flink-docs-release-1.6/concepts/runtime.html

關於架構,先上一個官方的圖,儘管對於Flink架構,上圖不是很準確(比如client與JobManager的通訊已經改爲REST 方式, 而非AKKA的actor system),我們還是可以知道一些要點:

  • FlinkCluster: Flink的分佈式運行環境是由一個(起作用的)JobManager 和多個TaskManager組成。每一個JobManager(JM)或TaskManager(TM)都運行在一個獨立的JVM裏,他們之間通過AKKA的Actor system (建立的RPC service)通訊。所有的TaskManager 都有JobManager管理,Flink distributed runtime實際上是一個沒有硬件資源管理 的軟件集羣 ( FlinkCluster ), JM是這個FlinkCluster 的master, TM是worker。  所以將Flink運行在真正的cluster 環境裏(能夠動態分配硬件資源的cluster,比如Yarn, Mesos, kubernetes), 只需要將JM 和 TM運行在這些集羣資源管理器分配的容器裏,配置網絡環境和集羣服務使AKKA能工作起來, Flink cluster 看起來就可以工作了。具體的關於怎麼將Flink 部署到不同的環境, 之後有介紹, 雖然沒有上面說的這麼簡單,還有一些額外的工作, 不過大概就是這樣:因爲Flink runtime 自身已經通過AKKA的 sharding cluster建立了FlinkCluster, 部署到外圍的集羣管理只是爲了獲取硬件資源服務。 Flink不是搭建在零基礎上的框架,任何功能都要自己重新孵化,實際上它使用了大量的優秀的開源框架, 比如用AKKA實現軟件集羣及遠程方法調用服務(RPC), 用ZooKeeper提高JM的高可用性, 用HDFS,S3, RocksDB 永久存儲數據, 用 Yarn/Mesos/Kubernetes做容器資管理, 用Netty 做高速數據流傳輸等等。
  • JobManager: JobManager 作爲FlinkCluster的manager,它是由一些Services 組成的,有的service 接受從flink client端 提交的Dataflow Graph(JobGraph),並將JobGraph schedule 到TaskManager裏運行,有的Service 協調做每個operator 的checkpoint以備job graph運行失敗後及時恢復到失敗前的現場從而繼續運行 , 有的Service負責資源管理, 有的service 負責高可用性,後面在詳細介紹。值得一提的是,集羣裏有且只有一個工作的JM, 它會對每一個job實例化一個Job
  • TaskManager : TaskManager是slot  的提供者和sub task的執行者。通常Flink Cluster裏會有多個TM, 每個TM都擁有能夠同時運行多個SubTask的限額,Flink稱之爲TaskSlot。當TM啓動後, TM 將slot限額註冊到Cluster裏的JM的ResourceManager(RM), RM知道從而Cluster中的slot 總量,並要求TM將一定數量的slot 提供給JM,從而JM 可以將Dataflow Graph的task(sub task)分配給TM 去執行。TM是運行並行子任務(sub Task) 的載體 (一個Job workflow 需要分解成很多task, 每一個task 分解成一個過多個並行子任務:sub Task) , TM需要把這些sub task在自己的進程空間裏運行起來, 而且負責傳遞他們之間的輸入輸出數據, 這些數據 包括是本地task的和運行在另一個TM裏的遠程Task 。關於如何具體excute Tasks, 和交換數據 後面介紹。
  • Client:Client端(Flink Program)通過invoke 用戶jar文件 (flink run 中提供的 jar file)的裏main函數 (註冊data source, apply operators, 註冊data sink, apply data sink),從而在ExecutionEnvironment或StreamingExecutionEnvironment裏建立sink operator 爲根的一個或多個FlinkPlan(以sink爲根, source 爲葉子, 其他operator爲中間節點的樹狀結構), 之後client用Flink-Optimizer將Plan優化成OptimimizedPlan(根據Cost estimator計算出來的cost 優化operator在樹中的原始順序, 同時加入了Operator與Operator連接的邊 , 並根據規則設置每個邊的shipingStrategy, 實際上OptimizedPlan已經從一個樹結構轉換成一個圖結構), 之後使用GraphGenerator(或StreamingGraphGenerator)將OptimizedPlan轉化成JobGraph提交給JobManager, 這個提交是通過JM的DispatcherRestEndPoint提交的。
  • Communication: JobManager 與Taskanager都是AKKA cluster裏的註冊的actor, 他們之間很容易通過AKKA(實現的RPCService)通訊。 client與JobManager在以前(Version 1.4及以前)也是通過AKKA(實現的RPCService)通訊的,但Version1.5及以後版本的JobManager裏引入DispatcherRestEndPoint (目的是使Client請求可以在穿過Firewall ?),從此client端與JobManager提供的REST EndPoint通訊。Task與Task之間的數據(data stream records)(比如一個reduce task的input來自與graph上前一個map, output 給graph上的另一個map), 如果這兩個Task運行在不同的TM上,數據是通過由TM上的channel manager 管理的tcp channels傳遞的。

 

1.2  JobManager

 (圖-2,JobManager的內部結構) 

 如上一章所述, JobManager 是一個單獨的進程(JVM),  它是一個Flink Cluster的 master 、中心和大腦, 他由一堆services 組成(主要是Dispather, JobMaster 和ResourceManager),連接cluster裏其他分佈式組件 (TaskManager, client及其他外部組件),指揮、獲得協助、或提供服務。

  • ClusuterEntryPoint是JobManager的入口,它有一個main method ,用來啓動HearBeatService, HA Sercie, BlobServer,  Dispather RESTEndPoint, Dispather, ResourceManager 。不同的FlinkCluster有不同的ClusuterEntryPoint 的子類,用於啓動這些Service在不同Cluster裏的不同實現類或子類。Flink目前(version1.6.1)實現的FlinkCluster 包括:
    • MiniCluster : JM和TM都運行在同一個JVM裏,主要用於在 IDE (IntelliJ或Eclipse)調試 Flink Program (也叫做 application )。
    • Standalone cluster : 不連接External Service (上圖中灰色組件,如HA,Distributed storage, hardware Resoruce manager), JM和TM運行在不同的JVM裏。 Flink release 中start-cluster.sh啓動的就是StandaloneCluster.
    • YarnCluster : Yarn管理的FlinkCluster, JM的ResourceManager連接Yarn的ResourceManager創建容器運行TaskManager。BlobServer, HAService 連接外部服務,使JM更可靠。
    • MesosCluster : Mesos管理的FlinkCluster, JM的ResourceManager連接Mesos的ResourceManager創建容器運行TaskManager。BlobServer, HAService 連接外部服務,使JM更可靠。
  • HighAvailabilityService:重複之前的話:JM是一個Flink Cluster的 master 、中心和大腦, 如果JM崩潰了,整個cluster就無法運行了。HAService能夠使多個JobManager同時運行,並選舉一個JM作爲Leader, 當Leader失敗後在重新選舉,使另個健康的JM取而代之成爲leader, 從HA存儲中讀取MetaData(Graph,snapshot)從而 繼續管理Cluster的運行。HighAvailabilityService 只保護JM裏的DispatcherRestEndpoint, Dispatcher, ResourceManager 和JobMaster 4個核心服務, 從理論上來講, 這些service的各自的leader有可能來自不同的JM, 這就要看外部做Coordination的服務的Leader Election策略會不會把他們都從一個JM 選了。目前,Flink支持的和在使用的HighAvailbilityService有ZooKeeperHaService和StandaloneHaService。
    • ZooKeeperHaService:連接外部的ZooKeeper cluster做多個JM的Leader Election,從指定的存儲(通常是HDFS)存取JM metadata, 從而當JM takeover 或重新啓動時能夠獲取失敗之前的snapshot or savepoint, 從而繼續服務。
    • StandaloneHaService : 不支持多個JM Election。但支持從指定的存儲存取JM metadata, 做失敗後重啓恢復。
  • BlobServer 使用來存儲Client端提交的Flink program jar,  jobGraph file, JM 的所有services , 和所有的TM都連接同一個BlobServer (可以是LocalDisk, HDFS, S3 , 或其他的 Blob數據庫)讀取這些數據。
  • HeatBeatService , 用來運行JM 與TaskManager之間的心跳服務。 比如 ResourceManager 與JobMaster和所有TaskManager之間的心跳, JobMaster與所有TaskManager之間的心跳。如果心跳消失, 相應的HA 容錯措施就要啓動。 比如一個TM與JM的心跳沒了,那麼相應的容錯措施就會執行了。比如JobMaster的心跳消失,HA就會重新選舉新的JobMaster Leader;TM的心跳消失,ResourceManager就要將task分配到其他空閒的TM的slot裏,如果沒有空閒的slot ,RM 就會向外部的ResoureManager申請新硬件和啓動新的 TM以提供空閒的 slot。Flink的心跳消息是通過AKKA 傳遞的。
  • DispatcherRESTEndPoint是JM的4大核心服務之一(其他三個分別爲Dispatcher, JobMaster和ResourceManager),受HAService的保護, 是Flink客戶端與JM交互的REST接口, 也是Flink custer 的WebMonitor。非核心服務實際上都是一些UtilityService, 他們非JM獨有,需要用時可隨時實例化:比如Client端也會使用HAService來獲取DispatcherRESTEndPoint的leader的地址和端口, TM也會使用BlobServer 。DispatcherRESTEndPoint是用Netty搭建的RESTService, 它創建了大概有290個handler 對應不容的資源地址及方法。這些handler大都需要通過RPC方式調用Dispatcher 的遠程方法來滿足客戶的請求。
  • Diaptcher是DispatcherRESTEndPoint的後端服務層,它實現了RestDispatcher接口, 從客戶端(包括FlinkClient和Flink Web Dashboard)提交給又有來自於EndPoint的請求,都由這個接口裏的方法服務, 這其中最總要的方法就是submitJob。當Dispather受到submitJob的調用時,他會先在本JVM裏創建一個JobMaster服務,並將 JobGraph和Flink applicaiton 的jar file , 轉交給這個JobMaster去安排job具體的運行。
  • JobMaster的是用於一個Job的Master, 當集羣裏由多個Job同時運行則會有多個JobMaster同時運行,每一個JobMaster只會負責一個job。當接收到jobGraph時, JobMaster首先會將jobGraph轉換成ExecutionGraph:一個可以指導task並行運行的數據流程圖,並向ResouceManager(RM)申請運行這個ExecutionGaph需要的資源(TaskSlot):比如一個並行度爲8的job,必須有8個TaskSlot才能運行起來, 然後按照ExecutionGraph將task schedle到Taskslot中去, 並定時的對task做checkpoint, 以備重啓時恢復到崩潰前的現場。
  • ResourceManager負責管理FlinkCluster裏所有TaskManager的TaskSlot資源(相當於TM裏的一個運行線程)。當一個TM啓動時,它會將自己的TaskSlot註冊到RM。當JobMaster向RM申請slot時,RM會要求TM將它空閒的slot(已註冊到RM,所以TM知道所有slot的狀態)提供給JobMaster使用,之後JobMaster纔會將相應的Task 安排到slot裏運行。如果集羣裏的TaskSlot不夠, RM會向外部的ResourceManager(比如Yarn/Mesos/Hubernetes)申請新的容器(container) 去啓動新的TM從而滿足JobMaster的slot資源的需求。

1.2.1  展開JobManager後的Flink架構

從以上所述, JobManager是一組Service的總稱, 其中真正管理Job調度的組件叫JobMaster ,負責資源管理的組件叫ResoruceManager, 負責接收client端請求的組件叫Dispatcher(包括Dispatcher和DispatchRestEndpoint)。其實Flink源碼裏有叫JobManager的包和類,功能上也是負責Job調度管理以及snapshot管理,但它應該在Flink某個版本以後就legacy了(估計是從version1.3開始)。這三個服務統稱爲還叫 JobManager,上真正管理作業的是JobMaster。這一點在讀code時讓人迷惑,比如JobManagerRunner啓動的卻是叫JobMaster的類。但是他不叫JobMasterRunner,這也體現了JobMaster實際是取代了JobManager類,保留legacy類是爲了向後兼容。以下是Client, 展開的JobManger(受HA 保護的Dispather, JobMaster, ResourceManager)和TaskManager處理submitJob的流程圖,這個比較圖-1更能體現當前的Flink runtime架構 (Flink 1.6):

 

(圖-3)展開JobManager後的Flink 架構, 來自於《 Stream Processing with Apache Flink》

 

以上的架構嚴格來講在Flink裏被稱作 SessionMode ( Cluster的EntryPoint類都是SessionClusterEntryPoint的子類),  如果沒有外部命令 terminate cluster, 在這種模式下的FlinkCluster 是Long running 的, 多個job可以同時運行在同一個flinkcluster裏。 SessionMode 在Flink的各種部署都是支持的, 包括Standalone, Kubernetes, Yarn, Mesos, 上圖其實是StandaloneSessionCluster的流程。 還有一種模式叫做JobMode, 區別就是Job(或application) 的main class 和 jar 和在JobManager 啓動時通過的啓動參數裝載的, 不需要submitJob的過程, job運行完畢, cluster自動終結, 所有資源釋放。 在這種模式下, Dispather並不負責處理job的提交, 但其他 Client發給DispathcherRESTEndPoint的請求(比如Query, CancelJob), 還是由Dispatcher處理。

Flink的每一種部署模式(deployment mode)都是既支持Session Mode又支持JobMode的 (或partialy support), 區別如上所述, 但在架構上是一致的。 當有由外部的ResourceManager協助硬件資源分配時,流程略有所有不同, 以 FlinkCluster in Yarn 爲例, SessionMode下, 區別只限於多了RM通過Yarn自動啓動TM 的過程(4,5)。

(圖-4)FlinkCluster in Yarn Session mode,  來自於《 Stream Processing with Apache Flink》

關於deployment的細節,請參照後面的將Deployment的章節。

1.2.2  JobMaster

如圖一所示,JobMaster的主要工作是:

1.  JobGraph的scheduler : 將Client提交的JobGraph按照邏輯的向後關係(source -> transform -> sink), 以及並行關係(每個operator的子任務只負責全部數據的中一部分), 將子任務分配到TaskManager的Slot中, 並定期的獲取每一個子任務的運行狀態 (status)。

2. 觸發和管理Job的checkpoint snapshot:對於streaming job,定期的將運行中的每個operator 的狀態(State)數據存入規定的存儲設備, 這些state數據可以用於在Job恢復運行時,恢復相關子任務的失敗前的現場。

 

 (圖-5)JobMaster內部結構

  •  ExcutionGraph (EG) 是JobMaster 最核心的組件,它承擔了JobMaster 上述的的兩大責任: job scheduling 和 checkpoint snapshot  。EG的細節下節展開。
  • SlotPool 存放由所有TM Offer 過來的slot 。Offer 的過程就是圖-3中的3,4,5 或圖-4中的3,4,5,6,7,8。當EG需要slot去執行給sub Task時, 它就從SlotPool里根據一定的策略poll 一個slot ,然後將SubTask打包 (這個在TM講解中展開) 發送相應的TM 去執行 。 SlotPool實現了一個RPCEndPoint : SlotPoolGateway, 如圖-5中所示,感覺這個Gatway是爲TM OfferSlot準備的。 實際上TM調用的是JobMasterGateway (到JobMaster), 然後JobMaster 通過SlotPoolGateway這個RPC 接口與SlotPool通訊的。 看代碼時看到SlotPoolGateway時比較奇怪的, 因爲它作爲JobMaster的組件,是沒有必要實現爲PCEndPoint的。集羣中運行的每一個Job, 都會由一個JobMaster創建出來爲之服務, 每一個JobMaster 都有一個SlotPool存放這個Job分配的Slot 。有一種可能是Slotpool的實現這打算將slotpool共享給所有的的JobMaster ? 如果那樣的Slotpool  需要由Zookeepr 管理做Leader Selection 和 FailOver, 其實也沒什麼必要。
  • JobMasterGateway 是外界(ResourceManager, TaskManager)用來 同JobMaster通訊的RPC接口。
  • RMConnection 和TMConnection(多個)是JobMaster 同TM 和TM 通訊的PRC 通道。這些通道里包裹了RM和TM的PRCEndPoint的AKKA地址,以及永遠RPC call 的 XXXXGateway接口。比如ResourceManagerGateway 和TaskManagerGateay。
  • HearbeatManager 會以Interval爲(10,000 ms),timeout 爲(50,000ms) 向TM和RM發送heartBeat, 如果timeout 發生則相應的ErrorHandling 會出發, 比如重新連接RM,切斷timeout的TM 。interval 和 timeout都是可配置的, 前面的兩個數值是缺省值。
  • FatalErrorHandler : 通常指向ClusterEntryPoint (回顧一下圖-2)。JobMaster 在無法連接和註冊有效的RM時會觸發FatalErrorHandler的onFatalError方法。onFatalError通常會簡單記下log, 然後推出JVM 。
  • RestartStrategy用於在EG中,但Job失敗時,嘗試重啓Job, RestartStrategy 可以在Flink Java/Scala API種指定 。
  • BackPressureTracker,  當一個operator的處理速度小於的上游的下發速度, 數據就會在input buffer 裏積壓, 當buffer滿了的情況, 數據就會無處可放。 Flink將這種情況稱作爲BackPressure 。Dispatch 會持續的通過JM的BackPressureTracker對每一個TM每個 sub Task做Stack trace(100 stack traces every 50ms , configurable) ,然後用可能有BP的stack trace (比如訪問buffer, 訪問網絡棧等)同total tack trace 的比例決定系統是否有Back Pressure風險 。比如 <10%是OK的, <50%是低危的, >50是高危的。這個比率是可以在Flink WebMonitor的Metrics裏看到的。如果是高危的怎麼辦, 實際上Flink就是把他通過Metrics發了出來,沒有做任何handling , 目的是讓用戶手工在工作流種做相應調整, 比如加速和降速Datasource 的輸出速率, 在某個operator 上加cache等。
  •  

 1.2.3  ExcutionGraph 

EG是面向Job 並行運行的圖結構,在JobGraph的基礎上它加入了對Operator並行執行的子任務,以及子任務的輸入輸出的描述 。 

                                                 圖-6 Execution Graph

 

  • ExecutionJobVertex : 對於每個 Operator 或Task(單獨的或chained Opertor) ,EG 都會創建一個ExecutionJobVertex(EJV)對應 。
  • ExecutionVertex: 對於它 的每一個並行子任務  (sub task), EVJ都會創建一個ExecutionVertex(EV)對應 。每一個EV都知道輸出到哪裏 (IntermediateResult), 到哪裏獲取input (ExecutionEdges : 底層數據也來自IRP ), 和執行的Operator類。
  • IntermediateResultPartition(IRP)  : 代表IntermediateResult(IR)的一個Partition 。 它描述了它是由哪個EJV提供數據, 並由哪個EE消費數據的。
  • ExecutionEdge (EE) : 是每個EV的input的描述, 比如source 來自與哪個partition, edge 是sub task的第幾個input 。
  • Execution: 但EV被分配執行時,Exception對象會被創建作爲EV的一次嘗試, 分配slot,  將EV打包 成 TDD(TaskDeploymentDescriptor)並同TaskManagerGateWay 發送給TM (submitTask)  執行。Exception如果失敗, 新的Exception會被創建作爲另一次嘗試。

  • TaskDeploymentDescriptor (TDD): 包括了該Sub Task 所有信息的描述: sub task的執行類 (operator 的類名), 輸入和輸出的描述, job的描述。 TaskManager收到TDD之後創建一系列物理對象執行的對象,把這些些創建在分散TM上對象拼在一張圖上, 實際就形成了EG的物理執行圖。 這個TM的章節在展開。
  • 總起來說EG通過EJV, EV,  IR, IRP, EE構成了一個包含了並行子任務 以及各個子任務間輸入輸出關係的總工作流圖。當scheduling EG的時候, 每個EV都打包成TDD發給TM。TM會將TDD裏的子任務,輸出Partition和輸入Channel創建在TM的物理機上。 把TM的這些物理對象拼接起來,就形成了該工作流物理執行圖 。 Dispatcher就是通過收集這些物理對象的metrics和狀態信息,從而在WebMonitor上更新EG的。

 1.2.3.1  任務分配執行(Scheduler)

EG的Scheduling模式由兩種,一個叫Lazy, 一個叫Eager 。

Lazy的方式適用於Batch Job, 它先將所有的處理數據輸入的sub task 分配執行, 當TaskManager 返回 (同過JobMasterGateway) 已分配成功的信息, EG在根據EJV的上下游關係, 再給相應的EJV分配slot執行。分配的過程如上一小節所述, EJV所有EV都會通過TDD打包,然後要求SlotPool提供slot, 然後將tdd和slot信息都發送給相應的TM去實例化這個sub task然後運行起來 (再TM細述這個時怎麼實現的)。值得一說的是, EJV所有EV都應該一起scheduling , 但當集羣裏沒有足夠的slot時, 同一個EJV可能只有部分EV被schedule了,如果那些沒有分配的相同EJV的EV再一個timeout(default 5分鐘)之後還無法得到slot, task 這時候會失敗, job 也會失敗。所以在計劃Job使用的資源時,計劃的總slot數 (比如當使用Yarn管理resource 時, yn * ys 是job向Yarn申請的總slot數 )一定要大於總的source sub task的總數量(source operator 的數量 * paralelism ), 否則部分soure sub task 得不到資源,再timeout之後就會出發failGlobal 使job 失敗。

Eager 方式使用與Streaming job, 在Eager模式下, 所有EV都必須都能得到slot, 否則schduling 失敗, job 失敗。

 

1.2.3.2  CheckpointCordinator

Flink的Checkpoint這個概念還是有必要簡單說明一下, 當然參考Flink文檔會得到更全面的理解。Flink主要是一個Stream computation的架構,(當然它也可以做Batch, 但Batch並不是Flink的強項), Flink Streaming processing 的一個特性就是Stated Streaming 。 意思就是在它的流式計算的工作流裏, operator的都是可以有狀態的。什麼是有狀態的?相當於一個人睡醒之後還記的自己是誰,然後還能繼續下來的生活,做完沒有做完的事情的意思 : 因爲大腦裏存儲了過去的信息。Flink 的Operator可以像這樣生活的,創建一下StateFull的變量 (比如ValueState<T> ), 然後週期性的將這些狀態存儲一個地方 ,當job重啓, operator 重新實例化的時候, 通過加載這些Sate信息,就能夠從新回到上次重啓前的狀態,然後繼續這個operator的人生。週期性的(Periodically)存儲operator的State 信息, Flink稱作Checkpoint, 每一次存儲叫做一次snapshot 。

CheckpointCordinator的主要功能就是協調促使工作流裏所有的operator都週期性的觸發Checkpoint snapshot 。

換句話說,CheckpointCordinator的主要功能就是向所有的Execution (看前面回顧一下Execution的概念) triggerCheckPoint 。 每一個EG都會創建一個CheckpointCordinator (CC), CC用內建的timer (Executor)定時(根據可配置的interval)的通過RPC向所有的TM觸發他們運載的所有Source Exection對應的Task的CheckpointBarrier。 上一句比較長,分解來說, 每一個EG的Execution 都會對應一個Task (準確的說時SubTask)運行在某一個TM 裏, triggerCheckPoint 就是CC定時的通過RPC調用所有 TM 上所有的Source Task ( 是工作流裏開始位置的處理Source 數據的Task, 不包括Sink Task 也不包括 普通的Transformation Task ) 的triggerCheckpointBarrier方法。當一個SourceTask收到triggerCheckpointBarrier時,  它會命令內嵌的Invokable對象 (Operator, 或Operator Chain的封裝對象)執行 performCheckPoint , 這個過程大概有如下幾步, 很多多步驟的時異步執行的:

  1. CC 對所有的Soruce Execution triggerCheckPoint 
  2. TM 對 所有的 SourceTask triggerCheckpointBarrier
  3. SourceTask對應的Invokable 對象執行performCheckPoint 
  4. 首先 做Barrier 前的工作: 比如對齊和比較多個input channel的barrier 等。
  5. 其次創建Barrier event (只有source需要創建)並向下遊傳遞 : 下游的Operator 的收到這個Barrier , 也會做這5個步驟 ,只是當有多個input channel的時候(Input), 步驟稍微複雜一些而已。Sink 不需要創建Barrier ,因爲沒有下游。
  6. 然後對Invokable對象的所有Sate述,拍Snapshot (克隆一份)
  7. 然後Invokable 將State數據傳遞迴JobMaster,
  8. 最後JobMaster再persist 到指定的存儲中。 

至於CheckPoint怎麼配置,State數據, StateBackend 包含那些, 以及CheckpointBarrier再工作流裏的聯動過程, 我就不贅述了,網上 應該很多介紹, 不過通過代碼閱讀,想強調如下幾點。

  • Checkpoint是對Streaming Job有效的, Batch Operator 不需要有狀態的。
  • Streaming Job 缺省狀態不開啓Checkpoint, 也不能通過Flink configuration 開啓, 只能通過Streaming API 再程序中開啓。 缺省狀態下, CheckPoint 週期被設置爲無窮大,因此永遠不會被執行。
  • AtLeastOnce只要開啓CheckPoint就能達到。
  • 對於ExtractlyOnce, 很多網上很多文章都聲稱這個 Flink的買點。 實際分析一下以上步驟, Checkpoint Snapshot 存儲的是上當barrier 到達operator是它的狀態, 但並不是Operator 意外退出的狀態。所以恢復時,只能恢復到觸發barrier 時的現場,這無法保證source的數據無重複下發。
  • 下面的文檔提供了ExactlyOnce的解決方案,這需要SinkOperator實現TwoPhaseCommitSinkFunction 。https://flink.apache.org/features/2018/03/01/end-to-end-exactly-once-apache-flink.html。 大概意思時SinkOperator向external 下游發送數據時需要分兩步走(TwoPhase), 不過目前沒目前 Flink1.6.1 的 Sink都沒有實現這個功能 。
  1. 數據先暫存到臨時存儲裏, 比如存儲在臨時文件或buffer裏 ,這個叫PreCommit .
  2. 當所有的非Sink operator 都做完了CheckPoint , 當barrier 到達時, Sink再將臨時存儲中的數據一次性發送給下游。
  3. 當然如果下游支持Trasaction的話 (比如, precommit, commit ), 臨時存儲就不需要了。

 

1.3  TaskManager

在源碼裏, TaskManager 類同 JobManager被JobMaster取代 一樣,TaskExecutor取代了legcy TaskManager 併發揮着它的作用。本文裏TM指的是TaskManager的整個進程, JM代表JobManager整個進程。JM的核心類是JobMaster (當然還有ClusterEntryPoint, Dispatcher, WebMonitor和ResourceManager, 但是都起的是輔助作用), TM裏的核心類是TaskExecutor 。還有一個比較混亂的Term就是Task。Task對應JobGraph的一個節點,是一個Operator。在ExecutionGraph裏Tast被分解爲多個並行執行的subtask 。每個subtask作爲一個excution分配到TM裏執行。但比較然人抓狂的是在TM裏這個subtask的概念由一個叫Task的類來實現。所以TM 裏談論的Task對象實際上對應的是EG裏的一個subtask ,如果需要表述Task的概念,用Operator。先澄清一下Terminology ,以免語言混亂。

 

                                           圖-7 TaskExecutor

  • TaskSlotTable 是TaskExeutor最核心的數據結構, 它存放着TM所有的TaskSlot以及再Slot裏運行的Task。 TaskSlot只是一個邏輯單位,它並不綁定或連接任何資源, 但它規定了TM裏能夠並行執行的SubTask的總數量。當TM 啓動時,總slot數由命令行參數傳入(-ys,default 爲1或flink configuration 裏設置的), TM創建這個指定數量的TaskSlot後供分配給SubTask使用。如前所示,TM裏的Task實際指的是EG裏的SubTask ,後面會詳述,Task的數據結構和執行過程。
  • NetworkBufferPool(NBP)是用來爲InputGate(IG)和ResultPartion(RP) 分配BufferPool的。一個Task要通過InputGate 從遠程另一個Task的ResultPartition 要input數據,這個Task 同時也要將輸出的數據放到自己的ResultPartition裏。IG和RP都需要Buffer,而這些Buffer都從NetworkBufferPool去申請, NBP的poolsize由flink configuraiton 指定。
  • MemoryManager : 用於大量分配內存。在Bash模式下,輸入數據unbouned 的。一些subtask  需要對全體輸入數據進行 Sort或者Hash, 比如outJoin 。此時MemoryManager用大快速和大量的分配內存。關於Flink的內存管理,後面有一節詳述 。
  • IOManager:用於將內存的數據和硬盤之間交換。同樣在Bash模式下,輸入數據unbouned 的,如果EG非常複雜,Task的數量巨大。此時NetworkBuffer Pool分配的buffer不是夠用的。 IOManager能夠用hard disk作爲Buffer還緩存數據,當Localbuffer夠用時,再將數據從硬盤裏換進,供本地或遠程消費。
  • ChannelManager是TaskExeutor非常關鍵的服務,他負責RP與IG之間快速的數據交換,後面專門有一節細述ChannelManager 。
  • BlobCacheService 用於加載將客戶的jar文件 ,Task裏年的Invokable需要調用jar文件裏代碼, 比如 source, sink, tranformation operator, 以及他們的依賴。
  • LocalStateStoreanager用於存取TM本地硬盤上的Sate數據, 但Task做CheckPoint是,除了向JM返回snapshot,它也會在本地存儲。
  • HeartBeatManager和FatalErrorHandler, 和JM裏的類似。
  • TaskExectorGatway, 同其他的Gateway一樣,是用於cluster裏的其他組件(JM和RM)遠程調用TM的stub interface .
  • ResourceManagerConnection 和 JobManagerConnections (注意,可以連接多個JM ) 用於遠程RPC call RM 和 JM。
  •  

1.3.1  Data Exchange (ChannelManager)

個人覺得Task之間的數據交換,是TM裏的核心和重點,也是 Flink runtime的核心、重點和難點,它的質量是區別於其他大數據系統的關鍵,它的質量直接影響Streaming/Batch 任務的延遲及吞吐量兩大指標。人們選擇一個大數據流式框架最想先了解的就是這兩個指標, 至於是不是Statefull, 可靠性可用性集成度怎麼樣,編程接口是否簡單不是不重要,但不是最關鍵的,是次要考慮的。所以本節作爲本文的重點,我們深入解讀一下TM管理的數據交換。

首先強調一下Task之間的在網絡裏或在本地的數據交換, 不是Task管理的,是由TM (或TaskExecutor)管理的。如下圖所示。

圖-8 JM和TM的數據交換

JM將EG的Execution 提交給 TM,TM將Execution轉化爲Task執行, 並負責Task之間的數據傳輸.

回想一下EG的數據結構:

  • 它包含多個EV對象, 每一個EV代表一個並行子任務(sub task ),
  • EV 產生的結果存儲在termediateResultPartition(IRP)裏。
  • 多個屬於相同EJV的EV連接的IRP組成了IR(IntermediateResult)代表由一個Task節點產生的結果數據,
  • 這個結果數據Re-Partition後,每個IRP需要根據新的Key重新分區(sub partition )然後通過EE (Execution Edge)發送給下游的SubTask 。
  • 每一條EE的source 是一個IRP, target 是另一個EV .
  • 如下圖所示。

(R

圖-9 EG 的數據結構 和 數據流

爲了將JM的EG的邏輯工作流在物理機上執行起來, TM創建了相應的物理數據結構。

ResultPartition(RP)概念上對應着EG中的IRP(IntermediateResultPartition), 它負責存放一個Task生成的所有結果數據。

ResultSubpartition (RS) 是對應着RP的數據Repartion之後shuffle數的中的一個parition ,它存放着RP重新分區後發送下游的某一個sub task的分區數據。

InputGate(IG): 在EE中, IRP是source, EV 是target, 它描述了分區數據的流向, 但對不物理實現這樣的結構還是不夠的。 IG 由此而引入, RP是EE上的數據發送端(IRP的物理實現),IG是接收端上與RP功能類似的組件, 負責收集來自上游(RP)的數據區 (data buffer)。

InputChannel(IC) 是接受端與RS功能類似的組件,IG使用不同的IC連接不同EE上的RP 中特定分區的數據區。 比如在data shuffle的過程中多個RP都產生鍵值爲a1的RS ,  這些RSa1中的數據最終都會流向同一個IG中的IC, 這其中每一個IC承擔接受上游的RP中每一個RS-a1中的數據。

數據在EE上的通訊, 由RP到IG ,數據是以binary 形式傳輸的。也就是說數據進入RP前,會由總Serializer 將data record 序列化成binary format, 並存入data buffer 中。 Data data buffer 由IG 接受傳遞給下游的Oparor 前, 由Deseriealizer將數據從DataBuffer反序列化成data record, 供該Task消費使用。 Data buffer相當於高速公路上運輸乘客的大巴車, buffer中的數據相當於乘客 。 每個大巴車的形狀, 運載量也是固定的,  不裝滿不發車 。它能極大的增加了總體數據傳遞傳輸量和創數率, 增加系統的吞吐量,  但同時也增加的單個數據的延遲 。缺省情況下2048 個buffer會被創建,   每一個32K字節 。對與比較大的record 需要多個buffer 承載 。每一個RS和IC有一些大buffer組成, RP和IG就是這些大巴車的裝載者和卸載者 。

關於DataBuffer如果創建和管理, 參考後面的內存管理章節。

 

另外值得一提的是, 不同的對於RS的實現決定了實際的數據傳輸的方式。

PipelinedSubpartition支持streaming模式下的數據傳輸 (大部分的RS都是這種實現): 數據壓滿一個buffer (buffer size是configuration 指定)就向下傳遞 。 SpillableSubpartition 只有當RP的類型爲BLOCKING是纔會創建出來(Batch job 中的部分RS 是這種實現)。它支持在事先分配的Buffer不夠用的情況下, 將Buffer中的數據Spill到硬盤中,從而該RS佔有的Buffer得以釋放。因爲他會涉及IOManager (應該只有它會使用到IOManager), 所以咱們細說一下。

那什麼是BLOCKING類型的? 從ResultPartitionType是這樣定義:BLOCKING(false, false, false)可以看到, 3個false 分別如下:

  1. BLOCKING類型的RP的DataExchangeMode不是PIPELINED,   對於Batch Job, 只要下游需要shuffle, DataExchangeMode 就會被設置爲 BATCH mode,而不是PIPELINED 。
  2. Task沒有BackPressure的, 對於Batch Job, 所有的 operator 都沒有Backpressure , Backpressure 在streaming job Backpressure纔會被enabled 。
  3. 數據不是Bounded 。對於Batch Job, 因爲BatchJob的數據流是unbounded (沒有window的界限), streaming job纔會有Window operaor, 纔會有界限。

簡單來說只有Batch job 裏的一些RP類型是Blocking的 ,因爲BatchJob總一些需要shffule 輸出的operator纔會有纔會啓動Blocking模式, SpillableSubpartition纔會被創建。

注意 上面說到了的三個模式:概念比較混亂。

1. JobMode (Batch or Streaming):任務模式, 由於決定ExecutionConext

2. ExecutionMode (PIPELINED (default), BATCH, PIPELINED_FORCE, BATCH_FORCE):可配置的數據流整體模式, 目的是通過一個可配置的ExecutionMode(可在ExecutionConfig中配置)來決定所有Operator的DataExchangeMode。它是Optimizer在優化Edge的shipStrategy和DataExchangeMode做策略選擇的依據。 詳細看DataExchgeMode代碼中,DataExchgeMode與ExecutionMode的Mapping關係。

 

3. DataExchangeMode(PIPELINED, BATCH, PIPELINE_WITH_BATCH_FALLBACK ) : 根據下游Operator,和 ExecutionMode, 由Optimizer 決定 數據下發模式 , 

下面是當JobMode 爲Batch, ExecutionMode 爲PIPELINE時, DataExchangeMode應該優化的結果。可以看出來只要下游需要Shuffle,  DataExchangeMode就會被優化成BATCH模式, 此時Flink會創建SpillableSubpartition 。

DataExchangeMode.PIPELINED,   // to map
DataExchangeMode.PIPELINED,   // to combiner connections are pipelined
DataExchangeMode.BATCH,       // to reduce
DataExchangeMode.BATCH,       // to filter
DataExchangeMode.PIPELINED,   // to sink after reduce
DataExchangeMode.PIPELINED,   // to join (first input)
DataExchangeMode.BATCH,       // to join (second input)
DataExchangeMode.PIPELINED,   // combiner connections are pipelined
DataExchangeMode.BATCH,       // to other reducer
DataExchangeMode.PIPELINED,   // to flatMap
DataExchangeMode.PIPELINED,   // to sink after flatMap
DataExchangeMode.PIPELINED,   // to coGroup (first input)
DataExchangeMode.PIPELINED,   // to coGroup (second input)
DataExchangeMode.PIPELINED    // to sink after coGroup

因爲batch 工作模式下的 shuffle通常會伴隨的對整個group的排序, aggregation等, 下游需要得到(該group的)全集纔可做這些操作。全局數據量比較大, 物理內存Buffer很可能不夠用, 這時候SpillableSubpartition(在IOManager的幫助下)可將一部分硬盤當Buffer來用, 者極大的幫助了RP端的數據緩存。但SpillToDisk必須實現在RP端嗎?不可以在接受端(InputGate)實現嗎? 接受端的計算需要做密集的內存訪問(sort, hash, etc ),  這些算法都是在整個數據集上的操作, 所以數據需要緩存在MemoryManager管理的MemorySegment中 從而提高存取效率, 也能爲使Flink對內存做有效的管理。可現實是Flink的BatchJob需要消耗巨大的內存, 這跟接收端不使用硬盤做Buffer由很大關係。至少Flink 1.6.1的Memory是不使用硬盤做緩存的,雖然有HybridOffHeapMemoryPool, 而且它也使用了DirectMemory 來分配MemorySegment, 但並沒有實現用磁盤文件來映射內存 , 所有當上游數據量很大, 但內存不足時, Flink task會很快的out of memory。之後看看Flink的最新版本是否有改善。

 

R

 

 圖-10 使用EG控制物流數據流(數據交換)

 圖-10描述了Task之間數據交換的大概流程。

  • 這是一個最簡單的MapReduce工作圖: 由一個Map和一個Reduce組成。這個Job並行度爲2, 並運行在兩個TM上。
  • M1, M2是同一個Map Operator的兩個並行的子任務。R1, R2是同一個Map Operator的兩個並行的子任務 。 
  • M1產生的數據存入RP1 中 (arrow 1)。RP1 通知JobManager(準確的說是JobMaster)(arrow 2) RP1中有數據產生。
  • RP1中其實產生一些SubParitition(RS) . JM 會通知R1和R2他們分別感興趣的RS已經準備好。(arrow 3a,3b)
  • R1, R2向 RP1發起數據請求 (4a,4b), 這些請求會觸發數據在兩個Task之間的傳輸 (5a,5b) 。之中5a是本地傳輸, 5b是跨TM通過網絡傳輸。

 

 圖-11, 跨TM的兩個Task之間的數據交換

 圖-11給出了跨TM的兩個Task之間的數據交換的更多細節。

  • M1中的MapDriver持續的產生record對象,然後傳遞給RecordWriter 對象。RecordWriter 由ChannelSelector,  一系列RecordSerializer (每一個下游的RS都有一個對應的Serializer)和一個BufferWritter(更準確的說ResultPartitionWriter )組成。
  • 不同ChannelSelector的實現 (BroadcastPartitioner, ShfflePartitioner, ForwardPartitioner, etc)會由不同的選擇RecordSerializer 的策略。比如BroadcastPartitioner會將record發給所有的RecordSerializer, ShfflePartitioner會根據record的key決定發送給相應的RecordSerializer 。
  • RecordSerializer 將record序列化成二進制數據, 並把他們存入一個固定大小的buffer中, 當buffer 寫滿後, 由BufferWriter將該Buffer 寫入相應的RS中 (本例中是RS2)。
  • RP通知JM 本RP中的RS2已經有填滿數據,可供消費。 JM通過EG查找所有的消費這個RS的Task,並通過TaskExecutor通知到這些Task對應的IC, 本例中爲IC1。
  • IC1向RP申請傳送RS2中的數據 。RP將Databuffer 交給基於netty實現的的ChannelManager,(發送後, RS2中的buffer 得以釋放,還給NetworkBufferPool) 並由它發送給對端TM的ChannelManager , 從而數據將存儲到IC1中。
  • ReduceDriver 或者其他的Driver/Invokable的run的方法是每個一個Task的Engine 。ReduceDriver 的 Run()方法是一個while循環, 不停的從RecordReader(或更準確的名字是MutableObjectIterator)
    讀取next record, 根據他們的key 來決定他們是否來自同一個group, 然後調用相應的ReduceFunction進行reduce 。
  • MutableObjectIterator的next record的binary實際上來自於IC(本里中爲IC2)中buffer, 並通過Deserializer將binary轉化爲相應的Record。  當buffer 中的數據全部讀完, 該Buffer 得以釋放,還給NetworkBufferPool 。
  • 從此完成一個buffer從M1到R1的數據傳遞。

 

1.3.2  Task 提交與執行 (TDD, Task, AbstractInvokable , Driver , ...)

根據前面的介紹,Task是由JM(通過EG) 經過Scheduler (申請和Offer slot resource和根據schedule 策略,等等) 提交給TM執行的。申請resource 和提交Task都是通過JM, RM 和TM之間的RPC通道完成的。那麼具體提交了什麼呢?Task如果執行的呢?Task如何知道誰是上游從而建立InputChannel的?

實際上這是一個 從EV->TDD->Task->AbstractInvokable->Driver ->具體的Oparator 實現類變形的過程。

 

1. 從EV 到TDD : 

TaskDeploymentDescriptor(TDD) : 是TM在submitTask是提交給TM的數據結構。 他包含了關於Task的所有描述信息:
  • TaskInfo : 包含該Task 執行的java 類 , 該類是某個 AbstractInvokable的實現類 , 當然也是某個operator的實現類 (比如DataSourceTask, DataSinkTask, BatchTask,StreamTask 等), 
  • IG描述 :通常包含一個或兩個InputGateDeploymentDescriptor(IGD),
    •   通常一個Operator 有一個或多個邏輯的輸入, 比如Map/redue 只會有一個輸入, join會有兩個輸入。所以IGD描述是一個數組。
    •   每一個IGD都會包含一組InputChannelDeploymentDescriptor(ICD), 每一個ICD是該子任務對應的一個inputChannel 。 
    •   每一個ICD 包含上游RP的ID和IP 地址。那麼IC怎麼知道應該消費RP的哪個RS呢?
      •  當IG 和 source RP 的並行度相同時, 每一個IC都會去消費各個SourceRP 中同子任務序號相同的RS .
      •  當IG 和 source RP 的並行度相同時,    通過對雙方平行度進行一番取餘, 來決定該IC 要消費的RS: 0個或多個。
  • 目標RP的描述: ParitionId, PartitionType, RS個數等等
  • 其他的一些描述 : JobId, ExecutionId, SlotNumber,  subTaskIndex (子任務序號) , 等等。

 

不難看出,EV包含上述的所有信息,EV的一個方法createDeploymentDescriptor,完成了上述變形。JM 在向TM submitTask時,傳遞的是TDD不是EV。爲什麼要做此變形的而不是將EV直接傳過去既然他們很類似? 我想這個一個設計模式問題, EV是作爲ExcutionGraph的中的頂點 ,它最好只存在於JobMaster 的物理圖中, 而不是作爲參數傳遞給其他組件,從而維護它的獨立性和單一性。參見Descripor設計模式。

當TaskExecutor(TM)接受submitTask 的RPC調用從而得到TDD時, 他會將TDD實例化爲一個在TM上可以執行的對象 : Task 。

 

2.  Task : 

Task 是一個Runnable 對象, TM接受到TDD 後會用它實例化成一個Task對象, 並啓動一個線程執行Task的Run方法。

Task實例化時, 他會將TDD中的IGD實例化成InputGate (IG) 和 InputChannel(IC), 將RPD實例化成RP . 在 Task 的Run 方法被調用時, 它根據TDD的 TaskInfo, 使用URLClassLoader將用戶的operator類從HDFS加載, 並實例化TaskInfo所描述的AbstractInvokable對象, 並將IG, IC, RP , 還有其他所有的AbstractInvokable需要的服務類(MemoryManager, IOManager, CheckoutResponder, TaskConfig etc )都傳遞進去, 然後調用AbstractInvokable 的 invoke 方法。

3.  AbstractInvokable

如之前給出的一些例子 : DataSourceTask, DataSinkTask, BatchTask, StreamTask 。 它們是Task.Run()時, 通過TaskInfo加載並實例化的。 這些Task Operator的源代碼都在

org.apache.flink.runtime.operators 或org.apache.flink.streaming.runtime.tasks下面 。每個BaskTask的需要的工具都是類似的, 只是計算的流程不同 , 所以BaskTask的invoke方法調用時, 它根據TaskConfig(信息繼承與TDD的TaskInfo) 加載和實例化不同的Driver 類, 並調用Driver的run方法由Driver指揮流程(input, calcuate, out , etc )。

4. Driver 

Driver類和AbstractInvokable位於同一個包內。 每一個Driver的run() 大都是一個循環。 不停向IG 要next record , 寫metrics, 調用function 計算, 然後見結果發給RP。只不過有些drver一次計算需要兩個record, 有些driver 需要兩個record 來自不同的IG , 有些需要將所有的input全部收完才計算, 有些在window expired後才計算, 等等。下面是FlatmapDriver 的run() 。

while (this.running && ((record = input.next()) != null)) {
   numRecordsIn.inc();
   function.flatMap(record, output);
}

 

1.3.3  內存管理(NetworkBufferPool, MemoryManager, IOManager 

在stream模式下數據處理是有界限(bondary)的, 每個window的處理所使用的內存是相對比較小 的,  所以Flink stream job 通常使用的內存較小。

但在batch 模式下, 數據處理是無界的 (所謂無界就是沒有Window),如前面所示, 很多job 需要將上游所有的input都取乾淨,纔開始計算, 如sort, hash join, cache 等, 此時所需要的內存是巨大的, 比如上游operator 讀取300G文件然後map成record, 下游operator 需要將這些record 同另一組輸入做outer join  。所以Flink內存管理主要針對的是這些batch job 。

總起來將, Flink的內存管理還是比較失敗的, 至少在我用的版本里(1.6.1) , 主要原因還是 MemoryManager並沒有聯合IOManager Disk去擴展內存。但我覺的MemoryManager的引入, 就是爲了管理Flink的內存, 以防止OutOfMemoryException的發生 (如Spark一樣 )。 防止溢出最直接的方法就是當系統內存不足或超過throttle時, 使用DiskFile以補充內存 , 從而完成 那些內存消耗巨大的操作。 只是version 1.6.1 還沒做好 ,或許以後版本會做好 。

先看下Flink的內存管理機制吧 。

從概念上將, Flink 將JVM的heap分成三個區域。

  • Network buffers pool:  是一些 32 KiByte MemorySegment用於TM之前數據的批量傳遞。回憶一下前面圍繞圖-11的介紹。Network buffers Pool 在 TaskManager啓動時分配的。 缺省2048 buffers 被分配,可通過 "taskmanager.network.numberOfBuffers"調整。
  • Memory Manager : 是另外大量的  32 KiBytes MemorySegment, runtime algorithms (sort/hash/cache)使用這些buffer 用於將 records存入緩衝區 之後應用算法 ,record 是以serialized 的形式存儲在 MemorySegment裏的 。 這些MemorySegment Pool 是在TaskManager啓動時分配的 。MemorySegment Pool 的大小,可以用兩種方式設置。
    •   Relative value (缺省): MemoryManager在所有的service 啓動後(包括network buffer pool ) 會計算heap 剩餘總量, 然後按一定比例 (by default 0.7) 作爲 pool size 。 這個比例可通過 "taskmanager.memory.fraction" 配置。
    •   Absolute value:  科通過 "taskmanager.memory.size" 配置一個絕對值, 比如10GB。
  • Remaining (Free) Heap:  剩下的Heap用於存儲 TaskManager's 數據結構, 比如 network buffer  Deserialized 後的 record 。

              

 

 圖-12 Flink Memory

Network buffers pool 和 memory segment pool 都是TM啓動時就分配的, 它們生存於JVM的老年代, 所以不會被GC回收的。只有Free Heap區域在新生代裏生存。 

IG/IC, 和RP/RS 的record存儲在network buffer 裏,有些RS (SpillableSubpartion)需要Spill to Disk ,因此他們需要IOManager 的幫助。

需要sort/hash/cache的Task瞬時內存消耗非常大,因此從IG接收record後就將他們存儲memory segment pool, 之後在對serialized record 應用 算法 。

其他的Task大都是pipeline 方式, 來一個消費一個, 瞬時內存消耗不會大。此時這些Task會使用FeekHeap 區域的內存。

如果沒有人爲錯誤, 系統不會有瞬時的大量內存申請, 所以不會有OutOfMemoryException , 所以FreeHeap的區域應該是比較輕鬆的。

可是問題是, memory segment pool 目前沒有使用DiskFile 作爲OffHeapMemory, 它能夠裝載上游下來的巨量 blocking 輸入嗎? 這就是Flink 1.6.1 的問題。不過這個MemoryManager實現的好不好的問題, 設計上是有防止OutOfMemoryException 的組件的。這個就期待Flink 新版本 補強這部分功能吧。

前面說“serialized record 應用 算法“,這是怎麼做到的呢?  Flink 實現了大量的 TypeSerializer 和 TypeComparator,  他們懂得record 是如何序列化到字節數組裏的,所以也知道如何將record部分地deserialize 。通常算法只是使用record的某個或某些field。 部分deserialize極大的降低內存使用, 提升了數據存取的速度, 從而極大的提成了內存的使用效率。 serialized 形式的record 數據是Flink 能夠將他們在TM之間, 跨進程和跨網絡的傳輸, 它是分佈式系統需要的數據形式, 同時, 它也爲MemoryManager 將它們在內存和硬盤之間交換提供了條件。

IOManager 使用FileChannel將MemorySegment同DiskFile建立映射, 從而實現將數據在MemorySegment和DiskFile之間寫入和讀回。

所以,雖然MemoryManager目前還有些問題,  在這種設計下, Flink的內存使用效率肯定會進化的極好 。

 

1.4  ResourceManager

RM在Flink內部是Flink cluster 裏slot資源的管理者 , TM 提供slot,JM (JobMaster)消費slot 。 RM同JM和TM 都要保持心跳,以保持slot市場的活躍 ,以及在TM或JM失敗的時候通知給對方。

總起來說RM的作用主要包括:

  • 啓動和獲得TM, 從而獲取Slots 。
  • 提供JM和TM發送對方的失敗通知。 如一個TM的心跳停止了, JM會通知消費該TM slot的JM。
  • 當註冊過來的TM的slot有剩餘時, RM會緩存起來。

第二、三項功能比較好實現。第一項功能需要同外部的集羣管理器合作才能實現。所用RM是一個隨環境不同而不同的組件。在不同的集羣環境裏, RM有不同的實現類。

市場上比較流行的集羣資源管理器主要有Yarn, Mesos, Kubernetes, 和 AWS ECS 。其中Yarn, Mesos中, 可以利用ApplicationMaster/Scheduler是資源調度器,只要將RM在ApplicationMaster中運行起來,理論上 RM就可以Yarn, Mesos的master通信,爲TM分配容器和啓動TM。實際上JM的所有組件(Dispatcher, RestEndPoint, JM, ClusterEntryPoint, etc)都在ApplicationMaster啓動的。

Kubernetes和ECS 沒有ApplicationMaster的概念, 但TM可以作爲一個有多個副本的deployment (K8s) 或 service (ECS) 運行。此時 TM 的多少(規模)是由deployment/service 根據一定策略自動擴容的而不是根據Flink需要的slot數量。Flink並沒有實現特殊的ResourceManager和K8s/ECS集成,此時的FlinkCluster使StandaloneCluster。如果能一個K8s/ECS 特殊的RM和集羣的master通訊,使TM能夠能按需擴容而不是自動擴容,那就和在Yarn裏一樣完美融合, 而且能夠用上Docker的服務, 畢竟Yarn目前只是使用JVM作爲容器,並沒有真正達到真正資源的隔離 。

RM在不同cluster以不同的方式啓動新的TM以補充slot資源,當然當一個job 結束時,它也會同集羣的master通訊,釋放container,關閉完全空閒的TM。

在不同的集羣環境裏,RM能夠管理TM的生命週期,那麼誰來啓動和結束JM 呢? 請參考後面的 Flink Deployment 。

1.5  HighAvailability 

 Flink的 HighAvialbility 主要有兩個服務組成 : LeaderEleketronService和LeaderRetrievalService 。

1.5.1.  LeaderEleketronService

該Service是用來選舉Leader的 。假設系統裏啓動了兩個Dispatcher 。先回顧一下什麼是Dispatcher , 看圖-2, JobMananager 的進程是ClusterEntryPoint的main 函數啓動的,ClusterEntryPoint啓動了 WebUI, Dispatcher 和ResourceManager。當用戶提交了 Job時, Dispather 實例化一個新的JobMaster 來管理這個job, Dispatcher也是CheckPointBarier 的發起者, 同時也是來自WebUI REST 後臺的請求的處理者。Dispatcher 的作用承上啓下, 作用非常核心,不可缺失,所以Flink啓用HA服務來保護它。 Flink 系統裏, 使用HA 保護的核心服務還包括ResourceManager ,JobMaster 還有WebUI 的REST Endpoint。當系統裏啓動兩個Dispatcher 誰來當Leader呢? 這就要LeaderEleketronService (LES) 的決定了。

目前Flink的起作用的LES使用ZooKeeper 實現的。實現方式就是兩個Dispather都試圖搶佔的特定ZooKeeper Path (dispather latch path)  的LeaderLatch(參見 curator framework),誰先搶到了, 就被選舉成Leader , leader的 AKKA URI 和UUID被被寫道一個特定的ZooKeeper path 中(leader path)  。只有Leader 是工作的,因爲其他組件在使用Dispather時會向獲取Leader Dispath的URI, 然後才RPC的 。 其他競爭者不會接受到RPC, 他們只有繼續監聽,如果當前的Leader 退出了, LeaderLatch被釋放了, 從而新的leader會被選舉出來。

1.5.2.  LeaderRetrievalService

那麼對於Dispatcher的使用者, 怎麼知道誰是Leader呢? 這就需要LeaderRetrievalService了 (LES ) 。 對於用ZooKeeper 實現的LES, 它只需要監聽一下leader path, 它就知道誰是leader 了 。

比如, 當你用fink run 命令提交job 時, 假如系統裏有多個Fink REST Enpoint, Flink的ClusterClient 對先使用LES獲取Leader REST Endpoint, 然後纔會將job  發送過去。 

 

除了ZooKeeper的實現, HAService 還可以以來外部的名字服務(如DNS, LoadBalencer , etc ) 實現。在LES和LRS 永遠返回 HA保護的服務的URI(無論是AKKA PRC URI, 或REST URI )。 該URI永遠保持不變, 如果提供服務的server失敗, 名字服務會將給URI映射到另外一個工作的server 。 具體請參考 StandaloneHaServices的代碼。

 

1.6 Flink Deployment

目前爲止, Flink 支持大概4種部署方式或4種cluster 類型,MiniCluster(或LocalCluster),  StandaloneCluster,YarnCluster,MesosCluster 。 雖然前面提到過Kubernetes , ECS , Docker , 但他們的本質是將JM和TM docker 化 , 從而是他們運行在真正的 dockers裏面 , cluster 還是 StandaloneCluster 。

圖-3 是StandaloneCluster , 圖-4是YarnCluster , MesosCluster 與YarnCluster 類似, MiniCluster是運行在IDE(e.g. IntelliJ )的虛擬cluster , 它主要用於 FlinkApplicaiton的調試 。在Session 模式下,表面上可以看來StandaloneCluster 與YarnCluster基本流程是一致的, 只不過是啓動JM 和TM的啓動機制不同而已。YarnCluster/MesosCluster 裏TM是由他們各自的ResourceManager實現根據需要啓動的。 在 StandaloneCluster下, TM是手工啓動的。其實在YarnCluster下, JM 也是Yarn啓動的。其實, 如前所述, StandaloneCluster與YarnCluster / MesosCluster的本質區別是, 前者的用於TM硬件資源是管理員手工分配的, 後置是有RM同集羣管理器協調自動分配的。

從Flink內部看, 是爲了支持這些異構環境, Flink 對於不同的Cluster, 實現了不同的ClusterEntryPoint用於啓動不同ResoureManager ,以及Dispatcher 。不同集羣類型的ClusterEntryPoint對JobMode和SessionMode有 不同的實現, 比如YearnSessionClusterEntryPoint和YarnJobClusterEntryPoint。Session模式下, Dispatcher使用的時StandaloneDispatcher, Job模式下,使用的是MiniDispather 。 關於什麼是JobMode和SessionMode, 請參考圖-3下面的解釋。

如前所述,JM接受jobGraph的對象或對象文件(序列化後),jobGraph由FlinkClient生成, FlinkClient和JM位於不同的JVM (MiniCluster除外)。如果JobCluster無法由FlinkClient啓動 (通過某種方式),則JobGraph生成以後需要存到指定位置,在手工啓動JobCluster讀入,這樣的過程比較複雜。基於目前的架構,儘管Flink在各種集羣環境裏對job mode , session mode 在代碼上都由支持, 但job mode 通常由於集成度不夠好,用戶無法方便使用。

 

我用一個表來描述他們跟具體的不同以及Flink是如何支持這些異構環境。爲了使這個表不至於太龐大,先把不同的cluster 類型表述下先。

1.  MiniCluser 

用於在IDE(IntelliJ, Eclipse) 運行FlinkApplication 的小型FlinkCluster環境,JM和TM運行在同一個進程裏,主要用於在IDE裏調試Flink Application。

2.   StandaloneCluster

可以運行在單機或多機上的FlinkCluster環境,  JM和TM運行在不同的JVM裏。只要有JRE , 無論在Windows ,Linux都可以搭建StandaloneCluster。

用戶可以使用FlinkClient 的command line,或API 直接向JM REST URI 提交job ,具體看Flink CLI 的 “-m" 選項。StandaloneCluster非常有利於搭建單元測試,集成測試以及演示環境。

3.   YarnCluster

在Hadoop集羣裏搭建的FlinkCluster, JM和TM都運行在Yarn管理的容器裏。JM做爲Yarn裏ApplicationMaster ,Flink cluster作爲Yarn的一個Application運行。

Yarn是Flink與之集成度最好的集羣管理器。 用戶可以直接使用FlinkClient 的command line,或API 直接向Yarn提交job , YARN 會自動的啓動(或連接現有的)Flink JM, 自動啓動需要的TM,而後在該Flinkcluster運行任務。

具體看Flink CLI 的“-m yarn-cluster" 和 ”applicationId" 命令行用法。


4.   MesosCluster

 在Mesos集羣裏搭建的FlinkCluster, JM做爲Mesos裏Scheduler , 一個 Flink cluster作爲Mesos的一個Framework運行, JM和TM都可以運行在Mesos管理的容器裏。 

TM由JM的resource Manager同Mesosmaster 協調按需自動啓動和銷燬。JM 需要先使用Marathon 創建服務, 由Marathon 啓動。Marathon服務的後端都是運行在Mesos的容器裏,

而且Marathon服務一個高可用性服務。  

用戶可以直接使用FlinkClient的command line,或API 直接向Marathon 創建服務提交job ,具體看Flink CLI 的 “-m" 選項。

 

個人覺得Flink 與Mesos的集成度可以在提高一些。目前用戶需要手動的爲JM創建Marathon Service, 可參考下面寫一個簡單的配置文件, 調用flink的shell腳本mesos-appmaster.sh啓動Flink

MesosSessionCluster的JM (該JM會啓動MemsosResourceManager用來啓動需要的TM)。

yhttps://ci.apache.org/projects/flink/flink-docs-release-1.6/ops/deployment/mesos.html

  

Flink 完全可以改進一下Client端, 添加集成Mesos Marathon需要的 ClusterDescrptor類,CLI類(比如MesosJobClusterDescripor, MesosSessionClusterDescripor,FlinkMesosSessionCLI ),豐富“-m" 選項,

由ClusterDescrptor類自動創建和銷燬JM (Marathon 服務)

 

5.   Kubernetes/ECS/Docker

Flink 對於Kubernetes/ECS/Docker在源碼級別並沒有任何支持。只是說,可以將StandaloneCluster的JM和TM運行在這三個以Docker 作爲容器服務的

集羣環境裏。所以是,Flink 與他們的集成度, 比較低 。Kubernetes的具體做法分別對StandaloneCluster的JM和TM做兩個deployment(就是可以平行擴展的docker group ), 它們分別啓動StandaloneCluster的JM和TM (通過start-console.sh腳本), 然後對JM的

deployment做一個Service使其具有高可用性,當然需要將該Service的URI傳遞給TM,前面說過StandaloneHAService是靠統一的URI來提供HA服務的。更詳細的請參考下面。

https://ci.apache.org/projects/flink/flink-docs-release-1.6/ops/deployment/kubernetes.html

ECS和Docer的實現同Kubernetes 類似, 只是在各自使用的技術名稱不同而已。ECS的AutoScalingGroup等價於Kubernetes的Deployment。 common sense是一樣的, 只是各家用各家喜歡的名字。

總之,將Flink的部署在上述集羣裏, 目前手工的工作還是比較多的,而且TM的數量是預先設定的,或按策略自動擴容的, 並不是最優的由Flink RM 指導的按需擴容。得到的好處最大應該是Docker的容器服務。畢竟Yarn或Mesos對Docker的支持並不是很好。

個人覺得Flink 與他們的集成度可以在提高一些。同提高Mesos的集成度的想法一樣, 在目前Fink的框架下,只需要改進FlinkClient,添加集成Kubernetes API-Server需要的 ClusterDescrptor類,CLI類(比如K8sJobClusterDescripor, K8sSessionClusterDescripor,

FlinkK8sSessionCLI ),豐富“-m" 選項,由ClusterDescrptor類自動創建和銷燬JM (JM 的Deployment和 Serice ), 添加K8sResourceManager, 用它來創建及擴容TM的deployment 。

 

Cluster Type  JM 啓動方式  TM啓動方式       ClusterEntryPoint   ResourceManager SuportedMode
 Mini

調試環境啓動的Flink Client是

LocalEnvironen或

LocalStreamEvronment,

它們的execute方法會

先啓動MiniClusterEntryPoint

由MiniClusterEntryPoint啓動  MiniClusterEntryPoint
.java

 StandaloneResource

Manager.java

只支持JobMode
 Standalone 手工,如sart-cluster.sh腳本
手工,
如flink-consol.sh
腳本

StandaloneSession

ClusterEntryPoint.java

 

StandaloneJobCluster

EntryPoint.java

 StandaloneResource

Manager.java

支持SessionMode

job mode 有支持,但用戶無法方便使用。 

 Yarn

jobmode下 由FlinkClient裏的

YarnClusterDescriptor,

調用Yarn的API通過YARN啓動。

JM做爲ApplicationMaster自動啓動。 

 

SessionMode JM需要手工運行

yarn-session.sh,

JM和TM的內存不是per-job,

只有管理員才能設定。

 JM 裏的YarnResourceManager

利用YARN的API啓動TM。

 YarnSessionCluster

EntryPoint.java

 

 YarnJobCluster

EntryPoint.java

 

 YarnResource

Manager.java

 支持JobMode和SessionMode

 

Mesos

通過Marathon Service啓動,

JM運行在Mesos的容器裏。

但Marathon Service需要手

工創建。

 JM 裏的MesosResource

Manager

利用Mesos的API啓動TM。

 

 MesosSessionCluster

EntryPoint.java

 

 MesosJobCluster

EntryPoint.java

 MesosResource

Manager.java

 支持SessionMode

job mode有支持,但不方便使用。需要

提升集成度。

Kubernetes

/ECS/Docker

 由JM service 啓動  由TM deployment啓動

StandaloneSession

ClusterEntryPoint.java

 

StandaloneJobCluster

EntryPoint.java

 StandaloneResource

Manager.java

 

支持SessionMode

job mode有支持,但不方便使用。

需要提升集成度。

 

 

 表-1 Flink Deployment

 

1.7 Failure Detection and Reaction

如前所述FinkCluster裏 (參考圖-3), 最核心的組件而且交互最頻繁的組件是Dispatcher, ResourceManager, JobMaster, 和TaskManager 。

其中對於Dispatcher, ResourceManager, JobMaster,通常是ClusterEntryPoint啓動Dispatcher和ResourceManager,然後當有job提交時,Dispatcher啓動JobMaster。(MiniCluster, JobCluster略有不同,但類似)。儘管他們之間使用RPC通訊,他們生存在同一個JVM裏,如果其中一個失敗,同時使整個JVM(由於異常崩潰了)失敗了,所有組件都失敗了。那麼他們中如果有承擔Leader責任的,也都會通過HA Service 切換到另一個工作的JM後端的RPCEndPoint。

如果TM失敗了,  JM能夠感知到heatbeat 機制感知到, JM 和RM都與主動向TM發送和hearbeat , 所以TM heartbeat 一旦timeout , JM會釋放該TM 提供 過來的slot,與此同時將slot上運行的Execution 的狀態設置爲失敗, 參考 SingleLogicalSlot::signalPayloadRelease .

在stream mode 下,JM 會嘗試將該TM上失敗的Task重現分配到別的TM上(如果slot資源時有的,或可以分配的)。

在Batch mode 下, JM 會將整個job 失敗,然後嘗試重新啓動整個job 。

重新啓動的streaming Task會根據上一次的checkpoint 回復狀態, 繼續運行。

 

1.8 Flink Security

to be filled .

2.  Flink的源碼結構

上一章介紹Flink了的架構, 它包括了那些主要組件? 組件是怎麼工作的? 組件之間是怎麼工作的?組件使用的資源(包括服務器,容器,內存和CPU和Disk)是如果分配的? 以及組件是如何部署的?一個job是怎樣分割成小的Task並行執行在cluster裏面的?

怎麼保證組件HignAvailablity 以及組件是如何做 失敗處理的?以及Flink關於系統安全的設計等等。 這些關於架構的介紹,重點在於瞭解flink cluster中各個組件的互操作行以及容錯處理, 瞭解框架, 以便於再出現問題的時候我們能夠對它有比較針對性的debug 。關於Flink引以爲傲的statefull operator, window watermak, checkpoint barrier , stream/batch API 以及web monitor本文都沒有介紹,請參考相關的Flink的官方文檔。

Flink的源碼還是比較清晰易懂的, 尤其是瞭解了她的架構後, 大部分的實現都非常符合common sense 。 不想在這裏貼一堆代碼段然後加註釋解讀了, 本文的篇幅已經太長了。過一下所有的包,解釋一下他們的主要作用,以及需要框架裏使用的主要類吧。用一個大表來列舉比較合適。

 Artifect    功能介紹
Flink-client

FlinkCommandLine 入口類 (CliFrontEnd)

解析Flink 命令行 (DefaultCLI, YarnSessionCLI )

負責將從用戶jar 文件中main函數生成Plan (PackagedProgramUtils),用通過LcoalExecutor或RemoteExecutor調用flink-Optimizer生成 jobGraph .

使用ClusterDescriptor啓動jobManager (比如在yarn), 使用並將jobGraph 用ClusterClient提交給本地或遠程的JM。

Flink-runtime

Flink 源碼的核心。 框架的核心組件都在裏面, 包括 Diskpatcher, JobMaster, TaskExecutor, ResourceManager, ClusterEntryPoint, WebUI以及他們依賴的子組件以及核心數據模型,

JobGroup, ExecutionGraph, Execution, Task, Invokable, Operator, Driver, Function, NetworkBufferPool, ChannelManager, IOMemory, MemoryManager, PRCService,

HAService, HeartbeatService , CheckPointCoordinator ,  BackPressure, etc .

這些類的名字和作用在上一章都或多或少提到過, 最好結合架構的介紹, 理解他們的代碼。

Flink-runtime-web

Flink Web monitor 的界面和handler .  handler會調用Dispacher 的方法處理客戶請求, 比如sumitJob.

Flink-java

Fink-scala

用Java 和 scala實現的Flink Batch API  , Dataset, ExecutionEnrionment, etc .

Flink-stream-java

Flink-stream-scala

用Java 和 scala實現的Flink stream API ,  DataStream, ExecutionEnrionment, , StreamExecutionEnvironment, windowing, etc .

和對streaming提供支持的runtime, Checkpoint, StreamTask, StreamPartitioner , BarrierTracker,  WindowOperator, etc .

Flink-optimizer

主要功能是優化Plan 和生成JobGraph。Flink-optimizer是一個很client端使用重要的庫,它決定了從客戶代碼(application) 到jobGraph形成過過程。 我個人覺得通常也是調查和解決

application問題的根源, 比如application 的寫法是不是合適的,有問題的, 或最優的等等。

在前面架構極少裏面,並沒有談及Flink-optimizer, 所以在這簡單介紹下。

fink application 開發者使用Flink API 編寫application, flink-client 爲了將application 最終能在Flink cluster裏運行起來是,

它首先通過讀取用戶jar 文件中的main函數生成Plan:以DataSink為根的一個或多個樹結構, 樹的節點都是application使用的 API operator, 每一個節點的輸入來自與樹的下一層節點。

不難想象樹的葉子節點都是DataSoruce: 他們沒有輸入節點,但有用於讀取數據源的inputFormat,  根節點是DataSink :他們既有輸入節點,也有用於寫入目標系統的outputFormat,

中間節點都會有一個多個的輸入節點。Plan數據結構描述了同用戶的application完全相同的數據流的節點, 但它只是一個邏輯樹狀結構, 並沒有對連接節點的邊做描述,

也沒有對application編寫的數據流做任何修改。o

然後使用Flink-optimizer將Plan根據時根據優化策略設置節點的邊上的數據傳輸方式,並同時用OptimizerNode生成多種優化方案, 最後選擇cost 最小的方案產生的方案 作爲OptimizedPlan 。

比如將數據裝載方式(ShipStrategyType) 設置 FORWARD, 而不是 PARTITION_HASH而 應爲 FORWARD 的網絡cost 最低(0) 。OptimizedPlan是一個圖結構, 圖中的頂點(PlanNode)

記錄了自身的cost以及從source 開始到它的累計的cost 。OptimizedPlan主要針對與join和interation操作。

然後Flink-optimizer將OptimizedPlan 編譯成JobGraph。編譯的過程應該基本上是一對一的翻譯(從PlanNode 到 JobVertex), 但如果一串PlanNode 滿足Chaining 條件

(比如數據在每個oparator 都不需重新分區, 流過operator 之後, 直接forward到下一個operator), Flink-optimizer就像這些 operator 連接到一塊然後在JobGraph裏面只創建一個

ChainedOperator jobVertex, ChainedOperator同Spark裏面的stage 概念類似, 是優化的一部分。 

最後flink-client將jobGraph提交給FlinkCluster ,jobGraph 變形爲 ExceutionGraph在JM和TM上執行。

可以從Optimizer 的compile 和JogGraphGenerator的 compileJobGraph展開看, 他們分別complie的是Plan和OptimizedPlan 。

Flink-optimizer優化的是parition 的選擇以及算法的選擇, 而不是DAG 的workflow 。

 

Flink-Table

 

用戶可以用flink-java/flink-stream-java裏的api編寫flink application, 也可以用 Flink-Table 的table api和 flink-sql寫 application 。 Flink-Table將數據源(Dataset, DataStream)都

generalize 成Table (row based ),用戶可以用類似關係型數據庫的操作方式操作Flink的數據源, 這種方式雖然屏蔽了一些flink-api的特性 (比如 broadcast), 但極大的降低了application

的開發難度,減少了客戶程序的代碼量,從而極大的提高系統的重用度。

Flink-table 的底層還是依賴dataset/datastream api, 所以基於table api 或SQL 的程序 (Program) 最終會翻譯成 Flink-optimizer 的Plan , 經過前面所述同樣的編譯優化過程,

最終一個EG的形式運行在JM和TM上。 當然,在被翻譯成Plan之前, Flink-table 的Program 也會有自身的優化過程, 比如SQL Plan optimization .

代碼需要看, 如何實現一個Table : StreamTableSource, BatchTableSource, BatchTableSink,AppendStreamTableSink

如何擴充FlinkSQL : UserDefinedFunction, ScalarFunction,TableFunction,AggregateFunction
TableEnvironment

Flink-yarn

Flink-mesos

Flink-container

 Flink 怎麼支持異構環境的, 包括不同環境裏的, 主要是異構環境裏的不同ResourceManager (啓動TM ),以及 ClusterDescriptor (啓動JM)如何實現的。

Flink-library

 flink-cep, flink-gelly, flink-ml

Flink-connector

Flink-format

連接外圍數據系統(數據源和輸出)系統的InputFormat, OutputFormat .

Flink-filesystem

 Flink支持的分佈式文件系統, hadoop, s3, mapr

Flink-metrics

 flink 的metrics 系統

Flink-statebackends

Flink-queryable-satate

 flink 的 state的存儲

flink-jepsen
flink-test

Flink的UT和集成測試。

 表-2 Flink packages                     

 

3.  Debug Flink

3.1.  Intellij 準備工作

需要安裝JDK1.8和Scala的plugin, 否則Flink無法在IntelliJ裏編譯。

下載flink1.6.1的代碼,到下面的地址 download ,然後用maven 編譯 。

 https://github.com/apache/flink/tree/release-1.6.1

 

下載例子程序:  https://github.com/kaixin1976/flink-arch-debug 。 由於github的限制, 上傳的instrument文件相對較小,讀者可以通過自我複製擴大文件尺寸。

運行例子程序, 有兩個參數 :

第一個是 joinType,可取值爲"normal" 或"broadcast"。normal 方式完全依賴FlinkOptimizer產生 優化的方案, broadcast是運用一下方法, 使join的第一路輸入廣播, 第二路輸入自由平均分配。

第二個使api類型, 可取值爲 "api" 或 "sql" 。api方式指的是Flink Java API, sql 使 Flink SQL API 。對於Join Flink Java API 可以指定JoinHint或Parateters從而影響優化, sql 沒有這些接口, 只能在DataSoource 上做文章。

具體看JoinTest::main()。

3.1.2  Intellij debug configration 

如前所述, Flink分佈式環境的主要包含三部分:

1.  FlinkCient : 生成、優化和提交JobGraph.

FlinkClient的main 函數在CliFrontend中, VM 的classpath 和工作路徑設置爲本地安裝的一個flink路徑 , 用於找到所有需要的jar文件和 flink confugration .

程序參數跟flink run 一樣, 

 圖-13  Flink Client debug configuration

 

2.  JM : 生成ExecutionGraph,併爲其中的所有的的子任務分配slot資源, 並將子任務發送到slot所在TM上運行。

JM 使用StandaloneSessionClusterEntrypoint , 他會啓動Standalone Session Cluster 的entry point .

 圖-14  Flink JobManager debug configuration

 

3.  TM: 運行ExecutionGraph的子任務。

  圖-14  Flink TaskManager debug configuration

  

3.2  例子程序 和問題描述

例子程序是一個利用Flink SQL  把一大一小兩個數據源join再一起的Flink  application 。大數據源叫instruments 包含了一些假造金融工具(比如股票期權等)的基礎數據(其中包括該工具交易貨幣的ID),小數據源是貨幣currencies  包含貨幣ID,和貨幣名稱。join目的是給與金融工具的貨幣ID獲得對應的貨幣名稱(比如,人民幣,歐元,美元等)。

join的過程是比較簡單的,首先 從文件創建兩個數據源(instrument 和 currency 的 TableSource), 然後用調用 SQL 做join, 最後創建TableSink將join的結果輸出到文件裏。程序的主體如下。

 public void joinSmallWithBig(String joinType) throws Exception {
    String currencies = "currencies";
    String instruments = "instruments";
    //0. create the table environment
    ExecutionEnvironment env = buildLocalEnvironment() ;

    env.setParallelism(4);
    BatchTableEnvironment tableEnv = BatchTableEnvironment.getTableEnvironment(env);

    //1. create and register instrument table source
    registerInstrumentTableSource(tableEnv, instruments, System.getProperty("user.dir") +"/data/instruments.csv");

    //2. create and register currency table source
    if(joinType.equalsIgnoreCase("broadcast")){
        this.registerCurrencyBroadcastTableSource(tableEnv,currencies, System.getProperty("user.dir") +
                                              "/data/currency.csv");
    }else {
        this.registerCurrencyTableSource(tableEnv,currencies, System.getProperty("user.dir") + "/data/currency.csv");
    }

    //3. join
    Table currencyTable = tableEnv.sqlQuery(
            "SELECT CurrencyId AS CurrencyId," +
                    "(CASE " +
                    "WHEN ISOCode IS NULL OR ISOCode ='' THEN ISOExtended ELSE ISOCode " +
                    "END ) AS CurrencyCode FROM " + currencies);
    tableEnv.registerTable("currencyTable", currencyTable);


    Table result = tableEnv.sqlQuery(
            "SELECT RIC,Asset,AssetClass,Exchange,Periodicity,ContractType,CallPutOption,ExpiryDate,StrikePrice," +        
        "StrikePriceMultiplier,LotSize,CurrencyCode,AssetState " +
              "FROM currencyTable join instruments on currencyTable.CurrencyId=instruments.CurrencyID ");

    //4. create sink and output the result
    CsvTableSink sink = new CsvTableSink(System.getProperty("user.dir") + "/data/sql_join_result.csv", ",", 1,
           FileSystem.WriteMode.OVERWRITE);
    result.writeToSink(sink);

    env.execute();

}

 

 表-3  例子程序

 

問題是join的過程發生了嚴重的數據傾斜 (data skew )。全世界的金融工具分佈式極不平均的, 絕大部分使用美元,歐元計價 (比如例子程序裏假造的instrument的數據源,大概有13/14 的instrument是以歐元計價的)。如果使用常用的 hash join 或 sort-merage join, 數據傾斜是必然發生的。 調用joinSmallWithBig,並傳入任意字符串 做爲joinType 時, 程序調用的registerCurrencyTableSource方法, 該方法就是創建了一個普通的CsvTableSourcei並註冊, 之後利用sql join 將兩個數據join起來,此時會發生數據傾斜。如下圖所示:

圖-15 flink-webmonitor 上的currency 數據源

 

  

 圖-16 flink-webmonitor 上的instrument數據源

可以看出currency 的CsvTableSource一共有492條記錄分4個split讀入,數據量確實小,instrument數據源記錄數大概14million 條記錄有,比較大,同樣分4個split讀入, 分配不可謂不均衡 。但當join後, 均衡徹底打破了,如下圖所示:

圖-17 flink-webmonitor 上的hash-join

兩個數據源的輸出策略(或JoinOperator的輸入策略)也就是連接數據源與JoinOperator的那條邊的ShipStrategyType被設置爲一個currencyId 爲key的HashPartition。  由於本例中約13/14的instrument以歐元計價,大概有13 million個instrument 記錄依照hash code 被髮送到一個task的IG裏, 剩下約1 million的其他三個task 。

借這張圖咱們需要重溫一下的前面提到的task, RP,RS,IG,IC的關係, instrument數據源 有4 個task所以4個RP, 每個RP會產生4個RS以裝載不同的hash code,共16個RS 。 下游join operator 有兩個IG (分別連接instrument數據源 和currency), 每一個IG 的ShipStrategy都是HashParition。每一個IG 有4個 IC, 每個IC 都連接上游標號相同的RS (比如JoinOperator task1的 IG1 的IC1,IC2,IC3,IC4)只連接上游 instrumentSource task1, 2,3,4 的 RS1) 。 JoinOperator是一個DualInputOperator, 通常的operator只有一個IG 。

上圖中13 million的record received (13,836,997)是joinOperator的第三個 task 的IG1 和 IG2中相同標號IC的記錄數量(比如IC2),由於currency數據量很小,構成13 million 這個數量級的來源是instrument中以某幾個貨幣計價的instrument, 如前述主要是歐元 , 此例中歐元計價佔比大概爲13/14 。

數據傾斜的結果造成joinOperator的第三個task需要多於別的 task的100倍的內存,通常會超過容器預先分配的內存配額。罪魁禍首是 join operator 輸入邊 的 ShipStrategy : hash partition 。 SQL 裏無法指定 join 使用哪種策略, 爲什麼flink 會在翻譯(將sql 翻譯成jobGraph)的過程中將 join 輸入冊率 優化成hash partition 呢?一個小數據集和大數據集join,最好的方式是將小數據集廣播給下游的所有task, 大數據平均分配,然後再join operator內部做hash join, 這樣的cost是最低的 。Flink爲什麼不能做到這樣的優化呢?如果使用Flink API (而不是 SQL) 情況會好嗎?

 

3.3  分析問題設置斷點

初步的感覺是Flink-optimizer 並沒有做到這樣的優化: 當join的兩個數據源大小懸殊時, 小數據集廣播,大數據集均分的輸入給join operator 。Flink這麼常用的優化都沒做到嗎?

讓我們通過debug Flink-optimizer 代碼, 看看他時怎麼將本例的join input的ShipStrategy優化成HashPartition 吧。

3.3.1. 首先,準備調式環境

下載例子代碼,然後編譯,package 成flink-arch-debug-0.0.1.jar, 然後下載flink-1.6.1 的代碼,編譯, 按照圖-13設置flink-client的運行選項。

program arguments 設置爲 run flink-arch-debug-0.0.1.jar normal sql, 使用 "normal ","sql" 方式, 如下:

 C:\projects\flink-arch-debug\target\flink-arch-debug-0.0.1.jar normal sql

3.3.2. 然後,瞭解ShipStrategy

Flink Optimizer 的優化過程(參考Optimizer::compile方法)大概是:

首先將利用GraphCreatingVisitor 將Flink API (API, Table, 或 SQL) 產生的Plan翻譯成dag 包裏的OptimizerNode和DagConnection組成圖,

然後從圖的SinkNode(一個或多個)先前遞歸的調用getAlternativePlans方法從產生最優的Plan , 最優的Plan 是由plan包裏的PlanNode和Channel組成的Graph 。ShipStratigy是channel的一個屬性, 它描述數據從一個operator以那種方式發佈到RP中的RS中的。

最後由JobGraphGenerator將最優圖翻譯成JobGraph

那麼有多少種ShipStrategy呢? 連續按兩下SHIFT, 再鍵入類名(ShipStategyType.java)。參考一下本文最後面的IntelliJ的常用鍵吧.

 

public enum ShipStrategyType {   

   NONE(false, false), //沒有策略
   FORWARD(false, false), //數據按原分區號傳遞到本地運行的Task相同分區,不跨網絡,不需要比較器(用於排序或Hash)
   PARTITION_RANDOM(true, false),//數據隨機傳遞到本地運行的Task任意分區,不跨網絡,不需要比較器
   PARTITION_HASH(true, true),//數據按照指定鍵值的Hash決定分區,到目標Task需要跨網絡傳遞,需要比較器
   PARTITION_RANGE(true, true),//數據按照指定鍵值的排序順序決定分區,到目標Task需要跨網絡傳遞,需要比較器  
  BROADCAST(true, false) // 將數據複製到任意一個分區中,不需要比較器
   PARTITION_FORCED_REBALANCE(true, false), // 將數據平均到各個分區中,不需要比較器
   PARTITION_CUSTOM(true, true); // 將數據按用戶指定的分區器分配分區,不需要比較器
...
表-4 ShipStrategyType

搜索一下PARTITION_HASH, 看誰將它設置給最優圖的Channel 對象, 過濾一下,應該由兩個地方:

一個是TwoInputNode::setInput 方法中, 它根據join operator 的JoinHint 或 Parameters 來設置 input Channel的ShipStrategy 。 比如, 如果想讓join的第一個input(Currencies)用廣播方式傳遞, 就可使用”BROADCAST_HASH_FIRST“ hint, 或者將這樣的參數"INPUT_LEFT_SHIP_STRATEGY"="SHIP_BROADCAST"傳遞給join operator 。不過這些都只能在Flink Java API中 使用, (具體參考flink-arch-debug中的ApiJointTest類),無法在SQL API 中使用。

第二個在TwoInputNode::getAlternativePlans中。如前文所述, getAlternativePlans首先會 OptimizerNode (本例中主要是JoinNode) 節點用於產生多個plan , 每個 plan的輸入輸出的節點都是相同, 不同的是邊上ShipStrategy 。對於每個Plan , getAlternativePlans都會調用RequestedGlobalProperties::parameterizeChannel從而設置這個Plan的InputChannel的ShipStrategy 。

在SQL application 中, 用戶無法像在使用Java API 時那樣通過設置JoinHint或Parameter 來選擇想要的ShipStategy , 實際上如果指定了這兩項中的任意一個,也等同於跳過了getAlternativePlans (優化過程)。 因爲既然用戶已選擇了想要的ShipStategy,就沒有必要用Optimizer 根據計算每個方案的cost 來選擇最優方案了。 可是即使是由Optimizer 來選擇, 對於本例(小數據集join大數據集), 根據表-4 ShipStrategyType的 定義,PARTITION_HASH的cost 肯定不是最低的 。對於大數據集 (instruments), 如果採用PARTITION_HASH將數據傳遞給下游, 網絡cost 必然是很高的, 因爲PARTITION_HASH是一個shuffule 的過程, 下游 必然有位於不同TM遠程Task , 因此網絡傳遞的cost 必然很高。 看看Flink 怎麼定義Cost 和怎麼估計cost 的?

public class Costs implements Comparable<Costs>, Cloneable {

   public static final double UNKNOWN = -1;
   
   private double networkCost;  // 上游數據傳遞到本節點在網絡上傳輸的cost, in transferred bytes

   private double diskCost;     //本節點緩存數據到磁盤上讀寫的cost , in bytes, 回憶一下前面說的SpillableSubpartition   
   private double cpuCost;     // 本節點算法在計算中使用CPU 的costs, 比如Hash, sort , merage, etc 
   
   private double heuristicNetworkCost; // 以下時假設的cost, 非常高不準確
   
   private double heuristicDiskCost;
   
   private double heuristicCpuCost;
...

表-5 Cost定義

根據表-5 Flink cost的定義, 一個節點(比如JoinNode)的cost ,包括上游數據由上游傳遞過來的network cost, 數據本地緩存的 disk cost, 還有算法的cpu cost 。 圖-17第二條邊(Instrments)使用的HASH_PARTITION策略肯定比FORWARD cost高很多, 因爲如果使用FORWARD 的network cost 爲0 (參考表-4的定義),diskCost和cpuCost 跟算法有關(比如使用HashTable或sort merage 去做join)。 第一條邊(Currencies)使用的HASH_PARTITION相比BROADCAST, cost 肯定低一些, 但是Currencies數據源 數量特別小, 它的cost 和 這兩個cost 之間的差別 在Instrments引起的cost面前都可以忽略不記。所以感覺上 HH(兩邊都是HASH_PARTITION)和BF(currency 邊Broadcuast, instrument邊Forward)相比, BF方案的cost一定低很多。 但爲什麼getAlternativePlans沒有選擇 BF呢?JoinNode沒有給出BF的選擇權嗎?看看JoinNode::getDataProperties()的代碼:

private List<OperatorDescriptorDual> getDataProperties(InnerJoinOperatorBase<?, ?, ?, ?> joinOperatorBase, JoinHint joinHint,   
Partitioner<?> customPartitioner )                                                                                                           
{
...
  
switch (joinHint) {
   case BROADCAST_HASH_FIRST:
      list.add(new HashJoinBuildFirstProperties(this.keys1, this.keys2, true, false, false));
      break;
   case BROADCAST_HASH_SECOND:
      list.add(new HashJoinBuildSecondProperties(this.keys1, this.keys2, false, true, false));
      break;
   case REPARTITION_HASH_FIRST:
      list.add(new HashJoinBuildFirstProperties(this.keys1, this.keys2, false, false, true));
      break;
   case REPARTITION_HASH_SECOND:
      list.add(new HashJoinBuildSecondProperties(this.keys1, this.keys2, false, false, true));
      break;
   case REPARTITION_SORT_MERGE:
      list.add(new SortMergeInnerJoinDescriptor(this.keys1, this.keys2, false, false, true));
      break;
   case OPTIMIZER_CHOOSES:
      list.add(new SortMergeInnerJoinDescriptor(this.keys1, this.keys2));
      list.add(new HashJoinBuildFirstProperties(this.keys1, this.keys2));
      list.add(new HashJoinBuildSecondProperties(this.keys1, this.keys2));
      break;
   default:
      throw new CompilerException("Unrecognized join hint: " + joinHint);
}

...
}

表-6  JoinNode的輸入節點要求的屬性

從表-6  JoinNode的輸入節點要求的屬性可知, 但由Optimizer 來決定(OPTIMIZER_CHOOSES)輸入節點的ShipStrategy(輸出分區策略)時, JoinNode對上游的要求時非常寬泛的,幾乎就是什麼都行 。

SortMergeInnerJoinDescriptor(this.keys1, this.keys2),HashJoinBuildFirstProperties(this.keys1, this.keys2),HashJoinBuildSecondProperties(this.keys1, this.key),
實際上就是他們爲JoinNode和它的input定義了三類需求 :
第一, Join算法可是時Sort-merge, 或者用一路輸入做HashTable的HashJoin ,或者用二路輸入做HashTable的HashJoin
第二, 第一路第二路輸入的partition 策略: 也就是所謂的RequestGlobalPropertities, 可以是RANDOM_PARTITIONED (對應FORWARD shipStategyType),或者HASH_PARTITIONED(PARTITION_HASH),或者FULL_REPLICATION(對應BROADCAST),
或者ANY_PARTITIONING(它是一個通配策略,意思是RANDOM or HASH都可以)
第三,輸入的排序策略 :就是所謂的RequestGlobalPropertities, 排序或者不排序。
想一想這三類需求的可選項組合起來,一共會有多少組合? 弄個表, 描述一下可能更清楚些。
     序號                Input1 的 
partition 備選 策略
 Input2 的 
partition 備選 策略
算法  Input1/Input2 的 排序備選策略  兼容性   network Cost   
1
FORWARD
 
HASH
SortMergeInnerJoin
 sort/sort yes  
2
FORWARD
 
HASH
 
HashJoinBuildFirst
 sort/sort  yes  
3
 
FORWARD
 
HASH
 
HashJoinBuildFirst
 not sort/ not sort  yes  
4
 
FORWARD
 
HASH
 
HashJoinBuildFirst
 sort/not sort  yes  
5
 
FORWARD
 
HASH
 
HashJoinBuildFirst
 not sort /sort  yes  
6
 
FORWARD
 
HASH
 
HashJoinBuildSecond
  sort/sort  yes  
7
 
FORWARD
 
HASH
 
HashJoinBuildSecond
 not sort/ not sort  yes  
8
 
FORWARD
 
HASH
 
HashJoinBuildSecond
  sort/not sort yes   
9  
FORWARD
 
HASH
 
HashJoinBuildSecond
 not sort /sort  yes  
表-6  GloabalProperties, LocalProperties 和算法的組合
從表-6 可知, 對於input1和input2的partition策略(表-6實際上用的是shipStategy, 他們是1-1對應的)一個組合FH(Forward, Hash ), 一共有9個可以接受的分區-算法-排序的方案,每一種方案都有自己的cost, getAlternativePlans會最後選擇cost最低的方案。 除了FH組合, 合法組合還有
HB:Hash-Broadcast
HH:Hash-Hash
BB:Broadcast-Broadcast
BF:Broadcast-Forward
FB:Forward-Broadcast
當然還有HF,FH ,FF,這些都是不合法的,對於join操作會有數據丟失的組合。 6個合法的input1和input2的partition策略組合,根據表-6可以計算, 一共能有6*9=54可互相兼容的分區-算法-排序的組合方案 。實際上如果在TwoInputNode.java:516行上設計斷點 (getAlternativePlans方法內)並運行例子程序,
你會發現變量 outputPlans裏有90個備選方案,那麼其中36個一定是重複和多徐的。

那麼, BF方案是既然在備選方案裏,而且它的cost理論上是最低的, 而且getAlternativePlans的算法是選擇cost最低方案, 爲什麼cost並不低的HH方案最終被選擇了 ?  答案只能有一個 : cost estimation有問題。

  

3.3 發現問題和給出解決方案 

 不貼Cost, CostEstimiator, DefualtCostEstimator (costs包的三個類)的代碼了。總起來所, CostEstimator 是根據OptimizerNode 的 estimatedOutputSize 成員變量(包括本節點和數據節點的)來計算 network和disk cost 的 , estimatedOutputSize是由節點成員函數computeOperatorSpecificDefaultEstimates()設定, 可以參考DataSourceNode中該函數的實現:estimatedOutputSize 被設置成來自於InputFormat的數據源實際物理尺寸。 可是FlatMapNode, MapNode 只是簡簡單單的設置了estimatedNumRecords (使之同 source的 一樣),而並沒有計算estimatedOutputSize 。

重看一下圖-4的ExcutionGraph可知, Join的第一個input是FlatMap , 如果Flatmap沒有estimatedOutputSize 會導致它的下一個節點JoionNode無法計算cost. 請參考DefualtCostEstimator::addHybridHashCosts()的實現, 當上遊的estimatedOutputSize爲負數(Unknown)時,本節點的cost爲被設置爲Unknown 。Unknown cost在選擇cheapest cost的plan時是不能做比較的, 只能依靠假設的Cost (如heuristicNetworkCost ), 這個東西很不靠譜, 同時也是HH方案被選上的原因。

解決方案應該修改Flink代碼在FlatmapNode::computeOperatorSpecificDefaultEstimates 加上對estimatedOutputSize的估計(比如保持和上游相同, 或加一個discount), 或者修改DefualtCostEstimator 使之 在計算Cost時能通過estimatedNumRecords估計estimatedOutputSize (下策, 沒有 前者好)。 總之,Flink Optimizter 在 Cost Estimation 做的不夠好, 改善是應該的, 我剛剛看了Flink 1.9 (最新版)的 Optimizer , 這一塊還是沒有修改。突然還有加入Flink community 的衝動。

 如果不想修改 Flink 源碼, 或改了也無法發佈, 那就看看有沒有替換方案了。

 

...
if (child1.getGlobalProperties().isFullyReplicated()) {
   // fully replicated input is always locally forwarded if parallelism is not changed
   if (dopChange1) {
      // can not continue with this child
      childrenSkippedDueToReplicatedInput = true;
      continue;
   } else {
      this.input1.setShipStrategy(ShipStrategyType.FORWARD);
   }
}
...

表-7 FullyReplicated DataSource 。

這段代碼表示如果上游節點是一個FullyReplicated DataSource , 那麼就不需要備選方案的選擇過程, 直接將這個輸入邊的shipStagy 設置爲Forward 。 FullyReplicated DataSource意思是, 這個Data Source 的每個Parition輸出的都是數據源的全集 而不是不部分。那麼只需要將Currencies做成這種數據源,問題不就解決了嗎?!

雖然ShipStategy是Forward, 但實際下游(Join Node)的每一個Task都得到了Currencies的這個數據集的全集, 這個同Broadcast的效果是一樣的。而且第一路爲F的組合有FF, FB , FH , 選擇的範圍小了。 這不對阿,前面不是說FF, FH, HF是不合法的嗎? 要注意此時的F是由於FullyReplicated DataSource而既決定的F,不是普通的Forward , 是合法的。FF, FB , FH產生了27種可選方案(參考表-6), 根據heuristicNetworkCost , FF是cost最低的。 那肯定是最低的阿, Forward的數據都是本地傳輸的, network cost 是 0 。

下面就是如何將Currencies做成FullyReplicated DataSource了, 根據DataSourceNode.java:92, DataSource 的InputFormat只要是ReplicatingInputFormat, 則就是FullyReplicated  。在看看ReplicatingInputFormat的代碼, 只需要將原有的InputFormat (比如CsvInputFormat)外面包一層ReplicatingInputFormat就可以了, 具體的看例子的代碼吧 : SqlJoinTest.java:95。運行結果如下。

 

  圖-18 flink-webmonitor 上FullyReplicated Currencies, 每一個partition都輸出數據集的全部, 492條記錄。

 

 圖-19 flink-webmonitor 上instrument , 跟之前沒什麼區別。

 

 圖-20 flink-webmonitor 上的join , 數據在各個並行Task上非常均衡。

 

雖然這個例子只用到了Flink-client上的知識, 並沒有與JM和TM聯合調試, 但是問題的發現,分析和解決的方法是一樣的。 都是需要根據對架構的認識,縮小懷疑的範圍,然會反覆閱讀和調式相關代碼,找到問題,以及找到解決方案, 試想一下,如果沒有架構的知識,你怎麼能夠懷疑這一定是Flink-optimizer的問題呢? 而且有時解決問題的方法並不一定是直接修改的出問題的代碼(當然這樣是最好的方案),根據相關代碼找到一個合適的替代方案也是解決問題的方法。

沒有想到描述一個這樣的小問題,這一章運用這麼長的篇幅, 對於複雜的問題,簡直能寫本書了。希望拋磚引玉,能夠運用這樣的方法學來解決使用Flink遇到的問題: 架構->縮小範圍->閱讀和調式代碼->發現和分析問題->找到解決方案。

 

 3.4. IntelliJ的常用鍵

CTRL+ALT+Enter to complete line

SHIFT+SHIT search class in source code

CTRL+SHIFT+F serarch string in soruce code

Alt + F1 show current java in project view
CTRL+ALT+B navigate to implementation classes


CTRL+U navigate to super method
ALT + F7 Show reference
ALT + 4 Show run window
ALT + F12 Show terminal window

ALT+Enter Create A testClass
ALT+Insert TestMethod

CTRL+home move to file start
CTRL+end move to file end

 

參考

Flink conference Page: https://cwiki.apache.org/confluence/display/FLINK/Flink+Internals

很不錯的Blog: https://www.cnblogs.com/bethunebtj/p/9168274.html

Flink1.6 documentation: https://ci.apache.org/projects/flink/flink-docs-release-1.6/

Flink Github: https://github.com/apache/flink

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