使用graphx进行图计算及相关事项梳理(持续更新)

630 阅读4分钟

一、前言

之前一直使用图数据库封装好的api进行图关系查询与计算,最近由于评估新业务,选择直接使用graphx进行计算,当然接下来也会将数据导入nebula进行对比看看。目前梳理了下进度。

需求:算法组同学提供给我一些用户id表,里面具有一些用户注册时的id/手机号,支付时验证的身份证/手机号,以及设备认证实名是同一个人等等的关系数据。比如说,有些用户使用一个手机号注册的两个业务账号,以及使用了同一个设备登陆过的两个uid/支付认证的关联手机号相同。如果可以挖掘出它们之间的关系,在用户使用新id登陆业务app时,推荐业务流就会有所记录,按着之前的推荐策略推送。

考虑: 如果把业务数据之间的关系构建为图,这些关系数据都是图中的一条边,那么相互有身份/设备/信息关联的用户uid就构成了一个小图,在一个小图中的uid都是具有信息关联的,我们需要把它找出来进行分析。换言之,在整个图数据库中存在的是相互关联的一个个连通子图。ok 那就找出它们,然后评估下数据。graphx封装了连通子图算法,我们只需要建起数据对接graohx即可进行运算。

二、graphx使用事项

使用graphx的优势是封装的经典算法多,提供spark接口,且我们需要的连通子图算法也封装其中。为了快速评估数据,决定使用graphx进行计算。不过在过程中也产生几个问题,这里主要考虑以下几个问题:

  1. 本次评估的目的是找出相互关联的数据,那么无论是A关联了B,还是B关联了A,我们都认为是有关系的,因此边的方向我们需要设为无向边,也就是双向边存储。也因此,我们使用graphx需要找出弱联通分量(weakly connected component)即可。联通分量算法(Connected Component)计算得到的是弱联通分量。 这里简单申述下连通图:

    • 在无向图G中,若V(G)中任意两个不同的顶点vi和vj都连通(即有路径),则称G为连通图(Con-nected Graph)
    • 有向图的底图(无向图)是连通图,则是弱连通图。简单来说就是把所有有向边变成无向边,若此时图连通,则是弱连通图。
  2. graphx中进行计算的所有点边均为LongID,但是我们的业务数据都为字符型数据。因此我们需要将业务数据编排出唯一的longid,这就需要将所有点数据进行汇总去重再加以索引,赋予每个点唯一id的映射。如果我每次都是自己来处理点id,那我就需要一直管理/增加id映射的关系。这里就体现出了使用图数据管理图数据的好处,可将原始数据导入,自动分配id,且nebula可对接graphx进行导出数据进行计算。 这里介绍一下nebula的做法:

    • nebula spark-tools将数据读取出为rdd后经过hash处理映射为LongID的类型,以便导入graphx进行计算。这里采用的是MurmurHash2算法,十亿数据冲突几率为1/10,缺点就是数据量大时冲突率大,且需要自行维护映射。否则就需要自己改nebula的graphx部分,将MurmurHash2算法改为其他或者直接给index,然后记录vertex-id映射列。

    image.png 这里是nebula做法:

def loadEdgesToGraphx(): RDD[NebulaGraphxEdge] = {
  val edgeDataset = loadEdgesToDF()
  implicit val encoder: Encoder[NebulaGraphxEdge] =
    Encoders.bean[NebulaGraphxEdge](classOf[NebulaGraphxEdge])

  val fields = edgeDataset.schema.fields
  edgeDataset
    .map(row => {
      val props: ListBuffer[Any] = ListBuffer()
      for (i <- row.schema.fields.indices) {
        if (i != 0 && i != 1 && i != 2) {
        // 源df为  src  tar prot1 prot2....
        //这里将prot 存入一个list
          props.append(row.get(i))
        }
      }
      val srcId = row.get(0).toString.getBytes()
      val dstId = row.get(1).toString.getBytes()
      val edgeSrc = if (row.schema.fields(0).dataType == LongType) {
        srcId.toString.toLong
      // 注意: 若源点id不是long类型id,则将其进行哈希返回一个long id,注意在哈希过程前后并未将源数 
      // 据与哈希后数据对应起来,graphx计算后无法根据其哈希值解密出原始点id,如果需要原始id,则在此函
      // 数执行完毕后需要进行源点和哈希过的long id的映射记录,以便对结果进行分析。
      } else {
        MurmurHash2.hash64(srcId, srcId.length, 0xc70f6907)
      }
      val edgeDst = if (row.schema.fields(0).dataType == LongType) {
        dstId.toString.toLong
      } else {
        MurmurHash2.hash64(dstId, dstId.length, 0xc70f6907)
      }
      val edgeProp = (row.get(2).toString.toLong, props.toList)
     // 将其构建为graphx需要的边rdd
      org.apache.spark.graphx
      // 由src tar prot构建edgeRdd
        .Edge(edgeSrc, edgeDst, edgeProp)
    })(encoder)
    .rdd
}
  1. 进行graphx计算过程,会消耗大量内存,尽管我给出了非常多的内存资源,但是苦于有70亿的数据,graphx并不适合如此体量的图计算,不过好在cc算法相对不吃内存,但如果不将数据落盘,依然分分钟oom,因此,在构建过程中,要记得将过程及结果数据全部落磁盘,而不要使用其默认的内存,并尽可能的多给资源。加快shuffle过程
    Graph.fromEdgeTuples(edgeRdd.rdd, "-1", None, StorageLevel.DISK_ONLY, StorageLevel.DISK_ONLY)
    

三、graphx经典算法

3.1 PageRank:

谷歌的搜索排名算法,可参考pagerank出处The PageRank Citation Ranking ,虽然发展到现在已经有了很多改进,且不会完全的依靠pr进行排名,但是其意义仍然很大,论文有相当一部分在陈述PageRank的定义,作用和实践,具体算法细节也可以看zhuanlan.zhihu.com/p/137561088 这篇文章教材,关于pr的计算部分比较详尽。 这里总结几点便于记忆:

  1. 离线多轮迭代计算,图固定,最终PageRank(下称pr)值会经历多轮迭代矩阵运算收敛到一个固定的值(顺便回忆了下马尔可夫链)
  2. 对于只有一条入边且互相链接到的(C->A,A->B,B->A) 无出边的网页,属于rank sink,不计算其pr值。
  3. 一个url的pr值和他的入边有关,而和出边无关。如对于url A ,从其他网页跳转到A的数量越多,且从其他网页跳转到A的url的pr值越高,A的pr值就越高。