基于MongoDB的SQL数据服务 原创 精华

Shineyang17
发布于 2022-6-2 09:22
浏览
4收藏

1. 业务背景

某省高速集团为了更好的分析交易流水数据,掌控各个阶段的流水数据的及时性和准确性,新上了对账系统。通过对账系统可以很方便地对交易流水进行统计和查找。例如分析流水是否已出站,交易是否已上传部中心,交易是否在银行已记账,银行是否已划账,部中心是否已拆分,省中心是否已下载拆分数据,省中心是否已清分,省中心是否已拆账,流水是否已上传发票等等。

分析对账系统牵涉的数据,主要特性如下:

  1. 数据种类多。目前数据源多,接近20种,以后可能还会扩展。

  2. 数据量大。每种数据源,记录都不小,其中光拆分数据每月记录接近4000万,存储接近200G。

  3. 性能。各种分析统计性能要求高,实时统计。

  4. 大字段。多个数据源存在大字段,JSON串。如拆分记录中存在多个大字段,计费金额组合、路段编号组合、收费单元编号组合、业主编号组合、收费单元交易金额组合、收费单元时间组合等等都是按固定格式形成的序列数据,长度不固定。在使用中需要解析成具体数据,然后再统计分析。

  5. 模型复杂。多个数据源的关联复杂,不同数据源的分区分表规则各异。

  6. 数据的安全性。对账系统的数据源基本都是核心数据,在安全性,冗余性,一致性都有严格要求。

针对数据特性,项目组初步提出了三种方案:

  1. Hive。Hive是基于Hadoop的一个数据仓库工具,可以用来对数据提取、转化、加载、存储、查询和分析。
    • 优点:
      • 类SQL语法,提供快速开发的能力(简单、容易上手)。
      • Hive的扩展性与集群的扩展性相同,通过集群的扩展获得性能的提升。
      • 支持用户自定义函数,编写符合自己业务需求的函数。
    • 缺点:
      • 执行开销较大,任务运行时间较长,延时较高。
      • 事务限制多,单行记录的频繁更新性能差。
      • 调优比较困难,粒度较粗。
      • Hive部署复杂,运营成本高。
  2. MySQL。MySQL是一个关系型数据库管理系统**,**最流行的关系型数据库管理系统之一。
    • 优点:

      • 易用性。使用最流行、最广泛的查询语言SQL,开发门槛低。
      • 事务简单,使用方便,性能好。
      • 部署简单,运营成本低。
    • 缺点:

      • 强数据模式,强制要求预先确定好数据表的列。模型变更约束多,修改表结构对业务影响大,大数据量情况下变更模型时间长。

      • 性能瓶颈。大数据情况下的汇总、统计性能一般。拆分系统的统计一般都要10个小时以上,严重影响效率。

  3. MongoDB。MongoDB是一个基于分布式文件存储的数据库。
  • 优点:

    • 模式自由。在MongoDB 中存储的数据是无模式的文档,无需事先确定数据的文档属性,通过删除或修改集合中的文档属性,以实现高度灵活性。
    • 性能高。分布式架构在处理海量数据时优势明显。
    • 多副本。数据多副本提高数据的安全性。
    • 分片模式。通过横向扩容的方式,为用户提供了近乎无限的空间。
  • 缺点:

    • 易用性不高。MongoDB使用的是非结构化的查询语言,对开发人员的亲和度不强。

    • 运营、监控、维护成本较高。没有MySQL那样成熟的维护工具。

根据系统的实际情况,讨论决定采用MongoDB来存储数据,主要原因如下:

  1. 性能。对账系统需要支持实时查询和统计,延时要求低。Hive和MySQL在大数据情况下经测试查询时间长,没法达到要求。MongoDB经过测试,性能可以满足要求,基本上响应时间都在秒级。
  2. 模型扩展性。目前对账系统已经有接近20种数据源,以后还会新增数据源。Hive、MySQL中每种数据源都有独立的模型,模型之间通过主外键关联,新增数据源,模型变更复杂。MongoDB中把数据源作为文档的一个属性存储,避免了关联。
  3. 关联查询。对账系统的查询和统计牵涉到多种数据源的关联。在Hive、MySQL中每种数据源的分区分表规则不同,维护关联关系需要耗费很多的精力。MongoDB数据源只是文档的属性,关联关系维护简单。

