大資料——Flink核心技術及原理

蜂蜜柚子加苦茶發表於2021-01-02

前言

Apache Flink(簡稱Flink)專案是大資料處理領域最近冉冉升起的一顆新星,其不同於其他大資料專案的諸多特性吸引了越來越多人的關注。本文將深入分析Flink的一些關鍵技術與特性,希望能夠幫助讀者對Flink有更加深入的瞭解,對其他大資料系統開發者也能有所裨益。本文假設讀者已對MapReduce、Spark及Storm等大資料處理框架有所瞭解,同時熟悉流處理與批處理的基本概念。

Flink簡介

Flink核心是一個流式的資料流執行引擎,其針對資料流的分散式就是那種提供了資料分佈、資料通訊以及容錯機制等功能。基於流行執行引擎,Flink提供了諸多更高抽象層API一遍使用者編寫分散式任務:

  • 1.DataSet API:對靜態資料進行批處理操作,將靜態資料抽象成分散式的資料集,使用者可以方便地使用Flink提供的各種操作符對分散式資料集進行處理,支援Java、Scala和Python。
  • 2.DataStream API:對資料流進行流處理操作,將流式的資料抽象成分散式的資料流,使用者可以方便地對分散式資料流進行各種操作,支援Java和Scala。
  • 3.Table API:對結構化資料進行查詢操作,將結構化資料抽象成關係表,並通過類SQL的DSL對關係表進行各種查詢操作,支援Java和Scala。

此外,Flink還針對特定的應用領域提供了領域庫,例如:

  • Flink ML:Flink的機器學習庫,提供了機器學習Pipelines API並實現了多種機器學習演算法。
  • Gelly:Flink的圖計算庫,提供了圖計算的相關API及多種圖計算演算法實現。

在這裡插入圖片描述
Flink的技術棧。

此外,Flink也可以方便地和Hadoop生態圈中其他專案整合,例如Flink可以讀取儲存在HDFS或HBase中的靜態資料,以Kafka作為流式的資料來源,直接重用MapReduce或Storm程式碼,或是通過YARN申請叢集資源等。

統一的批處理與流處理

在大資料處理領域,批處理任務與流處理任務一般被認為是兩種不同的任務,一個大資料專案一般會被設計為只能處理其中一種任務,例如Apache Storm、Apache Smaza只支援流處理任務,而Apache MapReduce、Apache Tez、Apache Spark只支援批處理任務。Spark Streaming是Apache Spark之上支援流處理任務的子系統,看似一個特例,實則不然——Spark Streaming採用了一種micro-batch的架構,即把輸入的資料流切分成細粒度的batch,併為每一個batch資料提交一個批處理的Spark任務,所以Spark Streaming本質上還是基於Spark批處理系統對流式資料進行處理,和Apache Storm、Apache Smaza等完全流式的資料處理方式完全不同。通過其靈活的執行引擎,Flink能夠同時支援批處理任務與流處理任務。

在執行引擎這一層,流處理系統與批處理系統最大不同在於節點間的資料傳輸方式。

對於一個流處理系統,其節點間資料傳輸的標準模型是:
當一條資料被處理完成後,序列化到快取中,然後立刻通過網路傳輸到下一個節點,由下一個節點繼續處理。

而對於一個批處理系統,其節點間資料傳輸的標準模型是:
當一條資料被處理完成後,序列化到快取中,並不會立刻通過網路傳輸到下一個節點,當快取寫滿,就持久化到本地硬碟上,當所有資料都被處理完成後,才開始將處理後的資料通過網路傳輸到下一個節點。

這兩種資料傳輸模式是兩個極端,對應的是流處理系統對低延遲的要求和批處理系統對高吞吐量的要求。

Flink的執行引擎採用了一種十分靈活的方式,同時支援了這兩種資料傳輸模型。Flink以固定的快取塊為單位進行網路資料傳輸,使用者可以通過快取塊超時值指定快取塊的傳輸時機。如果快取塊的超時值為0,則Flink的資料傳輸方式類似於上文所提到流處理系統的標準模型,此時系統可以獲得最低的處理延遲。如果快取塊的超時值為無限大,則Flink的資料傳輸方式類似於上文所提到批處理系統的標準模型,此時系統可以獲得最高的吞吐量。同時快取塊的超時值也可以設定為0到無限大之間的任意值。快取塊的超時值閾值越小,則Flink流處理執行引擎的資料處理延遲越低,但吞吐量也會降低,反之亦然。通過調整快取塊的超時閾值,使用者可根據需求靈活地權衡系統延遲和吞吐量。
在這裡插入圖片描述
Flink執行引擎資料傳輸模式。

