Flink- 物理分区、Sink输出

本文详细介绍了ApacheFlink中的数据流处理物理分区策略,包括随机分区、轮询分区、重缩放分区、广播和全局分区,以及自定义分区的实现方式。同时,文章也讨论了Flink的数据输出算子,如连接到外部系统、输出到文件、Kafka、Redis和MySQL等,展示了如何将处理后的数据写入这些系统。

物理分区

        随机分区(shuffle)

        轮询分区(Round-Robin) 

        重缩放分区(rescale) 

        广播(broadcast)

        全局分区(global)

        自定义分区(Custom)

输出算子(Sink)

        连接到外部系统

        输出到文件

        输出到 Kafka

        输出到 Redis

        输出到 MySQL(JDBC)


物理分区

        “分区”(partitioning)操作就是要将数据进行重新分布,传递到不同的流分区去进行下一 步计算。keyBy()是一种逻辑分区(logical partitioning)操作。 Flink 对于经过转换操作之后的 DataStream,提供了一系列的底层操作算子,能够帮我们 实现数据流的手动重分区。为了同 keyBy()相区别,我们把这些操作统称为“物理分区”操作。

        常见的物理分区策略有随机分区、轮询分区、重缩放和广播,还有一种特殊的分区策略— —全局分区,并且 Flink 还支持用户自定义分区策略。

        随机分区(shuffle)

        最简单的重分区方式就是直接“洗牌”。通过调用 DataStream 的 shuffle()方法,将数据随 机地分配到下游算子的并行任务中去。

        创建一个数据流类作为数据源 

class f4 extends SourceFunction[ Event ]{ //实现SourceFunction接口 泛型为之前定义好的样例类Event
  //标志位
  var running = true
  //重写抽象方法
  override def run(sourceContext: SourceFunction.SourceContext[Event]): Unit = {
    //随机数生成器
    val random = new Random ()
    //定义数据随机选择的范围
    val user = Array ("张三", "李四", "王五")
    val url = Array ("02", "01", "03", "04")
    //用标志位作为循环判断条件,不停的发出数据
    while (running) {
      val event = Event (user (random.nextInt (user.length) ), url (random.nextInt (url.length) ), Calendar.getInstance.getTimeInMillis)
      //调用ctx的方法向下游发送数据
      sourceContext.collect (event)
      //每隔1秒发送一条数据
      Thread.sleep (1000)
    }
  }

  override def cancel(): Unit = running = false
}

        进行随机分区操作

def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //设置全局并行度
    env.setParallelism(1)
    //读取数据流文件
    val stream: DataStream[Event] = env.addSource( new f4)

    //洗牌之后打印输出
    stream.shuffle.print().setParallelism(4) //并行度设置为4

    //执行
    env.execute()
  }

        分区结果基本都是均匀随机的 

 

        轮询分区(Round-Robin) 

         轮询也是一种常见的重分区方式。简单来说就是“发牌”,按照先后顺序将数据做依次分发。通过调用 DataStream的.rebalance()方法,就可以实现轮询重分区。rebalance() 使用的是 Round-Robin 负载均衡算法,可以将输入流数据平均分配到下游的并行任务中去 

def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //设置全局并行度
    env.setParallelism(1)
    //读取数据流文件
    val stream: DataStream[Event] = env.addSource( new f4)

    //轮询重分区之后打印输出
    stream.rebalance.print().setParallelism(4) //并行度设置为4

    //执行
    env.execute()
  }

        按照规律顺序的轮询分区输出

        重缩放分区(rescale) 

 重缩放分区和轮询分区非常相似。当调用 rescale()方法时,其实底层也是使用 Round-Robin算法进行轮询,但是只会将数据轮询发送到下游并行任务的一部分中。也就 是说,“发牌人”如果有多个,那么 rebalance()的方式是每个发牌人都面向所有人发牌;而 rescale()的做法是分成小团体,发牌人只给自己团体内的所有人轮流发牌。

def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //设置全局并行度
    env.setParallelism(1)
    //读取数据流文件
    val stream: DataStream[Int] = env.addSource(new RichParallelSourceFunction[Int] { //定义一个并行的数据源
      override def run(ctx: SourceFunction.SourceContext[Int]): Unit = {
        for (i <- 0 to 10) {
          //利用运行时上下文中的subTask的信息来控制数据由哪个并行子任务生成
          if( getRuntimeContext.getIndexOfThisSubtask == (i + 1) % 2 )
            ctx.collect(i + 1)
        }
      }

      override def cancel(): Unit = ???
    })setParallelism(2) //并行度设置为2

    //分区之后打印输出
    stream.rescale.print().setParallelism(4) //并行度设置为4

    //执行
    env.execute()
  }

        广播(broadcast)

        这种方式其实不应该叫作“重分区”,因为经过广播之后,数据会在不同的分区都保留一 份,可能进行重复处理。可以通过调用 DataStream 的 broadcast()方法,将输入数据复制并发送 到下游算子的所有并行任务中去。

