Flink 内容分享(四):Fink原理、实战与性能优化(四)

目录

Transformations

Sink

分区策略


Transformations

Transformations算子可以将一个或者多个算子转换成一个新的数据流,使用Transformations算子组合可以处理复杂的业务处理。

Map

DataStream → DataStream

遍历数据流中的每一个元素,产生一个新的元素。

FlatMap

DataStream → DataStream

遍历数据流中的每一个元素,产生N个元素 N=0,1,2......。

Filter

DataStream → DataStream

过滤算子,根据数据流的元素计算出一个boolean类型的值,true代表保留,false代表过滤掉。

KeyBy

DataStream → KeyedStream

根据数据流中指定的字段来分区,相同指定字段值的数据一定是在同一个分区中,内部分区使用的是HashPartitioner。

指定分区字段的方式有三种:

  • 根据索引号指定。

  • 通过匿名函数来指定。

  • 通过实现KeySelector接口  指定分区字段。

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<Long> stream = env.fromSequence(1, 100);
        stream.map((MapFunction<Long, Tuple2<Long, Integer>>) (Long x) -> new Tuple2<>(x % 3, 1), TypeInformation.of(new TypeHint<Tuple2<Long, Integer>>() {}))
                //根据索引号来指定分区字段:.keyBy(0)
                //通过传入匿名函数 指定分区字段:.keyBy(x=>x._1)
                //通过实现KeySelector接口  指定分区字段                
                .keyBy((KeySelector<Tuple2<Long, Integer>, Long>) (Tuple2<Long, Integer> value) -> value.f0, BasicTypeInfo.LONG_TYPE_INFO)
                .sum(1).print();
        env.execute("Flink Job");
    }

Reduce

适用于KeyedStream

KeyedStream:根据key分组 → DataStream

注意,reduce是基于分区后的流对象进行聚合,也就是说,DataStream类型的对象无法调用reduce方法

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Tuple2<String, Integer>> dataStream = env.fromElements(
                new Tuple2<>("apple", 3),
                new Tuple2<>("banana", 1),
                new Tuple2<>("apple", 5),
                new Tuple2<>("banana", 2),
                new Tuple2<>("apple", 4)
        );
        // 使用reduce操作,将input中的所有元素合并到一起
        DataStream<Tuple2<String, Integer>> result = dataStream
                .keyBy(0)
                .reduce((ReduceFunction<Tuple2<String, Integer>>) (value1, value2) -> new Tuple2<>(value1.f0, value1.f1 + value2.f1));
        result.print();
        env.execute();
    }

Aggregations

KeyedStream → DataStream

Aggregations代表的是一类聚合算子,上面说的reduce就属于Aggregations,以下是一些常用的:

  • sum(): 计算数字类型字段的总和。

  • min(): 计算最小值。

  • max(): 计算最大值。

  • count(): 计数元素个数。

  • avg(): 计算平均值。

另外,Flink 还支持自定义聚合函数,即使用 AggregateFunction 接口实现更复杂的聚合逻辑。

Union 真合并

DataStream → DataStream

Union of two or more data streams creating a new stream containing all the elements from all the streams

合并两个或者更多的数据流产生一个新的数据流,这个新的数据流中包含了所合并的数据流的元素

注意:需要保证数据流中元素类型一致

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Tuple2<String, Integer>> ds1 = env.fromCollection(Arrays.asList(Tuple2.of("a",1),Tuple2.of("b",2),Tuple2.of("c",3)));
        DataStream<Tuple2<String, Integer>> ds2 = env.fromCollection(Arrays.asList(Tuple2.of("d",4),Tuple2.of("e",5),Tuple2.of("f",6)));
        DataStream<Tuple2<String, Integer>> ds3 = env.fromCollection(Arrays.asList(Tuple2.of("g",7),Tuple2.of("h",8)));
        DataStream<Tuple2<String, Integer>> unionStream = ds1.union(ds2,ds3);
        unionStream.print();
        env.execute();
    }

