Flink 消费 Kafka 数据实时落Apache doris数据仓库(KFD)

  Apache Doris 代码仓库地址:apache/incubator-doris 欢迎大家关注加星
 


1.概述

Apache Doris(原百度 Palo)是一款基于大规模并行处理技术的分布式 SQL 数据仓库,由百度在 2017 年开源,2018 年 8 月进入 Apache 孵化器。

Apache Doris是一个现代化的MPP分析型数据库产品。仅需亚秒级响应时间即可获得查询结果,有效地支持实时数据分析。Apache Doris的分布式架构非常简洁,易于运维,并且可以支持10PB以上的超大数据集。

Apache Doris可以满足多种数据分析需求,例如固定历史报表,实时数据分析,交互式数据分析和探索式数据分析等。令您的数据分析工作更加简单高效!

2.场景介绍

这里我们介绍的是通过Doris提供的Stream load 结合Flink计算引擎怎么实现数据实时快速入库操作。

使用环境如下:

  mysql 5.x/8.x (主要是业务数据库)
  kafka 2.11 (消息队列)
  flink 1.10.1 (流式计算引擎)
  doris 0.14.7  (核心数仓)
  Canal (Mysql binlog数据采集工具)

3.实现方案

这里我们采用的历史数据离线处理+增量数据实时处理的架构

3.1 历史数据离线处理

历史数据离线处理方式,这里我们使用是Doris ODBC外表方式,将mysql的表映射到doris里,然后使用

 
insert into <doris_table_name>  select * from <mysql_odbc_external_table>

3.1.1 外表创建方法

  1. 首先Apache Doris 0.13.x以上版本

  2. 要在所有的BE节点安装对应数据的ODBC驱动

  3. 创建外表

具体可以参考我的另外一篇文章,这里不多做介绍

[Apache doris ODBC外表使用方式]  https://mp.weixin.qq.com/s/J0suRGPNkxD6oHSRFK6KTA 

3.2 增量数据实时处理

增量数据的实时处理,这里我们是通过 Canal 监控 Mysql binlog 解析并推送到指定的 Kafka 队列,然后通过 Flink 去实时消费Kafka队列的数据,然后你可以根据自己的需要对数据进行处理,算法等,最后将明细数据或者实时计算的中间结果保存到对应的doris数据表中,这里使用的是stream load,你可以使用Flink doris connector。

3.2.1 doris sink实现

这里我们首先实现一个Flink doris sink

import com.alibaba.fastjson.JSON;
 import org.apache.flink.configuration.Configuration;
 import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 ​
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 ​
 /**
  * 自定义flink doris sink
  */
 public class DorisSink extends RichSinkFunction<String> {
 ​
     private static final Logger log = LoggerFactory.getLogger(DorisSink.class);
 ​
     private final static List<String> DORIS_SUCCESS_STATUS = new ArrayList<>(Arrays.asList("Success", "Publish Timeout"));
 ​
     private DorisStreamLoad dorisStreamLoad;
 ​
     private String columns;
 ​
     private String jsonFormat;
 ​
     public DorisSink(DorisStreamLoad dorisStreamLoad, String columns, String jsonFormat) {
         this.dorisStreamLoad = dorisStreamLoad;
         this.columns = columns;
         this.jsonFormat = jsonFormat;
     }
 ​
     @Override
     public void open(Configuration parameters) throws Exception {
         super.open(parameters);
     }
 ​
 ​
     /**
      * 判断StreamLoad是否成功
      *
      * @param respContent streamload返回的响应信息(JSON格式)
      * @return
      */
     public static Boolean checkStreamLoadStatus(RespContent respContent) {
         if (DORIS_SUCCESS_STATUS.contains(respContent.getStatus())
                 && respContent.getNumberTotalRows() == respContent.getNumberLoadedRows()) {
             return true;
         } else {
             return false;
         }
     }
 ​
     @Override
     public void invoke(String value, Context context) throws Exception {
         DorisStreamLoad.LoadResponse loadResponse = dorisStreamLoad.loadBatch(value, columns, jsonFormat);
         if (loadResponse != null && loadResponse.status == 200) {
             RespContent respContent = JSON.parseObject(loadResponse.respContent, RespContent.class);
             if (!checkStreamLoadStatus(respContent)) {
                 log.error("Stream Load fail{}:", loadResponse);
             }
         } else {
             log.error("Stream Load Request failed:{}", loadResponse);
         }
     }
 }

