Akka Stream之集成

與Actor集成

為了將流的元素作為消息傳遞給一個普通的actor,你可以在mapAsync里使用ask或者使用Sink.actorRefWithAck

消息發送給流,可以通過Source.queue或者通過由Source.actorRef物化的ActorRef

mapAsync + ask

將流中元素的某些處理委托給actor的一個好方法是在mapAsync中使用ask。流的背壓由askFuture來維護,并且與mapAsync階段的parallelism相比,actor的郵箱將不會填充更多的消息。

import akka.pattern.ask
implicit val askTimeout = Timeout(5.seconds)
val words: Source[String, NotUsed] =
  Source(List("hello", "hi"))

words
  .mapAsync(parallelism = 5)(elem => (ref ? elem).mapTo[String])
  // continue processing of the replies from the actor
  .map(_.toLowerCase)
  .runWith(Sink.ignore)

請注意, 在參與者中接收的消息將與流元素的順序相同, 即并行度不會更改消息的順序。使用parallelism > 1有性能優勢(即使actor一次只處理一條消息),因為在actor完成前一條消息處理時,郵箱已經有一條消息。

actor 必須為來自流的每條消息答復sender()。該答復將完成askFuture, 它將是從mapAsync發給下游的元素。

class Translator extends Actor {
  def receive = {
    case word: String =>
      // ... process message
      val reply = word.toUpperCase
      sender() ! reply // reply to the ask
  }
}

通過發送 akka.actor.Status.Failure 作為參與者的答復, 以失敗來完成流。