在 Flink 中,Union 操作被称为 "真合并" 是因为它将两个或多个数据流完全融合在一起,没有特定的顺序,并且不会去除重复项。这种操作方式类似于在数学概念中的集合联合(Union)操作,所以被称为 "真合并"。

请注意,与其他一些数据处理框架中的 Union 操作相比,例如 Spark 中的 Union 会根据某些条件去除重复的元素,Flink 的 Union 行为更接近于数学上的集合联合理论。

Connect 假合并

DataStream,DataStream → ConnectedStreams

合并两个数据流并且保留两个数据流的数据类型,能够共享两个流的状态

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStream<String> ds1 = env.socketTextStream("localhost", 8888);
        DataStream<String> ds2 = env.socketTextStream("localhost", 9999);

        DataStream<Tuple2<String, Integer>> wcStream1 = ds1
                .flatMap(new Tokenizer())
                .keyBy(value -> value.f0)
                .sum(1);

        DataStream<Tuple2<String, Integer>> wcStream2 = ds2
                .flatMap(new Tokenizer())
                .keyBy(value -> value.f0)
                .sum(1);

        ConnectedStreams<Tuple2<String, Integer>, Tuple2<String, Integer>> connectedStreams = wcStream1.connect(wcStream2);
    }

union不同,connect只能连接两个流,并且这两个流的类型可以不同。connect后的两个流会被看作是两个不同的流,可以使用CoMap或者CoFlatMap函数分别处理这两个流。

CoMap, CoFlatMap

ConnectedStreams → DataStream

CoMap, CoFlatMap并不是具体算子名字,而是一类操作的名称

  • 凡是基于ConnectedStreams数据流做map遍历,这类操作叫做CoMap。

  • 凡是基于ConnectedStreams数据流做flatMap遍历,这类操作叫做CoFlatMap。

CoMap实现:

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 创建两个不同的数据流
        DataStream<Integer> nums = env.fromElements(1, 2, 3, 4, 5);
        DataStream<String> text = env.fromElements("a", "b", "c");
        // 连接两个数据流
        ConnectedStreams<Integer, String> connected = nums.connect(text);
        // 使用 CoMap 处理连接的流
        DataStream<String> result = connected.map(new CoMapFunction<Integer, String, String>() {
            @Override
            public String map1(Integer value) {
                return String.valueOf(value*2);
            }
            @Override
            public String map2(String value) {
                return "hello " + value;
            }
        });
        result.print();
        env.execute("CoMap example");
    }

CoFlatMap实现方式:

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Integer> nums = env.fromElements(1, 2, 3, 4, 5);
        DataStream<String> text = env.fromElements("a", "b", "c");
        ConnectedStreams<Integer, String> connected = nums.connect(text);
        DataStream<String> result = connected.flatMap(new CoFlatMapFunction<Integer, String, String>() {
            @Override
            public void flatMap1(Integer value, Collector<String> out) {
                out.collect(String.valueOf(value*2));
                out.collect(String.valueOf(value*3));
            }
            @Override
            public void flatMap2(String value, Collector<String> out) {
                out.collect("hello " + value);
                out.collect("hi " + value);
            }
        });
        result.print();
        env.execute("CoFlatMap example");
    }

Split/Select

DataStream → SplitStream

根据条件将一个流分成多个流,示例代码如下:

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<Long> data = env.generateSequence(0, 10);
        SplitStream<Long> split = data.split((OutputSelector<Long>) value -> {
            List<String> output = new ArrayList<>();
            if (value % 2 == 0) {
                output.add("even");
            } else {
                output.add("odd");
            }
            return output;
        });
        split.select("odd").print();
        env.execute("Flink SplitStream Example");
    }

select()用于从SplitStream中选择一个或者多个数据流。

split.select("odd").print();

SideOutput

注意:在Flink 1.12 及之后的版本中,SplitStream 已经被弃用并移除,一般推荐使用 Side Outputs(侧输出流)来替代 Split和Select

示例代码如下:

