Flink Stream 处理数据倾斜

文章讨论了数据倾斜在数据源(如Kafka分区不均)和Flink的keyBy操作中出现的情况,并提出了两种解决方案。对于数据源倾斜,可以通过shuffle、rebalance和rescale等重分区算子平衡数据分布。对于keyBy导致的倾斜,推荐使用combiner进行本地预聚合,通过随机值分散数据并进行两次聚合。文中提供了两种具体实现示例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数据倾斜的场景

  1. 在数据源发生的数据倾斜。例如,Kafka 的分区,有的分区数据量特别的少,有的特别的多,这样在消费数据后,各个 subtask 拿到的数据量就有了差异。
  2. 在 keyBy 之后,产生的数据倾斜。例如,wordcount 的场景中,可能有的单词特别的多,有的特别的少,那么就造成 keyBy 之后的聚合算子中,有的接收到的数据特表的大,有的特别的少。

如何处理数据倾斜

数据源造成的倾斜

Flink 为我们提供了重分区的 8 个算子,shuffle、rebalance、rescale、broadcast、global、forward、keyBy、partitionCustom ,我们可以使用 shuffle、rebalance、rescale 三个算子,做重分区。它们的功能是:

  1. shuffle 是 random().nextInt()%parallelism 随机的分发到下游的算子。
  2. rebalance 是 nextPartition = (nextPartition + 1)%numberOfChannel
  3. rescale 是 nextPartition = if ++nextPartition > numberOfChannel then 0 else nextPartition
    从上面的三个分区算法的查看,他们都能解决数据倾斜的问题。通过这三个算子之后,根据后续算子的并发度了重新分区,而且各个分区中的数据量是相同的。例如,在算子中,我们只有转化类型的算子,并没有分组聚合的需求,此时就可以使用这三个算子来解决问题。

keyBy 造成的倾斜

keyBy 造成的倾斜,通常的做法是 combiner 的做法,做本地预聚合,减少 keyBy 之后的数据量。具体的思路是,通过给每条数据指定一个随机值,然后分发到不同分区,这样相同的 word 会均匀的分发到分区中,然后使用算子来做第一次聚合,最后使用 keyBy + 聚合算子做第二次聚合。实际的实现有两种。

第一种,通过 shuffle、rebalance、rescale 实现将 word 随机落到分区中,然后可以使用 flatMap 将分区中的 word 做第一次聚合,最后使用 keyBy + 聚合算子做第二次聚合。

第二种, Tuple2(word,1) 转换为 Tuple3(word, UUID%10 , 1) ,然后对 uuid%10 做 keyBy ,这样也可以实现第一次随机聚合的步骤。第二次聚合和第一种实现方式相同。

第一种实现方式的代码:

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Tuple3<String, String, Integer>> src = env.socketTextStream("127.0.0.1", 6666)
                .flatMap(new RichFlatMapFunction<String, Tuple3<String, String, Integer>>() {
                    @Override
                    public void flatMap(String s, Collector<Tuple3<String, String, Integer>> collector) throws Exception {
                        Arrays.stream(s.split("\\s+")).forEach(x -> {
                            collector.collect(new Tuple3<String, String, Integer>(x, UUID.randomUUID().toString(), 1));
                        });
                    }
                }).setParallelism(1);
        src.rebalance()
                .flatMap(new LocalCombiner())
                .keyBy(new KeySelector<Tuple2<String,Integer> , String>(){
            @Override
            public String getKey(Tuple2<String, Integer> record) throws Exception {
                return record.f0;
            }
        }).sum(1).print("--------");
        env.execute();