选定了MongoDB作为对账系统的存储系统,项目组遇到一个现实问题。MongoDB不支持SQL查询语言,项目组开发人员大部分都是熟悉SQL,需要重新学习MongoDB的非结构化的查询语言。为了解决开发人员负担,项目组开发出SQL数据服务,开发人员通过数据服务提供的SQL支持来获取MongoDB的数据访问。

2. 技术实现

2.1 设计思路

MongoDB是一款文档型数据库,具有高性能、高可用、可水平扩展,且支持多种存储引擎,支持多种查询类型(比如文本检索、数据聚合查询等)。但是MongoDB原生是不支持SQL查询的,有没有办法使得MongDB支持SQL呢,答案是肯定的。

使得MongDB支持SQL的涉及到2个关键的开源组件/模块,即Spark Thrift Server和MongoDB Spark Connector。

Spark Thrift Server可以对外提供服务,客户端通过JDBC连接到服务后,可以向服务端提交SQL,服务端会解析SQL然后将其转化为Spark Job提交到Spark集群,从而转换为底层的基于Spark RDD的分布式计算,得到数据结果后返回给客户端。

MongoDB Spark Connector是一个集成MongoDB和Spark的模块,程序中引入该模块后,可以通过Spark来操作MongoDB,这里我们主要用它来通过Spark查询MongoDB的数据。

于是,可以对Spark Thrift Server进行二次开发,增加如下的功能:

  • 集成MongoDB Spark Connector,可以将MongoDB的表转换为Spark Thrift Server中的DataFrame,然后将DataFrame注册为Spark Thrift Server中Spark SQL的global temp view,然后就可以通过JDBC客户端使用SQL来查询global temp view,从而达到查询MongoDB的数据的目的。

集成MongoDB查询的Spark Thrift Server启动流程:

基于MongoDB的SQL数据服务-鸿蒙开发者社区

JDBC客户端查询MongoDB交互流程:

基于MongoDB的SQL数据服务-鸿蒙开发者社区

2.2 Spark Thrift Server介绍

Spark Thrift Server是Spark项目当中的一个模块,它是Apache Hive的HiveServer2的Spark SQL端口,允许JDBC/ODBC客户端在Apache Spark上通过JDBC和ODBC协议执行SQL查询。 实际上它通过继承Hive的HiveServer2并巧妙地通过覆盖某些方法来实现将Hive SQL查询替换为Spark SQL查询。

github地址:https://github.com/apache/spark

2.3 MongoDB Spark Connector介绍

MongoDB Spark Connector是MongoDB和Spark的连接器,提供MongoDB和Apache Spark之间的集成。 通过连接器,可以访问所有用于MongoDB数据集的Spark库:用于SQL分析的DataSet/DataFrame(受益于自动模式推断)、Spark Streaming、Machine Learning和Spark Graph。 也可以通过Spark Shell来使用MongoDB Spark Connector。

关于MongoDB Spark Connector的更详细的介绍可以参考官方文档:https://www.mongodb.com/docs/spark-connector/current/

github地址:https://github.com/mongodb/mongo-spark

2.4 核心逻辑实现

2.4.1 下载Spark项目源码

从github上下载Spark源代码,并基于v2.4.3版本(也可以基于其他版本,这里以该版本为例)创建开发分支。

git clone git@github.com:apache/spark.git
git checkout v2.4.3
git checkout -b dev_v2.4.3
  • 1.
  • 2.
  • 3.

2.4.2 修改pom.xml

1). 修改spark项目根目录的pom.xml,根据需要修改maven和hadoop的版本,增加MongoDB Spark Connector的依赖。最后,需要增加sql/hive-thriftserver模块,否则将不会编译Spark Thrift Server(默认是不编译的)。

基于MongoDB的SQL数据服务-鸿蒙开发者社区