在統一的流式執行引擎基礎上,Flink同時支援了流計算和批處理,並對效能(延遲、吞吐量等)有所保障。相對於其他原生的流處理與批處理系統,並沒有因為統一執行引擎而受到影響從而大幅度減輕了使用者安裝、部署、監控、維護等成本。

Flink流處理的容錯機制

對於一個分散式系統來說,單個程式或是節點崩潰導致整個Job失敗是經常發生的事情,在異常發生時不會丟失使用者資料並能自動恢復才是分散式系統必須支援的特性之一。本節主要介紹Flink流處理系統任務級別的容錯機制。

批處理系統比較容易實現容錯機制,由於檔案可以重複訪問,當某個任務失敗後,重啟該任務即可。但是到了流處理系統,由於資料來源是無限的資料流,從而導致一個流處理任務執行幾個月的情況,將所有資料快取或是持久化,留待以後重複訪問基本是上不可行的。Flink基於分散式快照與可部分重發的資料來源實現了容錯。使用者可以自定義對整個Job進行快照的時間間隔,當任務失敗時,Flink會將整個Job恢復到最近一次快照,並從資料來源重發快照之後的資料。Flink的分散式快照實現借鑑了Chandy和Lamport在1985年發表的一篇關於分散式快照的論文,其實現的主要思想如下:
按照使用者自定義的分散式快照間隔時間,Flink會定時在所有資料來源中插入一種特殊的快照標記資訊,這些快照標記訊息和其他資訊一樣在DAG中流動,但是不會被使用者定義的業務邏輯所處理。每一個快照標記資訊都將其所在的資料流分成兩部分:本次快照資料和下次快照資料。

在這裡插入圖片描述
Flink包含快照標記資訊的訊息流

快照標記資訊沿著DAG流經各個操作符,當操作符處理到快照標記資訊時,會對自己的狀態進行快照,並儲存起來。當一個操作符有多個輸入的時候,Flink會將先抵達的快照標記資訊及其之後的訊息快取起來,當所有的輸入中對應該次快照標記資訊全部抵達後,操作符對自己的狀態快照並儲存,之後處理所有快照標記資訊之後的已快取資訊。操作符對自己的狀態快照並儲存可以是非同步與增量的操作,並不需要阻塞訊息的處理。分散式快照的流程如圖所示:
在這裡插入圖片描述
Flink分散式快照流程圖

當所有的Data Sink(終點操作符)都收到快照標記資訊並對自己的狀態快照和儲存後,整個分散式快照就完成了,同時通知資料流釋放該快照標記訊息之前的所有訊息。若之後發生節點崩潰等一場情況時,只需要恢復之前儲存的分散式快照狀態,並從資料來源重發該快照以後的訊息就可以了。

Exactly-Once是流處理系統需要支援的一個非常重要的特性,它保證每一條訊息只被流處理系統處理一次,許多流處理任務的業務邏輯都依賴於Exactly-Once特性。相對於At-Least-Onece或是At-Most-Once,Exactly-Once特性對流處理系統更為嚴格,實現也更加困難。Flink基於分散式快照實現了Exactly-Once特性。

相對於其他流處理系統的容錯方案,Flink基於分散式快照的方案在功能和效能方面都具有很多優點,包括:

  • 低延遲:由於操作符狀態的儲存可以非同步,所以進行快照的過程基本上不會阻塞訊息的處理,因此不會對訊息延遲產生負面訊息。
  • 高吞吐量:當操作符狀態較少時,對吞吐量基本沒有影響。當操作符狀態較多時,相對於其他的容錯機制,分散式快照的時間間隔是使用者自定義的,所以使用者可以權衡錯誤恢復時間和吞吐量要求來調整分散式快照的時間間隔。
  • 與業務邏輯的隔離:Flink的分散式快照機制與使用者的業務邏輯是完全隔離的,使用者的業務邏輯不會依賴或是對分散式快照產生任何影響。
  • 錯誤恢復代價:分散式快照的時間間隔越短,錯誤恢復的時間越少,與吞吐量相關。

