ITPub博客

首页 > 数据库 > 数据库开发技术 > 一个Flink写Doris丢数据的场景

一个Flink写Doris丢数据的场景

原创 数据库开发技术 作者:安瑞哥是码农 时间:2023-09-11 21:30:59 0 删除 编辑


周末晚上直播的时候,演示了spark和flink面对kafka数据实时写HDFS的场景时(破站有直播回放),这两个计算引擎各自checkpoint的不同点对比,以及各自在恢复数据时的用法。


但是没想到,在演示到用Flink的checkpoint恢复数据时,翻车了,而且,翻的特别彻底。


上篇文章聊了,针对这个具体的案例,如果想要用Flink的checkpoint,来恢复程序停止期间向kafka写入的数据,除了要以恢复模式启动Flink任务外(启动命令中加入checkpoint的地址),还要确保当前的数据消费模式为“至少一次性消费”语义。


然而,本以为所有过程可以一切顺利地再来一遍的时候,却发现,当初的场景却再也复现不了,也就是无论我怎么折腾,Flink任务被kill期间,往kafka写入的这个数据,神奇般消失了。


在直播间的小伙伴看到这里,也都表示一脸懵逼,我也是第一次翻车翻地如此彻底。


后来群里有小伙伴表示不服,说你这个演示的场景角度是不是有点刁钻了,于是干脆,我又把之前写HDFS的代码,给改成了写Doris数据库,看这个丢数据的情况是不是还存在。


先给结论:涛声依旧(还是丢数据)。



0. 验证前准备


首先,版本为2.x的kafka集群,topic为test,一会手动向该topic推送数据;


然后,版本为1.15的Flink,配合版本为3.1的Hadoop集群,用Flink on yarn来消费kafka的数据;


接着,Flink对数据做简单的处理,然后写入Doris(版本为1.2.3)表,并且在写入过程中开启checkpoint。


其中doris的建表语句如下,用的非聚合功能表:


CREATE TABLE `dns_logs_from_flink01` (
  `client_ip` varchar(200) NULL,
  `domain` varchar(200) NULL,
  `time` varchar(200) NULL DEFAULT "99999",
  `target_ip` text NULL,
  `rcode` int(11) NULL DEFAULT "99",
  `query_type` int(11) NULL DEFAULT "99",
  `authority_record` varchar(500) NULL,
  `add_msg` varchar(200) NULL,
  `dns_ip` varchar(200) NULL
) ENGINE=OLAP
DUPLICATE KEY(`client_ip`, `domain`, `time`)
DISTRIBUTED BY HASH(`client_ip`) BUCKETS AUTO


为了保证验证的严谨,这里针对这个数据写入场景,分别准备了Flink的SQL API以及DataStream API。


因为Sparkd的流计算已经被验证过,只要设置了checkpoint,就不会存在“丢数据”的情况,所以,这次就不再重复验证,有兴趣的可以看上一篇文章。



1. 先看Flink的SQL API


完整代码如下:


package com.anryg.doris

import java.time.Duration

import org.apache.flink.contrib.streaming.state.EmbeddedRocksDBStateBackend
import org.apache.flink.streaming.api.CheckpointingMode
import org.apache.flink.streaming.api.environment.CheckpointConfig.ExternalizedCheckpointCleanup
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment

/**
  * @DESC: 通过SQL API读取kafka数据,写入到Doris
  * @Auther: Anryg
  * @Date: 2023/9/3 11:29
  */

object FlinkSQLFromKafka2Doris {

    def main(args: Array[String]): Unit = {
        val env = StreamExecutionEnvironment.getExecutionEnvironment

        env.enableCheckpointing(10000L)/**这个必须加上,否则数据写不进Doris*/
        env.setParallelism(args(0).toInt) //设置并行度

        env.setStateBackend(new EmbeddedRocksDBStateBackend(true)) //新的设置state backend的方式
        env.getCheckpointConfig.setCheckpointStorage("hdfs://192.168.211.106:8020/tmp/flink_checkpoint/FlinkSQLFromKafka2Doris")
        env.getCheckpointConfig.setExternalizedCheckpointCleanup(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION) //设置checkpoint记录的保留策略
        env.getCheckpointConfig.setAlignedCheckpointTimeout(Duration.ofMinutes(1L))
        env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.AT_LEAST_ONCE)

        val tableEnv = StreamTableEnvironment.create(env)

