意外的行为从数据流1.9更改为2.0 / 2.1

时间:2017-09-29 21:56:33

标签: google-cloud-dataflow apache-beam

对于非常简单的管道,我们发现Dataflow SDK 1.9和2.0 / 2.1之间存在一个非常奇怪的区别。

我们有CoGroupByKey步骤,通过键连接两个PCollections并输出两个PCollections(通过TupleTags)。例如,一个PCollection可能包含{" str1"," str2"},另一个可能包含{" str3"}。

这两个PCollections被写入GCS(在不同的位置),它们的并集(基本上,通过在两个PCollections上应用Flatten产生的PCollection)将被管道中的后续步骤使用。使用前面的示例,我们将在GCS下的相应位置存储{" str1"," str2"}和{" str3"},并且管道将进一步转换他们的联盟(Flattened PCollection){" str1"," str2"," str3"},依此类推。

在Dataflow SDK 1.9中,这正是发生的事情,我们围绕这个逻辑构建了我们的管道。 当我们慢慢迁移到2.0 / 2.1时,我们注意到不再观察到这种行为。相反,Flatten步骤所遵循的所有步骤都正确地运行并按预期运行,但是这两个PCollections(被展平)不再写入GCS,就好像它们不存在一样。但是在执行图中,显示了步骤,这对我们来说非常奇怪。

我们能够可靠地重现此问题,以便我们可以共享数据和代码作为示例。 我们在GCS中存储了两个文本文件:

DATA1.TXT:

k1,v1
k2,v2

data2.txt:

k2,w2
k3,w3

我们将阅读这两个文件来创建两个PCollections,每个文件都有一台PC。 我们将解析每一行以创建KV<String, String>(因此在此示例中键为k1, k2, k3。)

然后我们应用CoGroupByKey并生成PCollection以输出到GCS。 在CoGroupByKey步骤之后产生两个PCollections,取决于与每个键相关联的值的数量(它是一个人为的例子,但它是为了证明我们遇到的问题) - 数字是偶数还是奇数。 因此,其中一台PC将包含密钥&#34; k1,&#34;和&#34; k3&#34; (附加一些值字符串,请参阅下面的代码),因为它们各有一个值,另一个将包含一个键&#34; k2&#34;因为它有两个值(在每个文件中找到)。

这两台PC在不同的位置写入GCS,两台平板电脑也将写入GCS(但可能会进一步转换)。

三个输出文件应包含以下内容(行可能不是按顺序排列):

OUTPUT1:

k2: [v2],(w2)

OUTPUT2:

k3: (w3)
k1: [v1]

outputMerged:

k3: (w3)
k2: [v2],(w2)
k1: [v1]

这正是我们在Dataflow SDK 1.9中看到的(也是预期的)。