2). 修改sql/hive-thriftserver/pom.xml,该模块因为需要二次开发增加查询MongoDB的功能,故需要引入MongoDB Spark Connector的依赖。

基于MongoDB的SQL数据服务-鸿蒙开发者社区

2.4.3 新增UdfLoadUtils.scala

新增sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/server/UdfLoadUtils.scala

这个代码就是主要的新增逻辑,用于spark thrift server注册mongodb表和udf函数。

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.spark.sql.hive.thriftserver.server

import scala.collection.mutable.ArrayBuffer
import scala.io.Source
import java.io.File
import com.mongodb.spark._
import com.mongodb.spark.config.ReadConfig
import org.apache.spark.SparkConf
import org.apache.spark.internal.Logging
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.expressions.UserDefinedAggregateFunction
import org.apache.spark.sql.types._


/**
 * @Description: Register spark udf and mongodb tables
 * @author: lixinyang
 * @Date: Created on 2022/03/31
 * @version: v1.0
 */
object UdfLoadUtils extends Logging {

    var udfConfigArray: Array[String] = null
    var mongoTableConfigArray: Array[String] = null
    var mongoTableDLLConfigArray: Array[String] = null
    var mongoUri: String = null
    var collection: String = null

    /**
     * Read config file and parse the configurations.
     * @param spark SparkSession from Spark Thrift Server
     */
    def init(spark: SparkSession): Unit = {
        val sparkConf = spark.sparkContext.getConf

        mongoUri = getSparkConfigValue(sparkConf, "spark.mongodb.input.uri")
        if (mongoUri.isEmpty) throw new RuntimeException("spark.mongodb.input.uri not provided")
        logInfo(s"=====mongoUri config: ${mongoUri}")

        collection = getSparkConfigValue(sparkConf, "spark.mongodb.input.collection")
        if (collection.isEmpty) throw new RuntimeException("spark.mongodb.input.collection not provided")
        logInfo(s"=====collection config: ${collection}")

        val udfConfig = getSparkConfigValue(sparkConf, "spark.udf.config")
        if (udfConfig.isEmpty) throw new RuntimeException("spark.udf.config not provided")
        logInfo(s"=====udf config: ${udfConfig}")
        if (udfConfigArray == null) udfConfigArray = getConfigArray(udfConfig)

        val mongoTableConfig = getSparkConfigValue(sparkConf, "spark.mongo.table.config")
        if (mongoTableConfig.isEmpty) throw new RuntimeException("spark.mongo.table.config not provided")
        logInfo(s"=====mongo table config: ${mongoTableConfig}")
        if (mongoTableConfigArray == null) mongoTableConfigArray = getConfigArray(mongoTableConfig)

        val mongoTableDLLConfig = getSparkConfigValue(sparkConf, "spark.mongo.tableDLL.config")
        if (mongoTableDLLConfig.isEmpty) throw new RuntimeException("spark.mongo.tableDLL.config not provided")
        logInfo(s"=====mongo table dll config: ${mongoTableDLLConfig}")
        if (mongoTableDLLConfigArray == null) mongoTableDLLConfigArray = getConfigArray(mongoTableDLLConfig)

    }