        /**第一步:读取kafka数据源*/
        tableEnv.executeSql(
            """
             Create table dns_logs_from_kafka(
             |client_ip STRING,
             |domain STRING,
             |`time` STRING,
             |target_ip STRING,
             |rcode STRING,
             |query_type STRING,
             |authority_record STRING,
             |add_msg STRING,
             |dns_ip STRING
             |)
             |with(
             |'connector' = 'kafka',
             |'topic' = 'test',
             |'properties.bootstrap.servers' = '192.168.211.107:6667',
             |'properties.group.id' = 'FlinkSQLFromKafka2Doris',
             |'scan.startup.mode' = 'latest-offset',
             |'value.format'='csv',                                 //确定数据源为文本格式
             |'value.csv.field-delimiter'='|'                      //确定文本数据源的分隔符
             |)
            """
.stripMargin)

        /**第二步:创建Doris映射表*/
        tableEnv.executeSql(
            s"""
              |CREATE TABLE dns_logs_from_flink01 (
              |`client_ip` STRING,
             |domain STRING,
             |`time` STRING,
             |target_ip STRING,
             |rcode STRING,
             |query_type STRING,
             |authority_record STRING,
             |add_msg STRING,
             |dns_ip STRING
              |)
              |    WITH (
              |      'connector' = 'doris',
              |      'fenodes' = '192.168.221.173:8030',
              |      'table.identifier' = 'example_db.dns_logs_from_flink01',
              |      'username' = 'root',
              |      'password' = '',
              |     'sink.label-prefix' = '${args(1)}'  //导入标签前缀
              |)
            """
.stripMargin)

        /**第三步:数据写入到Doris表中*/
        tableEnv.executeSql(
            """
              |INSERT INTO dns_logs_from_flink01
              |select
              |*
              |from
              |dns_logs_from_kafka
            """
.stripMargin)

    }
} 


在启动任务前,先确保其checkpoint目录为空,以及表中没有数据:


确保checkpoint为空


确保表中无数据


然后,启动Flink任务:


flink run 
-m yarn-cluster 
-ys 2  
-yjm 1000 
-ytm 1200  
-ynm Flink2Doris
......

确保任务正常启动后,开始往kafka手动推数据,先推两条(注意思考这里的先后顺序,为什么要这样):


很快,就可以在Doris中查到这两条数据:


可以看到,这个时候,数据是可以正常写入到Doris表中的,说明写入数据的代码没有问题,而且对于checkpoint目录,也有记录对应的内容:


接下来,就是停掉当前Flink进程,然后再手动向kafka推入2条数据:


然后,根据Flink官方文档的要求,对当前任务进行恢复性启动:


先找到对应的checkpoint元数据绝对路径:


根据该路径,执行恢复启动命令如下:


flink run 
-s hdfs://192.168.211.106:8020/tmp/flink_checkpoint/FlinkSQLFromKafka2Doris/69aea640a59961021add33a5bf0350b0/chk-103/_metadata
-m yarn-cluster 
-ys 2  
-yjm 1000 
-ytm 1200  
-ynm Flink2Doris
......

执行后,很快,任务启动成功。



按理说,这应该是flink状态恢复的正确姿势了,那姿势正确了,刚推到kafka的那2条数据就应该会被写入到Doris表中,此时再查,就应该有4条数据了。


然鹅,事实并非如此(为了严谨,我先后查了不下10次):


这就意味着,刚才推到kafka的2条数据,丢....了....


但是呢,我心不死,想继续用DataStream API再试一次。



2.  再看Flink的DataStream API


还是看一眼代码:


package com.anryg.doris

import java.util.Properties

import com.alibaba.fastjson.JSONObject
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.connector.kafka.source.KafkaSource
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer
import org.apache.flink.streaming.api.CheckpointingMode
import org.apache.flink.streaming.api.environment.CheckpointConfig.ExternalizedCheckpointCleanup
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.doris.flink.cfg.DorisOptions
import org.apache.doris.flink.sink.DorisSink
import org.apache.doris.flink.cfg.DorisExecutionOptions
import org.apache.doris.flink.cfg.DorisReadOptions
import org.apache.doris.flink.sink.writer.{DorisRecordSerializer, SimpleStringSerializer}
import org.apache.flink.api.common.eventtime.WatermarkStrategy
import org.apache.flink.contrib.streaming.state.EmbeddedRocksDBStateBackend

/**
  * @DESC: 通过DataStream API读取kafka数据,写入到Doris
  * @Auther: Anryg
  * @Date: 2023/8/3 17:05
  */

object FlinkDSFromKafka2Doris {

