背景

  • 互联网金融,面对的业务方较多;风控部门的数据分析师,策略分析师,反欺诈分析师等,目前的数据量这些分析师使用Python以及MySQL是无法满足快速高效的分析的;商城、运营部门等的报表看板,定制化用户行为分析等。;目前的自主分析是使用的开源产品Superset做一部分的改造,接入Druid,ES,Impala,分析师们已经全部转到我们的平台,大部分的使用都是基于我们数仓的DWS,但是除此之外实时数据没有完全接入,这是目前的痛点,也是最需要做的;尝试使用HBase做映射使用Impala分析,但是只能按照固定的键高效查询,无法满足更加多元化的定制化分析场景;
  • 之前一直使用Spark Streaming进行实时数据的流转,此次我打算用Flink来做,此篇是一个测试过程,操作也是十分粗糙,比较低效;

摘要

关键字

  • Flink将Maxwell实时数据写入KUDU
  • Flink第一步

设计

  • 完整的设计如下
  1. 使用MySQL作为配置表,每日一次广播
  • 因为实时接入的业务库很多,所以需要进行配置化写入,需要才做添加;
  • 配置库名,表名详细参数等;
  1. Flink对接MaxWell将需要的数据写入KUDU;
  2. Impala集成KUDU于飞天(BI数据分析平台),数分、策略、算法等进行使用;

说明

  • 因为是一个简单的开始Demo,刚开始使用Flink写入,所以有很多待优化的地方,也是需要跟进学习的地方;
  • Demo为使用KUDU客户端,并非标准的Flink-KUDU-sink;
  • Demo只是关于表的创建与写入,过程顺畅之后多表无非是根据不同的配置进行不同的处理了;
  • 这篇帖子是针对与KUDU的一些基础操作,其中涉及到集成Spark,KUDU的原生客户端使用;

实现

  • main
object FlinkRealTimeMain {
  def main(args: Array[String]): Unit = {
    //环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    val kuduClientBuilder = new KuduClient.KuduClientBuilder("cdh-01.xxxxx.com:7051").build()

    FlinkRealTimeProcessing.processor(env , kuduClientBuilder)

    env.execute(this.getClass.getSimpleName)
  }
}
  • traits
trait FlinkProcessor {
  def processor(env: StreamExecutionEnvironment, kuduClientBuilder: KuduClient)
}
  • Processing
/**
 * Description:xxxx<br/>
 * Copyright (c) ,2020 , jackson <br/>
 * This program is protected by copyright laws. <br/>
 * Date: 2020年11月26日  
 *
 * @author xxx
 * @version : 1.0
 */
object FlinkRealTimeProcessing extends FlinkProcessor {