Flink流處理的時間視窗

對於流處理系統來說,流入的訊息不存在上限,所以對於聚合或是連線等操作,流處理系統需要對流入的訊息進行分段,然後基於每一段資料進行聚合或是連線。訊息的分段即稱為視窗,流處理系統支援的視窗有很多型別,最常見的就是時間視窗,基於時間間隔對訊息進行分段處理。本節主要介紹Flink流處理系統支援的各種時間視窗。

對於目前大部分流處理系統來說,時間視窗一般是根據Task所在節點的本地時鐘進行切分,這種方式實現起來比較容易,不會產生阻塞。但是可能無法滿足某些應用需求,比如:

訊息本身帶有時間戳,使用者希望按照訊息本身的時間特性進行分段處理。

由於不同節點的時鐘可能不同,以及訊息在流經各個節點的延遲不同,在某個節點屬於同一個時間視窗處理的訊息,流到下一個節點時可能被切分到不同的時間視窗中,從而產生不符合預期的結果。

Flink支援是三種型別的時間視窗,分別適用於用於對於時間視窗不同型別的要求:

  • 1.Operator Time:根據Task所在節點的本地時鐘來切分的時間視窗。
  • 2.Event Time:訊息自帶時間戳,根據訊息的時間戳進行處理,確保時間戳在同一個時間視窗的所有訊息一定會被正確處理。由於訊息可能亂序流入Task,所以Task需要快取當前時間視窗的訊息處理的狀態,直到確認屬於該時間視窗的所有訊息都被處理,才可以釋放,如果亂序的訊息延遲很高會影響分散式系統的額吞吐量和延遲。
  • 3.Ingress Time:有時訊息本身併不併不帶有時間戳訊息,但使用者依然希望按照訊息而不是節點時鐘劃分時間視窗,例如避免上面提到的第二個問題,此時可以在訊息源流入Flink流處理系統時自動生成增量的時間戳賦予訊息,之後處理的流程與Event Time相同。Ingress Time可以看成是Event Time的一個特例,由於其在訊息源處時間戳一定是有序的,所以在流處理系統中,相對於Event Time,其亂序的訊息延遲不會很高,因此對Flink分散式系統的吞吐量和延遲的影響也會更小。

Event Time時間視窗的實現

Flink借鑑了Google的MillWheel專案,通過WaterMark來支援基於Event Time的時間視窗。

當操作符通過基於Event Time的時間視窗來處理資料時,它必須在確定所有屬於該事件視窗的訊息全部流入此操作符後才能開始資料處理。但是由於訊息可能是亂序的,所以操作符無法直接確認何時所有屬於該時間視窗的訊息全部流入此操作符。WaterMark包含一個時間戳,Flink使用WaterMark標記所有小於該時間戳的訊息都已流入,Flink的資料來源在確認所有小於某個時間戳的訊息都已輸出到Flink流處理系統後,會生成一個包含該時間戳的WaterMark,插入到訊息流中輸出到Flink流處理系統中,Flink操作符按照時間視窗快取所有流入的訊息,當操作符處理到WaterMark時,它對所有小於該WaterMark時間戳的時間視窗資料進行處理併傳送到下一個操作符節點,然後也將WaterMark傳送到下一個操作符節點。

為了保證能夠處理所有屬於某個時間視窗的訊息,操作符必須等到大於這個時間視窗的WaterMark之後才能開始對該時間視窗的訊息進行處理,相對於基於Operator Time的時間視窗,Flink需要佔用更多記憶體,且會直接影響訊息處理的延遲時間。對此,一個可能的優化措施是,對於聚合類的操作符,可以提前對部分訊息進行聚合操作,當有屬於該時間視窗的新訊息流入時,基於之前的部分聚合結果繼續計算,這樣的話,只需快取中間計算結果即可,無序快取該時間視窗的所有訊息。

對於基於Event Time時間視窗的操作符來說,流入WaterMark的時間戳與當前節點的時鐘一致是最簡單理想的狀態,但是實際環境中是不可能的,由於訊息的亂序以及前面節點處理效率的不同,總是會有某些訊息流入時間大於其本身的時間戳,真實WaterMark時間戳與理想情況下WaterMark時間戳的差別稱為Time Skew,如下圖所示:
在這裡插入圖片描述
WaterMark的Time Skew圖

