引言
隨著實時數據的日漸普及,企業需要流式計算系統滿足可擴展、易用以及易整合進業務系統。Structured Streaming是一個高度抽象的API基于Spark Streaming的經驗。Structured Streaming在兩點上不同于其他的Streaming API比如Google DataFlow。
第一,不同于要求用戶構造物理執行計劃的API,Structured Streaming是一個基于靜態關系查詢(使用SQL或DataFrames表示)的完全自動遞增的聲明性API。
第二,Structured Streaming旨在支持端到端實時的應用,切將流處理與批處理以及交互式分析結合起來。
我們發現,在實踐中這種結合通常是關鍵的挑戰。Structured Streaming的性能是Apache Flink的2倍,是Apacha Kafka 的90倍,這源于它使用的是Spark SQL的代碼生成引擎。它也提供了豐富的操作特性,如回滾、代碼更新一集混合流\批處理執行。
我們通過實際數據庫上百個生產部署的案例來描述系統的設計和使用,其中最大的每個月處理超過1PB的數據。
1.介紹
許多高容量的數據源是實時產生數據的,比如傳感器、移動應用程序的日志以及物聯網。隨著組織在獲取這些數據方面做的越來越好,它們將目光放在了處理這些實時數據上,這可以為人類的分析帶來最新的數據以及驅動自動決策。支持廣泛的流計算訪問需要系統易于擴展、易于使用且易于集成到業務應用中。
盡管在過去的幾年中分布式流技術取得了巨大的進步,但在實際生產中使用它們還是有不小的挑戰。我們從描述這些挑戰開始,基于我們在Spark Streaming上的經驗,這是最早期的流處理引擎,它提供了高度抽象和函數式的API。
我們發現使用中頻繁的出現兩種挑戰:
第一,流處理系統時常要求用戶考慮復雜的物理執行概念,例如at-least-once delivery,狀態存儲和觸發模式,這些都是流處理系統獨有的挑戰。
第二,許多系統只關注流式計算,但是實際用例中,流通常是大型業務應用的一部分,它包含批處理,會和靜態數據進行連接,且會進行交互式查詢。集成這些帶有其他工作的流處理系統需要大量的工程工作。
基于這些挑戰,我們描述結構化流為一種新的用于流處理的高度抽象的API。Structured Streaming吸取了很多流處理系統的點子,比如Google Dataflow的分離事件發生事件和觸發器處理時間,使用關系型執行引擎獲得更好的性能,以及提供一個綜合性的API,旨在使他們更易用且整合進Apache Spark中。特別的,Structured Streaming在兩點上和廣泛使用的開源流數據處理API不同:
增量查詢模型:
Structured Streaming在靜態的數據集上通過Spark SQL和DataFrame API表現自動的增量查詢,這意味著用戶只需要了解Spark批處理API就可以編寫一個流數據查詢。事件事件概念在這個模型里易于表達及理解。盡管增量查詢引擎和試圖維護已有深入的研究,但Structured Streaming是第一個廣泛使用它們的開源系統。我們發現這個增量的API不僅適用于高級用戶,同時也適用于初學者。例如,高級用戶可以使用一組有狀態的處理操作符實現對自定義邏輯的細粒度控制,同時適用于增量模型。
端到端應用的支持
當與外部系統交互或集成進更大的應用程序時,Structured Steaming的API以及內置的連接器使得編寫“默認正確”的代碼變得容易。數據的sources和sinks遵循簡單的事務模型,默認情況下支持“exactly-once”。基于遞增的API使得用批處理作業方式開發一個流式查詢以及將流與靜態數據的連接變得容易。此外,用戶可以動態的管理多個流查詢并對流輸出的一致性快照做交互式查詢。
除了這些設計外,我們還做了其他的一些設計,簡化了Structured Steeaming的開發,并增強了其性能。
第一,Structured Streaming重用了Spark SQL的執行引擎,包括它的optimizer和runtime code generator。這樣與其他流處理系統相比,Structured Steaming具有了更高的吞吐量。(Flink的兩倍,Kafka的90倍),這也讓Structured Streaming從Spark SQL以后的更新中受益。引擎默認運行在microbatch的處理模式下,但是對于一些查詢,它也可以使用一個低延遲的連續操作。
第二,我們發現,操作一個流處理應用是具有挑戰性的,所以我們設計引擎支持對故障、代碼更新已輸出數據的重新計算。例如,一個常見的問題是流中心的數據導致應用程序崩潰,輸出一個錯誤的結果,用戶知道很久以后才會注意到(例如,由于錯誤解析字段)。在Structured Streaming中,每個應用程序維護一個write-ahead event log(WAL),使用JSON格式,管理者可以從任意點重新啟動應用程序。如果應用程序由于用戶定義函數中的錯誤而崩潰,管理員可以更新UDF并且從它停止的地方重啟,這時會自動的讀取WAL。如果應用程序輸出了錯誤的數據,管理員可以手動的回滾到問題開始之前,重新計算。
我們的團隊從2016年開始一直在Databricks的云服務中運行Structured Streaming,以及在內部使用它,所以我們用一些例子來總結本章。生產環境的應用程序范圍包括交互式網絡安全分析、自動報警增量提取以及ETL過程。最大的客戶應用程序每月處理超過1PB的數據,在數百臺機器上運行。在雅虎的Streaming Benchmark測試中,Structured Streaming的表現是Flink的2倍,Kafka的90倍。
本文的其余部分組織如下。第二章討論流處理的挑戰,第三章給出Structured Streaming的概述,第四章描述其API,第五章討論其查詢計劃,第六章討論其執行,第七章討論其操作特性。在第八章,我們描述在Databricks的用例以及用戶。第九章我們將要測試系統的性能。第十章討論相關工作,第十一章總結。
2.流處理的挑戰
盡管在過去的幾年里取得了廣泛的進展,分布式的流應用仍然難以開發和操作。在設計Structured Streaming之前,我們花了很多時間與用戶即設計師討論流處理系統的挑戰,包括Spark Streaming、Truviso,Storm,Dataflow以及Flink。
2.1 復雜和低級的API
流系統因為其API語義的復雜被認為相比批處理系統更難于使用。有些復雜性來源于只在流中出現的問題:比如,用戶需要考慮在系統接收到全部數據前應輸出什么樣的中間狀態,例如某網站上用戶的瀏覽會話。然而,一起復雜性的出現時因為其低級的API:這些API經常要求用戶處理復雜的物理執行操作,達不到聲明式級別。
作為一個具體的例子,Google Dataflow有一個功能強大的API,具有豐富的事件處理選項去處理聚合、窗口化和無序數據。然而在這個模型中,用戶需要指定窗口模式,觸發模式以及觸發細化模式。原始API要求用戶編寫一個物理操作視圖,而不是邏輯查詢,所以每個用戶都需要理解增量處理的復雜性。
其他的APIs,例如Spark Streaming和Flink的DataStream API,也基于編寫物理操作的DAG,且提供了復雜的選項去維護狀態。此外,應用程序變得更加復雜,這放松了exactly-once語義,要求用戶設計和實現一個一致性模型。為了解決這個問題,我們設計了Structured Streaming來實現簡單的增量查詢模型簡單的表示應用程序。此外,我們發現添加可定制的有狀態處理操作符仍然支持高級用戶構建自己的處理邏輯,比如基于會話的定制、窗口(這些操作符同樣可以在批任務中工作)。
2.2 集成到端到端應用程序
我們發現的第二個挑戰是幾乎所有的流處理任務必須運行在一個更大的應用程序中,這樣的集成通常需要大量的工程工作。很多流式APIs主要關注從source輸入,并將流輸出寫入到sink,但端到端的應用程序需要執行其他任務,包括:
(1)應用程序的業務目的可能是對最新數據進行交互式查詢。在本例中,一個流處理任務更新RDBMS或者Hive中的匯總表。重要的是,當流作業在更新結果的過程中,它是原子的,用戶不要看到部分結果。這對于基于文件的大數據系統比如Hive來說是困難的,Hive中的表被分割到不同的文件,甚至并行的加載到數據倉庫。
(2)在ETL作業中可能需要加入從另一個存儲系統加載靜態數據的流或使用批處理計算進行轉換。這種情況下,兩者間的一致性就變得異常重要(如果靜態數據被更新怎么辦?),在同一個API中編寫整個計算是很有用的。
(3)一個團隊可能偶爾需要用批處理方式運行它的流處理業務邏輯,例如:在舊數據上填充結果或者測試代碼的其他版本。用其他系統重寫代碼既費時又容易出錯。
我們通過Structured Streaming來解決這個挑戰,它與Spark批處理和交互API緊密結合。
2.3 業務挑戰
部署流應用程序的最大挑戰之一是實踐中的管理和運維。一些關鍵問題如下:
(1)失敗:這是研究中最受關注的問題。除了單節點故障外,系統還需要支持整個應用程序的優雅關閉和重啟,例如,操作人員將其遷移到一個新的集群。
(2)代碼更新:應用程序很少是完美的,所以開發者需要更新他們的代碼。更新之后,他們可能想要應用程序在停止的地方重新啟動,或者重新計算由于錯誤而導致的錯誤結果。流處理系統的狀態管理需要同時支持者兩者,且要實現故障恢復機制,系統還應支持運行時更新。
(3)重新調節:隨著時間推移,應用程序的負載會發生變化,長期來看,負載會不斷增大,所以用戶可能希望動態的對其進行縮放,特別是在云中。
(4)落單節點:系統中的節點可能會因為軟件或硬件問題而慢下來,這會拖慢整個應用程序的吞吐量,系統應該自動處理這種情況。
(5)監控:流處理系統需要讓用戶看到系統的負載、狀態以及其他的指標。
2.4 性能挑戰
除了運營和工程方面的問題,成本效益對于流應用程序可能是一個障礙,因為這些應用程序時24/7運行的。例如,如果沒有動態縮放,應用程序會在繁忙時間外浪費資源;即使有了動態縮放,運行一個連續計算的任務可能比運行定期批處理作業更昂貴。因為,我們設計Structured Streaming能利用Spark SQL中的所有執行優化。
到目前為止,我們以吞吐量為主要性能度量,因為我們發現在大規模的流應用程序中,吞吐量通常是最重要的度量。需要分布式流處理系統的應用程序通常有著來自外部數據源的大量數據(例如移動設備、傳感器或物聯網),數據可能在到達系統時已經產生了延遲。這就是為什么事件時間處理是這些系統中的重要特性。相比之下,延遲敏感的應用程序,如高頻交易或物理系統控制循環通常運行在單個放大器上,甚至是定制硬件如ASIC和FPGA上。然而,我們也設計Structured Streaming支持在延遲優化的引擎上執行,并實現了任務的連續處理模式,這些將在第6.3節中進行描述。這與Spark Streaming相比是一個很大的不同。
3 Structured Streaming概述
Structured Streaming旨在解決流處理的挑戰,通過API和執行引擎設計的結合。在本節中,我們將簡要概述系統的總體情況,圖1展示了Structured Streaming的核心組件。
輸入和輸出
Structured Streaming連接到各種I/O的輸入源和輸出源。為了提供“exactly-once”的輸出以及容錯,它對sources和sinks設定了兩條限制,和其他的exactly-once系統一樣:
(1)輸入sources必須可重讀,當一個節點崩潰的時候允許系統重新讀取最近的輸入數據。實踐中,組織需要使用可靠的消息總線,比如Kinesis或Kafka,或者一個持久的文件系統。
(2)輸出sinks必須支持冪等寫操作,確保在節點失敗時進行可靠的恢復。Structured Streaming對特定的sinks支持原子輸出,作業輸出的更新呈現原子性,即使它是由多個并行工作的節點輸出的。
除了外部系統,Structured Streaming還支持Spark SQL表的輸入和輸出。例如,用戶可以從Spark的任意批輸入源計算一個靜態表并將其與流進行連接操作,或請求Structured Streaming輸出一個內存中的Spark表用于交互式查詢。
API
用戶通過Spark SQL的批API:SQL和DataFrame來編寫Structured Streaming對一個或多個流或表進行查詢。這個查詢定義了一個用戶想要計算的輸出表,并假設每個輸入流被替換為一個實時接收數據的數據表。然后引擎決定以增量方式計算和寫入輸出表到sink中。不同的sink支持不同的輸出模式,這決定了系統如何寫出其結果:例如,有些sink是append-only的,而另一些允許按鍵更新記錄。
特別的,為了支持流,Structured Streaming增加了幾個API功能適應現有的Spark SQL API。
(1)Triggers控制引擎計算的頻率
(2)用戶可以將一列標記為event time(時間戳),并設置一個watermark決定event time的過期。
(3)有狀態操作符允許用戶跟蹤和更新可變狀態,通過鍵來實現復雜的處理,如定制基于會話的窗口。
Execution
一旦收到一個查詢,Structured Streaming會優化它,使其遞增化,并開始執行它。默認情況下,該系統使用類似于Spark Streaming離散流的微批模型,支持動態負載,動態縮放,故障恢復。此外,它還支持使用連續處理模型基于傳統的長時間運行操作符(6.3節)。
在這兩種情況下,Structured Streaming都使用以下兩種形式的持久化存儲來實現容錯。第一,通過WAL日志跟蹤哪些數據已被處理并可靠地寫入。對于一些sinks,這個日志可以與sink結合以對sink進行原子更新;第二,系統使用大規模的狀態存儲保存長時間運行的聚合操作的狀態快照。這些都是異步寫入,并且可能“落后”于最新寫入的數據。系統將自動跟蹤日志中最后一次更新的狀態,并從此處開始重新計算狀態。日志和狀態存儲都可以運行于可插拔存儲系統(HDFS或者S3)。
操作特性
使用WAL和狀態存儲,用戶可以實現多種形式的回滾和復原。一個Structured Streaming應用程序可以關閉并在新硬件上重啟。運行應用程序也能容忍節點崩潰、添加和掉隊,以及向新的node派遣任務。對于UDF的代碼更新,停止并重啟應用程序就夠了,它將開始使用新的代碼。此外,用戶還可以手動回滾應用程序到日志中之前的一點,重做部分計算,也可以從狀態存儲的舊快照開始運行。
接下來的章節會描述Structured Streaming API的細節,查詢計劃以及作業執行和操作。
4 編程模型
Structured Streaming結合了Google Dataflow,增量查詢和Spark Streaming來支持Spark SQL API下的流處理。本節中,我們首先展示一個簡短的示例,然后在Spark中添加的模型以及特定于流的操作符的語義。
4.1 簡短示例
Structured Streaming使用Spark結構化數據APIs:SQL,DataFrame和Dataset。對于用戶而言,主要的抽象是tables(由DataFrames或Dataset類表示)。當用戶從流中創建table/DataFrame并嘗試計算它,Spark自動啟動一個流計算。作為一個簡單的示例,我們從一個計數的批處理作業開始,這個作業計算一個web應用程序按照國家統計的點擊數。假設輸入的數據時JSON文件,輸出應該是Parquet。這個作業可以用Spark DataFrames寫出,如下所示:
//define a DataFrame to read from static data
data = spark.read.format("json").load("/in")
//Transform it to compute a result
counts = data.groupBy($"country").count()
//write to a static data sink
counts.write.format("parquet").save("/counts")
將此作業改為使用Structured Streaming,修改輸入和輸出源,不需要再中間做轉換。例如,如果新的JSON文件繼續上傳到/in目錄,我們可以修改任務通過只更改第一行和最后一行來進行持續更新/計數
//Define a DataFrame to read streaming data
data = spark.readStream.format("json").load("/in")
//Transform it to compute a result
counts = data.groupBy($"country").count()
//Write to a streaming data sink
counts.writeStream.format("parquet").outputMode("complete").start("/counts")
這里的output mode參數指定了Structured Streaming如何更新sink。本例中,complete模式表示為每個更新都寫出全量的結果文件,因為選擇的sink不支持細粒度更新。然而,其他接收器(如鍵值存儲)支持附加的輸出模式(例如,只更新已更改的鍵)。
在底層,Structured Streaming將由source到sink的轉換自動遞增化,并以流方式執行它。引擎也將自動維護狀態和檢查點到外部存儲-本例中,存在一個運行的計數聚合,因此引擎將跟蹤每個國家的計數。
最后,API自然支持窗口和事件時間,通過Spark SQL現有的聚合操作符。例如,我們不按國家來計數,而是設置一個一小時的滑動窗口,每5分鐘滑動一次,根據窗口進行計數
//Count events by windows on the "time" field
data.groupBy(window($“time”,"1h","5min")).count()
這里的time字段(event time)只是數據中的一個字段,類似country。用戶還可以在此字段上設置一個watermark讓系統在超時后拋棄舊窗口。
4.2 編程模型語義
我們定義了Structured Streaming的語義模型如下:
(1)每個輸入源都提供一部分有序的記錄。我們假設這里是部分記錄是因為一些消息總線系統是并行的且不保證整個記錄的順序——例如Kafka將流分成“分區”。
(2)用戶提供一個查詢,在輸入數據上執行,輸出一個結果表(result table),這個結果表可以在任意時間的任意點輸出。Structured Streaming在所有輸入源中的數據前綴上運行此查詢始終會產生一致的結果。也就是說,絕不會發生這樣的情況,結果表中合并了一條輸入的數據但沒有合并在它之前的數據。此外,這些前綴將隨著時間推移而增加。
(3)Triggers告訴系統何時運行新的增量計算,何時更新結果表。例如,在microbatch模式下,用戶可能每分鐘觸發一個增量更新。
(4)sink的output mode指定了結果表如何寫入到輸出系統中。引擎支持以下三種不同的模式:
complete
引擎一次性寫出整個結果表,例如,用一個新版本的文件替換HDFS中的整個舊版本文件。當結果很大時,這種方式會非常低效。
append
引擎只能向sink添加記錄。例如,一個只有map操作的作業,會單調遞增的輸出。
update
引擎根據一個鍵在合適的位置更新sink,只更新發生更改的記錄。
圖2直觀的說明了模型。這個模型中,最具吸引力的一點是結果表的內容(邏輯上只是一個視圖,不需要具體化)是獨立定義于輸出模式(是否需要再每個trigger時輸出整個結果表)。
另一個具有吸引力的特性是模型具有很強的一致性語義,我們稱之為前綴一致性。首先,它保證當輸入記錄屬于同一個源(例如,日志記錄來自同一設備),系統產生的結果會保證其順序(例如,從不跳過一條記錄)。第二,因為結果表是基于同時輸入前綴中的所有數據,我們知道在結果表中反映了所有輸入記錄。相反,在一些基于節點間消息傳遞的系統中,一個節點接收到一條記錄會發送一條更新到下游的兩個節點,但不能保證這兩個輸出是同步的。前綴一致性也使操作更容易,用戶可以將系統滾動到WAL(一個數據的特定前綴)中的一點,并從該點開始重新計算。
總之,使用Structured Streaming模型,只要用戶可以理解普通的Spark和DataFrame查詢,即可了解結果表的內容和將要寫入sink的值。用戶無需擔心一致性、失敗或不正確的處理順序。
最后,讀者可能會注意到我們定義的一些輸出模式與某些類型的查詢不兼容。例如,假設我們按照國家進行聚合技術,如上一節中代碼所示,我們希望使用append輸出模式。系統沒法保證什么時候停止接收某一特定國家的記錄,所以這個查詢和輸出模式的組合不正確。我們將在5.1節中描述允許的組合。
4.3 流中的特定操作符
許多Structured Streaming查詢可以使用Spark SQL中的標準操作符寫出,比如選擇,聚合和連接。然而,為了支持流的一些獨有需求,我們在Spark SQL中增加了兩個新的操作符:watermarking操作符告訴系統何時關閉一個時間事件窗口和輸出結果,并忘記其狀態,stateful操作符允許用戶寫入自定義邏輯以實現復雜的處理。至關重要的是,這兩個操作符仍然適合于Structured Streaming的增量語義,且它們都可用于批處理作業。
4.3.1 Event time watermarks
從邏輯的角度來看,event time的關鍵思想是將應用程序指定的時間戳看為數據中的任意字段,允許記錄不按照順序到達。我們可以使用標準運算符和增量運算符更新以event time分組的結果。實踐證明,對于處理系統而言,設置一些關于數據延遲到達的寬松界限是十分有用的,以下是兩個原因:
(1)允許任意延遲的數據可能需要存儲任意大的狀態。例如,如果我們按照1分鐘的event time窗口對數據進行計數,系統需要記錄每一個1分鐘的窗口計數,因為遲到的數據可能屬于任意一分鐘。這將迅速導致大量的狀態。
(2)一些sinks不支持數據回退,這使得它能在超時后為指定的event time寫出結果。例如,自定義下游應用程序希望使用“最終”結果啟動工作,但是它不支持回退。append輸出模式的sink也不支持回退。
Structured Streaming允許開發人員為event time列設置一個watermark,使用withWatermark操作符。這個操作符在一個給定的時間戳列C上設置一個系統的延遲閾值Tc。在任意時間,C的watermark為max(C)-Tc.請注意,這種watermark是健壯的,可以防止積壓數據:如果系統在一段時間內無法跟上輸入速率,則watermark不會隨意的往前移動,所有在T秒內到達的時間仍會被處理。
如果watermark存在,它會影響有狀態操作符忘記舊狀態,Structured Streaming可以以append模式輸出數據到sink。不同的輸入流會有不同的watermarks。
4.3.2 Stateful Operators
對于想要編寫流處理邏輯的開發人員,Structured Streaming有狀態的操作符(具有狀態的UDF)可以讓用戶在計算的同時融入Structured Streaming以及容錯機制。有兩種有狀態操作符,mapGroupsWithState和flatMapGroupsWithState。這兩種操作符會對數據指定一個key并使用groupByKey操作,并允許開發人員定制跟蹤和更新每個鍵的state,以及每個鍵的輸出記錄。它們的基礎是Spark Streaming的updateStateByKey操作符。
mapGroupsWithState操作符,用于分組數據集,數據集中的鍵類型為K,值的類型為V,接收用戶定義的具有以下參數的update function:
(1)key of type K
(2)newValue of type Iterator[V]
(3)state of type GroupState[S],where S is a user-specified class
//define an update function that simply tracks the
//number of events for each key as its state, returns
//that as its result, and times out keys after 30min
def updateFunc(key:Userid, newValues:Iterator[Event],
state:GroupState[Int]):Int = {
val totalEvents = state.get() + newValues.size()
state.update(totalEvents)
state.setTimeoutDuration("30 min")
return totalEvents
//Use this update function on a stream,returning a
//new table lens that contains the session lengths
lens = events.groupByKey(event => event.userId).
mapGroupsWithState(updateFunc)
當一個鍵接收到新的值時,運算符將調用這個函數。每次調用時,都會接收到從上次調用到現在該鍵接收到的所有值(為了提高效率,可以對多個值進行批處理)。同樣能接收到一個被用戶定義的數據類型S所包圍的state對象,允許用戶更新狀態,從狀態跟蹤中刪除此鍵,或者為這個特定的鍵設置超時時間。這允許用戶為Key存儲任意數據,以及為刪除狀態實現自定義邏輯(實現基于會話窗口的退出條件)。
最后,update函數返回用戶指定的返回類型R。mapGroupsWithState的返回值是一個新表,包含了數據中每組的最終R條輸出記錄(當group關閉或者超時)。例如,開發人員希望使用mapGroupsWithState跟蹤用戶在網站上的會話,并輸出為每個會話點擊的頁面總數。
圖3展示了如何使用mapGroupsWithState跟蹤用戶會話,其中會話被定義為一系列事件,使用相同的用戶標識,他們之間的間隔不到30分鐘。我們在每個會話中輸出時間的最終數量作為返回值R。然后,一個作業可以通過聚合結果表計算每個會話時間數的平均值。
另一個有狀態操作符,flatMapGroupsWithState跟mapGroupsWithState十分相似,但是其更新函數每次更新時可以返回0或者更多,而不只是1。例如,這個操作符可以用來手動實現stream-to-table的join操作。這兩個操作符也可以在批處理模式下工作,但是其更新函數只會被調用一次。
五.查詢計劃
我們使用Spark SQL中的Catalyst可擴展優化器實現Structured Streaming中的查詢計劃,這允許使用Scala中的模式匹配寫入可組合規則。查詢計劃分三個階段:(analysis)分析確定查詢是否有效、(incrementalization)遞增化以及(optimization)優化。
5.1 Analysis
查詢計劃的第一個階段是analysis,在這個階段引擎會驗證用戶的查詢并解析屬性和數據類型。Structured Streaming使用Spark SQL現有的analysis解析屬性和類型,但是增加了新規則,檢查查詢是否可被引擎遞增執行。本階段還檢查了用戶選擇的輸出模式是否對此查詢有效。例如,Append模式只能用于輸出為單調的查詢:也就是說,一條輸出記錄一旦被寫出就不會被移除。這種模式下,只有包含event time的選擇、連接和聚合是被允許的(這種情況下,引擎只有在watermark過期時才會輸出該值)。類似的,在complete輸出模式下,trigger每次觸發時都要寫出整張表。在Structured Streaming的官方文檔中可以獲得輸出模式的完整描述。
5.2 Incrementalization
查詢計劃的下一步是將用戶提供的靜態查詢遞增化,以有效的在新數據來臨時更新結果。一般來說,Structured Streaming的增量化器確保查詢的結果在新數據接收時及時被更新,而不依賴于目前收到的總行數。
引擎可以遞增化一個受限制的、不斷增長的查詢。從Spark2.3.0版本開始,支持的查詢包括:
-任意數量的選擇,投影和select distincts。
-流和表,兩個流之間的內連接、左外連接和右外連接。對一個流進行外部連接,連接條件必須包含一個watermark。
-有狀態操作符比如mapGroupsWithState
-最多一個聚合(可能在復合鍵上)
-聚合后的排序,只能在complete輸出模式下
引擎使用Catalyst轉換規則將這些支持的查詢映射為物理執行樹,執行計算和狀態管理。例如,用戶查詢中的一個聚合可能會映射到有狀態聚合操作符,并跟蹤Structured Streaming中的開放組的狀態存儲和輸出。在內部,Structured Streaming還跟蹤在增量化過程中產生的DAG重的每個物理操作符的輸出模式,類似于Dataflow中的細化模式。例如,一些操作會更新已發出的記錄(相當于update模式),另一些值更新發出的新記錄(append模式)。至關重要的是,在Structured Streaming中,用戶不必手動指定這些內部的DAG模式。
增量化是Structured Streaming研究中的一個活躍領域,但我們發現,即使是現今的很多受限的查詢集也適用于很多用例。在其他情況下,用戶利用Structured Streaming有狀態的操作符實現自定義增量處理邏輯,以保持其選擇的狀態。我們希望在引擎中增加更劍仙的自動化遞增技術。
5.3 Query Optimization
查詢計劃的最后一個階段是優化。Structured Streaming應用了Spark SQL中的大多數優化規則,例如謂詞下推,投影下推,表達式簡化等。此外,對于內存中的數據,使用Spark SQL的Tungsten二進制格式(避免Java內存開銷),它的運行時代碼生成器用于將連接符編譯為Java字節碼。這個設計意味著Spark SQL中的大多數邏輯和執行的優化能自動的應用到流上。
六.應用程序執行
Structured Streaming的最后一個組成部分是它的執行策略。本節中,我們將描述引擎如何跟蹤狀態,然后是兩種執行模式:基于細粒度任務的微批以及基于長時操作符的連續處理。然后,我們討論能夠簡化Structured Streaming應用程序管理和部署的操作特性。
6.1 狀態管理和恢復
在高層次抽象上,Structured Streaming以Spark Streaming類似的方式跟蹤狀態,不管在微批還是連續模式中。使用兩個外部存儲跟蹤應用程序的狀態:支持持久的、原子、低延遲寫入的WAL日志,可以存儲大量數據并允許并行訪問的state store(S3或HDFS)。Structured Streaming使用這兩種系統進行失敗恢復。引擎對sources和sinks在容錯上提出了兩個要求:第一,sources必須是可重放的,允許使用某種形式的標識符重讀最近的數據,比如流偏移量。持久化的消息總線系統比如Kafka和Kinesis滿足這個要求。第二,sinks應該是冪等的,允許Structured Streaming在失敗時重寫一些已經存在的數據。sinks可以用不同的方式實現它。
鑒于這些屬性,Structured Streaming使用以下機制來進行狀態跟蹤,如下圖所示:
(1)當輸入操作讀取數據時,Spark的Master根據每個輸入源中的offsets定義epochs。例如,Kafka和Kinesis將topic呈現為一系列分區,每個分區都是字節流,允許讀取在這些分區上使用偏移量的數據。Master在每個epoch開始和結束的時候寫日志。
(2)任何需要定期、異步檢查state store中狀態的操作都盡可能使用增量的檢查點。它們同時存儲了epoch ID和每個檢查點。這些檢查點不需要在每個epoch都發生或阻塞處理。
(3)輸出操作將提交的epoch寫入日志。Master節點在提交下一個epoch前等待所有運行操作的節點報告。根據sink的不同,如果sink支持多節點寫入,Master會運行多個節點完成寫入。這意味著如果流應用程序失敗,只有一個epoch會被部分寫入。
(4)恢復后,應用程序的新實例會查找log中最后一個未被提交到sink的epoch,其中包括其開始和結束offsets。然后使用之前epoch的offset重建應用程序內存內的狀態。這只需要加載舊的狀態并運行那些epoch,使用其禁用輸出時相同的偏移量。最后,系統重新運行上一個epoch,依賴于sink的冪等性寫出結果,然后開始新的epoch。
最后,狀態管理中的所有設計對用戶代碼來說都是透明的。聚合操作和用戶自定義狀態管理操作(例如mapGroupsWithState)自動向state store中存儲檢查點,不需要用戶自己編碼實現。用戶的數據類型只需要序列化即可。
6.2 微批處理模式
Structured Streaming的任務可以以兩種模式運行:微批和連續操作。微批模式使用離散化的流執行模型,這是從Spark Streaming的經驗中得來,并繼承了它的有點,比如動態負載平衡,縮放,掉隊,不需要整個系統回滾的故障恢復。在這種模式下,epoch通常設置為幾百毫秒到幾秒,每個epoch作為一個傳統Spark任務由一系列獨立的task組成DAG。和Spark Streaming一樣,這種模式具有以下優點:
(1)動態負載平衡:每個操作都可以被分成很小的、獨立的task在多個節點上進行調度,這樣系統就可以自動平衡這些節點(如果某些節點執行速度比其他節點慢)。
(2)細粒度的故障恢復:如果節點失敗,則可以僅僅執行其上的任務,而無需回滾整個集群到某檢查點,這和大多數基于拓撲的系統一樣。此外,丟失的任務可以并行的重新運行,這可以進一步減少恢復時間。
(3)失效節點處理:Spark將啟動備份副本,就像他在批處理作業中所做的,下游任務也會使用最先完成的輸出。
(4)重新調節:添加或刪除節點與task一樣簡單,這將自動在所有可用節點上自動調度。
(5)規模和吞吐量:因為這個模式重用了Spark的批處理執行引擎,它集成了這個引擎所有的優化,比如高性能的shuffle實現以及在數千個節點上運行的能力。
這種模式的主要缺點是延遲時間長,因為在Spark中啟動任務DAG是有開銷的。然而,幾秒的延遲在運行多步計算的大型集群上是可以實現的。
6.3 連續執行模式
在Spark 2.3中添加了一個新的連續處理引擎,它使用long-lived操作,如同傳統的流系統Telegraph和Borealis。這種模式的延遲較低,單操作靈活度較低(對在運行時重新調整作業的支持有限)。
這種執行模式的關鍵是選擇聲明性的API,不綁定到Structured Streaming的執行策略。例如,最早的Spark Streaming API有一些基于處理時間的操作泄露了微批的概念,這使其難以自動程序到另一種類型的引擎。相反,Structured Streaming的API和語義獨立于之執行引擎:連續執行類似于更多的trigger。注意,與純粹基于非同步的消息傳遞系統不同,如Storm,我們保留了trigger和epoch的概念在這個模型里,以便多個節點的輸出可以協調并一起提交到sink。
因為API支持細粒度的執行,所以Structured Streaming的作業理論上可以運行在任何分布式的流引擎上。在連續處理引擎中,我們在Spark建立了一個簡單的連續操作引擎,并且可以重用Spark的基礎調度引擎和每個節點的操作符(代碼生成操作)。Spark 2.3.0中的第一個版本只支持類似map的任務(沒有shuffle操作),這是用戶最常見的場景,但是后續的設計將會加入shuffle操作。
相比于批處理引擎,持續處理有兩點不同:
(1)master節點在輸入源的每個partition上啟動一個long-running任務,但是啟動多個epoch。如果其中一個任務失敗了,Spark會重啟它。
(2)epoch的協調是不同的。Master節點定期告訴node啟動一個新的epoch,并接收每個輸入partition上的一個開始offset,并將其寫入WAL中。當要求node啟動下一個epoch時,Master節點會接收到上一個epoch的結束offset,并將其寫入WAL,當寫入了所有結束offset后,會告訴節點提交這個epoch。
八.生產用例
我們在2016年就在Databricks的managed cloud service中支持了Structured Streaming,今天,我們的云上24小時7天不間斷的運行著數百個生產環境流應用程序。其中最大的每月處理超過1PB數據,且運行在數百臺服務器上。我們也用Structured Streaming去監控我們的服務,其中也包括Structured Streaming自身的運行。本節上,我們描述三種不同的客戶工作負載,以及我們的內部用例。
8.1 信息安全平臺
一個大客戶使用Structured Streaming開發一個大規模的安全平臺,允許超過100個分析通過網絡流量日志快速識別和響應安全事件,以及自動生成報警。這個平臺將流與批處理和交互相結合,是一個端到端應用程序的好例子。
圖5展示了這個平臺的架構。IDS(intrusion detection system)監控組織上所有的網絡流量,并將日志寫入S3。從這里開始,一個Structured Streaming的ETL作業存儲到一個緊湊的基于Apache Parquet的表中,存放于Databricks Delta,允許下游應用程序快且并發的訪問。其他的Structured Streaming作業將這些日志產生附加的表(通過和其他數據的連接操作)。分析師交互的查詢這些數據,使用SQL或者Dataframe,從而檢測和診斷新的攻擊模式。如果他們找到了危害,他們會回顧歷史數據跟蹤來自該攻擊者的活動。最后,并行的,另一個Structured Streaming的集群會處理Parquet日志根據預先編寫的規則生成實時的警報。
這個平臺最大的挑戰在:
(1)構建一個健壯且伸縮性強的流管道
(2)給分析人員提供一個高效的環境去查詢新老數據。
使用AWS上提供的標準工具和服務,一個20人的團隊花了6個多月的時間來構建和部署此平臺的最初版本。這個最初版本有很多的限制,比如只能存儲一小部分歷史數據由于使用傳統的數據倉庫。相比之下,一個五人的工程師團隊能夠在兩周內使用Structured Streaming重構這個平臺。這個新平臺支持更好的擴展性,且能夠支持更復雜的分析,這是因為可以使用Spark ML API。接下來,我們提供一些例子來說明Structured Streaming的優點使這些成為可能。
首先,Structured Streaming能夠自適應批的規模,使得開發人員可以構建一個能夠處理大量工作的流管道,同時還能滿足故障和代碼升級。考慮一個流作業,它可能因為失敗而離線,或者進行一次升級。當集群恢復上線時,它會開始自動處理離線時未處理的數據。最初,集群將使用大量的批處理去最大化吞吐量。一旦趕上,集群會切換為低延遲的小批量進行處理。這允許管理員定期升級集群,無需擔心過度停機。
第二,Structured Streaming可以與其他流進行join操作,與歷史表也可以,這樣大大簡化了分析。考慮一個簡單的任務,識別哪個設備是來源于TCP連接的。事實證明,這項任務是具有挑戰性的,因為移動設備的存在,因為這些設備的IP地址在每次它們加入網絡時都是動態的。因此,只依靠TCP日志,不可能跟蹤終端的連接。使用Structured Streaming,分析人員能夠簡單的解決這個問題。她可以簡單的將TCP日志與DHCP日志進行join,將IP地址和MAC地址映射起來,然后使用組織內部的數據網絡設備映射到MAC地址特定的機器和用戶。另外,用戶也可以即時的使用stateful operator進行join操作。
最后,使用相同的系統開發流、交互式查詢和ETL為開發人員提供了快速迭代的能力,以及部署新的警報。特別的,它使得分析師能夠構建和測試對檢測脫機數據供給的查詢,然后將這些查詢部署在報警集群上。在一個例子中,一個分析師通過DNS開發了一個查詢識別攻擊。在這次攻擊中,惡意軟件通過將此信息裝載到DNS中泄露機密信息,從而危及主機發送到攻擊者擁有的外部DNS服務器的請求。一個用于檢測這種攻擊的簡化查詢實際上計算了在一定時間間隔內每個主機發送的DNS請求的總大小。如果聚合大于給定的閾值,則查詢標記對應的主機可能受到危害。分析師利用歷史數據來設置這個閾值,從而達到平衡假正率和假負率之間的期望平衡。一旦滿足了結果,分析人員會簡單地將此查詢推到報警集群中去。
九.性能評價
本節中,我們將使用控制基準度量Structured Streaming的性能。我們在Yahoo的Streaming Benchmark研究Structured Streaming與其他系統的性能,可伸縮性,吞吐量-延遲與連續處理之間的權衡。
9.1 性能 vs 其他流系統
為了評估Structured Streaming相比于其他流引擎的性能,我們使用Yahoo的流基準平臺,一個在開源系統中廣泛使用的工作負載。此基準測試要求系統讀取廣告點擊事件,并按照活動ID加入到一個廣告活動的靜態表中,并在10秒的event-time窗口中輸出活動計數。我們比較了Kafka Streams 0.10.2、Apache Flink 1.2.1和Spark 2.3.0,在一個擁有5個c3.2*2大型Amazon EC2 工作節點和一個master節點的集群上(硬件條件為8個虛擬核心和15GB的內存)。對于Flink,我們使用優化版本的benchmark由dataArtisans發布。就像那個benchmark一樣,系統從一個擁有40個partition(每個內核一個)的kafka集群中讀取數據,并將結果寫入kafka。最初的Yahoo benchmark使用redis保存用于連接的靜態表,但是我們發現redis可能是一個瓶頸,所以我們用每個系統中的一個表替換它(Kafka中的KTable,Spark中的DataFrame,Flink中的in-memory哈希表)。
圖6(a)展示了每個系統最大穩定吞吐量(積壓前的吞吐量),我們發現流系統的性能有著很大的不同。Kafka Stream通過kafka消息總線實現了一個簡單的消息傳遞模型,但在我們擁有40個core的集群上性能只有每秒70萬記錄。Flink可以達到3300萬。而Structured Streaming可以達到6500萬,近乎兩倍于Flink。這個特殊的Structured Streaming查詢使用沒有UDF代碼的DataFrame操作實現。這個性能完全來自于Spark SQL的內置執行優化,包括將數據存儲在緊湊的二進制文件格式以及代碼生成。正如作者指出的那樣,對于Trill和其他類型,對于流過程,執行優化可以產生很大的影響。
9.2 可伸縮性
圖6(b)展示了Structured Streaming的性能在我們改變集群大小時的變化。我們使用1,5,10,20個c3.2xlarge Amazon EC2 worker。我們可以看到吞吐量的伸縮近乎為線性,從一個節點的11.5 million每秒到20個節點時的225 million每秒。
9.3 連續處理
我們在一臺4核服務器上對Structured Streaming的連續處理模式進行基準測試,該測試展示了延遲-吞吐量的權衡(因為分區是獨立運行的,我們希望延遲與節點數量保持一致)。
圖7展示了一個map任務的結果,這個map任務從Kafka中讀取數據,虛線展示了微批模式能達到的最大吞吐量。可以看到,在連續模式下,吞吐量不會大幅下降,但是延遲會更低。(小于10毫秒的延遲,只有微批處理模式最大吞吐量的一半)。它的最大穩定吞吐量也略高,因為微批處理模式由于任務調度而導致延遲。
結論
流應用是很有效的工具,但是流系統仍然難于使用,操作和集合進更大的應用系統。我們設計Structured Streaming來簡化這三個任務,同時與Apache Spark的其余部分進行集成。不同于其他的開源流引擎,Structured Streaming采用非常高級的API:增量化現有的Spark SQL或DataFrame查詢。這使得它可以被用戶廣泛使用。盡管Structured Streaming的API更具聲明性和約束性,但是我們發現,它在不同的范圍內都能很好的工作,包括哪些需要有狀態的自定義邏輯。除此之外,Structured Streaming還有其他一些強有力的特性,并且使用Spark SQL能實現更高的性能。數百個用戶用例表明可以利用Structured Streaming構建復雜的業務應用程序。