    /**
     * Register UDFs to Spark Thrift Server according to UDF configuration.
     * @param spark SparkSession from Spark Thrift Server
     * @param udfFilter UDF names which will be ignored
     */
    def udfRegister(spark: SparkSession, udfFilter: Set[String] = Set()): Unit = {
        // name,classname,returnType(udf need)
        udfConfigArray.filter(!_.startsWith("#")).foreach(record => {
            val registerInfoArray = record.split(",")
            if (registerInfoArray.size > 1
                    && (udfFilter.isEmpty || udfFilter.contains(registerInfoArray(0)))) {
                logInfo(s"register udf info begins: ${record}")
                try {
                    if (registerInfoArray.size == 2) {
                        val Array(udfName, className) = registerInfoArray
                        val instance = getUDAFInstanceByClass(className)
                        if (instance != null) spark.sqlContext.udf.register(udfName, instance)
                        logInfo(s"register udf info done. : ${record}")
                    } else if (registerInfoArray.size == 3) {
                        val Array(udfName, className, returnType) = registerInfoArray
                        var returnDataType: DataType = null
                        returnType match {
                            // Numeric types
                            case "ByteType" => returnDataType = DataTypes.ByteType
                            case "ShortType" => returnDataType = DataTypes.ShortType
                            case "IntegerType" => returnDataType = DataTypes.IntegerType
                            case "LongType" => returnDataType = DataTypes.LongType
                            case "FloatType" => returnDataType = DataTypes.FloatType
                            case "DoubleType" => returnDataType = DataTypes.DoubleType
                            // String types
                            case "StringType" => returnDataType = DataTypes.StringType
                            // Binary type
                            case "BinaryType" => returnDataType = DataTypes.BinaryType
                            // Boolean type
                            case "BooleanType" => returnDataType = DataTypes.BooleanType
                            // Datetime type
                            case "TimestampType" => returnDataType = DataTypes.TimestampType
                            case "DateType" => returnDataType = DataTypes.DateType
                            case "MapType(StringType:IntegerType)" => returnDataType =
                                DataTypes.createMapType(StringType, IntegerType)
                            case "ArrayType(StringType)" => returnDataType =
                                DataTypes.createArrayType(StringType)
                            case "ArrayType(IntegerType)" => returnDataType =
                                DataTypes.createArrayType(IntegerType)
                            case "ArrayType(LongType)" => returnDataType =
                                DataTypes.createArrayType(LongType)
                            case "ArrayType(FloatType)" => returnDataType =
                                DataTypes.createArrayType(FloatType)
                            case "ArrayType(DoubleType)" => returnDataType =
                                DataTypes.createArrayType(DoubleType)
                            case "ArrayType(ByteType)" => returnDataType =
                                DataTypes.createArrayType(ByteType)
                            case "ArrayType(BooleanType)" => returnDataType =
                                DataTypes.createArrayType(BooleanType)
                            case _ => None
                        }

                        spark.sqlContext.udf.registerJava(udfName, className, returnDataType)

                        logInfo(s"register udf info done. : ${record}")
                    }
                } catch {
                    case ex: Throwable =>
                        logInfo(s"record udf register error, " +
                                s"error info: ${ex.getCause} ...................... ")
                        ex.printStackTrace()

                }

            }
        })
    }

    /**
     * Register MongoDB tables to Spark Thrift Server according to MongoDB configuration.
     * @param spark SparkSession from Spark Thrift Server
     */
    def mongoTableRegister(spark: SparkSession): Unit = {

        val mongoTableConfigArrayDLLNew = mongoTableConfigArrayDLLProcess(
            mongoTableDLLConfigArray.map(_.trim).filter(!_.startsWith("#")))

        val billTableDescDLL = mongoTableConfigArrayDLLNew(0).split("->")(2).trim

        mongoTableConfigArray.filter(!_.startsWith("#")).foreach(record => {
            if (record.split(",").size == 3) {
                var arrays = record.split(",")
                var (caseClassName, tableName, collectionName) = (arrays(0), arrays(1), arrays(2))
                try {
                    logInfo(s"MongoTable record register begins: ${record}")
                    logInfo(s"caseClassName: ${caseClassName}, tableName: ${tableName}, " +
                            s"collectionName: ${collectionName}")

                    val readConfig = ReadConfig(Map("collection" -> collectionName),
                        Some(ReadConfig(spark.sparkContext)))
                    val collection = MongoSpark.load(spark.sparkContext, readConfig)
                            .toDF(StructType.fromDDL(billTableDescDLL))
                    collection.createOrReplaceGlobalTempView(tableName)

                    logInfo(s"MongoTable record register ends:   ${record}")
                } catch {
                    case x: Throwable =>
                        logInfo(s"MongoTable record register error. caseClassName: ${caseClassName}, " +
                                s"tableName: ${tableName}, collectionName: ${collectionName}")
                        x.printStackTrace()
                        logInfo("error info: " + x.getMessage)
                }
            }
        })

        mongoTableConfigArrayDLLNew.foreach(tableDesc => {
            logInfo(s" MongoTable record register from DLL, tableDesc begins: ${tableDesc}")
            try {
                val tableDescArray = tableDesc.split("->").map(_.trim)
                if (tableDescArray.size == 3 && !tableDescArray.contains("billsplit_record_d")) {
                    val tableName = tableDescArray(0)
                    val collectionName = tableDescArray(1)
                    val tableDescDLL = tableDescArray(2)

                    val readConfig = ReadConfig(Map("collection" -> collectionName),
                        Some(ReadConfig(spark.sparkContext)))
                    val collection = MongoSpark.load(spark.sparkContext, readConfig)
                            .toDF(StructType.fromDDL(tableDescDLL))
                    collection.createOrReplaceGlobalTempView(tableName)

                    logInfo(s" MongoTable record register from DLL success, table: ${tableName}")
                }
            } catch {
                case x: Throwable =>
                    logInfo(s"register from DLL error. tableDesc: $tableDesc")
                    x.printStackTrace()
                    logInfo("error info: " + x.getMessage)

            }
            logInfo(s" MongoTable record register from DLL, tableDesc ends: $tableDesc")
        })

    }