Time Skew決定了該WaterMark與上一個WaterMark之間的時間視窗所有資料需要快取的時間,Time Skew時間越長,該時間視窗資料的延遲越長,佔用記憶體的時間也越長,同時會對流處理系統的吞吐量產生負面影響。

基於時間戳的排序

在流處理系統中,由於流入的訊息時無限的,所以對訊息進行排序基本上被認為是不可行的。但是在Flink流處理系統中,基於WaterMark,Flink實現了基於時間戳的全域性排序。排序的實現思路如下:排序操作符快取所有流入的訊息,當其接收到WaterMark時,對時間戳小於該WaterMark的訊息進行排序,併傳送到下一個節點,在此排序操作符中釋放所有時間戳小於該WaterMark的訊息,繼續快取流入的訊息,等待下一個WaterMark觸發下一次排序。

由於WaterMark保證了在其之後不會出現時間戳比它小的訊息,所以可以保證排序的正確性。需要注意的是,如果排序操作符有多個節點,只能保證每個節點的流出訊息是有序的,節點之間的訊息不能保證有序,要實現全域性有序,則只能有一個排序操作符節點。

通過支援基於Event Time的訊息處理,Flink擴充套件了其流處理系統的應用範圍,使得更多的流處理任務可以通過Flink來執行。

定製的記憶體管理

Flink專案基於Java及Scala等JVM語言,JVM本身作為一個各種型別應用的執行平臺,其對Java物件的管理也是基於通用的處理策略,其垃圾回收器通過估算Java物件的生命週期對Java物件進行有效率的管理。

針對不同型別的應用,使用者可能需要針對該型別應用的特點,配置針對性的JVM引數更加有效率的管理Java物件,從而提高效能。這種JVM調優的黑魔法需要使用者對應用本身及JVM的各引數有深入瞭解,極大地提高了分散式計算平臺的調優門檻。Flink框架本身瞭解計算邏輯每個步驟的資料傳輸,相比於JVM垃圾回收器,其瞭解更多的Java物件生命週期,從而為更有效率地管理Java物件提供了可能。

JVM存在的問題

  • 1.Java物件開銷:相對於C/C++等更加接近底層的語言,Java物件的儲存密度相對偏低,例如[1],“abcd”這樣簡單的字串在UTF-8編碼中需要4個位元組儲存,但採用了UTF-16編碼儲存字串的Java則需要8個位元組,同時Java物件還有header等其他額外資訊,一個4位元組字串物件在Java中需要48位元組的空間來儲存。對於大部分的大資料應用,記憶體都是稀缺資源,更有效率地記憶體儲存,意味著CPU資料訪問吞吐量更高,以及更少磁碟落地的存在。
  • 2.物件儲存結構引發的cache miss:為了緩解CPU處理速度與記憶體訪問速度的差距,現代CPU資料訪問一般都會有多級快取。當從記憶體載入資料到快取時,一般是cache line為單位載入資料,所以當CPU訪問的資料如果是記憶體中連續儲存的話,訪問的效率會非常高。如果CPU要訪問的資料不在當前快取所有的cache line中,則需要從記憶體中載入對應的資料,這被稱為一次cache miss。當cache miss非常高的時候,CPU大部分的時間都在等待資料載入,而不是真正的處理資料,Java物件並不是連續的儲存在記憶體上,同時很多的Java資料結構的資料聚集性也不好。
  • 3.大資料的垃圾回收:Java的垃圾回收機制一直讓Java開發者又愛又恨,一方面它免去了開發者自己回收資源的步驟,提高了開發效率,減少了記憶體洩漏的可能,另一方面垃圾回收也是Java應用的不定時炸彈,有時秒級甚至是分鐘級的垃圾回收極大影響了Java應用的效能和可用性。在時下資料中心,大容量記憶體得到了廣泛的應用,甚至出現了單臺機器配置TB記憶體的情況,同時,大資料分析通常會遍歷整個源資料集,對資料進行轉換、清洗、處理等步驟。在這個過程總,會產生海量的Java物件,JVM的垃圾回收執行效率對效能有很大影響。通過JVM引數調優提高垃圾回收效率需要使用者對應用和分散式計算框架以及JVM的各引數有深入瞭解,而且有時候這也遠遠不夠。
  • 4.OOM問題:OutOfMemoryError是分散式計算框架經常會遇到的問題,當JVM中所有物件大小超過分配給JVM的記憶體大小時,就會出現OutOfMemoryError錯誤,JVM崩潰,分散式框架的健壯性和效能都會受到影響。通過JVM管理記憶體,同時試圖解決OOM問題的應用,通常都需要檢查Java物件的大小,並在某些儲存Java物件特別多的資料結構中設定閾值進行控制。但是JVM並沒有提高官方檢查Java物件大小的工具,第三方的工具類庫可能無法準確通用地確定Java物件大小[6]。侵入式的閾值檢查也會為分散式計算框架的實現增加很多額外與業務邏輯無關的程式碼。

