ITPub博客

首页 > 数据库 > 数据库开发技术 > Flink 将 Doris 作为外部数据源两种方案对比

Flink 将 Doris 作为外部数据源两种方案对比

原创 数据库开发技术 作者:安瑞哥是码农 时间:2024-07-17 11:06:23 0 删除 编辑


前面用4篇文章,聊了 Spark 如何引入外部数据源 Redis 的4种不同玩法。


那么接下来,咱们把角色一变, Spark 换成 Flink,同时也把 Redis 换成 Doris,同样来看看,在面对类似的场景时,这两者之间,能碰撞出什么样的火花出来。



0. 抛个问题


假如有这样一个场景,主数据是一批 IP 地址(可能在任何数据存储系统,比如Kafka),然后 Doris 数据库里面,存储有跟 IP 地址相关的其他信息(具体是啥,不用关心)。


计算的要求是,查询出这个主数据的每一条 IP 地址(ip字段),对应的,在 Doris 数据库表中(target_ip字段),有多少条对应的记录。


主数据


外部 Doris 数据


要求计算结果


但是,这个 Doris 中对应的表,比较大,有好几亿条数据,如果在计算的时候,把整张表的内容都读到进程里,除了占用内存不说,效率还低。


但如果是针对每一条要查询的 IP,通过查询动作,给「下推」到 Doris 端,然后 Doris 返回查询结果,那样效率就会高很多。


接下来,我们来对比一下,用两种不同的处理方式,看他们分别在执行效率,以及编码上面的优劣。



1. 解决方案一


也是我认为编码最简单的方案,利用 Flink 的 SQL(Table) API,编码如下:

 

package com.anryg.mysql.jdbc

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment

/**
  * @DESC: 利用 Flink SQL API 引入 Doris 外部数据源
  * @Auther: Anryg
  * @Date: 2024/7/16 17:37
  */

object FlinkSQLReadDoris {

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

        env.setRuntimeMode(RuntimeExecutionMode.BATCH)
        val tableEnv = StreamTableEnvironment.create(env)
      
        import org.apache.flink.streaming.api.scala._  //引入隐式转换函数

        /**模拟生成主数据,实际可以是任何数据源,比如Kafka等*/
        val mainDS = env.fromCollection(Seq(
            (1,"221.198.68.4"),
            (2, "192.168.100.1"),
            (3, "110.22.68.4"),
            (4, "172.64.33.195"),
            (5, "20.242.68.3"),
            (6, "1.2.3.99")
        ))

        /**将主数据映射成t1表*/
        val mainTable = tableEnv.fromDataStream(mainDS).as("id","ip")
        tableEnv.createTemporaryView("t1", mainTable)

        /**将Doris外部数据源映射成表t2*/
        tableEnv.executeSql(
            """
               |CREATE TABLE t2 (
               |`client_ip` STRING,
               |domain STRING,
               |`time` STRING,
               |target_ip STRING,
               |rcode INT,
               |query_type INT,
               |authority_record STRING,
               |add_msg STRING,
               |dns_ip STRING
               |)
               |    WITH (
               |      'connector' = 'doris',
               |      'fenodes' = '192.168.221.173:8030',
               |      'table.identifier' = 'example_db.test02',
               |      'username' = 'root',
               |      'password' = '***',
               |     'sink.label-prefix' = 'label_001'  //导入标签前缀
               |)
            "
"".stripMargin)

        /**用Flink SQL 对t1,t2进行关联得出结果*/
        tableEnv.executeSql(
            """
              |select t1.ip,
              |case when
              |t3.cnt is null then 0
              |else
              |t3.cnt
              |end as cnt
              |from
              |t1
              |left join
              |(
              |select target_ip,count(*) as cnt from t2 where target_ip in (select ip from t1) group by target_ip
              |) as t3
              |on t1.ip = t3.target_ip
            "
"".stripMargin).print()
    }
}

由于 Flink 提供了对 Doris 现成的 SQL API



所以对于它的编码,用 SQL 来写显得效率格外高。