    def main(args: Array[String]): Unit = {
        //获取流任务的环境变量
        val env = StreamExecutionEnvironment.getExecutionEnvironment
                .enableCheckpointing(10000, CheckpointingMode.AT_LEAST_ONCE) //打开checkpoint功能
        //env.setRuntimeMode(RuntimeExecutionMode.BATCH)

        env.setParallelism(args(0).toInt)

        env.setStateBackend(new EmbeddedRocksDBStateBackend(true)) //新的设置state backend的方式
        env.getCheckpointConfig.setCheckpointStorage("hdfs://192.168.211.106:8020/tmp/flink_checkpoint/FlinkDSFromKafka2Doris") //设置checkpoint的hdfs目录
        env.getCheckpointConfig.setExternalizedCheckpointCleanup(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION) //设置checkpoint记录的保留策略

        val kafkaSource = KafkaSource.builder()  //获取kafka数据源
                .setBootstrapServers("192.168.211.107:6667")
                .setTopics("test")
                .setGroupId("FlinkDSFromKafka2Doris")
                .setStartingOffsets(OffsetsInitializer.latest()) //确认消费模式为latest
                .setValueOnlyDeserializer(new SimpleStringSchema())
                .build()

        import org.apache.flink.streaming.api.scala._  //引入隐式转换函数,需要将从kafka读出来的数据做隐式转换

        /**配置Doris相关参数*/
        val dorisBuilder = DorisOptions.builder
        dorisBuilder.setFenodes("192.168.221.173:8030")
                .setTableIdentifier("example_db.dns_logs_from_flink01")
                .setUsername("root")
                .setPassword("")

        /**确定数据写入到Doris的方式,即stream load方式*/
        val executionBuilder = DorisExecutionOptions.builder
        executionBuilder.setLabelPrefix("flink-doris-label04") //streamload label prefix

        /**添加额外的数据格式设置,否则写不进去*/
        val properties = new Properties
        properties.setProperty("format", "json")
        properties.setProperty("read_json_by_line", "true")

        executionBuilder.setStreamLoadProp(properties)

        /**确定Doris的Sink数据方式*/
        val builder = DorisSink.builder[String] //注意这个数据类型String 需要加上
        builder.setDorisReadOptions(DorisReadOptions.builder.build) //确定数据读取策略
                .setDorisExecutionOptions(executionBuilder.build) //确定数据执行策略
                .setSerializer(new SimpleStringSerializer())  //确定数据数据序列化(写入)类型
                .setDorisOptions(dorisBuilder.build) //添加Doris配置

        /**读取数据源生成DataStream对象*/
        val kafkaDS = env.fromSource[String](kafkaSource, WatermarkStrategy.noWatermarks(),"kafka-data")

        /**对读取的数据进行处理*/
        val targetDS = kafkaDS.map(line => {
            val array = line.split("\\|")
            array
        }).filter(_.length == 9)
          .map(array => {
              val json = new JSONObject()  /**将数据封装为json对象*/
              json.put("client_ip",array(0))
              json.put("domain",array(1))
              json.put("time",array(2))
              json.put("target_ip",array(3))
              json.put("rcode",array(4))
              json.put("query_type",array(5))
              json.put("authority_record",array(6))
              json.put("add_msg",array(7))
              json.put("dns_ip",array(8))
              json.toJSONString /**转换为json string*/
          })

        targetDS.sinkTo(builder.build())

        env.execute("FlinkDSFromKafka2Doris")
    }
}

还是跟上面一样的步骤,停掉之前的任务,清空数据库的表,同时清除checkpoint目录。


然后启动Flink任务,接着向kafka推2条数据,确保数据成功写入Doris表,证明代码逻辑没有问题。


再停掉Flink进程,然后再向kafka推2条数据,再通过恢复模式启动当前的Flink任务,确保执行步骤跟之前的一样。


但是,然并卵。


依然消费不到后推入的那2条数据,再一次证明,在当前场景下Flink会丢数据。



3. yarn-session模式和yarn-cluster模式


为了能尽可能证明Flink的恢复数据能力,我算是各种情况都试了个遍,上面先是用yarn-cluster模式分别用Flink的SQL api以及DataStream api做了尝试。


然后我又用了yarn-session模式来提交同样的任务,以同样的流程又测试了一遍,发现,还是不行,说什么就是无法恢复那后续推入的2条数据。


而且发现一个奇怪的现象,明明是一毛一样的代码,且代码中也设置了checkpoint的相关配置,但是对于yarn-cluster模式,在其运行的UI界面里,看不到checkpoint的任何信息:



而如果是yarn-session模式呢,又是可以看到的:



但,虽然后者可以显示出checkpoint的信息,似乎也没有什么卵用,因为它恢复不了我想要的数据。



最后


其实我还尝试了对任务设置savepoint,用savepoint的方式来恢复,虽然整个任务运行都没有异常,但就是,恢  复  不  了。


对于Flink来说,这“消失的2条kafka数据”似乎成为了一桩悬案,我暂时是破解不了了。


不知道是我恢复的方式不对呢,还是说Flink的设定就不允许我这么玩。


也许你,可以根据这个场景也来试验一把,看问题到底出在哪...




你可以添加我的私人微信,拉你入技术讨论群,跟一群热爱技术的小伙伴一起成长...


原文作者:安瑞哥是码农;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2022-02-16

  • 博文量
    228
  • 访问量
    115458