Akka Stream之流中的錯誤處理

當流中的某個階段失敗時, 通常會導致整個流被拆掉。此時,每個階段的下游得到關于失敗通知和上游得到關于取消通知。

在許多情況下, 您可能希望避免完全的流失敗, 這可以通過幾種不同的方法完成:

  • recover發出最終的元素, 然后在上游故障上正常完成流
  • recoverWithRetries創建一個新的上游并從失敗開始處理
  • 在后退后重新啟動流的部分
  • 對支持監督策略的階段使用監督策略

除了這些內置的用于錯誤處理的工具之外, 一個常見的模式是將流包裝到一個actor中, 并讓actor在失敗時重新啟動整個流。

Recover

recover允許你注入一個最終元素,然后在上游失敗時完成流。通過一個偏函數,決定哪些異常這樣恢復。如果有個異常不匹配,流將失敗。

如果您希望在失敗時優雅地完成流, 而讓下游知道出現了故障, 則recover可能很有用。

Source(0 to 6).map(n =>
  if (n < 5) n.toString
  else throw new RuntimeException("Boom!")).recover {
  case _: RuntimeException => "stream truncated"
}.runForeach(println)

則輸出可能是:

0
1
2
3
4
stream truncated

recoverWithRetries

recoverWithRetries 允許你在失敗的地方放入一個新的上游,在失敗到指定的最大次數后恢復流。

通過一個偏函數,決定哪些異常這樣恢復。如果有個異常不匹配,流將失敗。

val planB = Source(List("five", "six", "seven", "eight"))

Source(0 to 10).map(n =>
  if (n < 5) n.toString
  else throw new RuntimeException("Boom!")).recoverWithRetries(attempts = 1, {
  case _: RuntimeException => planB
}).runForeach(println)

輸出將是

0
1
2
3
4
five
six
seven
eight

正如Akka為actor提供回退監督模式一樣, Akka stream也提供了一個RestartSourceRestartSinkRestartFlow, 用于實施所謂指數回退監控策略, 在某個階段失敗時再次啟動它, 每次重新啟動的延遲時間越來越長。

當某個階段因為外部資源是否可用而失敗或完成時,而且需要一些時間重新啟動,這種模式有用。一個主要的例子是當一個WebSocket連接因為HTTP服務器運行正在下降(可能因為超負荷)而失敗時。通過使用指數回退,避免進行緊密的重新連接,這樣既可以讓HTTP服務器恢復一段時間,又避免在客戶端使用不必要的資源。

以下代碼段顯示了如何使用akka.stream.scaladsl.RestartSource創建一個回退監管,它將監督給定的Source。本例中,Source是一個服務器發送事件(SSE),由akka-http提供。如果此處流失敗,將再次發送請求,以3,6,12,24和最終30秒的間隔增加(此處,由于 maxBackoff 參數,它將保持上限)。

val restartSource = RestartSource.withBackoff(
  minBackoff = 3.seconds,
  maxBackoff = 30.seconds,
  randomFactor = 0.2 // adds 20% "noise" to vary the intervals slightly
) { () =>
  // Create a source from a future of a source
  Source.fromFutureSource {
    // Make a single request with akka-http
    Http().singleRequest(HttpRequest(
      uri = "http://example.com/eventstream"))
      // Unmarshall it as a source of server sent events
      .flatMap(Unmarshal(_).to[Source[ServerSentEvent, NotUsed]])
  }
}

強烈建議使用 randomFactor 為回退間隔添加一點額外的方差, 以避免在完全相同的時間點重新啟動多個流, 例如, 因為它們由于共享資源 (如相同的服務器下線,并在相同間隔后重啟) 而停止。通過在重新啟動間隔中增加額外的隨機性, 這些流將在時間上稍有不同的點開始, 從而避免大量的通信量沖擊恢復的服務器或他們都需要聯系的其他資源。

上述 RestartSource 將永遠不會終止, 除非Sink被送入取消。將它與 KillSwitch 結合使用通常會很方便, 以便在需要時可以終止它:

val killSwitch = restartSource
  .viaMat(KillSwitches.single)(Keep.right)
  .toMat(Sink.foreach(event => println(s"Got event: $event")))(Keep.left)
  .run()

doSomethingElse()

killSwitch.shutdown()

Sink和flow也可以被監管,使用akka.stream.scaladsl.RestartSinkakka.stream.scaladsl.RestartFlow。RestartSink 在取消時重新啟動, 而在輸入端口取消、輸出端口完成或輸出端口發送錯誤時重新啟動 RestartFlow。

監管策略

注意
支持監管策略的各個階段都有明文規定, 如果一個階段的文檔中沒有說明它遵守監管策略, 就意味著它失敗, 而不是采用監管。

錯誤處理策略受actor監管策略的啟發, 但語義已經適應了流處理的領域。最重要的區別是, 監管不是自動應用到流階段, 而是每個階段必須顯式實現的東西。