Flink的處理策略

為了解決以上提到的問題,高效能分散式計算框架通常需要以下技術:

  • 定製的序列化工具:顯式記憶體管理的前提步驟就是序列化,將Java物件序列化成二進位制資料儲存在記憶體上(on heap或是off-heap)。通用的序列化框架,如Java預設使用java.io.Serizalizable將Java物件及其成員變數的所有元資訊作為其序列化資料的一部分,序列化後的資料包含了所有反序列化所需的資訊。這在某些場景中十分必要,但是對於Flink這樣的分散式計算框架來說,這些後設資料資訊可能是冗餘資料,定製的序列化框架,如Hadoop的org.apache.hadoop.io.Writable需要使用者實現該介面,並自定義類的序列化和反序列化方法。這種方式效率最高,但需要使用者額外的工作,不夠友好。
  • 顯式的記憶體管理:一般通用的做法是批量申請和釋放記憶體,每個JVM例項有一個統一的記憶體管理器,所有記憶體的申請和釋放都t通過該記憶體管理進行。這可以避免常見的記憶體碎片問題,同時由於資料以二進位制的方式儲存,可以大大減輕垃圾回收壓力。

快取友好的資料結構和演算法。對於計算密級的資料結構和演算法,直接操作序列化後的二進位制資料,而不是將物件反序列化後在進行操作。同時,只將操作相關的資料連續儲存,可以最大化利用L1/L2/L3快取,減少Cache miss的概率,提升CPU計算的吞吐量。以排序為例,由於排序的主要操作是對Key進行對比,如果將所有排序資料的Key與Value分開並對Key連續儲存,那麼訪問Key時的Cache命中率會大大提高。

定製的序列化工具

分散式計算框架可以使用定製序列化工具的前提是要待處理資料流通常是同一型別,由於資料集物件的型別固定,從而可以只儲存一份物件Schema資訊,節省大量的儲存空間。同時,對於固定大小的型別,也可通過固定的偏移位置儲存。在需要訪問某個物件成員變數時,通過定製的序列化工具,並不需要反序列化整個Java物件,而是直接通過偏移量,從而只需要反序列化特定的物件成員變數,如果物件的成員變數較多時,能夠大大減少Java物件的建立開銷,以及記憶體資料的拷貝大小。Flink資料集都支援任意Java或是Scala型別,通過自動生成定製序列化工具,既保證了API介面對使用者友好(不用像Hadoop那樣資料型別需要繼承實現org.apache.hadoop.io.Writable介面),也達到了和Hadoop類似的序列化效率。

Flink對資料集的型別資訊進行分析,然後自動生成定製的序列化工具類。Flink支援任意的Java或是Scala型別,通過呼叫Java Reflection框架分析基於Java的Flink程式UDF(User Define Function)的返回型別的型別資訊,通過Scala Compiler分析基於Scala的Flink程式UDF的返回型別的型別資訊。型別資訊由TypeInformatica類表示,這個類有諸多具體實現類,例如:

  • 1.BasicTypeInfo:任意Java型別(裝包或未裝包)和String型別。
  • 2.BasicArrayTypeInfo:任意Java基本型別陣列(裝包或未裝包)和String陣列。
  • 3.WritableTypeInfo:任意Hadoop的Writable介面的實現類。
  • 4.Tup了TypeInfo:任意的Flink tuple型別(支援Tuple1 to tuple25)。Flink tuples是固定長度固定型別Java Tuple實現。
  • 5.CaseClassTypeInfo:任意的Scala CaseClass(包括Scala tuples)。
  • 6.PojoTypeInfo:任意的POJO(Java or Scala),例如Java物件的所有成員變數,要麼是public修飾符定義,要麼有getter/setter方法。
  • 7.GenericTypeInfo:任意無法匹配之幾種型別的類。