private static final OutputTag<String> evenOutput = new OutputTag<String>("even"){};
private static final OutputTag<String> oddOutput = new OutputTag<String>("odd"){};

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<String> input = env.fromElements("1", "2", "3", "4", "5");
        SingleOutputStreamOperator<String> processed = input.process(new ProcessFunction<String, String>() {
            @Override
            public void processElement(String value, Context ctx, Collector<String> out){
                int i = Integer.parseInt(value);
                if (i % 2 == 0) {
                    ctx.output(evenOutput, value);
                } else {
                    ctx.output(oddOutput, value);
                }
            }
        });
        DataStream<String> evenStream = processed.getSideOutput(evenOutput);
        DataStream<String> oddStream = processed.getSideOutput(oddOutput);
        evenStream.print("even");
        oddStream.print("odd");
        env.execute("Side Output Example");
    }

Iterate

DataStream → IterativeStream → DataStream

Iterate算子提供了对数据流迭代的支持

一个数据集通过迭代运算符被划分为两部分:“反馈”部分(feedback)和“输出”部分(output)。反馈部分被反馈到迭代头(iteration head),从而形成下一次迭代。输出部分则构成该迭代的结果:

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Long> input = env.fromElements(10L);
        // 定义迭代流,最大迭代10次
        IterativeStream<Long> iteration = input.iterate(10000L);
        // 定义迭代逻辑
        DataStream<Long> minusOne = iteration.map((MapFunction<Long, Long>) value -> value - 1);
        // 定义反馈流(满足条件继续迭代)和输出流(不满足条件的结果)
        DataStream<Long> stillGreaterThanZero = minusOne.filter(value -> value > 0).setParallelism(1);;
        DataStream<Long> lessThanZero = minusOne.filter(value -> value <= 0);
        // 关闭迭代,定义反馈流
        iteration.closeWith(stillGreaterThanZero);
        // 打印结果
        lessThanZero.print();
        env.execute("Iterative Stream Example");
    }

普通函数 & 富函数

Apache Flink 中有两种类型的函数: 「普通函数(Regular Functions)」和 「富函数(Rich Functions)」。主要区别在于富函数相比普通函数提供了更多生命周期方法和上下文信息。

  • 普通函数:这些函数只需要覆盖一个或几个特定方法,如 MapFunction 需要实现 map() 方法。它们没有生命周期方法,也不能访问执行环境的上下文。

  • 富函数:除了覆盖特定函数外,富函数还提供了对 Flink API 更多的控制和操作,包括:

    • 生命周期管理:可以覆盖 open() 和 close() 方法以便在函数启动前和关闭后做一些设置或清理工作。

    • 获取运行时上下文信息:例如,通过 getRuntimeContext() 方法获取并行任务的信息,如当前子任务的索引等。

    • 状态管理和容错:可以定义和使用托管状态(Managed State),这在构建容错系统时非常重要。

简而言之,如果你需要在函数中使用 Flink 的高级功能,如状态管理或访问运行时上下文,则需要使用富函数。如果不需要这些功能,使用普通函数即可。

普通函数类富函数类
MapFunctionRichMapFunction
FlatMapFunctionRichFlatMapFunction
FilterFunctionRichFilterFunction
............

普通函数:

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        List<String> words = Arrays.asList("hello", "world", "flink", "hello", "world");
        env.fromCollection(words)
                .map(new MapFunction<String, Tuple2<String, Integer>>() {
                    @Override
                    public Tuple2<String, Integer> map(String value) {
                        return new Tuple2<>(value, 1);
                    }
                })
                .keyBy(0)
                .sum(1)
                .print();

        env.execute("Word Count Example");
    }