3.2.2 Stream Load 工具类

 
import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 ​
 ​
 import java.io.Serializable;
 import java.io.IOException;
 import java.io.BufferedOutputStream;
 import java.io.InputStream;
 import java.io.BufferedReader;
 import java.io.InputStreamReader;
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.util.Base64;
 import java.util.Calendar;
 import java.util.UUID;
 ​
 ​
 /**
  * doris streamLoad
  */
 ​
 public class DorisStreamLoad implements Serializable {
 ​
     private static final Logger log = LoggerFactory.getLogger(DorisStreamLoad.class);
     //连接地址,这里使用的是连接FE
     private static String loadUrlPattern = "http://%s/api/%s/%s/_stream_load?";
     //fe ip地址
     private String hostPort;
     //数据库
     private String db;
     //要导入的数据表名
     private String tbl;
     //用户名
     private String user;
     //密码
     private String passwd;
     private String loadUrlStr;
     private String authEncoding;
 ​
 ​
     public DorisStreamLoad(String hostPort, String db, String tbl, String user, String passwd) {
         this.hostPort = hostPort;
         this.db = db;
         this.tbl = tbl;
         this.user = user;
         this.passwd = passwd;
         this.loadUrlStr = String.format(loadUrlPattern, hostPort, db, tbl);
         this.authEncoding = Base64.getEncoder().encodeToString(String.format("%s:%s", user, passwd).getBytes(StandardCharsets.UTF_8));
     }
     //获取http连接信息
     private HttpURLConnection getConnection(String urlStr, String label, String columns, String jsonformat) throws IOException {
         URL url = new URL(urlStr);
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setInstanceFollowRedirects(false);
         conn.setRequestMethod("PUT");
         conn.setRequestProperty("Authorization", "Basic " + authEncoding);
         conn.addRequestProperty("Expect", "100-continue");
         conn.addRequestProperty("Content-Type", "text/plain; charset=UTF-8");
         conn.addRequestProperty("label", label);
         conn.addRequestProperty("max_filter_ratio", "0");
         conn.addRequestProperty("strict_mode", "true");
         conn.addRequestProperty("columns", columns);
         conn.addRequestProperty("format", "json");
         conn.addRequestProperty("jsonpaths", jsonformat);
         conn.addRequestProperty("strip_outer_array", "true");
         conn.setDoOutput(true);
         conn.setDoInput(true);
 ​
         return conn;
     }
 ​
     public static class LoadResponse {
         public int status;
         public String respMsg;
         public String respContent;
 ​
         public LoadResponse(int status, String respMsg, String respContent) {
             this.status = status;
             this.respMsg = respMsg;
             this.respContent = respContent;
         }
 ​
         @Override
         public String toString() {
             StringBuilder sb = new StringBuilder();
             sb.append("status: ").append(status);
             sb.append(", resp msg: ").append(respMsg);
             sb.append(", resp content: ").append(respContent);
             return sb.toString();
         }
     }
     //执行数据导入
     public LoadResponse loadBatch(String data, String columns, String jsonformat) {
         Calendar calendar = Calendar.getInstance();
         //导入的lable,全局唯一
         String label = String.format("flink_import_%s%02d%02d_%02d%02d%02d_%s",
                 calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DAY_OF_MONTH),
                 calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), calendar.get(Calendar.SECOND),
                 UUID.randomUUID().toString().replaceAll("-", ""));
 ​
         HttpURLConnection feConn = null;
         HttpURLConnection beConn = null;
         try {
             // build request and send to fe
             feConn = getConnection(loadUrlStr, label, columns, jsonformat);
             int status = feConn.getResponseCode();
             // fe send back http response code TEMPORARY_REDIRECT 307 and new be location
             if (status != 307) {
                 throw new Exception("status is not TEMPORARY_REDIRECT 307, status: " + status);
             }
             String location = feConn.getHeaderField("Location");
             if (location == null) {
                 throw new Exception("redirect location is null");
             }
             // build request and send to new be location
             beConn = getConnection(location, label, columns, jsonformat);
             // send data to be
             BufferedOutputStream bos = new BufferedOutputStream(beConn.getOutputStream());
             bos.write(data.getBytes());
             bos.close();
 ​
             // get respond
             status = beConn.getResponseCode();
             String respMsg = beConn.getResponseMessage();
             InputStream stream = (InputStream) beConn.getContent();
             BufferedReader br = new BufferedReader(new InputStreamReader(stream));
             StringBuilder response = new StringBuilder();
             String line;
             while ((line = br.readLine()) != null) {
                 response.append(line);
             }
             return new LoadResponse(status, respMsg, response.toString());
 ​
         } catch (Exception e) {
             e.printStackTrace();
             String err = "failed to load audit via AuditLoader plugin with label: " + label;
             log.warn(err, e);
             return new LoadResponse(-1, e.getMessage(), err);
         } finally {
             if (feConn != null) {
                 feConn.disconnect();
             }
             if (beConn != null) {
                 beConn.disconnect();
             }
         }
     }
 ​
 }