前六種型別資料集幾乎覆蓋了絕大部分的Flink程式,針對前六種型別資料集,Flink皆可以自動生成對應的TypeSerializable定製序列化工具,非常有效率地對資料集進行序列化和反序列化。對於第七種型別,Flink使用Kryo進行序列化和反序列化。此外,對於可被用作Key的型別,Flink還同時自動生成TypeComparator,用來輔助直接對序列化後的二進位制資料直接進行compare、hash等操作。對於Tuple、CaseClass、Pojo等組合型別,Flink自生成的TypeSerializer、TypeComparator同樣是組合的,並把其成員的序列化/反序列化代理給其成員對應的TypeSerializer、TypeComparator,如圖所示:
在這裡插入圖片描述
Flink組合型別序列化

此外如有需要,使用者可通過整合TypeInformation介面定製實現自己的序列化工具。

顯式的記憶體管理

垃圾回收是JVM記憶體管理迴避不了的問題,JDK8的G1演算法改善了JVM垃圾回收的效率和可用範圍,但對於大資料處理實際環境還遠遠不夠。這也和現在分散式框架的發展趨勢有所衝突,越來越多的分散式計算框架希望儘可能多地將待處理資料集放入記憶體,而對於JVM垃圾回收來說,記憶體中Java物件越少、存貨時間越短,其效率越高。通過JVM進行記憶體管理的話,OutOfMemoryError也是一個很難解決的問題。同時,在JVM記憶體管理中,Java物件有潛在的碎片化儲存問題(Java物件所有資訊可能在記憶體中裡連續儲存),也有可能在所有Java物件大小沒有超過JVM分配記憶體時,出現OutOfMemoryError問題。Flink將聶村分為3個部分,每個部分都有不同用途:

  • Network buffers:一些以32KB Byte陣列為單位的buffer,主要被網路模組用於資料的網路傳輸。
  • Memory Manager pool:大量以32KB Byte陣列為單位的記憶體池,所有的執行時演算法(例如Sort/Shuffle/Join)都從這個記憶體池申請記憶體,並將序列化後的資料儲存其中,結束後釋放會記憶體池。
  • Remaining(Free) Heap:主要留給UDF中使用者自己建立的Java物件,由JVM管理。

Network buffers在Flink中主要基於Netty的網路傳輸,無需多講。
Remaining Heap用於UDF中使用者自己建立的Java物件,在UDF中,使用者通常是流式的資料處理,並不需要很多記憶體,同時Flink也不鼓勵使用者在UDF中快取跟多資料,因為這會引起前面提到的諸多問題。
Memory Manager pool(以後以記憶體池代指)通常會配置為最大的一塊記憶體,接下來會詳細介紹。

在Flink中,記憶體池由多個MemorySegment組成,每個MemorySegment代表一塊連續的記憶體,底層儲存是byte[],預設32KB大小。MemorySegment提供了根據偏移量訪問資料的各種方法,如get/put int、long、float、double等,MemorySegment之間資料拷貝等方法和java.nio.ByteBuffer類似。對於Flink的資料結構,通常包括多個向記憶體池申請的MemorySegment,所有要存入的物件通過TypeSerializer序列化之後,將二進位制資料儲存在MemorySegment中,在取出時通過TypeSerializer反序列化。資料結構通過MemorySegment提供的set/get方法訪問具體的二進位制資料。Flink這種看起來比較複雜的記憶體管理方式帶來的好處主要有:

  • 二進位制的資料儲存大大提高了資料儲存密度,節省了儲存空間。
  • 所有的執行時資料結構和演算法只能通過記憶體池申請記憶體,保證了其使用的記憶體大小是固定的,不會因為執行時資料結構和演算法而發生OOM。對於大部分的分散式計算框架來說,這部分由於要快取大量資料最有可能導致OOM。
  • 記憶體池雖然佔據了大部分記憶體,但其中的MemorySegment容量較大(預設32KB),所以記憶體池中的Java物件其實很少,而且一直被儲存池引用,所有在垃圾回收時很快進入持久化,大大減輕了JVM垃圾回收的壓力。
  • Remaining Heap的記憶體雖然由JVM管理,但是由於其主要用來儲存使用者處理的流式資料,生命週期非常短,速度很快的Minor GC就會全部回收掉,一般不會觸發Full GC。