富函数:

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        List<String> words = Arrays.asList("hello", "world", "flink", "hello", "world");
        env.fromCollection(words)
                .map(new RichMapFunction<String, Tuple2<String, Integer>>() {
                    @Override
                    public void open(Configuration parameters) throws Exception {
                        super.open(parameters);
                        // 可以在这里设置相关的配置或者资源,如数据库连接等
                    }
                    @Override
                    public Tuple2<String, Integer> map(String value) throws Exception {
                        return new Tuple2<>(value, 1);
                    }
                    @Override
                    public void close() throws Exception {
                        super.close();
                        // 可以在这里完成资源的清理工作
                    }
                })
                .keyBy(0)
                .sum(1)
                .print();
        env.execute("Word Count Example");
    }

ProcessFunction(处理函数)

ProcessFunction属于低层次的API,在类继承关系上属于富函数。

我们前面讲的mapfilterflatMap等算子都是基于这层封装出来的。

越低层次的API,功能越强大,用户能够获取的信息越多,比如可以拿到元素状态信息、事件时间、设置定时器等

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<String> dataStream = env.socketTextStream("localhost", 9999)
                .map(new MapFunction<String, Tuple2<String, Integer>>() {
                    @Override
                    public Tuple2<String, Integer> map(String value) {
                        return new Tuple2<>(value, 1);
                    }
                })
                .keyBy(0)
                .process(new AlertFunction());
        dataStream.print();
        env.execute("Process Function Example");
    }

    public static class AlertFunction extends KeyedProcessFunction<Tuple, Tuple2<String, Integer>, String> {
        private transient ValueState<Integer> countState;
        @Override
        public void open(Configuration config) {
            ValueStateDescriptor<Integer> descriptor =
                    new ValueStateDescriptor<>(
                            "countState", // state name
                            TypeInformation.of(new TypeHint<Integer>() {}), // type information
                            0); // default value
            countState = getRuntimeContext().getState(descriptor);
        }
        @Override
        public void processElement(Tuple2<String, Integer> value, Context ctx, Collector<String> out) throws Exception {
            Integer currentCount = countState.value();
            currentCount += 1;
            countState.update(currentCount);
            if (currentCount >= 3) {
                out.collect("Warning! The key '" + value.f0 + "' has been seen " + currentCount + " times.");
            }
        }
    }

这里,我们创建一个名为AlertFunction的处理函数类,并继承KeyedProcessFunction。其中,ValueState用于保存状态信息,每个键会有其自己的状态实例。当计数达到或超过三次时,该系统将发出警告。这个例子主要展示了处理函数与其他运算符相比的两个优点:访问键控状态和生命周期管理方法(例如open())。

注意:上述示例假设你已经在本地的9999端口上设置了一个socket服务器,用于流式传输文本数据。如果没有,你需要替换这部分以适应你的输入源。

Sink

在Flink中,"Sink"是数据流计算的最后一步。它代表了一个输出端点,在那里计算结果被发送或存储。换句话说,Sink是数据流处理过程中的结束节点,负责将处理后的数据输出到外部系统,如数据库、文件、消息队列等。

Flink内置了大量Sink,可以将Flink处理后的数据输出到HDFS、kafka、Redis、ES、MySQL等。

Redis Sink

Flink处理的数据可以存储到Redis中,以便实时查询。

首先,需要导入Flink和Redis的连接器依赖:

<!-- Flink Redis connector -->
        <dependency>
            <groupId>org.apache.bahir</groupId>
            <artifactId>flink-connector-redis_${scala.binary.version}</artifactId>
            <version>1.1.0</version>
        </dependency>