    /**
     * Parse the original configuration lines for MongoDB fields description and return a configuration array 
     * that contains fields description for each table.
     * @param array array which contains original configuration lines for MongoDB fields description
     * @return Array[String]
     */
    def mongoTableConfigArrayDLLProcess(array: Array[String]): Array[String] = {

        val sbf = new StringBuffer()
        array.foreach(str => sbf.append(" ").append(str).append(" "))
        val tableDescArray = sbf.toString.split("end#")
        val tableDescArrayNew = tableDescArray.map(tableDesc => {
            if (tableDesc.split("->").size == 3) {
                var tableDescArray = tableDesc.split("->").map(_.trim)
                val tableName = tableDescArray(0)
                val collectionName = tableDescArray(1)
                val tableDescDLL = tableDescArray(2)
                val columnArray = tableDescDLL.split(",")
                val columnArrayProcess = columnArray.filter(_.split(" ").size >= 2).map(column => {
                    val valueArray = column.trim.split(" ")
                    valueArray(0) + " " + valueArray(1)
                })

                if (columnArrayProcess.size != columnArrayProcess.distinct.size) {
                    logInfo(s"mongoTableConfigArrayDLLProcess  !!!!!  :  config file  columnArrayProcess  distinct item! ")
                    columnArrayProcess.map(x => (x, 1)).groupBy(x => x._1).map(x => x._2)
                            .filter(_.length > 1).foreach(x => logInfo(s"distinct item : ${x.map(_._1).mkString(",")} "))
                }

                val value = tableName + " -> " + collectionName + " -> " + columnArrayProcess.distinct.mkString(",")
                logInfo(s"mongoTableConfigArrayDLLProcess  #####: ${value}")
                value
            } else ""
        })
        tableDescArrayNew
    }

    /**
     * Return an instance of UserDefinedAggregateFunction according to UDF class name.
     * @param className class name of UDF
     * @return UserDefinedAggregateFunction
     */
    def getUDAFInstanceByClass(className: String): UserDefinedAggregateFunction = {
        var instance: UserDefinedAggregateFunction = null
        try {
            instance = Class.forName(className).newInstance.asInstanceOf[UserDefinedAggregateFunction]
        } catch {
            case ex: Throwable => {
                logInfo(s" instance ${className} error, error info: ${ex.getCause} ...................... ")
                ex.printStackTrace()
            }
        }
        instance
    }

    /**
     * Read the configuration file and return an array which contains all lines of the configuration file.
     * @param configFile the path which stands for a configuration file.
     * @return Array[String]
     */
    def getConfigArray(configFile: String): Array[String] = {
        val configArray = new ArrayBuffer[String]()
        try {
            val configPath = configFile
            val configFileName = configPath.substring(configPath.lastIndexOf(File.separator) + 1);
            logInfo(s"local ${configFileName}, path: ${configPath}")
            val source = Source.fromFile(configPath)
            val localFiles = source.getLines().toArray
            configArray ++= localFiles
            logInfo(s"local ${configFileName}, path: ${configPath} done!")
        } catch {
            case x: Throwable =>
        }

        configArray.toArray
    }

