关于scala:Akka流通过流量限制并行性/处理流的吞吐量

Akka streams pass through flow limiting Parallelism / throughput of processing flow

我有一个用例,我想向外部系统发送消息,但是发送此消息的流程采用并返回我不能在下游使用的类型。对于通过流程来说,这是一个很好的用例。我在这里使用实现。最初,我担心如果processingFlow使用mapAsyncUnordered,那么该流程将无法正常工作。由于处理流程可能会重新排序消息,并且zip可能会推出带有不正确对的元组。例如,在以下示例中。

1
2
3
4
5
6
7
8
  val testSource = Source(1 until 50)
  val processingFlow: Flow[Int, Int, NotUsed] = Flow[Int].mapAsyncUnordered(10)(x => Future {
    Thread.sleep(Random.nextInt(50))
    x * 10
  })
  val passThroughFlow = PassThroughFlow(processingFlow, Keep.both)

  val future = testSource.via(passThroughFlow).runWith(Sink.seq)

我希望处理流程可以相对于其输入重新排序其输出,并且我将得到如下结果:

1
[(30,1), (40,2),(10,3),(10,4), ...]

右边(通过的对象总是按顺序排列),但穿过我的mapAsyncUnordered的左边可能与不正确的元素结合在一起,形成一个错误的元组。

实际上我得到了:

1
[(10,1), (20,2),(30,3),(40,4), ...]

每次。经过进一步调查,我发现代码运行缓慢,尽管我的地图异步无序,但实际上根本没有并行运行。我尝试在异步边界之前和之后引入一个缓冲区,但是它似乎总是按顺序运行。这解释了为什么总是订购但我希望我的处理流程具有更高的吞吐量。

我想出了以下解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
object PassThroughFlow {

  def keepRight[A, A1](processingFlow: Flow[A, A1, NotUsed]): Flow[A, A, NotUsed] =
    keepBoth[A, A1](processingFlow).map(_._2)

  def keepBoth[A, A1](processingFlow: Flow[A, A1, NotUsed]): Flow[A, (A1, A), NotUsed] =
    Flow.fromGraph(GraphDSL.create() { implicit builder => {
      import GraphDSL.Implicits._

      val broadcast = builder.add(Broadcast[A](2))
      val zip = builder.add(ZipWith[A1, A, (A1, A)]((left, right) => (left, right)))

      broadcast.out(0) ~> processingFlow ~> zip.in0
      broadcast.out(1) ~> zip.in1

      FlowShape(broadcast.in, zip.out)
    }
    })
}

object ParallelPassThroughFlow {


  def keepRight[A, A1](parallelism: Int, processingFlow: Flow[A, A1, NotUsed]): Flow[A, A, NotUsed] =
    keepBoth(parallelism, processingFlow).map(_._2)

  def keepBoth[A, A1](parallelism: Int, processingFlow: Flow[A, A1, NotUsed]): Flow[A, (A1, A), NotUsed] = {
    Flow.fromGraph(GraphDSL.create() { implicit builder =>
      import GraphDSL.Implicits._

      val fanOut = builder.add(Balance[A](outputPorts = parallelism))
      val merger = builder.add(Merge[(A1, A)](inputPorts = parallelism, eagerComplete = false))

      Range(0, parallelism).foreach { n =>
        val passThrough = PassThroughFlow.keepBoth(processingFlow)
        fanOut.out(n) ~> passThrough ~> merger.in(n)
      }

      FlowShape(fanOut.in, merger.out)
    })
  }

}

两个问题:

  • 在原始实现中,传递流内部的zip为什么限制了无序映射异步的并行度?
  • 我的工作是围绕声音还是可以进行改进?我基本上将输入的内容散布到传递流程的多个堆栈中,然后将它们全部合并回去。它似乎具有我想要的属性(即使处理流程重新排序,也可以并行但仍保持顺序),但感觉有些不对劲

  • 您所看到的行为是broadcastzip的工作方式的结果:broadcast在所有输出均发出信号需求时向下游发射; zip在发信号通知需求(并向下游发射)之前等待其所有输入。

    1
    2
    broadcast.out(0) ~> processingFlow ~> zip.in0
    broadcast.out(1) ~> zip.in1

    考虑上图中第一个元素(1)的移动。 1同时广播到processingFlowzipzip立即接收其输入之一(1),并等待其另一输入(10),这将花费更长的时间。只有当zip同时获得110时,它才会从上游拉取更多元素,从而触发第二个元素(2)在流中移动。依此类推。

    至于您的ParallelPassThroughFlow,我不知道为什么您觉得"某些事情不对劲"。