下面的代码展示了"Word Count"(词频统计)操作,并将结果存储到Redis数据库中:

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<String> text = env.fromElements(
                "Hello World",
                "Hello Flink",
                "Hello Java");
        DataStream<Tuple2<String, Integer>> counts =
                text.flatMap(new Tokenizer())
                        .keyBy(value -> value.f0)
                        .sum(1);
        FlinkJedisPoolConfig conf = new FlinkJedisPoolConfig.Builder().setHost("localhost").build();
        counts.addSink(new RedisSink<>(conf, new RedisExampleMapper()));
        env.execute("Word Count Example");
    }

    public static final class Tokenizer implements FlatMapFunction<String, Tuple2<String, Integer>> {
        @Override
        public void flatMap(String value, Collector<Tuple2<String, Integer>> out) {
            String[] words = value.toLowerCase().split("\\W+");

            for (String word : words) {
                if (word.length() > 0) {
                    out.collect(new Tuple2<>(word, 1));
                }
            }
        }
    }

    public static final class RedisExampleMapper implements RedisMapper<Tuple2<String, Integer>> {
        @Override
        public RedisCommandDescription getCommandDescription() {
            return new RedisCommandDescription(RedisCommand.HSET);
        }
        @Override
        public String getKeyFromData(Tuple2<String, Integer> data) {
            return data.f0;
        }
        @Override
        public String getValueFromData(Tuple2<String, Integer> data) {
            return data.f1.toString();
        }
    }

Kafka Sink

处理结果写入到kafka topic中,Flink也是支持的,需要添加连接器依赖,跟读取kafka数据用的连接器依赖相同,之前添加过就不需要再添加了。

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka_2.12</artifactId>
    <version>1.13.6</version>
</dependency>

还是用上面词频统计的例子:

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<String> text = env.fromElements(
                "Hello World",
                "Hello Flink",
                "Hello Java");
        DataStream<Tuple2<String, Integer>> counts =
                text.flatMap(new Tokenizer())
                        .keyBy(value -> value.f0)
                        .sum(1);
        // Define Kafka properties
        Properties properties = new Properties();
        properties.setProperty("bootstrap.servers", "localhost:9092");
        // Write the data stream to Kafka
        counts.map(new MapFunction<Tuple2<String,Integer>, String>() {
                    @Override
                    public String map(Tuple2<String,Integer> value) throws Exception {
                        return value.f0 + "," + value.f1.toString();
                    }
                })
                .addSink(new FlinkKafkaProducer<>("my-topic", new SimpleStringSchema(), properties));
        env.execute("Word Count Example");
    }

MySQL Sink

Flink处理结果写入到MySQL中,这并不是Flink默认支持的,需要添加MySQL的驱动依赖:

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>

因为不是内嵌支持的,所以需要基于SinkFunction自定义Sink。

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<String> text = env.fromElements(
                "Hello World",
                "Hello Flink",
                "Hello Java");
        DataStream<Tuple2<String, Integer>> counts =
                text.flatMap(new Tokenizer())
                        .keyBy(value -> value.f0)
                        .sum(1);
        // Transform the Tuple2<String, Integer> to a format acceptable by MySQL
        DataStream<String> mysqlData = counts.map(new MapFunction<Tuple2<String, Integer>, String>() {
            @Override
            public String map(Tuple2<String, Integer> value) throws Exception {
                return "'" + value.f0 + "'," + value.f1.toString();
            }
        });
        // Write the data stream to MySQL
        mysqlData.addSink(new MySqlSink());
        env.execute("Word Count Example");
    }

    public static class MySqlSink implements SinkFunction<String> {
        private Connection connection;
        private PreparedStatement preparedStatement;

        @Override
        public void invoke(String value, Context context) throws Exception {
            if(connection == null) {
                connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password");
                preparedStatement = connection.prepareStatement("INSERT INTO my_table(word, count) VALUES("+ value +")");
            }
            preparedStatement.executeUpdate();
        }
    }
}

HBase Sink

需要导入HBase的依赖:

        <!-- https://mvnrepository.com/artifact/org.apache.hbase/hbase-client -->
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-client</artifactId>
            <version>2.5.2</version>
        </dependency>