但是,它的缺点也非常明显,那就是上面提到的,对于当前的数据处理场景来说,执行效率非常低。


原因在于,当前映射的这张 Doris 表,必须将所有的数据全部都要加载到进程里,没有办法根据查询条件,执行有效的「下推」操作,好家伙,好几亿条数据,不慢才怪。


而且,如果你是在本地执行,默认内存设置情况下,压根就跑不出来,关键它抛出的异常,根本就不会告诉你是因为数据量过大导致的。



而事实上,当你把这张 Doris 的大表,给换成一张小的,它就可以跑出来了,很讨厌。



2. 解决方案二


既然上面这种方式不行,那咱就换一种思路,上面的编码方式不能让查询「下推」,那就改为支持下推的。


用原始的 JDBC 方式读取 Doris,只不过这一次,只能用 DataStream API。


package com.anryg.mysql.jdbc

import java.sql.{Connection, DriverManager}

import org.apache.flink.api.common.RuntimeExecutionMode
import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment

/**
  * @DESC: 测试 Flink 在处理主数据的时候,用JDBC方式额外引入 Doris 数据源
  * @Auther: Anryg
  * @Date: 2024/7/16 11:06
  */

object FlinkDSReadDoris {

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

        import org.apache.flink.streaming.api.scala._  //引入隐式转换函数

        /**模拟生成主数据,实际可以是任何数据源,比如Kafka等*/
        val mainDS = env.fromCollection(Seq(
            (1,"221.198.68.4"),
            (2, "192.168.100.1"),
            (3, "110.22.68.4"),
            (4, "172.64.33.195"),
            (5, "20.242.68.3"),
            (6, "1.2.3.99")))

        /**利用富函数*/
        mainDS.map(new RichMapFunction[(Int, String), (String, Long)] {
            var connection:Connection = null
            val url = "jdbc:mysql://192.168.221.173:9030/example_db?useUnicode=true&characterEncoding=utf8&useTimezone=true&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true"

            /**创建jdbc连接*/
            override def open(parameters: Configuration): Unit = {
                Class.forName("com.mysql.cj.jdbc.Driver")
                connection = DriverManager.getConnection(url, "root", "***")
            }

            /**对每一条主数据挨个处理*/
            override def map(value: (Int, String)): (String, Long) = {
                val ip = value._2
                val ps = connection.prepareStatement("select count(*) as count from dns_logs_from_kafka where target_ip = ?")               
                ps.setString(1, ip)
                val result = ps.executeQuery()              
                var count = 0L
                while (result.next()){
                    count = result.getLong("count")
                }

                (ip, count)
            }

            /**任务完成,关闭连接,如果是流任务则不要关*/
            override def close(): Unit = {
                if (!connection.isClosed || connection != null) connection.close()
            }

        }).print()

        env.execute() //启动任务按钮
    }
}


很明显,这次的编码方式要更复杂一点,这里主要需要注意的是,对 Doris 的连接管理问题(关于这一点,后面文章会展开聊)。


当然,对应的,这里需要你引入 JDBC 的包。



这种方式最明显的优点是,执行效率高,因为用的 JDBC,所以它自带「查询下推」属性,不用傻了吧唧把所有表数据都薅出来。


所以很快,这个结果就跑出来了,就在本地,不用额外设置任何参数。




最后


这次测试用的两种 Flink 读取外部 Doris 数据源的方案,应该是我们在平时都会用到的,而且你会发现,类似的方案,在前面的 Spark 读取外部数据源文章中,也出现过。


从对比效果来看,方案一很明显要更「费IO」以及「费内存」,效率低不说,而且如果运行的资源参数设置过小,很容易嗝屁。


而方案二虽然编码复杂一些,但对数据的处理逻辑则「更聪明」,不会去浪费没有必要的 IO 跟内存空间,所以效率更高,且更节省资源。


那么你平时在工作中,主要用的哪种呢?




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


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

下一篇: 啪!打脸了。
请登录后发表评论 登录
全部评论

注册时间:2022-02-16

  • 博文量
    228
  • 访问量
    115433