    /**
     * Return spark conf value according to spark conf key. if sparkConf does not contain confKey,
     * then the default will be returned.
     * @param sparkConf the instance of SparkConf
     * @param confKey a key of sparkConf
     * @param default the default value returned if sparkConf does not contain confKey
     * @return String
     */
    def getSparkConfigValue(sparkConf: SparkConf, confKey: String, default: String = ""): String = {
        var value: String = null;
        if (sparkConf.contains(confKey)) {
            value = sparkConf.get(confKey)
        } else {
            logWarning("not found config key by " + confKey + ".")
            value = default
        }
        value
    }


}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.
  • 227.
  • 228.
  • 229.
  • 230.
  • 231.
  • 232.
  • 233.
  • 234.
  • 235.
  • 236.
  • 237.
  • 238.
  • 239.
  • 240.
  • 241.
  • 242.
  • 243.
  • 244.
  • 245.
  • 246.
  • 247.
  • 248.
  • 249.
  • 250.
  • 251.
  • 252.
  • 253.
  • 254.
  • 255.
  • 256.
  • 257.
  • 258.
  • 259.
  • 260.
  • 261.
  • 262.
  • 263.
  • 264.
  • 265.
  • 266.
  • 267.
  • 268.
  • 269.
  • 270.
  • 271.
  • 272.
  • 273.
  • 274.
  • 275.
  • 276.
  • 277.
  • 278.
  • 279.
  • 280.
  • 281.
  • 282.
  • 283.
  • 284.
  • 285.
  • 286.
  • 287.
  • 288.
  • 289.
  • 290.
  • 291.
  • 292.
  • 293.
  • 294.
  • 295.
  • 296.
  • 297.
  • 298.
  • 299.
  • 300.
  • 301.
  • 302.
  • 303.
  • 304.
  • 305.
  • 306.
  • 307.
  • 308.
  • 309.
  • 310.
  • 311.
  • 312.
  • 313.
  • 314.
  • 315.

2.4.4 修改HiveThriftServer2.scala

修改sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServer2.scala

修改的目的是将2.5.3的新增代码集成到Spark Thrift Server中, 修改的代码行集中在main方法当中,这里仅列出main方法的代码。

  def main(args: Array[String]) {
    Utils.initDaemon(log)
    val optionsProcessor = new HiveServer2.ServerOptionsProcessor("HiveThriftServer2")
    optionsProcessor.parse(args)

    logInfo("Starting SparkContext")
    SparkSQLEnv.init()

    ShutdownHookManager.addShutdownHook { () =>
      SparkSQLEnv.stop()
      uiTab.foreach(_.detach())
    }

    val executionHive = HiveUtils.newClientForExecution(
      SparkSQLEnv.sqlContext.sparkContext.conf,
      SparkSQLEnv.sqlContext.sessionState.newHadoopConf())

    try {
      val server = new HiveThriftServer2(SparkSQLEnv.sqlContext)
      server.init(executionHive.conf)
      server.start()
      logInfo("HiveThriftServer2 started")
      listener = new HiveThriftServer2Listener(server, SparkSQLEnv.sqlContext.conf)
      SparkSQLEnv.sparkContext.addSparkListener(listener)
      uiTab = if (SparkSQLEnv.sparkContext.getConf.getBoolean("spark.ui.enabled", true)) {
        Some(new ThriftServerTab(SparkSQLEnv.sparkContext))
      } else {
        None
      }

      //added by lixinyang on 2022-03-31
      //register udf and mongodb tables after spark thrift server started.
      logInfo("===== begin to register udf and mongodb tables")
      val sparkSession = SparkSQLEnv.sqlContext.sparkSession
      UdfLoadUtils.init(sparkSession)
      UdfLoadUtils.udfRegister(sparkSession)
      UdfLoadUtils.mongoTableRegister(sparkSession)
      logInfo("===== end to register udf and mongodb tables")

      // If application was killed before HiveThriftServer2 start successfully then SparkSubmit
      // process can not exit, so check whether if SparkContext was stopped.
      if (SparkSQLEnv.sparkContext.stopped.get()) {
        logError("SparkContext has stopped even if HiveServer2 has started, so exit")
        System.exit(-1)
      }
    } catch {
      case e: Exception =>
        logError("Error starting HiveThriftServer2", e)
        System.exit(-1)
    }
  }

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.

