如何按键分区并按Elixir Flow顺序连接

时间:2018-01-26 15:28:52

标签: elixir

我试图围绕Flow包围以构建具有以下特征的并行处理管道:

  1. 传入的事件的格式为{offset, %{"id" => id}}
  2. offset是一个单调递增的整数
  3. id是一个任意数字
  4. 我只需要保留每个id的顺序,因此可以并行地按顺序计算不同的ID。因此分区。
  5. 以下是生成无限量这些元组的示例流:

    stream = Stream.unfold(1, fn i ->
      offset = i+1
      element = {offset, %{"id" => Enum.random(1..10_000)}}
      {element, offset}
    end)
    

    我想按键id对流进行分区。我知道如何做到这一点,例如开始8个平行阶段:

    Flow.from_enumerable(stream)
    |> Flow.partition(
      key: fn {_, m} -> Map.get(m, "id") end,
      stages: 8
    )
    

    我在此流程中遵循的每个操作现在都是并行发生的,订单仅由分区键id保留。

    现在我不明白:如何将流加入一个阶段,按offset排序?

    要清楚,这是一个无限的流,所以我们需要记住,我们需要通过窗口加入(我已经尝试了多种方式。毕竟10秒的超时可以开始丢弃没有的事件从处理中及时到达。

    以下是我应该如何对其进行图像处理的说明:

    INCOMING
    |
    V
    * PARTIONING in N-stages by `id`
    |\
    |-\
    |--\
    |||| PARALLEL PROCESSING in order by `id`
    |--/
    |-/
    |/
    | JOIN in order by `offset`
    | timing out after 10 seconds, moving on with the smallest known offset
    |
    | SEQUENTIAL PROCESSING of each offset of the JOIN
    

1 个答案:

答案 0 :(得分:3)

加入部分可以通过再次调用partition/2,但将阶段数设置为1来完成。

下面是一个示例脚本,它通过发出带有偏移和随机分区的元组来重现您的用例:

1..10000
|> Stream.map(fn i -> {i, Enum.random([:a, :b, :c, :d])} end)
|> Flow.from_enumerable()
|> Flow.partition(key: {:elem, 1})
|> Flow.reduce(fn -> [] end, fn x, acc -> [process_x(x) | acc] end)
|> Flow.emit(:state)
|> Flow.partition(stages: 1)
|> Flow.reduce(fn -> [] end, &Kernel.++/2)
|> Flow.map_state(&Enum.sort/1)
|> Flow.emit(:state)
|> Enum.to_list()
|> IO.inspect

棘手的部分是分区。 分区后,您必须累积状态

因此,在第一个分区之后,我们调用Flow.reduce/3,处理元素,然后将它们放在列表的顶部。通过调用您必须实现的process_x来完成处理。处理完所有条目后,我们要求将整个分区状态(即事件列表)发送到下一步。

然后我们再次进行分区,但这一次进入一个阶段,简单地连接先前分区的结果,然后在最后对它们进行排序。

上面示例中未考虑的另一点是您的流量是无限的,因此您需要添加一些窗口。您需要选择从每个分区发出项目的频率。对于第一个分区,您可以发出批量为1000个元素。对于已连接的分区,您提到您希望它每10秒发生一次。所以让我们添加它们。

最后,请注意上面的代码不是最有效的,因为在最后一个分区中运行的所有内容都是串行的(单个阶段)。理想情况下,您希望在第一个分区中进行排序,并在您定义的merge_sorted函数的帮助下简单地合并最后一个分区中的排序结果。

以下是最终结果:

partition_window = Flow.Window.global |> Flow.Window.trigger_every(1000, :reset)
join_window = Flow.Window.global |> Flow.Window.trigger_periodically(10, :second, :reset)

1..10000
|> Stream.map(fn i -> {i, Enum.random([:a, :b, :c, :d])} end)
|> Flow.from_enumerable()
|> Flow.partition(key: {:elem, 1}, window: partition_window)
|> Flow.reduce(fn -> [] end, fn x, acc -> [process_x(x) | acc] end)
|> Flow.map_state(&Enum.sort/1)
|> Flow.emit(:state)
|> Flow.partition(stages: 1, window: join_window)
|> Flow.reduce(fn -> [] end, &merge_sorted/2)
|> Flow.emit(:state)
|> Enum.to_list()
|> IO.inspect