繁体   English   中英

如何在使用 coGroup() 加入 Flink 后排空窗口?

[英]How to drain the window after a Flink join using coGroup()?

我想加入来自两个 Kafka 主题(“左”和“右”)的数据。

匹配记录将使用 ID 连接,但如果缺少“左”或“右”记录,则应在一定超时后将另一记录传递到下游。 因此我选择使用coGroup功能。

这可行,但有一个问题:如果根本没有消息,则始终至少有一条记录永久保留在内部缓冲区中。 当新消息到达时它会被推出。 否则会卡住。

预期的行为是在达到配置的空闲超时后应该推出所有记录。

一些可能相关的信息

  • Flink 1.14.4
  • Flink 并行度设置为 8,两个 Kafka 主题中的分区数也是如此。
  • Flink 检查点已启用
  • 将使用事件时间处理
  • 使用 Lombok:所以val就像final var

一些代码片段:

相关加入设置

public static final int AUTO_WATERMARK_INTERVAL_MS = 500;

public static final Duration SOURCE_MAX_OUT_OF_ORDERNESS = Duration.ofMillis(4000);
public static final Duration SOURCE_IDLE_TIMEOUT = Duration.ofMillis(1000);

public static final Duration TRANSFORMATION_MAX_OUT_OF_ORDERNESS = Duration.ofMillis(5000);
public static final Duration TRANSFORMATION_IDLE_TIMEOUT = Duration.ofMillis(1000);

public static final Time JOIN_WINDOW_SIZE = Time.milliseconds(1500);

创建KafkaSource

private static KafkaSource<JoinRecord> createKafkaSource(Config config, String topic) {
    val properties = KafkaConfigUtils.createConsumerConfig(config);

    val deserializationSchema = new KafkaRecordDeserializationSchema<JoinRecord>() {
        @Override
        public void deserialize(ConsumerRecord<byte[], byte[]> record, Collector<JoinRecord> out) {
            val m = JsonUtils.deserialize(record.value(), JoinRecord.class);

            val copy = m.toBuilder()
                    .partition(record.partition())
                    .build();

            out.collect(copy);
        }

        @Override
        public TypeInformation<JoinRecord> getProducedType() {
            return TypeInformation.of(JoinRecord.class);
        }
    };

    return KafkaSource.<JoinRecord>builder()
            .setProperties(properties)
            .setBootstrapServers(config.kafkaBootstrapServers)
            .setTopics(topic)
            .setGroupId(config.kafkaInputGroupIdPrefix + "-" + String.join("_", topic))
            .setDeserializer(deserializationSchema)
            .setStartingOffsets(OffsetsInitializer.latest())
            .build();
}

创建DataStreamSource

然后DataStreamSource建立在KafkaSource

  • 配置“最大无序”
  • 配置“空闲”
  • 从记录中提取时间戳,用于事件时间处理
private static DataStreamSource<JoinRecord> createLeftSource(Config config,
                                                             StreamExecutionEnvironment env) {
    val leftKafkaSource = createLeftKafkaSource(config);

    val leftWms = WatermarkStrategy
            .<JoinRecord>forBoundedOutOfOrderness(SOURCE_MAX_OUT_OF_ORDERNESS)
            .withIdleness(SOURCE_IDLE_TIMEOUT)
            .withTimestampAssigner((joinRecord, __) -> joinRecord.timestamp.toEpochSecond() * 1000L);

    return env.fromSource(leftKafkaSource, leftWms, "left-kafka-source");
}

使用keyBy

键控源是在DataSource实例之上创建的,如下所示:

  • 再次配置“乱序”和“空闲”

  • 再次提取时间戳

     val leftWms = WatermarkStrategy .<JoinRecord>forBoundedOutOfOrderness(TRANSFORMATION_MAX_OUT_OF_ORDERNESS) .withIdleness(TRANSFORMATION_IDLE_TIMEOUT) .withTimestampAssigner((joinRecord, __) -> { if (VERBOSE_JOIN) log.info("Left : " + joinRecord); return joinRecord.timestamp.toEpochSecond() * 1000L; }); val leftKeyedSource = leftSource .keyBy(jr -> jr.id) .assignTimestampsAndWatermarks(leftWms) .name("left-keyed-source");

使用coGroup加入

然后,连接将左键源和右键组合在一起

    val joinedStream = leftKeyedSource
            .coGroup(rightKeyedSource)
            .where(left -> left.id)
            .equalTo(right -> right.id)
            .window(TumblingEventTimeWindows.of(JOIN_WINDOW_SIZE))
            .apply(new CoGroupFunction<JoinRecord, JoinRecord, JoinRecord>() {
                       @Override
                       public void coGroup(Iterable<JoinRecord> leftRecords, 
                                           Iterable<JoinRecord> rightRecords,
                                           Collector<JoinRecord> out) {
                           // Transform
                           val result = ...;

                           out.collect(result);
                       }

将流写入控制台

生成的joinedStream被写入控制台:

    val consoleSink = new PrintSinkFunction<JoinRecord>();
    joinedStream.addSink(consoleSink);
  • 如何配置此连接操作,以便在配置的空闲超时后将所有记录推送到下游?
  • 如果不能这样做:还有其他选择吗?

这是预期的行为。 withIdleness不会尝试处理所有流都空闲的情况。 它仅在仍有事件从至少一个源分区/分片/拆分流出的情况下才有帮助。

要获得您想要的行为(在连续流作业的上下文中),您必须实施自定义水印策略,该策略根据处理时间计时器推进水印。 这是一个使用旧版水印 API 的实现。

另一方面,如果作业已完成,并且您只想在关闭作业之前排空最终结果,则可以在停止作业时使用--drain选项。 或者,如果您使用有界来源,这将自动发生。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM