Apache flink 对同一个键进行链接keyBy调用的事件顺序

Apache flink 对同一个键进行链接keyBy调用的事件顺序,apache-flink,flink-streaming,Apache Flink,Flink Streaming,据我所知,在以下生成的outDataStream中,来自某个inDataStream的事件顺序按键保留: outDataStream = inDataStream.keyBy(...) .timeWindow(...) .reduce(...) 例如,如果我们从inDataStream输入了以下事件(我们对键执行keyBy): (1,键1),(2,键1),(3,键2),(4,键1),(5,键2) 然后outDataStream将为key1的事件和key2的事件保留相同的顺序。因

据我所知,在以下生成的outDataStream中,来自某个inDataStream的事件顺序按键保留:

outDataStream = inDataStream.keyBy(...)
    .timeWindow(...)
    .reduce(...)
例如,如果我们从inDataStream输入了以下事件(我们对键执行keyBy):

(1,键1),(2,键1),(3,键2),(4,键1),(5,键2)

然后outDataStream将为key1的事件和key2的事件保留相同的顺序。因此outDataStream的这种结果永远不会发生:

(2,键1),(1,键1),(3,键2),(4,键1),(5,键2)

(因为1和2切换了)

到目前为止,我说的对吗? 然后,如果我们链接另一个keyBy/进程,我们会以相同的顺序再次得到结果,对吗?因为我们只是再次申请同样的担保。。由于相同密钥的顺序对我们至关重要,因此为了确保我们在同一页上,我制作了简化版:

// incoming events. family is used for keyBy grouping.
case class Event(id: Int, family: String, value: Double)
// the aggregation of events
case class Aggregation(latsetId: Int, family: String, total: Double)

// simply adding events into total aggregation
object AggFunc extends AggregateFunction[Event, Aggregation, Aggregation] {
override def add(e: Event, acc: Aggregation) = Aggregation(e.id, e.family, e.value + acc.total)
override def createAccumulator() = Aggregation(-1, null, 0.0)
override def getResult(acc: Aggregation) = acc
}

object ProcessFunc extends ProcessFunction[Aggregation, String] {
override def processElement(agg: Aggregation, ctx: ProcessFunction[Aggregation, String]#Context, out: Collector[String]) =
  out.collect(s"Received aggregation combined with event ${agg.latsetId}. New total=${agg.total}")
}

def main(args: Array[String]): Unit = {

val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)
// incoming events from a source have 2 families: "A", and "B"
env.fromElements(Event(1, "A", 6.0), Event(2, "B", 4.0), Event(3, "A", -2.0), Event(4, "B", 3.0),
    Event(5, "A", 8.0), Event(6, "B", 1.0), Event(7, "A", -10.0))
  .keyBy(_.family)
  .timeWindow(Time.seconds(1))
  .trigger(CountTrigger.of(1)) // FIRE any incoming event for immediate aggregation and ProcessFunc application
  .aggregate(AggFunc)
  .keyBy(_.family)
  .process(ProcessFunc)
  .print()
}
因此,对于按照该顺序进入第一个keyBy的此类事件-对于任何操作员并行性和集群部署,我们保证接收器(此处为print())将始终接收以下族“A”的聚合,并且按照该顺序(但可能与族“B”的聚合混合):


这是正确的吗?

我不相信您可以安全地假设流元素的绝对顺序将在parallelism>1时保持

此外,我认为,一旦到达聚合操作符,就根本无法假定顺序。聚合运算符的输出基于内部窗口计时器,不应假定键按任何特定顺序保留


如果您需要排序,那么我认为您最好的选择是在数据出来后按照您的需要进行排序

Flink只保证并行分区内的顺序,也就是说,它不跨分区通信并保留数据以保证顺序

这意味着,如果您有以下运算符:

map(Mapper1).keyBy(0).map(Mapper2)
并以2的并行度运行,即

Mapper1(1) -\-/- Mapper2(1)
             X
Mapper1(2) -/-\- Mapper2(2)
然后,来自
Mapper1(1)
的具有相同键的所有记录将按顺序到达
Mapper2(1)
Mapper2(2)
,具体取决于键。当然,对于
Mapper1(2)
中具有相同键的所有记录也是如此

因此,只要具有相同密钥的记录分布在多个分区上(这里是
Mapper1(1)
Mapper1(2)
),就不会对不同分区的记录提供排序保证,而只对同一分区中的记录提供排序保证


如果顺序很重要,您可以将并行度降低到1,或者使用事件时语义实现运算符,并利用水印来解释记录的无序性。

谢谢Fabian。我想确保在同一个键字段上的后续keyBy调用将在该键上保持顺序:map(Mapper1).keyBy(0).map(Mapper2).keyBy(0).map(Mapper3).addSink(Sink),因此所有键为k1的元素都将到达同一个Mapper3子任务,然后进入唯一一个Sink子任务,按照相同的顺序,他们进入Mapper2子任务。键函数及其从元素的输入始终是相同的。这是正确的,因为sink和
Mapper3
具有相同的并行性(并且
Mapper2
不会更改键字段),相同密钥的元素顺序将在Mapper3的传出流和sink的传入流之间保留-对于并行度为=1的sink和具有任何并行度的Mapper3。对吗?我的意思是,键“k1”的所有元素都以某种顺序进入一个Mapper3子任务(所以Mapper3具有哪种并行性并不重要),它们将被处理,并以该顺序再次离开该Mapper3子任务,然后进入该sink子任务并由其处理-再次,以相同的顺序。你同意吗?是的,没错。显然,接收器将处理具有所有键的记录,但具有相同键的记录不会被洗牌。
Mapper1(1) -\-/- Mapper2(1)
             X
Mapper1(2) -/-\- Mapper2(2)