在許多階段, 實現對監管策略的支持可能甚至沒有意義, 對于連接到外部技術的階段尤其如此, 例如, 失敗的連接如果立即嘗試新連接, 可能仍然會失敗。

對于實現監管的階段, 在通過使用屬性物化流時, 可以選擇處理流元素的異常處理策略。

有三種方法可以處理應用程序代碼中的異常:

  • Stop - 流以失敗完成。
  • Resume - 元素被丟棄,流繼續執行
  • Restart - 元素被丟棄,且流在重啟該階段后繼續執行。重新啟動階段意味著任何累積狀態都被清除。 這通常通過創建階段的新實例來執行。

默認情況下, 停止策略用于所有異常, 即在拋出異常時, 流將以失敗完成。

implicit val materializer = ActorMaterializer()
val source = Source(0 to 5).map(100 / _)
val result = source.runWith(Sink.fold(0)(_ + _))
// division by zero will fail the stream and the
// result here will be a Future completed with Failure(ArithmeticException)

可以在materializer的設置中定義流的默認監管策略。

val decider: Supervision.Decider = {
  case _: ArithmeticException => Supervision.Resume
  case _ => Supervision.Stop
}
implicit val materializer = ActorMaterializer(
  ActorMaterializerSettings(system).withSupervisionStrategy(decider))
val source = Source(0 to 5).map(100 / _)
val result = source.runWith(Sink.fold(0)(_ + _))
// the element causing division by zero will be dropped
// result here will be a Future completed with Success(228)

在這里你可以看到, 所有的 ArithmeticException 將恢復處理, 即導致除以零的元素被丟棄了。

注意
請注意, 丟棄元素可能會導致具有循環的圖中出現死鎖。

還可以為flow的所有操作定義監管策略。

implicit val materializer = ActorMaterializer()
val decider: Supervision.Decider = {
  case _: ArithmeticException => Supervision.Resume
  case _ => Supervision.Stop
}
val flow = Flow[Int]
  .filter(100 / _ < 50).map(elem => 100 / (5 - elem))
  .withAttributes(ActorAttributes.supervisionStrategy(decider))
val source = Source(0 to 5).via(flow)

val result = source.runWith(Sink.fold(0)(_ + _))
// the elements causing division by zero will be dropped
// result here will be a Future completed with Success(150)

重新啟動的工作方式與恢復類似,除了故障處理階段的累加狀態(如果有的話)將被重置。

implicit val materializer = ActorMaterializer()
val decider: Supervision.Decider = {
  case _: IllegalArgumentException => Supervision.Restart
  case _ => Supervision.Stop
}
val flow = Flow[Int]
  .scan(0) { (acc, elem) =>
    if (elem < 0) throw new IllegalArgumentException("negative not allowed")
    else acc + elem
  }
  .withAttributes(ActorAttributes.supervisionStrategy(decider))
val source = Source(List(1, 3, -1, 5, 7)).via(flow)
val result = source.limit(1000).runWith(Sink.seq)
// 負數元素導致scan階段重啟
// 即再次從0開始
// 結果將是以Success(Vector(0, 1, 4, 0, 5, 12))完成的Future 

來自mapAsync錯誤

流監管也可以應用于mapAsyncmapAsyncUnordered的future,即使這些錯誤發生于future而不是在階段自身。

假設我們使用外部服務來查找電子郵件地址,我們希望丟棄那些無法找到的地址。

我們開始于推文的作者流:

val authors: Source[Author, NotUsed] =
  tweets
    .filter(_.hashtags.contains(akkaTag))
    .map(_.author)

假設我們可以使用以下方式查找其電子郵件地址:

def lookupEmail(handle: String): Future[String] =

當電子郵件沒有找到時,FutureFailure完成。

通過使用lookupEmail服務以及使用mapAsync, 可以將作者流轉換為電子郵件地址流, 并使用Supervision.resumingDecider丟棄未知電子郵件地址:

import ActorAttributes.supervisionStrategy
import Supervision.resumingDecider

val emailAddresses: Source[String, NotUsed] =
  authors.via(
    Flow[Author].mapAsync(4)(author => addressSystem.lookupEmail(author.handle))
      .withAttributes(supervisionStrategy(resumingDecider)))

如果不使用Resume而是默認的停止策略,那么流將在第一個帶有Failure完成的Future時,以失敗完成流。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容

  • 1 基本流處理 讓我們首先看看使用akka-stream處理流的真正含義。圖1展示了在某個處理節點上,元素是一個個...
    樂言筆記閱讀 2,677評論 1 1
  • (1)viaMat[T, Mat2, Mat3](flow: Graph[FlowShape[Out, T], M...
    樂言筆記閱讀 2,428評論 0 0
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,810評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,693評論 25 708
  • 《道德經》有云,上善若水,水善利萬物而不爭,處眾人之所惡,故幾于道也。 道為何物,老子又說道可道,非常道,名可名,...
    lin秀閱讀 1,041評論 0 0