Nebula & 数仓血缘关系数据的存储与读写

ywz888
发布于 2022-10-10 16:19
浏览
0收藏

我是数据开发的实习生,在这个岗位上工作四个月左右的时间了,期间负责开发数据平台的功能

我是数据开发的实习生,在这个岗位上工作四个月左右的时间了,期间负责开发数据平台的功能。


因为工作中涉及的一些数据的读写性能较低,所以在调研后,选择部署一个 Nebula 集群。之所以选择 Nebula, 是因为它的技术体系比较成熟,社区也比较完善,对刚刚接触的它的人非常友好,所以很快就开始投入使用了。在使用过程中,有一些自己的见解,和遇到的一些问题及解决方法,在这里向大家分享一下自己的使用经验。




>>>>  选择 Nebula 的原因




性能优越

  • 查询速度极快
  • 架构分离,易扩展(目前的机器配置低,后续可能扩展)
  • 高可用(由于是分布式,所以从使用到现在没有出现过宕机情况)




上手容易

  • 介绍全(熟悉架构和性能)
  • 部署快(经过手册的洗礼,快速部署简单的集群)
  • 使用简便(遇到需要的数据,查询手册获取对应的 nGQL,针对性查询)
  • 答疑优秀(遇到问题,可以先翻论坛,如果没有,那就发布帖子,开发人员的帮助很及时)




开源且技术稳定

  • 因为实践企业多,用起来也放心。

>>>>

业务需求背景介绍

为方便数据治理、元数据管理及数据质量监控,将调度系统生成的数仓血缘保存起来。




血缘数据流程

从采集、存储到平台展示的数据全流程:

Nebula & 数仓血缘关系数据的存储与读写-鸿蒙开发者社区

在查询平台的部分数据查询展示:

Nebula & 数仓血缘关系数据的存储与读写-鸿蒙开发者社区

>>>>

我的具体实践




版本选择

Nebula Graph v3.0.0、Nebula Java v3.0.0
Nebula Graph 和 Java 客户端两个相兼容的版本。




集群部署

  • 机器配置:
    四台实体机,同配置:
    10C * 2 / 16G * 8 / 600G

  • 安装方式
    RPM 安装。
  • 通过 wget 拉取安装包后安装。
  • 更改配置文件

主要更改参数

Meta 服务的所有机器 —— meta_server_addrs=ip1:9559, ip2:9559, ip3:9559

当前机器 ip(如果是 meta/graph/storage,填对应 meta/graph/storage 机器的 ip) —— local_ip

  • 启动后通过 console 简单测试 add hosts ip:port 增加自己的机器 ip 后,show hosts,如果是 online,即可开始测试相关 nGQL。




数据导入

目前分两种情况更新数据。

  • 实时监控调度平台
  • 监控每个任务实例,通过依赖节点获取上下游的关系,将关系实时打入到MySQL 和 Nebula 中,更新 Nebula 数据通过 Spark Connector 实现。
    (MySQL 做备份,因为 Nebula 不支持事务,可能存在数据偏差)
  • 定时调度校正数据
  • 通过 MySQL 中的血缘关系,通过 Spark 任务定时校正 Nebula 数据,更新数据同样通过 Spark Connector 实现。
  • Spark Connector 的使用:NebulaConnectionConfig 初始化配置,然后通过连接信息、插入的点与边的相关参数及实体 Tag、Edge 创建 WriteNebulaVertexConfig 和 WriteNebulaEdgeConfig 对象,以备写入点和边的数据。




数据平台查询

数据平台查询血缘的应用:

  • 获取 Nebula 数据实现过程
  • 通过初始化连接池 Nebula pool,实现单例工具类,方便在整个项目中调用并使用 Session。
  • 这里一定要注意,连接池只可以有一个,而 Session 可以通过 MaxConnectionNum 设置连接数,根据实际业务来判断具体参数(平台查询越频繁,连接数就要设置的越多一些)。而每次 Session 使用完毕也是要释放的。
  • 查询数据,转换为 ECharts 需要的 JSON
  • 通过 getSubGraph 获取当前表或字段的所有上下游相关点,这一点通过获取子图的方法,很方便。
  • 需要通过结果,解析出其中两个方向数据的点,然后递归解析,最后转为一个递归调用自己的 Bean 类对象。
  • 写一个满足前端需要的 JSON 串的 toString 方法,得到结果后即可。

这里分享一下自己的工具类,和核心逻辑代码

工具类:

object NebulaUtil {

  private val log: Logger = LoggerFactory.getLogger(NebulaUtil.getClass)

  private val pool: NebulaPool = new NebulaPool

  private var success: Boolean = false

