Flink原理:并行度与keyGroup桶

Flink应用场景和开发方式

数字银行风控场景:kafka -> Flink -> redis/hbase ,外部数据源先进kafka,Flink消费做流式聚合计算、出特征指标,然后存redis或hbase。 后边的各风控服务直接按需读指标、输入模型进行推理预测。

一般来说似乎开发人员就是用Flink框架写流式计算Job,打成jar包传到JM上去: 设置一下数据源、水位线策略、写sink之外,最主要是用Java写窗口聚合计算逻辑
本例中我们使用了两个聚合计算的算子,AggregateFunctionProcessWindowFunction,分别是对窗口内收到的数据进行累计,以及最后窗口结束(水位线触发)的时候进行一次性的计算。ProcessWindowFunction 中其实也可以通过itrator获取所有的原始数据进而做AggregateFunction累计类型的计算,但是一般实践方式是分成两步处理,提高性能。

但这本质上其实有点像是数据分析,只不过用的是Java来写计算逻辑,属于是数据分析人员做的事情。
作为架构师,要理解的更深入一些的原理。

通常设置kafka Partition和parallelism相等

往kafka里写消息的时候,为了充分利用kafka分布式架构的优势、提高吞吐处理,可以按需设置分区进行并行写入,我们假设按照hash(企业ID)%3 进行3分区写入。
Flink侧一般也会按照同样数量设置parallelism,即source subTask数,这里也设置3,flink JM会在空闲的Slot中找3个Slot,也就是source subTask 0、1、2可能会分布在不同的TM

分区=并行度是为了 避免 Source 层的不均衡
Partition = Parallelism 解决的:
✅ 每个 Source Subtask 读 1 个分区,负载均衡
如果Partition ≠ Parallelism 会额外造成:
❌ 有的 Subtask 读 2 个分区(累死),有的空转(浪费)

这里很容易认为是这3个线程去分别处理对应的分区数据,之后走后续的流式计算,即按时间窗口聚合、按顺序计算聚合算子,然后sink。但是实际上不是这样。

两层分片和shuffle机制

前面说的source subTask相当于3个flink线程对kafka消费做了分片处理,parallelism除了是source subtask并行数之外,也代表window subtask数,即真正执行窗口聚合计算的线程。
但这里有第二次分片。murmurhash(企业ID) % 128, keyBy企业ID,把企业ID按照上述分片算法丢到128个keyGroup桶里,然后3个window subtask平分处理3组桶中的计算任务。

第一轮分片:Kafka 消费层

Producer(ENT001) → hash(ENT001) % 3 = 1 → Partition 1
Producer(ENT002) → hash(ENT002) % 3 = 0 → Partition 0
Producer(ENT004) → hash(ENT004) % 3 = 2 → Partition 2

          Partition 0      Partition 1      Partition 2
              │                 │                 │
              ▼                 ▼                 ▼
         Source Subtask 0  Source Subtask 1  Source Subtask 2
         (读到 ENT002)     (读到 ENT001)     (读到 ENT004)

第二轮分片:KeyBy 计算层


 

Source Subtask 0              Source Subtask 1              Source Subtask 2
  ENT002                        ENT001                        ENT004
    │                             │
    │  KeyBy(enterpriseId)        │                             │
    │  = MurmurHash % 128         │
    │                             │                             │
    ▼                             ▼                             ▼
  KeyGroup 11                   KeyGroup 82                   KeyGroup 55
  (归 Window Subtask 0)         (归 Window Subtask 2)         (归 Window Subtask 1)


                ┌──────────────────────────────────┐
                │         网络 Shuffle              │
                │                                  │
Source Subtask 0│  ENT002(KeyGroup 11)             │
                │     → 本地,因为 KeyGroup 11       │
                │       恰好归 Window Subtask 0     │
                │                                  │
Source Subtask 1│  ENT001(KeyGroup 82)             │
                │     → 跨线程,因为 KeyGroup 82     │
                │       归 Window Subtask 2        │
                │                                  │
Source Subtask 2│  ENT004(KeyGroup 55)             │
                │     → 跨线程,因为 KeyGroup 55     │
                │       归 Window Subtask 1        │
                └──────────────────────────────────┘

Window Subtask 0          Window Subtask 1          Window Subtask 2
KeyGroup [0..42]          KeyGroup [43..85]         KeyGroup [86..127]
   ENT002(本地)              ENT004(来自 S2)           ENT001(来自 S1)
   + 其他同范围企业          + 其他同范围企业          + 其他同范围企业
       ↓                         ↓
  窗口聚合 + Sink           窗口聚合 + Sink           窗口聚合 + Sink

两轮分片对比

┌────────┬──────────────────────────┬───────────────────────────────────┐
│        │     第一轮(Kafka)        │          第二轮(KeyBy)          │
├────────┼──────────────────────────┼───────────────────────────────────┤
│ 干嘛的  │ 把数据从 Kafka 读出来       │ 把数据按 key 路由给正确的计算线程     │
├────────┼──────────────────────────┼───────────────────────────────────┤
│ 分片数  │ 3(= 分区数)             │ 128(= maxParallelism)           │
├────────┼──────────────────────────┼───────────────────────────────────┤
│ 算法    │ hash(key) % 3            │ MurmurHash(key) % 128             │
├────────┼──────────────────────────┼───────────────────────────────────┤
│ 谁干    │ Source Subtask           │ KeyBy 算子                        │
├────────┼──────────────────────────┼───────────────────────────────────┤
│ 结果    │ 1 个 Subtask 读 1 个分区  │ 1 个 Subtask 扛 ~43 个 KeyGroup    │
└────────┴──────────────────────────┴───────────────────────────────────┘

Source Subtask ≠ Window Subtask。 虽然并行度都是 3,但它们是两套独立的线程。 中间 KeyBy 做了一次重新分配——企业 ID 在 Kafka 分区 1,不代表它的 KeyGroup 也归 Subtask1。 这个重新分配就是 "JM 下发任务"时的网络 shuffle。

之所以搞这么两层映射,是为了扩并行度方便,桶可以不变,线程重新分配自己处理的桶的范围就行了
128 个桶是固定底座,线程数可以变,桶的归属可以重新分配,但桶本身不动:

不搞两层映射:                        搞两层映射(KeyGroup):
key → hash % parallelism               key → hash % 128 → KeyGroup → Subtask

扩并行度: 3 → 6                       扩并行度: 3 → 6
hash(key) % 3 → hash(key) % 6         KeyGroup 桶不动
       ↑                                    ↑
  模数变了,每个 key 的分片结果全变了       只重新分桶的归属:
  → 状态全量迁移                         K0..K21 → S0    K22..K42 → S1
  → 必须停机重建                         K43..K63 → S2    K64..K85 → S3
                                         K86..K106 → S4   K107..K127 → S5

                                  状态跟着桶走,桶在哪个 Subtask 状态就在哪
                                  → 在线平滑扩缩,不停机

所以 KeyGroup 不是"搞复杂了",是一个变更隔离层——把"改并行度"这件对系统影响最大的操作,锁在一个可控的范围内。

posted on 2026-08-03 14:23  幽州散人  阅读(2)  评论(0)    收藏  举报

导航