1 基本流處理
讓我們首先看看使用akka-stream處理流的真正含義。圖1展示了在某個處理節(jié)點上,元素是一個個如何被處理的。一次處理一個元素是防止內(nèi)存溢出的關(guān)鍵。還可以看到,有限內(nèi)存可用于處理鏈上的某些位置。
與actor的相似性是顯而易見的。如圖1所示,不同點在,生產(chǎn)者和消費者之間的信號,該信號描述了在有限內(nèi)存中可以處理什么。如果直接使用actor來實現(xiàn),這部分你要自己來實現(xiàn)。圖2展示了用于日志流處理的線性處理鏈例子,包括了過濾、轉(zhuǎn)換和幀化日志事件。
日志流處理器需要做的不僅僅是從一個生產(chǎn)者那里讀取, 而是寫到一個消費者, 這就是處理圖的來源。處理圖使得可以從現(xiàn)有的處理節(jié)點中構(gòu)建出更高級的處理邏輯。例如,如圖3,一個圖合并了兩個流并過濾元素。本質(zhì)上,任何處理節(jié)點都是一個圖;圖是一個處理元素,并含有一定數(shù)目的輸入和輸出。
日志流處理器服務(wù)的最終版本將使用HTTP從網(wǎng)絡(luò)上接收許多服務(wù)的應(yīng)用程序日志,并將合并不同類型的流。它將過濾、分析、變換,最終將結(jié)果發(fā)送到其他服務(wù)。圖4展示了一個假想的服務(wù)的使用用例。
圖中展示了日志流處理器從一個票據(jù)應(yīng)用程序的不同部分接收日志事件。日志事件立即或有些延遲后發(fā)送到日志流處理器。票據(jù)的應(yīng)用程序 和 HTTP 服務(wù)在事件發(fā)生時發(fā)送事件, 而日志轉(zhuǎn)發(fā)器服務(wù)在從第三方服務(wù)日志聚合后發(fā)送事件。
在圖4 所示的用例中, 日志流處理器將標(biāo)識的日志事件發(fā)送到存檔服務(wù), 以便用戶以后可以執(zhí)行查詢。日志流服務(wù)還識別應(yīng)用程序服務(wù)中出現(xiàn)的特定問題, 并使用通知服務(wù)在需要人工干預(yù)時通知團隊。一些事件被轉(zhuǎn)換為度量值,這些度量值送入提供更多分析圖表的服務(wù)中。
將日志事件轉(zhuǎn)換為歸檔事件、通知、度量值和審計跟蹤,將在不同的處理流中進行,每個流程需要一個單獨的處理邏輯,所有的處理都是對傳入日志事件進行的。
這個日志流處理器示例將突出幾個目標(biāo),其解決方案將在本章的下一節(jié)中討論:
- 有限內(nèi)存的使用——因為日志數(shù)據(jù)不會整個都裝入內(nèi)存中,所以日志流處理器不會耗盡內(nèi)存。它應(yīng)該一個接一個地處理事件,可能收集事件到臨時緩沖區(qū)中,但從不試圖將所有日志事件讀取到內(nèi)存中。
- 異步,非阻塞的I/O——應(yīng)該有效地使用資源,并且阻塞線程應(yīng)該盡可能地受到限制。例如,日志流處理器不能按順序向每個服務(wù)發(fā)送數(shù)據(jù),并等待所有用戶依次響應(yīng)。
- 不同的速度——生產(chǎn)者和消費者應(yīng)該能夠以不同的速度運行。
日志流處理器的最后一個化身是HTTP流服務(wù),我們將從一個更簡單的版本開始展示,從一個文件中處理事件,然后將結(jié)果寫入另一個文件。正如你將看到的,akka-stream相當(dāng)靈活。將處理邏輯與讀寫流的類型解耦是相對容易的。在下一節(jié)中, 我們將逐步構(gòu)建日志流處理應(yīng)用程序, 從一個簡單的流復(fù)制應(yīng)用程序開始。在前進的路上,我們將探討akka-stream API。
1.1 使用source和sink拷貝文件
作為構(gòu)建日志流處理應(yīng)用程序的第一步,我們將看一個流拷貝示例。從源流讀取的每個字節(jié)都將寫入到目標(biāo)流中。
一如既往,我們將在構(gòu)建文件中增加依賴,如下所示。
“com.typesafe.akka”%%"akka-stream"%version,
使用akka-stream通常包含兩步:
- 定義一個藍圖——一個流處理組件圖。這個圖定義了流應(yīng)當(dāng)如何被處理。
- 執(zhí)行這個藍圖——在一個ActorSystem中運行這個圖。圖轉(zhuǎn)換為actor,以執(zhí)行實際流數(shù)據(jù)所需的所有工作。
這個圖 (藍圖) 可以在整個程序中共享。創(chuàng)建后,它是不可變的。圖可以多次運行,并且每次運行都由一組新的actor執(zhí)行。一個運行圖,可以從流處理的組件中返回結(jié)果。在本章的后面, 我們將詳細(xì)討論所有這些工作的細(xì)節(jié)。 如果現(xiàn)在還不完全清楚,不要擔(dān)心。
我們將從一個非常簡單的日志流問題的前身開始,創(chuàng)建一個只復(fù)制日志的應(yīng)用程序。這個例子的藍圖是一個非常簡單的管道。從一個流中接收到的所有數(shù)據(jù),寫入到另外一個流。下面展示了最相關(guān)的代碼,前者定義一個藍圖,后者執(zhí)行這個藍圖。
首先,通過FileIO.fromPath 和 FileIO.toPath定義了一個source和一個sink。
Source和Sink都是流端點。Source有一個開放的輸出,而Sink有一個開放的輸入。
Source和Sink是強類型的,此例中兩者的流元素類型都是ByteString。
將Source和Sink連接在一起而形成一個RunnableGraph,如圖5所示:
此例中我們使用FileIO,因為很容易在文件中驗證輸入和輸出,而且由FileIO創(chuàng)建source和sink很簡單。
source和sink的類型轉(zhuǎn)換相對容易,也就是說,將文件轉(zhuǎn)換為其它介質(zhì)。
注意:由FileIO創(chuàng)建的Source和Sink內(nèi)部使用了阻塞文件I/O。FileIO的source和sink創(chuàng)建的actor運行在不同的調(diào)度器(dispatcher)上,可以在akka.stream.blocking-io-dispatcher全局定義。也可以通過withAttributes (使用一個ActorAttributes)為圖元素自定義調(diào)度器(dispatcher)。1.2節(jié)會展示使用ActorAttributes來設(shè)置supervisorStrategy (監(jiān)管策略)。
文件I/O的阻塞并不像你想象的那么糟糕。例如,磁盤的延遲遠遠低于網(wǎng)絡(luò)的傳輸。FileIO的異步版本將來可能提供,如果它能夠為許多并發(fā)文件流提供更好的性能。
物化值
當(dāng)圖運行時,source和sink能夠提供一個輔助值,稱為物化值(materialized value)。此例中,是一個Future[IOResult],包含了讀或者寫了多少字節(jié)。在1.2節(jié)中,我們將討論更多的物化細(xì)節(jié)。
這個流復(fù)制應(yīng)用程序創(chuàng)建一個最簡單的圖——我們通過source.to(sink)定義而來——它創(chuàng)建了一個RunnableGraph,這個RunnableGraph從source獲取數(shù)據(jù),并直接輸送到sink。
創(chuàng)建source和sink的語句是聲明式的。它們并沒有創(chuàng)建文件或者打開文件的句柄,而是簡單的捕捉所有信息,用于稍后RunnableGraph的運行。
還要注意到,創(chuàng)建RunnableGraph 并沒有執(zhí)行任何事情。它只是簡單的定義了一個藍圖。
下面代碼展示了RunnableGraph 是如何執(zhí)行的:
運行這個runnableGraph ,將導(dǎo)致字節(jié)從source復(fù)制到sink——此例中,從一個文件到另一個文件。一旦圖開始運行稱為該圖被物化。
此例中,一旦所有的數(shù)據(jù)復(fù)制完畢,此圖就會停止運行。我們將在下一節(jié)中詳細(xì)討論。
FileIO對象是akka-stream的一部分,它提供了創(chuàng)建文件source和sink的簡便方法。連接source和sink,一旦RunnableGraph被物化,導(dǎo)致從文件source讀取的每一個ByteString傳遞到文件sink,一次一個。
運行本例
本例代碼在https://github.com/RayRoestenburg/akka-in-action的chapter-stream文件夾下
通常,你可以從sbt控制臺運行本例。你可以將程序所需要的參數(shù)傳遞給run命令。
有一個很方便運行程序的插件是sbt-revolver,可以用來啟動一個應(yīng)用程序,或者重啟,或者停止(使用re-start和re-stop),而不需要退出sbt控制臺。可以從https://github.com/spray/sbt-revolver獲取。
在GitHub項目的chapter-stream文件夾下,還包含一個GenerateLogFile 應(yīng)用程序,它可以創(chuàng)建大體積的測試日志文件。
復(fù)制一個比JVM最大內(nèi)存設(shè)置(通過-Xmx parameter設(shè)置)更大的文件,來驗證應(yīng)用程序沒有偷偷的將整個文件加載到內(nèi)存中,這是一個你可以嘗試的練習(xí)。
1.2 物化可運行圖
RunnableGraph的run方法需要一個Materalizer在隱式范圍內(nèi)(可以通過scala的隱式參數(shù)來了解這里所說的隱式范圍)。一個ActorMaterializer 將RunnableGraph轉(zhuǎn)換為actor,這些actor來執(zhí)行圖的要求。
我們來看看文件復(fù)制例子中具體細(xì)節(jié)。其中一些細(xì)節(jié)可能會發(fā)生變化, 因為它們是akka的私有內(nèi)部, 但是跟蹤代碼并了解工作原理是非常有用的。圖6展示了StreamingCopy 圖如何物化的簡化版本。
- ActorMaterializer 檢查圖中的Source和Sink是否完美連接,要求Source和Sink內(nèi)部建立資源。在內(nèi)部,fromPath從一個FileSource創(chuàng)建Source(FileSource是一個SourceShape的內(nèi)部實現(xiàn))。
- FileSource被要求創(chuàng)建它的資源和創(chuàng)建一個FilePublisher,這是一個打開FileChannel的actor。
- toPath方法從一個FileSinkSinkModule創(chuàng)建一個Sink。FileSink創(chuàng)建了一個FileSubscriber actor,它打開了一個FileChannel。
- to方法用于連接source和sink,內(nèi)部會將source模塊和sink模塊結(jié)合為一個模塊。
- ActorMaterializer 根據(jù)模塊是如何連接的,將訂閱者訂閱到發(fā)布者,本例中是將FileSubscriber 訂閱到FilePublisher。
- FilePublisher 從文件讀取ByteStrings直到尾部,一旦它停止關(guān)閉文件。
- FileSubscriber 將從FilePublisher 收到的ByteStrings 寫入到輸出文件中。一旦FileSubscriber 停止就會關(guān)閉FileChannel 。
- 一旦FilePublisher 從文件中讀取了所有文件,將完成流。FileSubscriber 將會收到OnComplete 消息,并關(guān)閉寫入的文件。
可以通過take、takeWhile和takeWithin來取消流,它們分別代表處理元素的最大數(shù)目時取消流,謂詞函數(shù)返回true時取消流,設(shè)定的時間已經(jīng)過去時取消流。在內(nèi)部,這些操作符用類似方式完成流。
在那刻,執(zhí)行此工作而創(chuàng)建的所有actor將停止。再次運行這個RunnableGraph 將重新創(chuàng)建一組actor,整個執(zhí)行將重頭開始。
如果FilePublisher從文件讀取的所有數(shù)據(jù)加載到內(nèi)存中(它并不會這么做),將會導(dǎo)致內(nèi)存溢出異常,那它是如何做的呢?答案在于Publisher 和Subscriber 相互是如何交互的,如圖7所示:
FilePublisher 能夠根據(jù)FileSubscriber的請求只發(fā)布一定數(shù)目的元素。
此例中,如果sink一邊的FileSubscriber 請求更多的數(shù)據(jù),source一邊的FilePublisher 可以從文件中讀取更多的數(shù)據(jù)。這意味著,從source中讀取數(shù)據(jù)的速度與sink寫入數(shù)據(jù)的速度相同。這個簡單的例子中,圖里只有兩個組件;在更復(fù)雜的圖中,需求從圖的末尾一直移動到圖的開頭, 確保沒有任何發(fā)布者比訂閱者的需求更快地發(fā)布。
akka-stream所有圖組件都用類似的方式工作。最終,每個部分轉(zhuǎn)換為響應(yīng)式流發(fā)布者或訂閱者。正是這個API讓 akka-stream能夠在有限內(nèi)存中處理無限的流數(shù)據(jù),并且設(shè)置發(fā)布者和訂閱者間交互的規(guī)則,例如絕不發(fā)送比請求的元素更多的內(nèi)容。
我們在這里大大簡化了發(fā)布者和訂閱者之間的協(xié)議。最重要的是, 訂閱者和發(fā)布者異步發(fā)送有關(guān)供求關(guān)系的消息。他們不會以任何方式阻塞對方。需求和供給被指定為固定數(shù)量的元素。訂閱者可以向發(fā)布者發(fā)出信號, 表明它只能處理較少的數(shù)據(jù), 或者它可以向發(fā)布者發(fā)出信號, 表明它可以處理更多數(shù)據(jù)。訂閱者執(zhí)行此功能的能力稱為非阻塞背壓。
響應(yīng)式流倡議
響應(yīng)式流是一個倡議,它提供了一個使用非阻塞背壓進行異步流處理的標(biāo)準(zhǔn)。有些庫已經(jīng)實現(xiàn)了響應(yīng)式流API,這些庫能夠相互集成。Akka-stream實現(xiàn)了響應(yīng)式流API,并在此上提供了高級別API。可以從www.reactive-streams.org了解更多信息。
內(nèi)部緩沖
Akka-stream內(nèi)部使用緩沖來優(yōu)化吞吐量。不是請求和發(fā)布單一元素,而是內(nèi)部批量請求和發(fā)布。
FileSubscriber 可以每次請求固定數(shù)目的元素。akka-stream庫確保讀取和寫入文件時有足夠的內(nèi)存。這不是你必須擔(dān)心的,但是你可能好奇,你可能想知道任何時間FileSubscriber可以請求的最大數(shù)目。
如果你深入到代碼中,你會看到FileSubscriber通過WatermarkRequestStrategy 策略使用了一個高水位線來設(shè)置最大輸入緩沖區(qū)大小。FileSubscriber 不能請求比該設(shè)置更多的元素。
然后是元素本身的大小。此例中,是讀取文件塊的大小,我們可以在fromPath 方法中設(shè)置,默認(rèn)是8KB。
最大輸入緩沖大小設(shè)置了元素的最大數(shù)目,可以在配置文件中定義akka.stream.materializer.max-input-buffer-size來設(shè)置。默認(rèn)的設(shè)置是16,所以此例中大概128KB數(shù)據(jù)可以在處理中。
最大輸入緩沖也可以通過ActorMaterializerSettings設(shè)置,它可以將設(shè)置傳遞給materializer 或者具體的圖組件。ActorMaterializerSettings 可以配置的內(nèi)容包括,執(zhí)行圖的actor們所使用的dispatcher和圖組件如何被監(jiān)管。
操作符融合
我們會在source和sink間使用更多的節(jié)點,akka-stream使用了一種叫做操作符融合的優(yōu)化技術(shù),在線性鏈圖中盡可能刪除不必要的異步邊界。
默認(rèn)情況下,圖的各個階段應(yīng)當(dāng)盡可能多的在單一actor中運行,以便消除線程間傳遞元素、需求和供給信號的開銷。async 方法可以用于明確在圖中創(chuàng)建一個異步邊界,以便通過async調(diào)用分別處理元素,保證后面運行在不同的actor上。
操作符融合在物化時發(fā)生。可以通過設(shè)置akka.stream.materializer.auto-fusing=off關(guān)閉。也可以通過Fusing.aggressive(graph)預(yù)融合一個圖(在它被物化之前)。
組合物化值
正如前面提到的,source和sink能夠提供一個輔助值當(dāng)圖物化的時候。文件source和sink提供了一個Future[IOResult],一旦它們完成的時候,包含了讀取和寫入的字節(jié)數(shù)。
RunnableGraph 當(dāng)它運行時,返回了一個物化值,那么它是如何決定哪個值通過圖傳遞?
to方法是toMat的簡化版,toMat是一個采用附加函數(shù)參數(shù)來組合物化值的方法。為此Keep對象定義了幾個標(biāo)準(zhǔn)函數(shù)。
默認(rèn)情況下,to方法使用Keep.left保持左邊的物化值,這解釋了為什么StreamingCopy 例子中圖返回的是讀取文件的物化值Future[IOResult]。如圖8所示:
如下所示,你可以選擇保留左邊、右邊、空或者兩個值,通過toMat 方法。
Keep.left, Keep.right, Keep.both, 和 Keep.none是簡單的函數(shù)分別用于返回左邊,右邊,兩個或者空參數(shù)。 Keep.left是一個好的默認(rèn)值;在一個長的圖中圖的起始物化值得以保留。如果Keep.right是物化值,你就不得不在每一步中指定Keep.left 保留起始物化值。
到目前為止,你已經(jīng)看到了一個source和一個sink如何連接。下一節(jié),我們將回到日志事件例子,并且介紹一個Flow組件。在處理和過濾事件的上下文中,我們將更緊密地看到流操作。
1.3 使用flow處理事件
現(xiàn)在你知道了定義和物化圖的基礎(chǔ),現(xiàn)在來看一個比復(fù)制字節(jié)更復(fù)雜的例子。我們將開始日志處理的第一個版本。
EventFilter 應(yīng)用,是一個簡單的命令行應(yīng)用程序,包含三個參數(shù):一個包含日志事件的輸入文件,一個寫有JSON格式化事件的輸出文件,和過濾的事件狀態(tài)值(擁有該狀態(tài)的事件將被寫到輸出文件中)。
我們討論下日志事件格式。日志事件以文本行的形式編寫,日志事件的每一個元素使用管道符(|)與下一個元素分離開。如下所示:
my-host-1 | web-app | ok | 2015-08-12T12:12:00.127Z | 5 tickets sold.||
my-host-2 | web-app | ok | 2015-08-12T12:12:01.127Z | 3 tickets sold.||
my-host-1 | web-app | ok | 2015-08-12T12:12:02.127Z | 1 tickets sold.||
my-host-2 | web-app | error | 2015-08-12T12:12:03.127Z | exception!!||
我們第一個例子中,一個日志事件行由主機名,服務(wù)名,狀態(tài),時間和描述字段組成。狀態(tài)值可以是'ok', 'warning', 'error', 或者 'critical'。每行的終點有個換行符(\n)。
文件中每一個文本行將被分析,并轉(zhuǎn)化為一個Event樣例類。
case class Event(
host: String,
service: String,
state: State,
time: ZonedDateTime,
description: String,
tag: Option[String] = None,
metric: Option[Double] = None
)
spray-json庫用于將一個Event 轉(zhuǎn)化為JSON。此處省略的EventMarshalling 特質(zhì),包含了Event樣例類的JSON格式。EventMarshalling 特征可以在 GitHub 倉庫中找到, 以及本章中所示的所有代碼, 在chapter-stream目錄中。
我們將在Source和Sink中使用Flow,如圖9所示:
這個flow將捕捉所有的流處理邏輯,我們將在后面本例的HTTP版本中再次使用。Source和Flow都提供方法來操作流。圖10展示了概念上在事件過濾流中的操作:
我們遇到的第一個問題是事實上這個flow將從source接受到任意大小的ByteStrings 元素。我們不能假設(shè)接收到的一個ByteStrings恰好包含一個日志事件行。
Akka-stream有幾個預(yù)定義的流用于幀化,來在一個流中識別數(shù)據(jù)幀。在本例中,我們可以使用Framing.delimiter flow,它在流中檢測特定的ByteString 作為分隔符。它緩沖最大值的maxLine字節(jié)來根據(jù)分隔符查找?guī)枰_保破壞性輸入不會導(dǎo)致內(nèi)存溢出異常。
下面展示了幀flow將任意大小的ByteStrings轉(zhuǎn)換為ByteStrings幀(由換行符來分隔)。在我們的格式中,這表示一個完整的日志事件行。
我們現(xiàn)在準(zhǔn)備分析日志行到一個Event樣例類中。我們省略了解析日志行的實際邏輯(可以在GitHub項目中找到)。我們將簡單的重新映射元素,將字符串轉(zhuǎn)變?yōu)橐粋€Event,如下所示:
流不是集合
你可能注意到許多流操作看起來像集合操作,例如map,filter和collect。這些可能會讓你認(rèn)為流僅僅是另外一種標(biāo)準(zhǔn)集合,但事實不是這樣的。最大的不同是流的大小是不知道的,然而幾乎所有的集合類像List,Set和Map大小是知道的。由于無法遍歷流的所有元素, 因此你可能在流上所期望的某些方法 (基于你對集合API 的經(jīng)驗) 是不可用的。
Flow[String]創(chuàng)建了一個將String元素作為輸入和輸出的Flow。
在本例中,物化值的類型是不重要的。在創(chuàng)建Flow[String]的時候,沒有合理的類型可供選擇。NotUsed用于表示物化值不重要并且不使用。分析flow輸入字符串并輸出Event。
下一步是過濾。
val filter: Flow[Event, Event, NotUsed] = Flow[Event].filter(_.state == filterState)
某特定filterState事件將通過過濾flow,而其它的將被拋棄。
下面展示的是序列化flow。
Flow能夠使用via方法來組合。下面的代碼展示了一個完整的事件過濾flow以及它是如何物化的:
val composedFlow: Flow[ByteString, ByteString, NotUsed] =
frame.via(parse)
.via(filter)
.via(serialize)
val runnableGraph: RunnableGraph[Future[IOResult]] =
source.via(composedFlow).toMat(sink)(Keep.right)
runnableGraph.run().foreach { result =>
println(s"Wrote ${result.count} bytes to '$outputFile'.")
system.terminate()
}
這里我們使用了toMat來保留右邊的物化值,即Sink的物化值,所以我們能夠打印出寫入到輸出文件中字節(jié)數(shù)。當(dāng)然,也可以如下一次性定義flow:
val flow: Flow[ByteString, ByteString, NotUsed] =
Framing.delimiter(ByteString("\n"), maxLine)
.map(_.decodeString("UTF8"))
.map(LogStreamProcessor.parseLineEx)
.collect { case Some(e) => e }
.filter(_.state == filterState)
.map(event => ByteString(event.toJson.compactPrint))
下一節(jié),我們將看一看當(dāng)錯誤發(fā)生時會發(fā)生什么,例如在日志文件中有一個破壞性的行時。
1.4 在流中處理錯誤
EventFilter 應(yīng)用程序遇到錯誤時,有些稚嫩。LogStreamProcessor.parseLineEx方法遇到無法解析的行時,會拋出異常,但這僅僅是可能遇到錯誤中的一種。你可能傳入一個根本不存在的文件路徑。
默認(rèn)情況下,當(dāng)異常發(fā)生時,流處理會停止。RunnableGraph的物化值將是一個保護異常的失敗Future。在這種情況下不太方便。忽略無法解析的日志行會更有意義。
首先,我們來看看忽略無法解析的日志行。你可以定義一個監(jiān)管策略,類似于為actor定義的監(jiān)管策略。下面的代碼展示了如何使用恢復(fù)來刪除引起異常的元素,從而導(dǎo)致流處理繼續(xù)進行。
監(jiān)管策略使用withAttributes傳遞,它可以對所有圖組件有用。你也可以使用ActorMaterializerSettings對整個圖的監(jiān)管策略進行設(shè)置,如下所示:
流的監(jiān)管策略支持Resume, Stop, 和 Restart。一些流操作建有狀態(tài),當(dāng)使用Restart時,這些狀態(tài)會被拋棄;而Resume則不會。
將錯誤作為流的元素
另外一個錯誤處理選項是捕獲異常并使用一種錯誤類型,將它和其它元素一樣在流中傳遞。例如,你可以引入一個UnparsableEvent 樣例類,而Event和UnparsableEvent 都繼承于一個普通的sealed特質(zhì)Result,這樣可以模式匹配。完整的流將是一個Flow[ByteString, Result, NotUsed]。另外一個選項是使用Either 類型并編碼錯誤為left、事件為right,結(jié)果像這樣Flow[ByteString, Either[Error, Result], NotUsed]。在社區(qū)里,有比Either更好的選擇,例如Scalaz的Disjunction, Cats的 Xor類型,或者 Scalactic的 Or 類型。
現(xiàn)在我們簡要地討論了如何處理流錯誤, 我們將研究如何將序列化協(xié)議與篩選事件的邏輯分開。EventFilter 是一個非常簡單的應(yīng)用——主要的邏輯就是過濾有特殊狀態(tài)的事件。如果我們能夠更好地重用解析、過濾和序列化步驟, 那就太好了。此外, 我們開始相當(dāng)武斷地只支持日志格式作為輸入和 JSON 輸出。例如, 如果我們還能支持 JSON 輸入和文本日志格式輸出, 那就太好了。在下一節(jié)中, 我們將查看雙向flow, 以定義可重用的序列化協(xié)議, 我們可以疊加在過濾flow之上。
1.5 使用BidiFlow來創(chuàng)建協(xié)議
BidiFlow是一個擁有兩個開放輸入和兩個開放輸出的圖組件。使用BidiFlow的一個方法是作為適配器疊加在一個flow之上。
我們將使用BidiFlow作為兩個flow合并使用,但需要注意的是有許多創(chuàng)建BidiFlow的方法,不僅僅是通過兩個flow。
讓我們重寫EventFilter應(yīng)用以便它基本上只處理過濾方法,一個Flow[Event, Event, NotUsed],從事件到事件。從輸入字節(jié)如何讀取事件和事件如何重寫應(yīng)當(dāng)作為協(xié)議適配重用。圖11展示了BidiFlow的結(jié)構(gòu)。
BidiEventFilter 應(yīng)用將序列化協(xié)議從過濾事件的邏輯中分離,如圖12所示。在本例中,“out”flow只包含一個序列化的flow,因為本例中幀化元素(換行符)是自動添加到序列化上的。
下面代碼展示了從命令行參數(shù)中如何創(chuàng)建一個特定的BidiFlow。除 "json" 之外的任何內(nèi)容都將被解釋為日志文件格式。
JsonFraming 幀化輸入的字節(jié)到JSON對象中。我們使用spray-json解析字節(jié)包含的JSON對象,并轉(zhuǎn)換它到Event。JsonFraming 包括在GitHub上的項目,這是抄襲Konrad Malawski的初步工作編組JSON流(預(yù)計在Akka的未來版本中包含)。
fromFlows方法從兩個flow創(chuàng)建BidiFlow,用于反序列化和序列化。BidiFlow可以使用join方法加在過濾flow之上,如下所示:
另一種方式來思考,可以認(rèn)為BidiFlow提供了兩個flow,你可以一個連接在現(xiàn)有的flow之前,一個連接在之后,適配輸入側(cè)和輸出側(cè)遇到的問題。本例中,用于讀取和寫入一致性的格式。
在下一節(jié)中, 我們將構(gòu)建一個流式 HTTP 服務(wù), 并向日志流處理器添加更多功能, 以接近實際的應(yīng)用程序。到目前為止, 我們只使用了流操作的直線管道。我們還將關(guān)注廣播和合并流。