然而,在2.0和2.1中,output1和output2变为空(并且甚至没有执行TextIO步骤,就像没有输入任何元素一样;我们通过在中间添加虚拟ParDo来验证这一点,并且它根本没有被调用。

这使我们非常好奇为什么突然在1.9和2.0 / 2.1之间进行这种行为改变,以及什么是我们实现1.9所做的最佳方式。 具体来说,我们为归档目的生成输出1/2,同时我们将两台PC压平以进一步转换数据并产生另一个输出。

以下是您可以运行的Java代码(您必须正确导入,更改存储桶名称,并正确设置选项等)。

1.9的工作代码:

//Dataflow SDK 1.9 compatible.
public class TestJob {
  public static void execute(Options options) {
    Pipeline pipeline = Pipeline.create(options);
    PCollection<KV<String, String>> data1 =
        pipeline.apply(TextIO.Read.from(GcsPath.EXPERIMENT_BUCKET + "/data1.txt")).apply(ParDo.of(new doFn()));

    PCollection<KV<String, String>> data2 =
        pipeline.apply(TextIO.Read.from(GcsPath.EXPERIMENT_BUCKET + "/data2.txt")).apply(ParDo.of(new doFn()));

    TupleTag<String> inputTag1 = new TupleTag<String>() {
      private static final long serialVersionUID = 1L;
    };
    TupleTag<String> inputTag2 = new TupleTag<String>() {
      private static final long serialVersionUID = 1L;
    };

    TupleTag<String> outputTag1 = new TupleTag<String>() {
      private static final long serialVersionUID = 1L;
    };
    TupleTag<String> outputTag2 = new TupleTag<String>() {
      private static final long serialVersionUID = 1L;
    };

    PCollectionTuple tuple = KeyedPCollectionTuple.of(inputTag1, data1).and(inputTag2, data2)
        .apply(CoGroupByKey.<String>create()).apply(ParDo.of(new doFn2(inputTag1, inputTag2, outputTag2))
            .withOutputTags(outputTag1, TupleTagList.of(outputTag2)));
    PCollection<String> output1 = tuple.get(outputTag1);
    PCollection<String> output2 = tuple.get(outputTag2);
    PCollection<String> outputMerged = PCollectionList.of(output1).and(output2).apply(Flatten.<String>pCollections());

    outputMerged.apply(TextIO.Write.to(GcsPath.EXPERIMENT_BUCKET + "/test-job-1.9/outputMerged").withNumShards(1));
    output1.apply(TextIO.Write.to(GcsPath.EXPERIMENT_BUCKET + "/test-job-1.9/output1").withNumShards(1));
    output2.apply(TextIO.Write.to(GcsPath.EXPERIMENT_BUCKET + "/test-job-1.9/output2").withNumShards(1));

    pipeline.run();
  }

  static class doFn2 extends DoFn<KV<String, CoGbkResult>, String> {
    private static final long serialVersionUID = 1L;

    final TupleTag<String> inputTag1;
    final TupleTag<String> inputTag2;
    final TupleTag<String> outputTag2;

    public doFn2(TupleTag<String> inputTag1, TupleTag<String> inputTag2, TupleTag<String> outputTag2) {
      this.inputTag1 = inputTag1;
      this.inputTag2 = inputTag2;
      this.outputTag2 = outputTag2;
    }

    @ProcessElement
    public void processElement(ProcessContext c) throws Exception {
      String key = c.element().getKey();
      List<String> values = new ArrayList<String>();
      int numValues = 0;
      for (String val1 : c.element().getValue().getAll(inputTag1)) {
        values.add(String.format("[%s]", val1));
        numValues++;
      }
      for (String val2 : c.element().getValue().getAll(inputTag2)) {
        values.add(String.format("(%s)", val2));
        numValues++;
      }
      final String line = String.format("%s: %s", key, Joiner.on(",").join(values));
      if (numValues % 2 == 0) {
        c.output(line);
      } else {
        c.sideOutput(outputTag2, line);
      }
    }
  }

  static class doFn extends DoFn<String, KV<String, String>> {
    private static final long serialVersionUID = 1L;

    @ProcessElement
    public void processElement(ProcessContext c) throws Exception {
      String[] tokens = c.element().split(",");
      c.output(KV.of(tokens[0], tokens[1]));
    }
  }
}

2.0 / 2.1的工作代码:

// Dataflow SDK 2.0 and 2.1 compatible.
public class TestJob {
  public static void execute(Options options) {
    Pipeline pipeline = Pipeline.create(options);
    PCollection<KV<String, String>> data1 =
        pipeline.apply(TextIO.read().from(GcsPath.EXPERIMENT_BUCKET + "/data1.txt")).apply(ParDo.of(new doFn()));

    PCollection<KV<String, String>> data2 =
        pipeline.apply(TextIO.read().from(GcsPath.EXPERIMENT_BUCKET + "/data2.txt")).apply(ParDo.of(new doFn()));

    TupleTag<String> inputTag1 = new TupleTag<String>() {
      private static final long serialVersionUID = 1L;
    };
    TupleTag<String> inputTag2 = new TupleTag<String>() {
      private static final long serialVersionUID = 1L;
    };

    TupleTag<String> outputTag1 = new TupleTag<String>() {
      private static final long serialVersionUID = 1L;
    };
    TupleTag<String> outputTag2 = new TupleTag<String>() {
      private static final long serialVersionUID = 1L;
    };

    PCollectionTuple tuple = KeyedPCollectionTuple.of(inputTag1, data1).and(inputTag2, data2)
        .apply(CoGroupByKey.<String>create()).apply(ParDo.of(new doFn2(inputTag1, inputTag2, outputTag2))
            .withOutputTags(outputTag1, TupleTagList.of(outputTag2)));
    PCollection<String> output1 = tuple.get(outputTag1);
    PCollection<String> output2 = tuple.get(outputTag2);
    PCollection<String> outputMerged = PCollectionList.of(output1).and(output2).apply(Flatten.<String>pCollections());

    outputMerged.apply(TextIO.write().to(GcsPath.EXPERIMENT_BUCKET + "/test-job-2.1/outputMerged").withNumShards(1));
    output1.apply(TextIO.write().to(GcsPath.EXPERIMENT_BUCKET + "/test-job-2.1/output1").withNumShards(1));
    output2.apply(TextIO.write().to(GcsPath.EXPERIMENT_BUCKET + "/test-job-2.1/output2").withNumShards(1));

    PipelineResult pipelineResult = pipeline.run();
    pipelineResult.waitUntilFinish();

  }

  static class doFn2 extends DoFn<KV<String, CoGbkResult>, String> {
    private static final long serialVersionUID = 1L;

    final TupleTag<String> inputTag1;
    final TupleTag<String> inputTag2;
    final TupleTag<String> outputTag2;

    public doFn2(TupleTag<String> inputTag1, TupleTag<String> inputTag2, TupleTag<String> outputTag2) {
      this.inputTag1 = inputTag1;
      this.inputTag2 = inputTag2;
      this.outputTag2 = outputTag2;
    }

    @ProcessElement
    public void processElement(ProcessContext c) throws Exception {
      String key = c.element().getKey();
      List<String> values = new ArrayList<String>();
      int numValues = 0;
      for (String val1 : c.element().getValue().getAll(inputTag1)) {
        values.add(String.format("[%s]", val1));
        numValues++;
      }
      for (String val2 : c.element().getValue().getAll(inputTag2)) {
        values.add(String.format("(%s)", val2));
        numValues++;
      }
      final String line = String.format("%s: %s", key, Joiner.on(",").join(values));
      if (numValues % 2 == 0) {
        c.output(line);
      } else {
        c.output(outputTag2, line);
      }
    }
  }

  static class doFn extends DoFn<String, KV<String, String>> {
    private static final long serialVersionUID = 1L;

    @ProcessElement
    public void processElement(ProcessContext c) throws Exception {
      String[] tokens = c.element().split(",");
      c.output(KV.of(tokens[0], tokens[1]));
    }
  }
}

此外,如果有用,执行图如下所示。 (对于Google工程师,还指定了作业ID。)

1.9(职位编号2017-09-29_14_35_42-15149127992051688457): enter image description here

2.1(职位编号2017-09-29_14_31_59-991964669451027883): enter image description here

TextIO.Write 2,3不会在2.0 / 2.1下产生任何输出。 展平,其后续步骤正常。

1 个答案:

答案 0 :(得分:3)

这确实是一个缺陷。修复程序正在进行中,应记录在Service Release Notes中。

同时解决方法是使用1.9.1 SDK,因为此错误仅影响2.x SDK。

有兴趣提前修复的用户也可以使用Beam的最新夜间版本(建议解锁开发,而不是生产,因为它是每日构建)。 Instructions here