我正在处理管道,我需要将控制数据广播到每个DoFn转换实例。理想情况下,我想获得所有这些控制数据,而不仅仅是最后一个状态。我将示例简化为非常简单的示例 - 对于side和main输入有两个CountingInput,为第一个过滤30个第一个tick并查找sideInput。
PCollection<Long> iDs =
p.apply(CountingInput.unbounded().withRate(1, Duration.millis(200)))
.apply(ParDo.of(new DoFn<Long, Long>() {
@Override
public void processElement(ProcessContext c) {
Long cnt = c.element();
if (cnt <= 30) {
logger.info("ID=" + cnt);
c.output(cnt);
}
}
}));
PCollectionView<List<Long>> iDsView = iDs
.apply(Window.<Long>into(new GlobalWindows())
.triggering(Repeatedly.forever(AfterPane.elementCountAtLeast(1)))
.discardingFiredPanes()
)
.apply(View.asList());
p.apply(CountingInput.unbounded().withRate(1, Duration.millis(1000)))
.apply(ParDo
.withSideInputs(iDsView)
.of(new DoFn<Long, String>() {
@Override
public void processElement(ProcessContext c) {
Long in = c.element();
List<Long> si = c.sideInput(iDsView);
StringBuilder sb = new StringBuilder();
si.forEach(x -> sb.append(",").append(x));
logger.info("invocation=" + in
+ " class=" + this.toString()
+ " sideInput=[" + sb.toString().substring(1) + "]");
}
}));
使用InProcessPipelineRunner,我最终会看到类似
的输出INFO: invocation=10 class=com.sandbox.dw.WriteLogsToBQ$2@221a6d4a sideInput=[30]
Jan 08, 2017 12:38:44 AM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: invocation=11 class=com.sandbox.dw.WriteLogsToBQ$2@449a2a7a sideInput=[30]
Jan 08, 2017 12:38:45 AM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: invocation=12 class=com.sandbox.dw.WriteLogsToBQ$2@4a289e60 sideInput=[30]
但是当我使用--runner = BlockingDataflowPipelineRunner --numWorkers = 4运行它时 我看到4名工人之间的sideInlut相当不一致,我跑了几分钟也是如此:
00:47:16.586
invocation=138 class=com.sandbox.dw.WriteLogsToBQ$2@312aa182 sideInput=[0]
00:47:15.709
invocation=137 class=com.sandbox.dw.WriteLogsToBQ$2@2d0b6481 sideInput=[3,6,9,12,18,21,24,30]
00:47:14.445
invocation=136 class=com.sandbox.dw.WriteLogsToBQ$2@5153b895 sideInput=[0]
00:47:11.760
invocation=134 class=com.sandbox.dw.WriteLogsToBQ$2@65683230 sideInput=[3,6,9,12,18,21,24,30]
00:47:11.231
invocation=132 class=com.sandbox.dw.WriteLogsToBQ$2@5ee8917a sideInput=[0]
00:47:10.775
invocation=133 class=com.sandbox.dw.WriteLogsToBQ$2@16000b0 sideInput=[3,6,9,12,18,21,24,30]
00:47:09.477
invocation=123 class=com.sandbox.dw.WriteLogsToBQ$2@6ffe3f47 sideInput=[15]
00:47:08.977
invocation=130 class=com.sandbox.dw.WriteLogsToBQ$2@458bc76b sideInput=[3,6,9,12,18,21,24,30]
00:47:07.505
invocation=129 class=com.sandbox.dw.WriteLogsToBQ$2@2c6fcbcf sideInput=[0]
00:47:07.200
invocation=128 class=com.sandbox.dw.WriteLogsToBQ$2@1bf63883 sideInput=[3,6,9,12,18,21,24,30]
00:47:06.033
invocation=127 class=com.sandbox.dw.WriteLogsToBQ$2@5fd02daf sideInput=[3,6,9,12,18,21,24,30]
00:47:05.573
invocation=119 class=com.sandbox.dw.WriteLogsToBQ$2@7ba4a88b sideInput=[15]
00:47:04.502
invocation=126 class=com.sandbox.dw.WriteLogsToBQ$2@a7d0a48 sideInput=[0]
我还注意到每个输入元素都重新创建了DoFn实例。 任何人都可以建议最好的方法来保证使用sideInput向每个变换广播PubSub数据吗?
以下是分享我的疑虑的简化示例:
PCollectionView<List<Long>> iDsView =
p.apply(CountingInput.unbounded().withRate(1, Duration.millis(1000)))
.apply(Window.<Long>into(new GlobalWindows())
.triggering(Repeatedly.forever(AfterPane.elementCountAtLeast(1)))
.discardingFiredPanes()
)
.apply(Max.longsGlobally())
.apply(ParDo.of(new DoFn<Long, Long>() {
@Override
public void processElement(ProcessContext c) {
Long elem = c.element();
logger.info("MaxElement=" + elem);
c.output(elem);
}
}))
.apply(View.asList());
p.apply(CountingInput.unbounded().withRate(1, Duration.millis(300)))
.apply(ParDo
.withSideInputs(iDsView)
.of(new DoFn<Long, Long>() {
@Override
public void processElement(ProcessContext c) {
Long in = c.element();
List<Long> si = c.sideInput(iDsView);
StringBuilder sb = new StringBuilder();
si.forEach(x -> sb.append(",").append(x));
logger.info("MainInput=" + in
+ " sideInput=[" + sb.toString().substring(1) + "]");
}
}));
它在本地跑步者上运作良好,但是对于单个工作者来说,使用BlockingDataflowPipelineRunner不会更新侧输入。我可以看到触发器是如何触发并将功能发送结果组合在日志中。但是侧输入返回值的相同值。
本地选手日志:
INFO: MaxElement=6
Jan 12, 2017 9:22:52 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=21 sideInput=[6]
Jan 12, 2017 9:22:52 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=22 sideInput=[6]
Jan 12, 2017 9:22:53 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=23 sideInput=[6]
Jan 12, 2017 9:22:53 PM com.sandbox.dw.WriteLogsToBQ$1 processElement
INFO: MaxElement=7
Jan 12, 2017 9:22:53 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=24 sideInput=[7]
Jan 12, 2017 9:22:53 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=25 sideInput=[7]
Jan 12, 2017 9:22:53 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=26 sideInput=[7]
Jan 12, 2017 9:22:54 PM com.sandbox.dw.WriteLogsToBQ$1 processElement
INFO: MaxElement=8
Jan 12, 2017 9:22:54 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=27 sideInput=[8]
Jan 12, 2017 9:22:54 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=28 sideInput=[8]
Jan 12, 2017 9:22:54 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=29 sideInput=[8]
Jan 12, 2017 9:22:55 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=30 sideInput=[8]
Jan 12, 2017 9:22:55 PM com.sandbox.dw.WriteLogsToBQ$1 processElement
INFO: MaxElement=9
Jan 12, 2017 9:22:55 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=31 sideInput=[9]
Jan 12, 2017 9:22:55 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=32 sideInput=[9]
Jan 12, 2017 9:22:56 PM com.sandbox.dw.WriteLogsToBQ$2 processElement
INFO: MainInput=33 sideInput=[9]
Jan 12, 2017 9:22:56 PM com.sandbox.dw.WriteLogsToBQ$1 processElement
DataFlow日志:
MaxElement=61
21:26:00.225
MainInput=207 sideInput=[0]
21:26:00.676
MainInput=208 sideInput=[0]
21:26:00.924
MainInput=209 sideInput=[0]
21:26:01.258
MainInput=210 sideInput=[0]
21:26:01.260
MaxElement=62
21:26:01.518
MainInput=211 sideInput=[0]
21:26:01.748
MainInput=212 sideInput=[0]
21:26:02.071
MainInput=213 sideInput=[0]
21:26:02.313
MainInput=214 sideInput=[0]
21:26:02.466
MaxElement=63
21:26:02.677
MainInput=215 sideInput=[0]
21:26:02.994
MaxElement=64
21:26:03.113
MainInput=216 sideInput=[0]
21:26:03.335
MainInput=217 sideInput=[0]
21:26:03.614
MainInput=218 sideInput=[0]
21:26:04.132
MainInput=219 sideInput=[0]
21:26:04.142
MaxElement=65
21:26:04.193
MainInput=220 sideInput=[0]
21:26:04.538
MainInput=221 sideInput=[0]
21:26:04.706
MainInput=222 sideInput=[0]
21:26:05.250
MaxElement=66
21:26:05.531
MainInput=224 sideInput=[0]
我担心的是,侧输入会显示过时的结果,我会在触发触发时期望侧输入缓存失效。
答案 0 :(得分:2)
您将遇到主输入元素与侧输入上的多个触发之间的一致性问题。一致性模型非常宽松:
对于触发本身,请记住它也是不确定的:
因此,您的管道布局最自然地适用于侧输入表示朝向某个最终值(可能是无限的,因此收敛可能永远持续)的非确定性收敛的情况。相反,我认为您需要CoGroupByKey
或Sam答案中建议的直接边线频道订阅才能正确加入。
根据您的使用案例的实际细节,可能还有其他更深奥的解决方案使用自定义WindowFn
。主输入将阻塞,直到侧输入在匹配窗口中至少有一个触发元素。
最后,关于积累模式的快速说明:
discardingFiredPanes()
替换为accumulatingFiredPanes()
,但这会导致边输入无限制地增长。accumulatingFiredPanes()
将更好地工作 - 所有工作人员最终将收到所有控制消息,并且窗口到期时窗口的累积消息将被释放。但是不能保证主输入元素实际到达的时间足够晚以查看最终值!所以它仍然可能不是正确的方法。答案 1 :(得分:1)
根据您的一致性需求,您可以拥有广播控制消息的Pub / Sub主题的每进程静态订阅者。这将是一个对象,在初始化时,创建对主题的随机订阅并开始收听该主题。每当它收到一条消息时,它就会将其内容提供给DoFn的处理代码。
请注意,没有很好的方法可以确保清理这些订阅,因此如果您经常启动和停止管道,则需要一些定期清理方法。
答案 2 :(得分:0)
在Beam中,它将构成Dataflow 2.x的基础,您可以使用状态对缓慢变化的侧输入尺寸进行更严格的控制。
对于空间,我将假设Either
类型存在且具有默认编码器。然后,如果您有一些类型Key
,MainInput
和SideInput
以及您希望更新补充状态的任何自定义代码,那么实现您想要的有状态DoFn可能如下所示:< / p>
new DoFn<KV<Key, Either<MainInput, SideInputUpdate>>, KV<Key, Output>>() {
@StateId("side")
private final StateSpec<ValueState<SideInput>> detailsSpec =
StateSpecs.value(SideInput.getCoder());
@ProcessElement
public void processElement(
ProcessContext ctx,
@StateId("side") ValueState<SideInput> sideState) {
SideInput side = sideState.read();
if (ctx.element().getValue().isRight()) {
SideInputUpdate update = ctx.element().getValue().getRight();
sideState.write(sideInput.applyUpdate(update));
} else {
MainInput element = ctx.element().getValue().getLeft();
// do whatever you want to do with the element
}
}
}
要使用此DoFn
,您可以将主要和侧面输入注入PCollection<Either<MainInput, SideInputUpdate>>
,如下所示:
PCollection<MainInput> mainInput = ...
PCollection<SideInputUpdate> sideInputUpdates = ...
PCollection<Either<MainInput, SideInputUpdate>> injectedMain =
mainInput.apply(MapElements.via(... in left ...));
PCollection<Either<MainInput, SideInputUpdate>> injectedSide =
sideInputUpdates.apply(MapElements.via(... in right ...);
PCollection<KV<Key, Either<MainInput, SideInputUpdate>>> =
PCollectionList.of(injectedMain).and(injectedSide)
.apply(Flatten.pCollections())
.apply(WithKeys.of(...))
结果仍然是不确定的 - 您的侧输入更新和主输入元素没有排序保证。但是延迟会更低且相当可预测,因为状态将在对该键和窗口进行任何进一步处理之前进行更新。
有关此新功能的更多信息,请参阅this blog post on the Beam blog。