如果請求因超時而失敗, 則流將以TimeoutException失敗完成。如果這不是想要的結果, 你可以在ask Future``上使用recover```。

如果你不關心回復值, 只用它們作為背壓信號, 你可以在mapAsync階段之后使用Sink.ignore, 然后actor實際上是流的一個sink。

同樣的模式可以與Actor routers一起使用。然后, 如果不關心發給下游的元素(答復)順序, 則可以使用mapAsyncUnordered來提高效率。

Sink.actorRefWithAck

sink將流的元素發送到給定的 ActorRef, ActorRef發送背壓信號。第一個元素總是 onInitMessage, 然后流等待actor的確認消息, 這意味著actor準備處理元素。它還要求每個流元素后返回確認消息,以便進行回壓工作。

如果目標actor終止, 流將被取消。當流成功完成時, onCompleteMessage將被發送到目標actor。當流以失敗完成時, 將向目標actor發送akka.actor.Status.Failure消息。

注意
使用Sink.actorRef或從map使用普通的tell或 foreach , 意味著沒有來自目標actor的背壓信號, 也就是說, 如果actor沒有足夠快地處理消息,該actor的郵箱將增長, 除非使用設置mailbox-push-timeout-time為0的有界郵箱或使用前面的速率限制階段。不過,使用Sink.actorRefWithAck或者在mapAsync中使用ask更好。

Source.queue

Source.queue可用于從actor(或流外部運行的任何東西)發送元素給流。元素將被緩沖直到流可以處理它們。可以offer元素給隊列,如果有下游的需求,它們將發送給流,否則將緩沖到收到需求的請求為止。

根據定義的 OverflowStrategy, 如果緩沖區中沒有可用空間, 它可能會丟棄元素。OverflowStrategy.backpressure策略不支持這種Source類型,也就是說如果填充緩沖的速度比流可以處理的速度快,元素會被丟棄。如果你想要一個背壓的actor接口,應當考慮使用Source.queue

流可以通過發送akka.actor.PoisonPillakka.actor.Status.Success給actor引用,成功完成。

流可以通過發送akka.actor.Status.Failure給actor引用,失敗完成。

當流完成時,actor將終止,并失敗或取消下游,也就是說,當發生這種情況時, 你可以觀察它得到通知。

與外部服務集成

可以使用mapAsyncmapAsyncUnordered來執行涉及外部基于非流的服務的流轉換和副作用。

例如,使用外部電子郵件服務向所選推文的作者發送電子郵件:

def send(email: Email): Future[Unit] = {
  // ...
}

我們從推文的作者推特流開始:

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

假設我們可以使用以下內容查找他們的電子郵件地址:

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

通過使用 lookupEmail 服務, 使用mapAsync可以將作者流轉換為電子郵件地址流:

val emailAddresses: Source[String, NotUsed] =
  authors
    .mapAsync(4)(author => addressSystem.lookupEmail(author.handle))
    .collect { case Some(emailAddress) => emailAddress }

最終,發送電子郵件:

val sendEmails: RunnableGraph[NotUsed] =
  emailAddresses
    .mapAsync(4)(address => {
      emailServer.send(
        Email(to = address, title = "Akka", body = "I like your tweet"))
    })
    .to(Sink.ignore)

sendEmails.run()

mapAsync 應用于給定的函數, 當元素通過這個處理步驟時, 將為它們每一個調用外部服務。函數返回Future,并把future的值發送給下游。將并行運行的Future數量作為 mapAsync 的第一個參數。這些Future可能以任何順序完成, 但發送給下游的元素的順序與從上游接收的順序相同。

這意味著背壓如預期的工作。例如,如果emailServer.send是瓶頸,將會限制傳入推文的檢索速度和email地址查找的速度。

這條管道的最后一塊是產生通過電子郵件管道提取tweet作者信息的需求:我們附加一個Sink.ignore,使其全部運行。 如果我們的電子郵件處理將返回一些有趣的數據進行進一步的轉換,那么我們當然不會忽視它,而是將結果流發送到進一步的處理或存儲。

請注意, mapAsync 保留流元素的順序。在這個例子中, 順序并不重要, 我們可以使用更有效的 mapAsyncUnordered

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

val emailAddresses: Source[String, NotUsed] =
  authors
    .mapAsyncUnordered(4)(author => addressSystem.lookupEmail(author.handle))
    .collect { case Some(emailAddress) => emailAddress }

val sendEmails: RunnableGraph[NotUsed] =
  emailAddresses
    .mapAsyncUnordered(4)(address => {
      emailServer.send(
        Email(to = address, title = "Akka", body = "I like your tweet"))
    })
    .to(Sink.ignore)

sendEmails.run()

在上述示例中, 服務方便地返回了一個Future結果。如果不是這樣,你需要用Future來包裹調用。如果服務調用涉及阻塞, 還必須確保在專用執行上下文中運行它, 以避免“饑餓”和系統中其他任務的干擾。

val blockingExecutionContext = system.dispatchers.lookup("blocking-dispatcher")

val sendTextMessages: RunnableGraph[NotUsed] =
  phoneNumbers
    .mapAsync(4)(phoneNo => {
      Future {
        smsServer.send(
          TextMessage(to = phoneNo, body = "I like your tweet"))
      }(blockingExecutionContext)
    })
    .to(Sink.ignore)

sendTextMessages.run()

"blocking-dispatcher"的配置可能類似于:

blocking-dispatcher {
  executor = "thread-pool-executor"
  thread-pool-executor {
    core-pool-size-min    = 10
    core-pool-size-max    = 10
  }
}

阻塞調用的另一種替代方法是在map操作中執行這些操作, 但仍使用專用的調度器。

val send = Flow[String]
  .map { phoneNo =>
    smsServer.send(TextMessage(to = phoneNo, body = "I like your tweet"))
  }
  .withAttributes(ActorAttributes.dispatcher("blocking-dispatcher"))

val sendTextMessages: RunnableGraph[NotUsed] =
  phoneNumbers.via(send).to(Sink.ignore)

sendTextMessages.run()

但是, 這與mapAsync不完全相同, 因為mapAsync可能同時運行多個調用, 但map一次執行一次。

對于一個服務作為一個actor公開,或者一個actor作為一個外部服務前的網關,你可以使用ask

import akka.pattern.ask

val akkaTweets: Source[Tweet, NotUsed] = tweets.filter(_.hashtags.contains(akkaTag))

implicit val timeout = Timeout(3.seconds)
val saveTweets: RunnableGraph[NotUsed] =
  akkaTweets
    .mapAsync(4)(tweet => database ? Save(tweet))
    .to(Sink.ignore)

請注意, 如果請求在給定的超時時間內未完成, 則流將通過失敗完成。如果這不是想要的結果, 你可以使用在ask Future上的recover

對順序和并行性的說明

讓我們再看看另一個例子, 以更好地了解 mapAsync 和 mapAsyncUnordered 的順序和并行特性。

幾個 mapAsync 和 mapAsyncUnordered future可能同時運行。并發的future數量受到下游需求的限制。例如, 如果下游要求的5個元素, 將有最多5個future在進行中。

mapAsync以收到元素的順序發送future結果。這意味著,已完成的結果只有在先前的結果都已完成并發送后,才發送給下游。因此,一個緩慢的調用將延遲所有連續調用的結果, 盡管它們在慢速調用之前完成。

mapAsyncUnordered當future結果一完成就發送出去,也就是說,可能發送給下游元素的順序不與從上游收到的順序相同。因此,只要下游有多個元素的需求,一個緩慢的調用不會延遲連續調用的結果。

這里是一個虛擬的服務, 我們可以用它來說明這些方面。

class SometimesSlowService(implicit ec: ExecutionContext) {

  private val runningCount = new AtomicInteger

  def convert(s: String): Future[String] = {
    println(s"running: $s (${runningCount.incrementAndGet()})")
    Future {
      if (s.nonEmpty && s.head.isLower)
        Thread.sleep(500)
      else
        Thread.sleep(20)
      println(s"completed: $s (${runningCount.decrementAndGet()})")
      s.toUpperCase
    }
  }
}

以小寫字母開頭的元素被模擬為需要較長的處理時間。

下面是我們如何使用它與 mapAsync:

implicit val blockingExecutionContext = system.dispatchers.lookup("blocking-dispatcher")
val service = new SometimesSlowService

implicit val materializer = ActorMaterializer(
  ActorMaterializerSettings(system).withInputBuffer(initialSize = 4, maxSize = 4))

Source(List("a", "B", "C", "D", "e", "F", "g", "H", "i", "J"))
  .map(elem => { println(s"before: $elem"); elem })
  .mapAsync(4)(service.convert)
  .runForeach(elem => println(s"after: $elem"))

輸出可能如下所示:

before: a
before: B
before: C
before: D
running: a (1)
running: B (2)
before: e
running: C (3)
before: F
running: D (4)
before: g
before: H
completed: C (3)
completed: B (2)
completed: D (1)
completed: a (0)
after: A
after: B
running: e (1)
after: C
after: D
running: F (2)
before: i
before: J
running: g (3)
running: H (4)
completed: H (2)
completed: F (3)
completed: e (1)
completed: g (0)
after: E
after: F
running: i (1)
after: G
after: H
running: J (2)
completed: J (1)
completed: i (0)
after: I
after: J

注意,after行的順序與before行相同,即使元素以不同順序完成。例如H在g之前完成,但仍在后面發送。

括號中的數字說明同一時間內正在進行的調用數。因此,這里下游需求和并發調用的數量由ActorMaterializerSettings緩沖大小 (4) 限制 。

下面是我們在 mapAsyncUnordered 里使用相同服務:

implicit val blockingExecutionContext = system.dispatchers.lookup("blocking-dispatcher")
val service = new SometimesSlowService

implicit val materializer = ActorMaterializer(
  ActorMaterializerSettings(system).withInputBuffer(initialSize = 4, maxSize = 4))

Source(List("a", "B", "C", "D", "e", "F", "g", "H", "i", "J"))
  .map(elem => { println(s"before: $elem"); elem })
  .mapAsyncUnordered(4)(service.convert)
  .runForeach(elem => println(s"after: $elem"))

輸出可能如下所示:

before: a
before: B
before: C
before: D
running: a (1)
running: B (2)
before: e
running: C (3)
before: F
running: D (4)
before: g
before: H
completed: B (3)
completed: C (1)
completed: D (2)
after: B
after: D
running: e (2)
after: C
running: F (3)
before: i
before: J
completed: F (2)
after: F
running: g (3)
running: H (4)
completed: H (3)
after: H
completed: a (2)
after: A
running: i (3)
running: J (4)
completed: J (3)
after: J
completed: e (2)
after: E
completed: g (1)
after: G
completed: i (0)
after: I

注意,after行的順序與before行不同。例如,H趕上了慢G。

括號中的數字說明同一時間內正在進行的調用數。因此,這里下游需求和并發調用的數量由ActorMaterializerSettings緩沖大小 (4) 限制 。

與響應式流集成

響應式流為異步流非阻塞式背壓處理定義了一個標準。它使能夠連接到符合標準的流庫成為可能。Akka Stream就是一個這樣的庫。

其它實現的不完整列表:

  • Reactor (1.1+)
  • RxJava
  • Ratpack
  • Slick

在響應式流中兩個最重要的接口是PublisherSubscriber

import org.reactivestreams.Publisher
import org.reactivestreams.Subscriber

假設有這樣的一個庫提供了一個推文的發布者:

def tweets: Publisher[Tweet]

而另外一個庫知道如何將作者信息存儲到數據庫:

def storage: Subscriber[Author]

使用Akka Streams Flow ,可以轉換流并連接它們:

val authors = Flow[Tweet]
  .filter(_.hashtags.contains(akkaTag))
  .map(_.author)

Source.fromPublisher(tweets).via(authors).to(Sink.fromSubscriber(storage)).run()

Publisher作為一個輸入Source使用到流,而Subscriber作為一個輸出Sink。

一個Flow也可以轉換到RunnableGraph[Processor[In, Out]],當run()被調用時,它將物化到一個Processor。run()可以被多次調用,每次都會產生一個新的Processor實例。

val processor: Processor[Tweet, Author] = authors.toProcessor.run()

tweets.subscribe(processor)
processor.subscribe(storage)

一個發布者可以通過subscribe方法與一個訂閱者連接。

也可以使用Publisher-Sink,將Source作為Publisher

val authorPublisher: Publisher[Author] =
  Source.fromPublisher(tweets).via(authors).runWith(Sink.asPublisher(fanout = false))

authorPublisher.subscribe(storage)

Sink.asPublisher(fanout = false)創建的publisher僅支持單一訂閱。其它的訂閱嘗試將被拒絕(帶有IllegalStateException)。

使用fan-out/broadcasting 創建發布者,可以支持多個訂閱者:

def alert: Subscriber[Author]
def storage: Subscriber[Author]
val authorPublisher: Publisher[Author] =
  Source.fromPublisher(tweets).via(authors)
    .runWith(Sink.asPublisher(fanout = true))

authorPublisher.subscribe(storage)
authorPublisher.subscribe(alert)

該階段的輸入緩沖區大小控制最慢的訂閱者與最快訂閱者之間的距離,然后才能減慢流的速度。

要使圖完整, 還可以通過使用Subscriber-Source將Sink公開為Subscriber:

val tweetSubscriber: Subscriber[Tweet] =
  authors.to(Sink.fromSubscriber(storage)).runWith(Source.asSubscriber[Tweet])

tweets.subscribe(tweetSubscriber)

也可以通過傳遞一個創建Processer實例的工廠函數,將Processor實例解包為一個Flow:

// An example Processor factory
def createProcessor: Processor[Int, Int] = Flow[Int].toProcessor.run()

val flow: Flow[Int, Int, NotUsed] = Flow.fromProcessor(() => createProcessor)

請注意, 工廠是必要的, 以實現可重用的Flow結果。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容