  {
    //首先初始化连接池

    val nebulaPoolConfig = new NebulaPoolConfig
    nebulaPoolConfig.setMaxConnSize(100)


    // 初始化ip和端口
    val addresses = util.Arrays.asList(new HostAddress("10.xx.xx.88", 9669))
    success = pool.init(addresses, nebulaPoolConfig)

  }

  def getPool(): NebulaPool = {
    pool
  }

  def isSuccess(): Boolean = {
    success
  }

  //TODO query: 创建空间、进入空间、创建新的点和边的类型、插入点、插入边、执行查询
  def executeResultSet(query: String, session: Session): ResultSet = {

    val resp: ResultSet = session.execute(query)
    if (!resp.isSucceeded){
      log.error(String.format("Execute: `%s', failed: %s", query, resp.getErrorMessage))
      System.exit(1)
    }

    resp
  }

  def executeJSON(queryForJson: String, session: Session): String = {

    val resp: String = session.executeJson(queryForJson)
    val errors: JSONObject = JSON.parseObject(resp).getJSONArray("errors").getJSONObject(0)
    if (errors.getInteger("code") != 0){
      log.error(String.format("Execute: `%s', failed: %s", queryForJson, errors.getString("message")))
      System.exit(1)
    }

    resp
  }


  def executeNGqlWithParameter(query: String, paramMap: util.Map[String, Object], session: Session): Unit = {

    val resp: ResultSet = session.executeWithParameter(query, paramMap)
    if (!resp.isSucceeded){
      log.error(String.format("Execute: `%s', failed: %s", query, resp.getErrorMessage))
      System.exit(1)
    }

  }



  //获取ResultSet中的各个列名及数据
  //_1 列名组成的列表
  //_2 多row组成的列表嵌套    单个row的列表 包含本行每一列的数据
  def getInfoForResult(resultSet: ResultSet): (util.List[String], util.List[util.List[Object]]) = {

    //拿到列名
    val colNames: util.List[String] = resultSet.keys

    //拿数据
    val data: util.List[util.List[Object]] = new util.ArrayList[util.List[Object]]

    //循环获取每行数据
    for (i <- 0 until resultSet.rowsSize) {
      val curData = new util.ArrayList[Object]
      //拿到第i行数据的容器
      val record = resultSet.rowValues(i)
      import scala.collection.JavaConversions._

      //获取容器中数据
      for (value <- record.values) {
        if (value.isString) curData.add(value.asString)
        else if (value.isLong) curData.add(value.asLong.toString)
        else if (value.isBoolean) curData.add(value.asBoolean.toString)
        else if (value.isDouble) curData.add(value.asDouble.toString)
        else if (value.isTime) curData.add(value.asTime.toString)
        else if (value.isDate) curData.add(value.asDate.toString)
        else if (value.isDateTime) curData.add(value.asDateTime.toString)
        else if (value.isVertex) curData.add(value.asNode.toString)
        else if (value.isEdge) curData.add(value.asRelationship.toString)
        else if (value.isPath) curData.add(value.asPath.toString)
        else if (value.isList) curData.add(value.asList.toString)
        else if (value.isSet) curData.add(value.asSet.toString)
        else if (value.isMap) curData.add(value.asMap.toString)
      }
      //合并数据
      data.add(curData)
    }

    (colNames, data)
  }

  def close(): Unit = {
    pool.close()
  }

}