3.2.3 Flink Job

这个地方演示的是单表,如果是你通过Canal监听的多个表的数据,这里你需要根据表名进行区分,并和你mysql表和doris里的表建好对应关系,解析相应的数据即可

 
import org.apache.doris.demo.flink.DorisSink;
 import org.apache.doris.demo.flink.DorisStreamLoad;
 import org.apache.flink.api.common.serialization.SimpleStringSchema;
 import org.apache.flink.streaming.api.datastream.DataStreamSource;
 import org.apache.flink.streaming.api.environment.CheckpointConfig;
 import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
 import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
 ​
 import java.util.Properties;
 ​
 /**
  *
  * This example mainly demonstrates how to use flink to stream Kafka data.
  *  And use the doris streamLoad method to write the data into the table specified by doris
  * <p>
  * Kafka data format is an array, For example: ["id":1,"name":"root"]
  */
 ​
 public class FlinkKafka2Doris {
     //kafka address
     private static final String bootstrapServer = "xxx:9092,xxx:9092,xxx:9092";
     //kafka groupName
     private static final String groupName = "test_flink_doris_group";
     //kafka topicName
     private static final String topicName = "test_flink_doris";
     //doris ip port
     private static final String hostPort = "xxx:8030";
     //doris dbName
     private static final String dbName = "db1";
     //doris tbName
     private static final String tbName = "tb1";
     //doris userName
     private static final String userName = "root";
     //doris password
     private static final String password = "";
     //doris columns
     private static final String columns = "name,age,price,sale";
     //json format
     private static final String jsonFormat = "[\"$.name\",\"$.age\",\"$.price\",\"$.sale\"]";
 ​
     public static void main(String[] args) throws Exception {
 ​
         Properties props = new Properties();
         props.put("bootstrap.servers", bootstrapServer);
         props.put("group.id", groupName);
         props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
         props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
         props.put("auto.offset.reset", "earliest");
         props.put("max.poll.records", "10000");
 ​
         StreamExecutionEnvironment blinkStreamEnv = StreamExecutionEnvironment.getExecutionEnvironment();
         blinkStreamEnv.enableCheckpointing(10000);
         blinkStreamEnv.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
 ​
         FlinkKafkaConsumer<String> flinkKafkaConsumer = new FlinkKafkaConsumer<>(topicName,
                 new SimpleStringSchema(),
                 props);
 ​
         DataStreamSource<String> dataStreamSource = blinkStreamEnv.addSource(flinkKafkaConsumer);
 ​
         DorisStreamLoad dorisStreamLoad = new DorisStreamLoad(hostPort, dbName, tbName, userName, password);
 ​
         dataStreamSource.addSink(new DorisSink(dorisStreamLoad,columns,jsonFormat));
 ​
         blinkStreamEnv.execute("flink kafka to doris");
 ​
     }
 }

然后将Flink Job提交到集群上就可以运行了,数据就可以试试入库

这里其实是一个微批处理,你可以自己完善以下几部分:

  1. 每个批次最大入库记录数,或者每个多少秒进行一次入库,如果你的实时数据量比较小,或者你的数据比较大,这两条件哪个先到执行哪个

  2. 这里连接是FE,你可以通过FE的 rest api接口拿到所有的BE节点,直接连接BE进行入库,URL地址只是将FE的ip和端口换成BE的IP及http 端口即可

  3. 为了避免你连接这个BE或者FE的时候,正好这个节点挂了,你可以进行重试其他FE或者BE

  4. 为了避免单个节点压力,你可以进行轮训BE节点,不要每次都连接同一个BE节点

  5. 设置最大重试次数,如果超过这个次数,可以将导入失败的数据推送到Kafka队列,以方便后续人工手动处理

4.总结