public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<String> text = env.fromElements(
                "Hello World",
                "Hello Flink",
                "Hello Java");
        DataStream<Tuple2<String, Integer>> counts =
                text.flatMap(new Tokenizer())
                        .keyBy(value -> value.f0)
                        .sum(1);
        counts.addSink(new HBaseSink());
        env.execute("Word Count Example");
    }

    public static final class Tokenizer implements FlatMapFunction<String, Tuple2<String, Integer>> {
        @Override
        public void flatMap(String value, Collector<Tuple2<String, Integer>> out) {
            String[] words = value.toLowerCase().split("\\W+");
            for (String word : words) {
                if (word.length() > 0) {
                    out.collect(new Tuple2<>(word, 1));
                }
            }
        }
    }

    public static class HBaseSink extends RichSinkFunction<Tuple2<String, Integer>> {
        private org.apache.hadoop.conf.Configuration config;
        private org.apache.hadoop.hbase.client.Connection connection;
        private Table table;
        @Override
        public void invoke(Tuple2<String, Integer> value, Context context) throws IOException {
            Put put = new Put(Bytes.toBytes(value.f0));
            put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("count"), Bytes.toBytes(value.f1));
            table.put(put);
        }

        @Override
        public void open(Configuration parameters) throws Exception {
            config = HBaseConfiguration.create();
            config.set("hbase.zookeeper.quorum", "localhost");
            config.set("hbase.zookeeper.property.clientPort", "2181");
            connection = ConnectionFactory.createConnection(config);
            table = connection.getTable(TableName.valueOf("my-table"));
        }

        @Override
        public void close() throws Exception {
            table.close();
            connection.close();
        }
    }

HBaseSink类是RichSinkFunction的实现,用于将结果写入HBase数据库。在invoke方法中,它将接收到的每个二元组(单词和计数)写入HBase。在open方法中,它创建了与HBase的连接,并指定了要写入的表。在close方法中,它关闭了与HBase的连接和表。

分区策略

在 Apache Flink 中,分区(Partitioning)是将数据流按照一定的规则划分成多个子数据流或分片,以便在不同的并行任务或算子中并行处理数据。分区是实现并行计算和数据流处理的基础机制。Flink 的分区决定了数据在作业中的流动方式,以及在并行任务之间如何分配和处理数据。

在 Flink 中,数据流可以看作是一个有向图,图中的节点代表算子(Operators),边代表数据流(Data Streams)。数据从源算子流向下游算子,这些算子可能并行地处理输入数据,而分区就是决定数据如何从一个算子传递到另一个算子的机制。

下面介绍Flink中常用的几种分区策略。

shuffle

场景:增大分区、提高并行度,解决数据倾斜。

DataStream → DataStream

分区元素随机均匀分发到下游分区,网络开销比较大

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Long> stream = env.fromSequence(1, 10).setParallelism(1);
        System.out.println(stream.getParallelism());
        stream.shuffle().print();
        env.execute();
    }

输出结果:上游数据比较随意地分发到下游

1> 7
7> 1
2> 8
4> 5
8> 3
1> 9
8> 4
8> 10
6> 2
6> 6

rebalance

场景:增大分区、提高并行度,解决数据倾斜

DataStream → DataStream

轮询分区元素,均匀的将元素分发到下游分区,下游每个分区的数据比较均匀,在发生数据倾斜时非常有用,网络开销比较大

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Long> stream = env.fromSequence(1, 10).setParallelism(1);
        System.out.println(stream.getParallelism());
        stream.rebalance().print();
        env.execute();
    }

输出:上游数据比较均匀的分发到下游

2> 2
1> 1
8> 8
5> 5
7> 7
4> 4
3> 3
6> 6
1> 9
2> 10

rescale

场景:减少分区,防止发生大量的网络传输,不会发生全量的重分区

DataStream → DataStream

通过轮询分区元素,将一个元素集合从上游分区发送给下游分区,发送单位是集合,而不是一个个元素

和其他重分区策略(如 rebalance、forward、broadcast 等)不同的是,rescale 在运行时不会改变并行度,而且它只在本地(同一个 TaskManager 内)进行数据交换,所以它比其他重分区策略更加高效

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStream<String> dataStream = env.fromElements("1", "2", "3", "4", "5");

        // 使用MapFunction将元素转换为整数类型
        DataStream<Integer> intStream = dataStream.map(new MapFunction<String, Integer>() {
            @Override
            public Integer map(String value) {
                return Integer.parseInt(value);
            }
        });
        // 使用rescale()进行重分区
        DataStream<Integer> rescaledStream = intStream.rescale();
        rescaledStream.print();
        env.execute("Rescale Example");
    }