第二种实现方式:

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        SingleOutputStreamOperator<Tuple2<String, Integer>> firstPhase = env.socketTextStream("127.0.0.1", 6666)
                .flatMap(new RichFlatMapFunction<String, Tuple3<String, String, Integer>>() {
                    @Override
                    public void flatMap(String s, Collector<Tuple3<String, String, Integer>> collector) throws Exception {
                        Arrays.stream(s.split("\\s+")).forEach(x -> {
                            collector.collect(new Tuple3<String, String, Integer>(x, UUID.randomUUID().toString(), 1));
                        });
                    }
                }).keyBy(new KeySelector<Tuple3<String, String, Integer>, Integer>() {
                    @Override
                    public Integer getKey(Tuple3<String, String, Integer> t3) throws Exception {
                        return t3.f1.hashCode() % 10;
                    }
                }).timeWindow(Time.seconds(10))
                .process(new ProcessWindowFunction<Tuple3<String, String, Integer>, Tuple2<String, Integer>, Integer, TimeWindow>() {
                    @Override
                    public void process(Integer integer, ProcessWindowFunction<Tuple3<String, String, Integer>, Tuple2<String, Integer>, Integer, TimeWindow>.Context context, Iterable<Tuple3<String, String, Integer>> iterable, Collector<Tuple2<String, Integer>> collector) throws Exception {
                        Map<String, Integer> wordCnt = new HashMap<>();
                        iterable.forEach(x -> {
                            wordCnt.computeIfPresent(x.f0, (k, oldValue) -> oldValue + 1);
                            wordCnt.computeIfAbsent(x.f0, k -> 1);
                        });
                        wordCnt.entrySet().forEach(x -> {
                            collector.collect(new Tuple2<String, Integer>(x.getKey(), x.getValue()));
                        });
                    }
                });
        firstPhase.keyBy(new KeySelector<Tuple2<String,Integer> , String>(){

            @Override
            public String getKey(Tuple2<String, Integer> record) throws Exception {
                return record.f0;
            }
        }).sum(1).print("--------");
        env.execute();

### Flink 数据倾斜优化方案 #### 识别数据倾斜 在分布式计算框架中,数据倾斜是指某些任务分配到的数据量远大于其他任务的情况。对于 Flink 来说,这可能导致部分并行实例的工作负载显著增加,进而影响整体作业性能[^3]。 #### 调整并行度 通过调整算子的并行度可以有效缓解因数据分布不均引起的压力不平衡现象。具体做法是在创建 DataStream 或者 TableEnvironment 实例时指定合适的 parallelism 参数值来控制整个应用程序或者特定操作符的最大并发执行数目[^1]。 ```java // 设置全局默认并行度 env.setParallelism(8); // 对单个 operator 设定不同级别的并行数 stream.keyBy(<key selector>) .process(new MyProcessFunction()) .setParallelism(4); ``` #### 使用预聚合减少 shuffle 阶段传输的数据量 如果业务逻辑允许,在 map/reduce 前先做一次局部汇总能够降低后续阶段所需交换的信息总量,从而减轻网络带宽占用和内存消耗带来的瓶颈效应。 ```scala val result = input .map(x => (x._2, x)) // 将 key 提取出来作为 tuple 的第一个元素 .keyBy(_._1) // 按照新的键分组 .reduce((a,b) => (a._1,(a._2._1+b._2._1,a._2._2))) // 局部求和 .map{case(k,v)=>v} // 移除不再需要的辅助字段 ``` #### 合理设计 Key Selector 函数 精心挑选用于分区的关键字有助于使输入记录更加均匀地分布在各个 worker 上面;反之,则容易造成热点问题。因此应该基于实际场景分析哪些属性最适合作为划分依据,并考虑引入随机因子打散高度聚集的数据流。 ```python def custom_key_selector(record): # 添加一定范围内的随机扰动项以打破完全相同的 hashcode 所致的集中趋势 return record['category'] + '_' + str(random.randint(0,9)) data_stream\ .key_by(custom_key_selector)\ .window(TumblingEventTimeWindows.of(Time.seconds(5)))\ .apply(MyWindowFunction()) ``` #### 应用自定义 Partitioner 进行更细粒度调控 除了依靠内置机制外,还可以编写专门针对项目特点定制化的 partitioning strategy ,实现诸如按地理位置、时间戳或者其他维度来进行更为灵活的任务调度安排。 ```java public class CustomPartitioner implements org.apache.flink.api.common.functions.Partitioner<Integer> { @Override public int partition(Integer key, int numPartitions) { // 自定义分区策略代码... return Math.abs(key.hashCode()) % numPartitions; } } source.partitionCustom(new CustomPartitioner(), "id"); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值