本文只是抛砖引玉的方式给大家一个使用Stream load进行数据接入的使用方式及示例,Doris还有很多数据接入的方式等待大家去探索

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
上层应用业务对实时数据的需求,主要包含两部分内容:1、 整体数据实时分析。2、 AB实验效果的实时监控。这几部分数据需求,都需要进行的下钻分析支持,我们希望能够建立统一的实时OLAP数据仓库,并提供一套安全、可靠的、灵活的实时数据服务。目前每日新增的曝光日志达到几亿条记录,再细拆到AB实验更细维度时,数据量则多达上百亿记录,多维数据组合下的聚合查询要求秒级响应时间,这样的数据量也给团队带来了不小的挑战。OLAP层的技术选型,需要满足以下几点:1:数据延迟在分钟级,查询响应时间在秒级2:标准SQL交互引擎,降低使用成本3:支持join操作,方便维度增加属性信息4:流量数据可以近似去重,但订单行要精准去重5:高吞吐,每分钟数据量在千W级记录,每天数百亿条新增记录6:前端业务较多,查询并发度不能太低通过对比开源的几款实时OLAP引擎,可以发现Doris和ClickHouse能够满足上面的需求,但是ClickHouse的并发度太低是个潜在的风险,而且ClickHouse的数据导入没有事务支持,无法实现exactly once语义,对标准SQL的支持也是有限的。所以针对以上需求Doris完全能解决我们的问题,DorisDB是一个性能非常高的分布式、面向交互式查询的分布式数据库,非常的强大,随着互联网发展,数据量会越来越大,实时查询需求也会要求越来越高,DorisDB人才需求也会越来越大,越早掌握DorisDB,以后就会有更大的机遇。本课程基于真实热门的互联网电商业务场景为案例讲解,具体分析指标包含:AB版本分析,下砖分析,营销分析,订单分析,终端分析等,能承载海量数据实时分析,数据分析涵盖全端(PC、移动、小程序)应用。整个课程,会带大家实践一个完整系统,大家可以根据自己的公司业务修改,既可以用到项目中去,价值是非常高的。本课程包含的技术:开发工具为:IDEA、WebStormFlink1.9.0DorisDBHadoop2.7.5Hbase2.2.6Kafka2.1.0Hive2.2.0HDFS、MapReduceFlume、ZookeeperBinlog、Canal、MySQLSpringBoot2.0.8.RELEASESpringCloud Finchley.SR2Vue.js、Nodejs、Highcharts、ElementUILinux Shell编程等课程亮点:1.与企业接轨、真实工业界产品2.DorisDB高性能分布式数据库3.大数据热门技术Flink4.支持ABtest版本实时监控分析5.支持下砖分析6.数据分析涵盖全端(PC、移动、小程序)应用7.主流微服务后端系统8.天级别与小时级别多时间方位分析9.数据库实时同步解决方案10.涵盖主流前端技术VUE+jQuery+Ajax+NodeJS+ElementUI11.集成SpringCloud实现统一整合方案12.互联网大数据企业热门技术栈13.支持海量数据实时分析14.支持全端实时数据分析15.全程代码实操,提供全部代码和资料16.提供答疑和提供企业技术方案咨询企业一线架构师讲授,代码在老师的指导下企业可以复用,提供企业解决方案。  版权归作者所有,盗版将进行法律维权。 
课程总体架构请观看89讲。数据仓库是一个面向主题的、集成的、随时间变化的、但信息本身相对稳定的数据集合,用于对管理决策过程的支持。数据仓库的应用有:1.数据分析、数据挖掘、人工智能、机器学习、风险控制、无人驾驶。2.数据化运营、精准运营。3.广告精准、智能投放等等。数据仓库是伴随着企业信息化发展起来的,在企业信息化的过程中,随着信息化工具的升级和新工具的应用,数据量变的越来越大,数据格式越来越多,决策要求越来越苛刻,数据仓库技术也在不停的发展。数据仓库有两个环节:数据仓库的构建与数据仓库的应用。随着IT技术走向互联网、移动化,数据源变得越来越丰富,在原来业  务数据库的基础上出现了非结构化数据,比如网站log,IoT设备数据,APP埋点数据等,这些数据量比以往结构化的数据大了几个量级,对ETL过程、存储都提出了更高的要求。互联网的在线特性也将业务需求推向了实时化 ,随时根据当前客户行为而调整策略变得越来越常见,比如大促过程中库存管理,运营管理等(即既有中远期策略型,也有短期操作型)。同时公司业务互联网化之后导致同时服务的客户剧增,有些情况人工难以完全处理,这就需要机器 自动决策 。比如欺诈检测和用户审核。总结来看,对数据仓库的需求可以抽象成两方面: 实时产生结果、处理和保存大量异构数据。本课程基于真实热门的互联网电商业务场景为案例讲解,结合分层理论和实战对数仓设计进行详尽的讲解,基于Flink+DorisDB实现真正的实时数仓,数据来及分析,实时报表应用。具体数仓报表应用指标包括:实时大屏分析、流量分析、订单分析、商品分析、商家分析等,数据涵盖全端(PC、移动、小程序)应用,与互联网企业大数据技术同步,让大家能够学到大数据企业级实时数据仓库的实战经验。本课程包含的技术: 开发工具为:IDEA、WebStorm Flink 1.11.3Hadoop 2.7.5Hive 2.2.0ZookeeperKafka 2.1.0、Spring boot 2.0.8.RELEASESpring Cloud Finchley.SR2Flume 、Hbase 2.2.6DorisDB 0.13.9、RedisVUE+jQuery+Ajax+NodeJS+ElementUI+Echarts+Datav等课程亮点: 1.与企业接轨、真实工业界产品2.DorisDB高性能分布式数据库3.大数据热门技术Flink最新版4.真正的实时数仓以及分层设计5.海量数据大屏实时报表6.数据分析涵盖全端(PC、移动、小程序)应用7.主流微服务后端系统8.数据库实时同步解决方案9.涵盖主流前端技术VUE+jQuery+Ajax+NodeJS+ElementUI+Echarts+Datav10.集成SpringCloud实现统一整合方案11.互联网大数据企业热门技术栈12.支持海量数据实时数仓报表分析13.支持全端实时实时数仓报表分析14.全程代码实操,提供全部代码和资料 15.提供答疑和提供企业技术方案咨询企业一线架构师讲授,代码在老师的指导下企业可以复用,提供企业解决方案。  版权归作者所有,盗版将进行法律维权。 
首先,你需要在 Scala 代码中引入以下依赖: ```scala libraryDependencies += "org.apache.flink" %% "flink-scala" % flinkVersion libraryDependencies += "org.apache.flink" %% "flink-streaming-scala" % flinkVersion libraryDependencies += "org.apache.flink" %% "flink-connector-kafka" % flinkVersion libraryDependencies += "org.apache.flink" %% "flink-connector-hive" % flinkVersion libraryDependencies += "org.apache.flink" %% "flink-connector-jdbc" % flinkVersion ``` 然后,你可以使用以下代码来消费 Kafka 数据: ```scala import org.apache.flink.streaming.api.scala._ import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer val env = StreamExecutionEnvironment.getExecutionEnvironment val kafkaConsumer = new FlinkKafkaConsumer[String]("topic", new SimpleStringSchema(), properties) val stream = env.addSource(kafkaConsumer) // 对数据进行处理 val result = stream.map(...) ``` 其中,`properties` 是一个 `Properties` 对象,用于配置 Kafka 的连接信息。 接下来,你需要将处理后的数据写入到 Hive 和 Doris 中。可以使用以下代码: ```scala import org.apache.flink.table.api.bridge.scala._ import org.apache.flink.table.catalog.hive.HiveCatalog import org.apache.flink.streaming.api.scala.StreamTableEnvironment val tableEnv = StreamTableEnvironment.create(env) val hiveCatalog = new HiveCatalog("myHiveCatalog", "myDatabase", "/path/to/hive/conf", "2.3.4") tableEnv.registerCatalog("myHiveCatalog", hiveCatalog) tableEnv.useCatalog("myHiveCatalog") tableEnv.executeSql("CREATE TABLE myHiveTable (...) WITH (...)") result.toTable(tableEnv, "myResultTable") tableEnv.executeSql("INSERT INTO myHiveTable SELECT * FROM myResultTable") val jdbcUrl = "jdbc:mysql://localhost:3306/my_database" tableEnv.executeSql(s"CREATE TABLE myDorisTable (...) WITH (...)") tableEnv.executeSql(s"INSERT INTO myDorisTable SELECT * FROM myResultTable") ``` 其中,`myHiveCatalog` 是 Hive 的 Catalog 名称,`myDatabase` 是 Hive 中的数据库名称,`/path/to/hive/conf` 是 Hive 的配置文件所在路径,`2.3.4` 是 Hive 的版本号。 `myHiveTable` 和 `myDorisTable` 是你要写入数据的表名,`(...)` 是表的列定义和其他属性,`myResultTable` 是处理后的数据表名。 `jdbcUrl` 是 Doris 数据库的连接信息,你需要根据实际情况进行修改。 你需要将上述代码中的 `...` 替换为实际的处理逻辑和表定义。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值