def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //设置全局并行度
    env.setParallelism(1)
    //读取数据流文件
    val stream: DataStream[Event] = env.addSource( new f4)

    //分区之后打印输出
    stream.broadcast.print().setParallelism(4) //并行度设置为4

    //执行
    env.execute()
  }

        全局分区(global)

        全局分区也是一种特殊的分区方式。这种做法非常极端,通过调用.global()方法,会将所 有的输入流数据都发送到下游算子的第一个并行子任务中去。这就相当于强行让下游任务并行 度变成了 1,所以使用这个操作需要非常谨慎,可能对程序造成很大的压力。

 def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //设置全局并行度
    env.setParallelism(1)
    //读取数据流文件
    val stream: DataStream[Event] = env.addSource( new f4)

    //分区之后打印输出
    stream.global.print().setParallelism(4) //并行度设置为4

    //执行
    env.execute()
  }

        执行后全部被划分到一个分区中

        自定义分区(Custom)

        当Flink提供的所有分区策略都不能满足用户的需求时,我们可以通过使用partitionCustom()方法来自定义分区策略。 在调用时,方法需要传入两个参数,第一个是自定义分区器(Partitioner)对象,第二个 是应用分区器的字段,它的指定方式与 keyBy 指定 key 基本一样:可以通过字段名称指定, 也可以通过字段位置索引来指定,还可以实现一个 KeySelector 接口。

def main(args: Array[String]): Unit = {
 val env = StreamExecutionEnvironment.getExecutionEnvironment
 env
 .fromElements(1,2,3,4,5,6,7,8)
 .partitionCustom(
 new Partitioner[Int] {
 // 根据 key 的奇偶性计算出数据将被发送到哪个分区
 override def partition(key: Int, numPartitions: Int): Int = key % 2
 },
 data => data // 以自身作为 key
 )
 .print()
 env.execute()
 }

输出算子(Sink)

        连接到外部系统

        与 Source 算子非常类似,除去一些 Flink 预实现的 Sink,一般情况下 Sink 算子的创建是 通过调用 DataStream 的 addSink()方法实现的。

stream.addSink(new SinkFunction(…))

        addSource 的参数需要实现一个 SourceFunction 接口;类似地,addSink 方法同样需要传入 一个参数,实现的是 SinkFunction 接口。在这个接口中只需要重写一个方法 invoke(),用来将指 定的值写入到外部系统中。这个方法在每条数据记录到来时都会调用:

default void invoke(IN value, Context context) throws Exception

        输出到文件

case class Event(user: String, url: String, timestamp: Long)
 def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //设置全局并行度
    env.setParallelism(1)
    //创建当前样例类Event的对象

    val stream: DataStream[Event] = env.fromElements(
      Event("张三", "01", 1000L),
      Event("李四", "04", 2000L),
      Event("王五", "01", 6000L),
      Event("赵六", "03", 1000L)
    )

    //以文本形式分布式的写入文件中
      //定义fileSink
    val fileSink: StreamingFileSink[String] = StreamingFileSink
      .forRowFormat(new Path("D:\\Flink\\datas"), new SimpleStringEncoder[String]("UTF-8"))
      .build()

      //转换为String              StreamingFileSink.forRowFormat("路径","编码器")
    stream.map(_.toString).addSink(fileSink)

    //执行
    env.execute()
  }

        输出到 Kafka

        Flink 官方为 Kafka 提供了 Source 和 Sink 的连接器,我们可以用它方便地从 Kafka 读写数 据。Flink 与 Kafka 的连接器提供了端到端的精确一次(exactly once)语义保证,这在实际项 目中是最高级别的一致性保证。

配置步骤:

  1. 添加 Kafka 连接器依赖 。
  2. 启动 Kafka 集群
  3. 编写输出到 Kafka 的示例代码

        我们可以直接将用户行为数据保存为文件 clicks.csv,读取后不做转换直接写入 Kafka,主 题(topic)命名为“clicks”。 

def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //设置全局并行度
    env.setParallelism(1)
    
    //将数据写入到kafa
        //创建连接配置
    val properties = new Properties()
    properties.put("bootstrap.servers", "hadoop102:9092")
    val stream = env.readTextFile("input/clicks.csv")

      stream.addSink(new FlinkKafkaProducer[String](
        "clicks",
        new SimpleStringSchema(),
        properties
      ))
    //执行
    env.execute()
  }

        我们可以直接将用户行为数据保存为文件 clicks.csv,读取后不做转换直接写入 Kafka,主 题(topic)命名为“clicks”。

bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic clicks

        输出到 Redis

        Flink 没有直接提供官方的 Redis 连接器,不过 Bahir 项目还是担任了合格的辅助角色,为 我们提供了 Flink-Redis 的连接工具。但版本升级略显滞后,目前连接器版本为 1.1,支持的 Scala 版本最新到 2.11。

导入的 Redis 连接器依赖

<dependency>
 <groupId>org.apache.bahir</groupId>
 <artifactId>flink-connector-redis_2.11</artifactId>
 <version>1.0</version>
</dependency>
 def main(args: Array[String]): Unit = {
 val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 //创建连接配置
 val conf = new FlinkJedisPoolConfig.Builder().setHost("hadoop102").build()
 env.addSource(new ClickSource)
 .addSink(new RedisSink[Event](conf, new MyRedisMapper())) //写入
 env.execute()
 }

        输出到 MySQL(JDBC)

        添加依赖

<dependency>
 <groupId>org.apache.flink</groupId>
 <artifactId>flink-connector-jdbc_${scala.binary.version}</artifactId>
 <version>${flink.version}</version>
</dependency>
<dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <version>5.1.47</version>
</dependency>

        准备数据表 

        写入mysql 

def main(args: Array[String]): Unit = {
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //设置全局并行度
    env.setParallelism(1)
    //将数据写入到mysql
    val stream: DataStream[Event] = env.fromElements(
      Event(4, "aa", 18),
      Event(5, "bb", 23),
      Event(6, "cc", 60),
      Event(7, "dd",15)
    )
      //JdbcSink.sink(需要写入的sql语句,添加的元素,连接jdbc的配置)
    stream.addSink( JdbcSink.sink(
      "INSERT INTO user(id,name,age) VALUES(?,?,?)", //定义写入mysql的语句
      new JdbcStatementBuilder[Event] {
        override def accept(t: PreparedStatement, u: Event): Unit = {
          //setInt(索引位置,元素)
          t.setInt(1,u.id)
          t.setString(2,u.name)
          t.setInt(3,u.age)
        }
      },
      //创建JDBC连接的配置项
      new JdbcConnectionOptions
        .JdbcConnectionOptionsBuilder()
        .withUrl("jdbc:mysql://master:3306/spark-sql")
        .withDriverName("com.mysql.jdbc.Driver") //添加驱动类
        .withUsername("root") //指定用户名
        .withPassword("p@ssw0rd") //指定密码
        .build()
    ))

    //执行
    env.execute()
  }

         如果运行时报如下错误:

 Wed Mar 22 20:36:28 CST 2023 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.

        警告:不建议在没有服务器身份验证的情况下建立SSL连接。根据MySQL 5.5.45+、5.6.26+和5.7.6+的要求,如果没有设置显式选项,默认必须建立SSL连接。为了符合不使用SSL的现有应用程序,verifyServerCertificate属性被设置为'false'。您需要通过设置useSSL=false显式禁用SSL,或者设置useSSL=true并为服务器证书验证提供信任存储区。

        hive中conf目录下的hive-site.xml中在mysql连接字符串的url中添加配置 ?useSSL=false即可

useSSL=false              //禁用SSL
useServerPrepStmts=true   //开启预编译功能

         到数据表中查看添加成功