  override def processor(env: StreamExecutionEnvironment, kuduClientBuild: KuduClient): Unit = {
    /**
     * 获取基础参数
     */
    val bootstrapserversnew = Contant.BOOTSTRAP_SERVERS_NEW
    import org.apache.flink.api.scala._

    /**
     * kudu建表
     * 因为之前总结过的KUDU客户端是spark-kudu,同步,异步几种,这个地方使用client就没有在做封装了,不过流程是一模一样的;只是一个简单的Demo
     */
    import scala.collection.JavaConversions._
    if (kuduClientBuild.tableExists("kudu_client_test_table_risk_task")) {
      println("kudu_client_test_table_risk_task已存在!")
    } else {
      /**
       * 创建建表配置项
       */
      val createTableOptions = new CreateTableOptions
      createTableOptions.setNumReplicas(1)
      createTableOptions.addHashPartitions(ListBuffer("id"), 3)

      /**
       * 字段结构属性建立
       */
      val idSchemaBuilder = new ColumnSchema.ColumnSchemaBuilder("id", Type.INT32)
      val orderNoSchemaBuilder = new ColumnSchema.ColumnSchemaBuilder("order_no", Type.STRING)
      val applyTimeSchemaBuilder = new ColumnSchema.ColumnSchemaBuilder("apply_time", Type.STRING)
      val customerNameSchemaBuilder = new ColumnSchema.ColumnSchemaBuilder("customer_name", Type.STRING)

      /**
       * 配置结构图
       */
      val schema = new Schema(ListBuffer(
        idSchemaBuilder.key(true).build(),
        orderNoSchemaBuilder.key(false).build(),
        applyTimeSchemaBuilder.key(false).build(),
        customerNameSchemaBuilder.key(false).build()
      ))

      kuduClientBuild.createTable(
        "kudu_client_test_table_risk_task",
        schema,
        createTableOptions
      )
    }

    /**
     * 定义kafka-source得到DataStream
     */
    val topic = "opic"
    //将kafka中数据反序列化,
    val valueDeserializer: DeserializationSchema[String] = new SimpleStringSchema()

    val properties = new Properties()
    properties.put("bootstrap.servers", bootstrapserversnew)

    println(Contant.BOOTSTRAP_SERVERS_NEW)
    val kafkaSinkDStream = env.addSource(new FlinkKafkaConsumer[String](topic, valueDeserializer, properties))

    /**
     * key操作
     */
    kafkaSinkDStream.keyBy(line => {
      val kuduClientBuilder = new KuduClient.KuduClientBuilder("host:port").build()
      /**
       * 获取kudu连接的session
       */
      val kuduSession = kuduClientBuilder.newSession()

      /**
       * 获取kudu表连接
       */
      val kuduTable = kuduClientBuilder.openTable("kudu_client_test_table_risk_task")


      val jsonLine = JSON.parseObject(line)
      val database = jsonLine.get("database").toString
      val table = jsonLine.get("table").toString
      val data = jsonLine.get("data").toString
      println(
        s"""
           |database:${database} ,
           |table:${table} ,
           |data:${data}
           |""".stripMargin)

      /**
       * 判断表库进行操作,此处只针对单表,如果后面需要配置化获取配置多表判断即可
       */
      if (database == "rms") {
        if (table == "risk_task") {
          /**
           * 择取需要的数据进行数据存储
           */
          val dataJson = JSON.parseObject(data)
          val id = dataJson.get("id")
          val order_no = dataJson.get("order_no")
          val apply_time = dataJson.get("apply_time")
          val customer_name = dataJson.get("customer_name")

          /**
           * 建立table连接进行数据操作
           */
          val upsert = kuduTable.newUpsert()
          val row = upsert.getRow
          row.addInt("id", id.toString.toInt)
          row.addString("order_no", order_no.toString)
          row.addString("apply_time", apply_time.toString)
          row.addString("customer_name", customer_name.toString)
          upsert.setRow(row)
          kuduSession.apply(upsert)
        }
      }
      kuduSession.flush()

      println("kuduTableLimit:", kuduClientBuilder.newScannerBuilder(kuduTable)
        .build()
        .iterator().toList.count(data => true))

      kuduSession.close()
      kuduClientBuilder.close()

    }).reduce(_ + _)

    kuduClientBuild
      .close()
  }
}

部署

#!/bin/bash
flink run -m yarn-cluster \
-c com.xxxx.flink.FlinkRealTimeMain \
-p 8 \
/home/cdh/xxxx/2020/11/FlinkToKuduDemo/realtime_source_dw.jar

优化

低效点

  • 操作客户端、kudu-session、table-client在流中建立,很低效,目前还没有找到类似于Spark中Partition中的操作这种概念,也就是Flink的Task Manager中,避免task不能序列化,只能这么低效;
  • 除了广播DataStream之外也没有找到类似于广播配置源的操作;类似于任何一个对象的广播或者连接的广播;
  • 由于无法进行Partition的操作,所以就无法进行很多的KUDU客户端使用,包括KuduTable的创建还有刷新KUDU表的此类操作,都没有做到最优;
  • 具体建立Flink客户端时更多参数的优化;
  • 启动参数的优化;

优化过程

  • 后续针对这些点进行优化更新;

纠正

  • 昨天有些着急,很多点还是从微批的角度来理解了,所以其实上面低效点中关于Partition的描述并不准确,具体的处理是按照数据流来的,所以直接操作的就是每个Task Manager细化到的Task,具体后续从源码的角度来描述;