當流中的某個階段失敗時, 通常會導致整個流被拆掉。此時,每個階段的下游得到關于失敗通知和上游得到關于取消通知。
在許多情況下, 您可能希望避免完全的流失敗, 這可以通過幾種不同的方法完成:
-
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也提供了一個RestartSource
、RestartSink
和 RestartFlow
, 用于實施所謂指數回退監控策略, 在某個階段失敗時再次啟動它, 每次重新啟動的延遲時間越來越長。
當某個階段因為外部資源是否可用而失敗或完成時,而且需要一些時間重新啟動,這種模式有用。一個主要的例子是當一個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.RestartSink
和akka.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錯誤
流監管也可以應用于mapAsync
和mapAsyncUnordered
的future,即使這些錯誤發生于future而不是在階段自身。
假設我們使用外部服務來查找電子郵件地址,我們希望丟棄那些無法找到的地址。
我們開始于推文的作者流:
val authors: Source[Author, NotUsed] =
tweets
.filter(_.hashtags.contains(akkaTag))
.map(_.author)
假設我們可以使用以下方式查找其電子郵件地址:
def lookupEmail(handle: String): Future[String] =
當電子郵件沒有找到時,Future
以Failure
完成。
通過使用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時,以失敗完成流。