<think>嗯,用户现在想了解Flink中如何实现精确的流数据分区控制。结合之前的对话历史,用户已经对Flink流处理程序的基本步骤有了一定了解,现在深入到更具体的分区控制层面。从引用内容来看,[^2]提到Flink将批流统一抽象为有界流和无界流,这暗示分区控制需要适应不同数据特性。[^3][^4]强调了状态管理和检查点机制对一致性的重要性,这对分区控制同样关键。用户可能正在设计一个需要精确数据分布的流处理系统,比如需要保证相同key的数据绝对落在同一分区,或者需要自定义分区策略满足业务需求。ta的深层需求应该是确保数据处理过程中数据分布的准确性和可控性。在技术层面,Flink实现精确分区控制主要依赖三个维度:首先是基础分区策略,包括keyBy这种基于哈希的硬性分区,能保证相同key的数据物理聚集。但用户可能需要更灵活的控制,比如轮询或广播,这就要用到partitionCustom方法。其次是状态本地化问题,[^3]提到状态存储靠近计算发生的地方,这意味着分区策略直接影响状态访问效率。如果分区不当,跨节点状态访问会拖慢整个流程。最后是端到端一致性,[^4]提到检查点和sink端事务写入的配合。分区控制必须与检查点机制协同工作,在故障恢复时保持分区状态一致性,否则重新分区可能导致数据错位。用户可能没意识到但实际需要关注的点包括:自定义分区函数的性能影响(比如避免复杂计算)、并行度变更导致的分区重组问题、以及与sink分区能力的匹配(如Kafka分区数对齐)。需要特别强调keyBy和partitionCustom的区别:前者是逻辑分组(可能跨物理分区),后者是物理分区重分布。这在调优时很关键,比如reduce操作只需要keyBy,而自定义输出到特定分区就需要partitionCustom。</think>在 Flink 中实现精确的流数据分区控制,需结合分区策略、状态管理和容错机制协同工作。以下是核心方法与配置: --- ### 一、**基础分区策略** 1. **KeyBy 逻辑分区** ```java DataStream<T> keyedStream = stream.keyBy(keySelector); ``` - **原理**:根据 Key 的哈希值分配数据到下游算子子任务 - **精确性保障**:相同 Key 的数据**必然分配到同一分区** - **限制**:仅逻辑分组,不改变物理分区位置[^1] 2. **物理分区** ```java // 随机均匀分布 stream.shuffle(); // 轮询分发(避免数据倾斜) stream.rebalance(); // 指定分区器(完全自定义控制) stream.partitionCustom(partitioner, "fieldName"); ``` - **自定义分区器**:实现`Partitioner<T>`接口 ```java public class UserIdPartitioner implements Partitioner<String> { @Override public int partition(String key, int numPartitions) { // 按用户ID尾号分配分区 return Integer.parseInt(key.substring(key.length()-1)) % numPartitions; } } ``` --- ### 二、**状态本地化与分区一致性** 1. **Keyed State 绑定分区** - 使用 `KeyBy` 后,状态自动与 Key 绑定 - **状态访问优化**:状态存储与处理数据的 TaskManager **物理同节点**,避免跨节点网络开销[^3] - **代码示例**: ```java keyedStream.map(new RichMapFunction<...>() { private ValueState<Integer> state; @Override public void open(Configuration config) { state = getRuntimeContext().getState(new ValueStateDescriptor<>("state", Integer.class)); } }); ``` 2. **Operator State 控制** - 适用场景:Kafka Source 分区分配、广播状态 - **精确分配**:实现`CheckpointedFunction`手动控制状态分区[^4] ```java public class ControlledPartitionOperator extends RichMapFunction<...> implements CheckpointedFunction { private ListState<String> partitionState; @Override public void snapshotState(FunctionSnapshotContext context) { // 持久化分区状态 } @Override public void initializeState(FunctionInitializationContext context) { // 故障恢复时加载分区状态 } } ``` --- ### 三、**端到端精确一次语义配置** 1. **检查点机制** ```java StreamExecutionEnvironment env = ...; // 启用检查点(每5秒,精确一次) env.enableCheckpointing(5000, CheckpointingMode.EXACTLY_ONCE); // 对齐检查点(保障状态一致性) env.getCheckpointConfig().setAlignmentTimeout(Duration.ofSeconds(10)); ``` - **作用**:故障恢复时**精确恢复分区状态**[^3] 2. **Sink分区控制** - Kafka Sink 分区绑定: ```java KafkaSink<String> sink = KafkaSink.<String>builder() .setDeliverGuarantee(DeliveryGuarantee.EXACTLY_ONCE) .setTransactionalIdPrefix("txn-") // 事务ID前缀 .build(); ``` - **关键配置**: - `transactional.id`:确保分区级事务提交 - `enable.idempotence=true`:生产者幂等性[^4] --- ### 四、**高级调优技巧** 1. **并行度与分区数对齐** - Kafka Topic 分区数 = Flink Source 并行度 - 避免 `rebalance()` 导致的数据倾斜 2. **自定义分区器性能优化** - 分区函数避免复杂计算(影响吞吐量) - 使用 `open()` 初始化资源(如数据库连接池) 3. **动态扩缩容处理** ```java env.setParallelism(4); // 初始并行度 ``` - 启用 `Rescaling` 模式: ```bash flink run -p 8 ... # 运行时调整并行度 ``` - Flink 自动重组 Key 到新分区(需状态后端支持) --- ### 分区控制核心机制对比 | **方法** | 适用场景 | 精确性保障 | 性能影响 | |------------------|----------------------------------|--------------------------|---------------| | `keyBy()` | 按Key聚合/状态计算 | ⭐⭐⭐⭐ (强一致性) | 中等(需序列化)| | `partitionCustom`| 物理分区控制(如地理分片) | ⭐⭐⭐⭐ (自定义逻辑) | 取决于分区函数 | | `rebalance()` | 负载均衡 | ⭐⭐ (无状态操作) | 低 | | 事务性Sink | 端到端精确输出 | ⭐⭐⭐⭐⭐ (EXACTLY_ONCE) | 较高(事务开销)| > **关键实践**: > - 有状态计算必须用 `keyBy()` 绑定分区[^3] > - 物理分区调整需配合并行度优化[^1] > - 检查点间隔需平衡延迟与容错开销(建议1~10秒)[^4]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

open_test01

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

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

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

打赏作者

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

抵扣说明:

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

余额充值