对于非常简单的管道,我们发现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):
2.1(职位编号2017-09-29_14_31_59-991964669451027883):
TextIO.Write 2,3不会在2.0 / 2.1下产生任何输出。 展平,其后续步骤正常。
答案 0 :(得分:3)
这确实是一个缺陷。修复程序正在进行中,应记录在Service Release Notes中。
同时解决方法是使用1.9.1 SDK,因为此错误仅影响2.x SDK。
有兴趣提前修复的用户也可以使用Beam的最新夜间版本(建议解锁开发,而不是生产,因为它是每日构建)。 Instructions here