2.4.5 项目编译打包

1). 在项目根目录下执行如下命令,打包项目。

mvn clean
mvn package -X -Denforcer.skip=true -DskipTests -f pom.xml
  • 1.
  • 2.

2). 生成spark二进制安装包(在spark项目根目录下执行如下命令)

find ./ -name *.sh | xargs chmod +x
find ./ -name "mvn" | xargs chmod +x
./dev/make-distribution.sh --name custom-spark --pip --tgz -Pyarn -Phadoop-2.7 -Dhadoop.version=2.7.3 -Phive -Phive-thriftserver -DskipTests
  • 1.
  • 2.
  • 3.

因为spark-2.4.3与hadoop3.X不兼容,只好用hadoop2.7,且因为生成环境的hadoop是3.1版本,所以这里编译生成spark二进制包的时候需要将hadoop依赖也打包进来。另外需要同时指定hive和hive-thriftserver的profile,否则将不会编译Spark Thrift Server. 因为Spark项目很大,编译并生成二进制安装包需要较长的时间,请耐心等待(根据机器配置不同耗时会有所不同,一般情况下会需要1个小时左右)。

上述命令执行完成后,会在spark项目根目录下生成spark-2.4.3-bin-custom-spark.tgz文件,这个就是spark的二进制安装包。后续的安装部署是基于这个包来进行的。

2.5 程序部署

2.5.1 配置文件

1). mongotable.config

#caseClassName,tableName,collectionName

Test,stu_table,stu_collection
  • 1.
  • 2.
  • 3.

2). mongotabledll.config

#tableName,collectionName,fieldsDesc
stu_table -> stu_collection ->
_id String,
range_key Int,
stu_name String,
age Int,
class_no Int
end#

class_table -> class_collection ->
_id String,
range_key Int,
class_no String,
class_name Int
end#
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

3). udf.config

#functionName,className,ReturnType
mongoupdate,org.example.data.udf.common.udf.MongoRecordListSink,StringType
  • 1.
  • 2.

2.5.2 启动脚本

run9994.sh(这里以提交到Spark Standalone集群为例,如果是提交到Yarn集群,需要将–master改为yarn,且删掉–total-executor-cores,并增加–queue, --deploy-mode, --num-executors 等参数。脚本中的参数仅供参考,发布到生产环境后应该根据实际服务器资源和业务负载做相应调整。)

export HADOOP_CONF_DIR=/usr/hdp/3.1.0.0-78/hadoop/conf/
export SPARK_HOME=/home/spark-thrift-server/spark-2.4.3-bin-custom-spark

cd $SPARK_HOME/home/conf

