是否可以在两个PCollection的Apache Beam中执行zip操作?

时间:2019-04-03 00:10:19

标签: google-cloud-dataflow apache-beam

我有一个PCollection[str],我想生成随机对。

来自Apache Spark,我的策略是:

  1. 复制原始PCollection
  2. 随机洗牌
  3. 使用原始PCollection压缩它

但是我似乎找不到一种压缩2个PCollections的方法...

2 个答案:

答案 0 :(得分:2)

这很有趣,并且不是很常见的用例,因为正如@chamikara所说,Dataflow中没有订单保证。但是,我考虑过实现一种解决方案,在该解决方案中,您可以混洗输入的PCollection,然后基于state配对连续的元素。我发现了一些注意事项,但我认为仍然值得分享。

首先,我使用了Python SDK,但是Dataflow Runner尚不支持有状态DoFn。它可以与Direct Runner一起使用,但是:1)它不可扩展,并且2)如果不使用多线程,则很难对记录进行洗牌。当然,后者的一个简单解决方案是将已经改组的PCollection馈送到管道中(我们可以使用其他作业来预处理数据)。否则,我们可以使该示例适应Java SDK。

就目前而言,我决定尝试改组并将其与单个管道配对。我真的不知道这是否有助于解决问题或使事情变得更复杂,但是可以在here中找到代码。

简而言之,有状态DoFn会查看缓冲区,如果缓冲区为空,则会将其放入当前元素。否则,它将从缓冲区中弹出前一个元素,并输出(previous_element,current_element)元组:

class PairRecordsFn(beam.DoFn):
  """Pairs two consecutive elements after shuffle"""
  BUFFER = BagStateSpec('buffer', PickleCoder())
  def process(self, element, buffer=beam.DoFn.StateParam(BUFFER)):
    try:
      previous_element = list(buffer.read())[0]
    except:
      previous_element = []
    unused_key, value = element

    if previous_element:
      yield (previous_element, value)
      buffer.clear()
    else:
      buffer.add(value)

管道根据需要将键添加到输入元素以使用有状态DoFn。这里会有一个折衷,因为您可以使用beam.Map(lambda x: (1, x))将相同的键分配给所有元素。这不能很好地并行化,但这不是问题,因为无论如何我们都在使用Direct Runner(如果使用Java SDK,请记住这一点)。但是,它不会随机播放记录。相反,如果改用大量键,则会得到大量无法配对的“孤立”元素(因为每个键都保留了状态,并且我们随机分配它们,所以我们可以有奇数个记录每个键):

pairs = (p
  | 'Create Events' >> beam.Create(data)
  | 'Add Keys' >> beam.Map(lambda x: (randint(1,4), x))
  | 'Pair Records' >> beam.ParDo(PairRecordsFn())
  | 'Check Results' >> beam.ParDo(LogFn()))

就我而言,我得到了类似的东西:

INFO:root:('one', 'three')
INFO:root:('two', 'five')
INFO:root:('zero', 'six')
INFO:root:('four', 'seven')
INFO:root:('ten', 'twelve')
INFO:root:('nine', 'thirteen')
INFO:root:('eight', 'fourteen')
INFO:root:('eleven', 'sixteen')
...

编辑:我想到了使用Sample.FixedSizeGlobally组合器的另一种方法。好处是,它可以更好地对数据进行洗牌,但是您需要先验知道元素的数量(否则我们需要对数据进行一次初始传递),并且似乎将所有元素一起返回。简要地说,我两次初始化相同的PCollection,但是应用了不同的洗牌顺序,并在有状态的DoFn中分配索引。这将确保索引在同一PCollection中的元素之间是唯一的(即使无法保证顺序)。在我的情况下,两个PCollections对于[0,31]范围内的每个键都只有一条记录。 CoGroupByKey转换将在同一索引上连接两个PCollection,因此具有随机的元素对:

pc1 = (p
  | 'Create Events 1' >> beam.Create(data)
  | 'Sample 1' >> combine.Sample.FixedSizeGlobally(NUM_ELEMENTS)
  | 'Split Sample 1' >> beam.ParDo(SplitFn())
  | 'Add Dummy Key 1' >> beam.Map(lambda x: (1, x))
  | 'Assign Index 1' >> beam.ParDo(IndexAssigningStatefulDoFn()))

pc2 = (p
  | 'Create Events 2' >> beam.Create(data)
  | 'Sample 2' >> combine.Sample.FixedSizeGlobally(NUM_ELEMENTS)
  | 'Split Sample 2' >> beam.ParDo(SplitFn())
  | 'Add Dummy Key 2' >> beam.Map(lambda x: (2, x))
  | 'Assign Index 2' >> beam.ParDo(IndexAssigningStatefulDoFn()))

zipped = ((pc1, pc2)
           | 'Zip Shuffled PCollections' >> beam.CoGroupByKey()
           | 'Drop Index' >> beam.Map(lambda (x, y):y)
           | 'Check Results' >> beam.ParDo(LogFn()))

完整代码here

结果:

INFO:root:(['ten'], ['nineteen'])
INFO:root:(['twenty-three'], ['seven'])
INFO:root:(['twenty-five'], ['twenty'])
INFO:root:(['twelve'], ['twenty-one'])
INFO:root:(['twenty-six'], ['twenty-five'])
INFO:root:(['zero'], ['twenty-three'])
...

答案 1 :(得分:0)

ParDo转换应用于将键附加到元素的两个PCollection并通过CoGroupByKey转换运行两个PCollection怎么样?

请注意,Beam不保证PCollection中元素的顺序,因此输出元素在任何步骤之后都可能会重新排序,但是对于您的用例来说似乎应该可以,因为您只需要一些随机顺序即可。