核心代码:

 //bean next 指针为可变数组
  //获取子图
  //field_name 起始节点, direct 子图方向(true 下游, false 上游)
  def getSubgraph(field_name: String, direct: Boolean, nebulaSession: Session): FieldRely = {

    // field_name 所在节点
    val relyResult = new FieldRely(field_name, new mutable.ArrayBuffer[FieldRely])

    // out 为下游, in 为上游
    var downOrUp = "out"
    // 获取当前查询的方向
    if (direct){
      downOrUp = "out"
    } else {
      downOrUp = "in"
    }

    //1 查询语句 查询下游所有子图
    val query =
      s"""
         | get subgraph 100 steps from "$field_name" $downOrUp field_rely yield edges as field_rely;
         |""".stripMargin

    val resultSet = NebulaUtil.executeResultSet(query, nebulaSession)

    //[[:field_rely "dws.dws_order+ds_code"->"dws.dws_order_day+ds_code" @0 {}], [:field_rely "dws.dws_order+ds_code"->"tujia_qlibra.dws_order+p_ds_code" @0 {}], [:field_rely "dws.dws_order+ds_code"->"tujia_tmp.dws_order_execution+ds_code" @0 {}]]
    //非空则获取数据
    if (!resultSet.isEmpty) {
      //非空,则拿数据,解析数据
      val data = NebulaUtil.getInfoForResult(resultSet)
      val curData: util.List[util.List[Object]] = data._2

      //正则匹配引号中数据
      val pattern = Pattern.compile("\"([^\"]*)\"")

      // 上一步长的所有节点数组
      // 判断节点的父节点, 方便存储
      var parentNode = new mutable.ArrayBuffer[FieldRely]()


      //2 首先获取步长为 1 的边
      curData.get(0).get(0).toString.split(",").foreach(curEdge =>{
        //拿到边的起始和目的点
        val matcher = pattern.matcher(curEdge)
        var startPoint = ""
        var endPoint = ""

        //将两点赋值
        while (matcher.find()){
          val curValue = matcher.group().replaceAll("\"", "")
          // 上下游的指向是不同的 所以需要根据上下游切换 开始节点和结束节点的信息获取
          // out 为下游, 数据结构是 startPoint -> endPoint
          if(direct){
            if ("".equals(startPoint)){
              startPoint = curValue
            }else{
              endPoint = curValue
            }
          }else {
            // in 为上游, 数据结构是 endPoint -> startPoint
            if ("".equals(endPoint)){
              endPoint = curValue
            }else{
              startPoint = curValue
            }
          }

        }
        //合并到起点 bean 中
        relyResult.children.append(new FieldRely(endPoint, new ArrayBuffer[FieldRely]()))
      })

      //3 并初始化父节点数组
      parentNode = relyResult.children




      //4 得到其余所有边
      for (i <- 1 until curData.size - 1){
        //储存下个步长的父节点集合
        val nextParentNode = new mutable.ArrayBuffer[FieldRely]()
        val curEdges = curData.get(i).get(0).toString

        //3 多个边循环解析, 拿到目的点
        curEdges.split(",").foreach(curEdge => {

          //拿到边的起始和目的点
          val matcher = pattern.matcher(curEdge)
          var startPoint = ""
          val endNode = new FieldRely("")

          //将两点赋值
          while (matcher.find()){
            val curValue = matcher.group().replaceAll("\"", "")
//            logger.info(s"not 1 curValue: $curValue")
            if(direct) {
              if ("".equals(startPoint)){
                startPoint = curValue
              }else{
                endNode.name = curValue
                endNode.children = new mutable.ArrayBuffer[FieldRely]()
                nextParentNode.append(endNode)
              }
            }else {
              if ("".equals(endNode.name)){
                endNode.name = curValue
                endNode.children = new mutable.ArrayBuffer[FieldRely]()
                nextParentNode.append(endNode)
              }else{
                startPoint = curValue
              }
            }

          }

          //通过 startPoint 找到父节点, 将 endPoint 加入到本父节点的 children 中
          var flag = true
          //至此, 一条边插入成功
          for (curFieldRely <- parentNode if flag){
            if (curFieldRely.name.equals(startPoint)){
              curFieldRely.children.append(endNode)
              flag = false
            }
          }

        })

        //更新父节点
        parentNode = nextParentNode
      }

    }
//    logger.info(s"relyResult.toString: ${relyResult.toString}")
    relyResult
  }

Bean toString 代码:

class FieldRely {

  @BeanProperty
  var name: String = _  // 当前节点字段名

  @BeanProperty
  var children: mutable.ArrayBuffer[FieldRely] = _  // 当前节点对应的所有上游或下游子字段名

  def this(name: String, children: mutable.ArrayBuffer[FieldRely]) = {
    this()
    this.name = name
    this.children = children
  }

  def this(name: String) = {
    this()
    this.name = name
  }


  override def toString(): String = {
    var resultString = ""
    //引号变量
    val quote = "\""

    //空的话直接将 child 置为空数组的json
    if (children.isEmpty){
      resultString += s"{${quote}name${quote}: ${quote}$name${quote}, ${quote}children${quote}: []}"
    }else {
      //child 有数据, 添加索引并循环获取
      var childrenStr = ""
//      var index = 0

      for (curRely <- children){
        val curRelyStr = curRely.toString
        childrenStr += curRelyStr + ", "
//        index += 1
      }

      // 去掉多余的  ', '
      if (childrenStr.length > 2){
        childrenStr = childrenStr.substring(0, childrenStr.length - 2)
      }

      resultString += s"{${quote}name${quote}: ${quote}$name${quote}, ${quote}children${quote}: [$childrenStr]}"
    }
    resultString
  }
}




得到的优化结果

在查询子图步长接近 20 的情况下,基本上接口返回数据可以控制在 200ms 内(包含后端复杂处理逻辑)

>>>>

结语

本文为 Nebula 社区用户 shixingr 参与 Nebula 社区首届征文活动 🔗 的原创文章,希望这篇文章能够在 Nebula 的使用上切实帮助到大家,也欢迎大家来围观~🥳


文章转载自公众号:Nebula Graph Community

分类
标签
已于2022-10-10 16:19:34修改
收藏
回复
举报
回复
    相关推荐