$SPARK_HOME/sbin/start-thriftserver.sh \
--hiveconf hive.server2.thrift.port=9994 \
--hiveconf hive.server2.thrift.bind.host=0.0.0.0 \
--hiveconf hive.server2.idle.session.timeout=0 \
--hiveconf hive.server2.idle.operation.timeout=-3600000 \
--hiveconf hive.server2.session.check.interval=3000 \
--conf spark.sql.hive.thriftServer.singleSession=true \
--conf spark.sql.codegen=true \
--conf spark.sql.shuffle.partitions=36 \
--conf spark.locality.wait.process=0 \
--conf spark.locality.wait.node=0 \
--conf spark.locality.wait.rack=0 \
--conf spark.hadoop.mapreduce.input.fileinputformat.input.dir.recursive=true \
--conf spark.hive.mapred.supports.subdirectories=true \
--conf spark.mongodb.input.uri="mongodb://dbuser:dbpassword@127.0.0.1:27017/db" \
--conf spark.mongodb.input.collection="stu_collection" \
--conf spark.mongodb.input.partitioner='MongoShardedPartitioner' \
--conf spark.mongodb.input.partitionerOptions.shardkey='range_key' \
--conf spark.udf.config="$SPARK_HOME/home/conf/udf.config" \
--conf spark.mongo.table.config="$SPARK_HOME/home/conf/mongotable.config" \
--conf spark.mongo.tableDLL.config="$SPARK_HOME/home/conf/mongotabledll.config" \
--conf spark.driver.extraJavaOptions="-Dlog4j.configuration=log4j.properties" \
--conf spark.executor.extraJavaOptions="-Dlog4j.configuration=log4j-executor.properties" \
--master spark://master:7077 \
--driver-memory 6g \
--executor-memory 16g \
--executor-cores  40 \
--total-executor-cores 480   \
--jars $SPARK_HOME/jars/dataservice-data-udf.jar,$SPARK_HOME/jars/mongo-java-driver-3.12.5.jar,$SPARK_HOME/jars/mongo-spark-connector_2.11-2.4.3.jar,$SPARK_HOME/jars/mysql-connector-java-8.0.16.jar,$SPARK_HOME/jars/mongo-spark-1.0-shaded.jar

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.

2.5.3 安装部署

1). 上传2.5.5节中生成的spark部署包spark-2.4.3-bin-custom-spark.tgz到CentOS服务器的某个目录下,执行如下命令

tar -zxvf spark-2.4.3-bin-custom-spark.tgz
cd spark-2.4.3-bin-custom-spark
  • 1.
  • 2.

2). 接着上传home.tar.gz, jars.tar.gz到该目录,并解压。分别表示spark thrift server需要用到的配置文件和启动脚本(就是上面2.6.1和2.6.2的配置文件和启动脚本,需要根据实际情况修改)和程序所依赖的一些第三方jar包。

tar -zxvf home.tar.gz
tar -zxvf jars.tar.gz
rm -f home.tar.gz jars.tar.gz
#创建日志目录
mkdir logs
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

home.tar.gz的内容:

基于MongoDB的SQL数据服务-鸿蒙开发者社区

基于MongoDB的SQL数据服务-鸿蒙开发者社区

jars.tar.gz的内容:

基于MongoDB的SQL数据服务-鸿蒙开发者社区

3). 启动程序

这里的例子是Spark Thrift Server运行在Spark Standalone集群,故需要先启动集群,再启动Spark Thrift Server;如果是运行在Yarn集群中,且Yarn集群是已经启动的话,则只需要启动Spark Thrift Server,并需要相应修改启动脚本run9994.sh.

#启动Spark Standalone集群
cd $SPARK_HOME
./sbin/start-all.sh
#启动Spark Thrift Server
cd $SPARK_HOME/home/bin
./run9994.sh
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

2.6 程序测试

上面2.6程序部署和启动完成后,我们用DbVisualizer通过JDBC连接到Spark Thrift Server.

基于MongoDB的SQL数据服务-鸿蒙开发者社区

2.6.1 查询测试

1). 查询mongodb测试1(耗时1.6s)

select * from global_temp.stu_table limit 10;
  • 1.

基于MongoDB的SQL数据服务-鸿蒙开发者社区

2). 查询mongodb测试2(耗时1.2s)

select * from global_temp.class_table 
where _id='20210600_18606952';
  • 1.
  • 2.

基于MongoDB的SQL数据服务-鸿蒙开发者社区

2.7 开发Rest接口提供SQL查询MongoDB服务

根据上面部署成功的Spark Thrift Server, 为了进一步方便客户使用,可以在此基础上开发Java程序,此程序后台通过JDBC连接到Spark Thrift Server,并对外提供Rest接口服务。客户可以通过调用此Rest接口,将SQL语句提交到Spark Thrift Server,Spark Thrift Server查询到MongoDB的数据后,Java程序将数据通过Rest接口返回给客户。限于篇幅,这里就不再展开论述了。仅提供一个接口测试的截图,供大家参考。

基于MongoDB的SQL数据服务-鸿蒙开发者社区

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2022-6-2 09:22:38修改
5
收藏 4
回复
举报
5
4


回复
    相关推荐