在这个例子中,我们创建了一个字符串类型的DataStream然后通过map()将每一个元素转换为整数。然后,我们对结果DataStream应用rescale()操作来重分区数据。

值得注意的是,rescale()的实际影响取决于你的并行度和集群环境,如果不同的并行实例都在同一台机器上,或者并行度只有1,那么可能不会看到rescale()的效果。而在大规模并行处理的情况下,使用rescale()操作可以提高数据处理的效率。

此外,我们不能直接在打印结果中看到rescale的影响,因为它改变的是内部数据分布和处理方式,而不是输出的结果。如果想观察rescale的作用,需要通过Flink的Web UI或者日志来查看任务执行情况,如数据流的分布、各个子任务的运行状态等信息。

broadcast

场景:需要使用映射表、并且映射表会经常发生变动的场景

DataStream → DataStream

上游中每一个元素内容广播到下游每一个分区中

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Integer> dataStream = env.fromElements(1, 2, 3, 4, 5);
        DataStream<String> broadcastStream = env.fromElements("2", "4");
        MapStateDescriptor<String, String> descriptor = new MapStateDescriptor<>(
                "RulesBroadcastState",
                BasicTypeInfo.STRING_TYPE_INFO,
                BasicTypeInfo.STRING_TYPE_INFO);
        BroadcastStream<String> broadcastData = broadcastStream.broadcast(descriptor);
        dataStream.connect(broadcastData)
                .process(new BroadcastProcessFunction<Integer, String, String>() {
                    @Override
                    public void processElement(Integer value, ReadOnlyContext ctx, Collector<String> out) throws Exception {
                        if (ctx.getBroadcastState(descriptor).contains(String.valueOf(value))) {
                            out.collect("Value " + value + " matches with a broadcasted rule");
                        }
                    }
                    @Override
                    public void processBroadcastElement(String rule, Context ctx, Collector<String> out) throws Exception {
                        ctx.getBroadcastState(descriptor).put(rule, rule);
                    }
                }).print();
        env.execute("Broadcast State Example");
    }

上述代码首先定义了一个主流和一个要广播的流。然后,我们创建了一个MapStateDescriptor,用于存储广播数据。接着,我们将广播流转换为BroadcastStream

最后,我们使用connect()方法连接主流和广播流,并执行process()方法。在这个process()方法中,我们定义了两个处理函数:processElement()processBroadcastElement()processElement()用于处理主流中的每个元素,并检查该元素是否存在于广播状态中。如果是,则输出一个字符串,表明匹配成功。而processBroadcastElement()则用于处理广播流中的每个元素,并将其添加到广播状态中。

注意:在分布式计算环境中,每个并行实例都会接收广播流中的所有元素。因此,广播状态对于所有的并行实例都是一样的。不过,在Flink 1.13版本中,广播状态尚未在故障恢复中提供完全的保障。所以在事件出现故障时,广播状态可能会丢失数据。

global

场景:并行度降为1

DataStream → DataStream

在 Apache Flink 中,Global 分区策略意味着所有数据都被发送到下游算子的同一个分区中。这种情况下,下游算子只有一个任务处理全部数据。这是一种特殊的分区策略,只有在下游算子能够很快地处理所有数据,或者需要全局排序或全局聚合时才会使用。

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        // 创建一个从1到100的数字流
        DataStream<Long> numberStream = env.fromSequence(1, 100);
        // 对流应用 map function
        DataStream<Long> result = numberStream.global()
                .map(new MapFunction<Long, Long>() {
                    @Override
                    public Long map(Long value) {
                        System.out.println("Processing " + value);
                        return value * 2;
                    }
                });
        result.print();
        env.execute("Global Partition Example");
    }