Flink當前的記憶體管理在最底層是基於byte[],所以資料最終還是on-heap,最近Flink增加了off-heap的記憶體管理支援。Flink off-heap的記憶體管理相對於on-heap的優點主要在於:

  • 啟動分配了大記憶體(例如100G)的JVM很耗費時間,垃圾回收也很慢。如果採用off-heap,剩下的Network buffer和Remaining heap都會很小,垃圾回收也不用考慮MemorySegment中的Java物件了。
  • 更有效率的IO操作。在off-heap下,將MemorySegment寫到磁碟或是網路可以支援zeor-copy技術,而on-heap的話則至少需要一次記憶體拷貝。
  • off-heap可用於錯誤恢復,比如JVM崩潰,在on-heap時資料也隨之丟失,但在off-heap下,off-heap的資料可能還在。此外,off-heap上的資料還可以和其他程式共享。

快取友好的計算

磁碟IO和網路IO之前一直被認為是Hadoop系統的瓶頸,但是隨著Spark、Flink等新一代分散式計算框架的發展,越來越多的趨勢使得CPU/Memory逐漸成為瓶頸,這些趨勢包括:

  • 更先進的IO硬體逐漸普及。10GB網路和SSD硬碟等已經被越來越多的資料中心使用。
  • 更高效的儲存格式。Parquet、ORC等列式儲存被越來越多的Hadoop專案支援,其非常高效的壓縮效能大大減少了落地儲存的資料量。
  • 更高效的執行計劃。例如很多SQL系統執行計劃優化器的Filter-Push-Down優化會將過濾條件儘可能的提前,甚至提前到Parquet的資料訪問層,使得在很多實際的工作負載中並不需要很多的磁碟IO。

由於CPU處理速度和記憶體訪問速度的差距,提升CPU的處理效率的關鍵在於最大化的利用L1/L2/L3/Memory,減少任何不必要的Cache miss。定製的序列化工具工具給Flink提供了可能,通過定製的序列化工具,Flink訪問的二進位制資料本身,因為佔用記憶體較小,儲存密度比較大,而且還可以在設計資料結構和演算法時儘量連續儲存,減少記憶體碎片化對Cache命中率的影響,甚至更進一步,Flink可以只是將需要操作的部分資料(如排序時的Key)連續儲存,而將其他部分的資料儲存在其他地方,從而最大可能地提升Cache命中的概率。

Flink排序演算法

以Flink中的排序為例,排序通常是分散式計算框架中一個非常重要的操作,Flink通過特殊設計的排序演算法獲得了非常好的效能,其排序演算法的實現如下:

  • 將待排序的資料經過序列化後儲存在兩個不同的MemorySegment集中,資料全部的序列化值存放於其中一個MemorySegment集中。資料序列化後的Key和指向第一個MemorySegment集中值的指標存放於第二個MemorySegment集中。
  • 對第二個MemorySegment集中的Key進行排序,如需交換Key位置,只需交換對應的Key+Pointer的位置,第一個MemorySegment集中的資料無需改變。當比較兩個Key大小時,TypeComparator提供了直接基於二進位制資料的對比方法,無需反序列化任何資料。
  • 排序完成後,訪問資料時,按照第二個MemorySegment集中Key的順序訪問,並通過Pointer值找到資料在第一個MemorySegment集中的位置,通過TypeSerializer反序列化成Java物件返回。

在這裡插入圖片描述
Flink演算法排序

這樣實現的好處有:

  • 通過Key和Full data分離儲存的方式儘量將被操作的資料最小化,提高Cache命中的概率,從而提高CPU的吞吐量。
  • 移動資料時,只需移動Key+Pointer,而無須移動資料本身,大大減少了記憶體拷貝的資料量。
  • TypeComparator直接基於二進位制資料進行操作,節省了反序列化的時間。

通過定製的記憶體管理,Flink通過充分利用記憶體與CPU快取,大大提高了CPU的執行效率,同時由於大部分記憶體都由框架自己控制,也很大程度提升了系統的健壯性,減少了OOM出現的可能。

總結

本文主要介紹了Flink專案的一些關鍵特效型,Flink是一個擁有諸多特色的專案,包括其統一的批處理和流處理執行引擎,通用大資料計算框架與傳統資料庫系統的技術結合,以及流處理系統的諸多技術創新等。

相關文章