以上代码创建了一个顺序生成 1-100 的数字流,并应用了 Global Partition,然后对每个数字进行乘2的操作。实际运行此代码时,你会观察到所有的数字都由同一任务处理,打印出来的处理顺序是连续的。这就是 Global Partition 的作用:所有数据都被发送到下游算子的同一实例进行处理。

需要注意的是,此示例只是为了演示 Global Partition 的工作原理,实际上并不推荐在负载均衡很重要的应用场景中使用这种分区策略,因为它可能导致严重的性能问题。

forward

场景:一对一的数据分发,默认的分区策略,数据在各个算子之间不会重新分配。map、flatMap、filter 等都是这种分区策略

DataStream → DataStream

上游分区数据分发到下游对应分区中

partition1->partition1;partition2->partition2

注意:必须保证上下游分区数(并行度)一致,不然会有如下异常:

Forward partitioning does not allow change of parallelism. Upstream operation: Source: Socket Stream-1 parallelism: 1, downstream operation: Map-3 parallelism: 8 You must use another partitioning strategy, such as broadcast, rebalance, shuffle or global.
public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Integer> dataStream = env.fromElements(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).setParallelism(1);
        DataStream<Integer> forwardStream = dataStream.forward().map(new MapFunction<Integer, Integer>() {
            @Override
            public Integer map(Integer value) throws Exception {
                return value * value;
            }
        }).setParallelism(1);
        forwardStream.print();
        env.execute("Flink Forward Example");
    }

此代码首先创建一个从1到10的数据流。然后,它使用 Forward 策略将这个数据流送入一个 MapFunction 中,该函数将每个数字平方。然后,它打印出结果。注意:以上代码中的forward调用实际上并没有改变任何分区策略,因为forward是默认分区策略。这里添加forward调用主要是为了说明其存在和使用方法。

keyBy

场景:与业务场景匹配

DataStream → DataStream

根据上游分区元素的Hash值与下游分区数取模计算出,将当前元素分发到下游哪一个分区

MathUtils.murmurHash(keyHash)(每个元素的Hash值) % maxParallelism(下游分区数)
public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Tuple2<Integer, Integer>> dataStream = env.fromElements(
                new Tuple2<>(1, 3),
                new Tuple2<>(1, 5),
                new Tuple2<>(2, 4),
                new Tuple2<>(2, 6),
                new Tuple2<>(3, 7)
        );
        // 使用 keyBy 对流进行分区操作
        DataStream<Tuple2<Integer, Integer>> keyedStream = dataStream
                .keyBy(0) // 根据元组的第一个字段进行分区
                .sum(1);  // 对每个键对应的第二个字段求和
        keyedStream.print();
        env.execute("KeyBy example");
    }

以上程序首先创建了一个包含五个元组的流,然后使用 keyBy 方法根据元组的第一个字段进行分区,并对每个键对应的第二个字段求和。执行结果中,每个键的值集合都被映射成了一个新的元组,其第一个字段是键,第二个字段是相应的和。

注意:在以上代码中,keyBy(0) 表示根据元组的第一个字段(索引从0开始)进行分区操作。另外,无论什么情况,都需要确保你的 Flink 集群是正常运行的,否则程序可能无法执行成功。

PartitionCustom

DataStream → DataStream

通过自定义的分区器,来决定元素是如何从上游分区分发到下游分区

public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStream<Integer> data = env.fromElements(1,2,3,4,5,6,7,8,9,10);
        // 使用自定义分区器进行分区
        data.partitionCustom(new MyPartitioner(), i -> i).print();
        env.execute("Custom partition example");
    }

    public static class MyPartitioner implements Partitioner<Integer> {
        @Override
        public int partition(Integer key, int numPartitions) {
            return key % numPartitions;
        }
    }

这个程序将创建一个数据流,其中包含从1到10的整数。然后,它使用了一个自定义的分区器MyPartitioner来对这个数据流进行分区。这个分区器根据元素的值对numPartitions取模来决定数据去到哪个分区。

  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值