四两拨千斤:借助Spark GraphX将QQ千亿关系链计算提速20倍

腾讯QQ有着国内最大的关系链,而共同好友数,属于社交网络分析的基本指标之一,是其它复杂指标的基础。借助Spark GraphX,我们用寥寥100行核心代码,在高配置的TDW-Spark集群上,只花了2个半小时,便完成了原来需要2天的全量共同好友计算。这标志着QQ千亿级别的关系链计算进入了小时级别时代,并具备复杂图模型的快速计算能力。

问题描述

共同好友数可以用于刻画用户与用户间的关系紧密程度,包括 陌生人/熟人分析,好友亲密度,好友推荐,社团划分等各个方面,是社交网络分析的最基础指标。其计算逻辑非常简单明了。为了简化模型和降低计算量,这里加了几个约束:

  1. 只有好友之间才进行计算
  2. 好友关系是有向的
  3. 不关注具体的好友

显而易见,用户5和6的共同好友数为4。这个计算貌似非常简单,但是当图的规模扩展到腾讯的级别:用户数(点)为十亿级别,关系数(边)为千亿级别时,那这个问题就一点都不简单了。

大致估算一下,假设每个节点平均的好友数是100,每个点id为Long型,占用8个字节,如果用普通Join计算的话,那么中间的数据量大概是1 billion* 800*800B=640TB,需要通过网络传输640TB的数据,这个量非常的恐怖了,想在一个SQL中完成,几乎是不可能的。原来旧的计算方式,只能通过分而治之的方法实现。通过把关系链表,按号段拆分60份,分别连接用户好友全量表,分成多个SQL任务运行。这样做每个SQL任务都需要载入一次全量关系链,磁盘 I/O 时间严重拖慢计算进度,整个过程需要耗费超过两天的计算时间。

其实共同好友数是典型的图问题之一,因此很自然的,我们想到了引入新的图计算框架和模型,来优化计算过程,让这个过程更加高效和科学。

框架选择

目前的分布式图计算框架,并没有太多好的选择,在过去一年半中,业界的分布式图计算框架,进展基本停滞。经过反复选择,我们还是选择了GraphX,主要原因有如下3个:

  1. 进展
    • 虽然GraphX本身没什么进展,但是Spark本身的发展很快,从1.4到1.6版本,Spark Core在性能和稳定性上有了不少的提升。GraphX某种程度上,多多少少还是得到了好处
  2. 语义
    • GraphX的语义和运算符相对丰富,可以进行比较好的图算法描述,适合变化多样的图需求
  3. 门槛
    • GraphX的最大消耗是内存,某种程度上,这是个比较低门槛的投入,可以在预期内得到解决

基于这样的考虑,我们选择了GraphX作为共同好友算法的底层框架,并从软件和硬件两方面入手,进行了一系列的优化。

模型简化

基于GraphX的图模型思想,我们进行数好友模型的简化。整个过程分为两个阶段:

  • Phrase1——找邻居 这个阶段,其实就是一放一收,和Map-Reduce模型有异曲同工之妙,分3步 1. 每个顶点,将自己的id,发送给自己所有的邻居 2. 每个顶点,将收到的所有邻居id,合并为一个List 3. 对新List进行排序,并和原来的图进行关联,附到顶点之上

这个阶段,使用GraphX的aggregateMessages,定义好sendMsg和mergeMsg的方法,就可轻松实现。

  • Phrase2——数好友 这个阶段,只要一步,但是非常的关键,而且计算量也非常大,需要充分的利用了Triplet的特性 1. 遍历所有的Triplet,对2个好友的有序好友List进行扫描匹配,数出共同好友数,并将其更新到edge之上

这个阶段,需要充分利用GraphX的Triplet特性。需要为了实现Triplet功能,GraphX在设计上消耗了很多的内存,无论我们是否使用,它都在那里,静静的占用着内存,所以我们要充分利用好它,将数好友的过程,简化为一次Triplet的遍历。 如果没有这个特性,为了数好友之间的共同好友,就要把一个好友的所有一度好友,发送到它所有的邻居之上,这样的消息广播量,是非常巨大的,会形成消息风暴,相当于计算二跳邻居。根据我们在腾讯数据量上的测试,这个在现有的硬件下,是无法实现的。

这2个阶段经过最终优化之后,代码非常的简洁,只要聊聊的数十行代码,便完成了20亿级别的点,千亿级别的边的共同好友计算,可谓于无声处听惊雷。将众多的技术难点,都通过GraphX的优化,化解消弭于无形之中。由于产品的需求,这个计算需要是精确数,而不能是近似值,在数好友的过程中,很多的优化方法,不能被用上,否则的话,可以进一步的提升速度。

硬件选择

在分布式系统中,往往会倾向于用大量的小低配机器,来完成巨大的计算任务。其思想是即便再复杂的计算,只要将大数据,分解为足够小的数据片,总能在足够多的机器上,通过性能的降低和时间的拖延,来完成计算任务。但是很遗憾,在图计算这样的场景下,尤其是GraphX的设计框架,这个是行不通的。

要发挥GraphX的最佳性能,最少要有128G以上的内存

主要原因有两个是:

  1. 节点复制——越小越浪费 GraphX使用了点切割的方式,这是一种用空间换时间的方法,通过将浪费一定的内存,将点和它的邻居放到一起,减少Executor之间的通信。 如果用小内存的Executor来运行图算法,假设1个节点,需要10个Executor才能放下它的邻居,那么它就需要被复制10份,才能进行计算。如果用大内存的Executor,1个Executor就能放下它的所有邻居,理论上它就只需要被复制一次,大大减少空间占用。
  2. 节点膨胀——越小越慢 图计算中,常常会进行消息的扩散和收集,并把最终的结果,汇总到单个节点之上。 以共同好友数模型为例,第一步需要将节点的一跳好友都收集到该节点上。即便根据邓巴的“150定律”,将一跳好友的个数,限制在150之内。那么图的占用空间,还是很可能会膨胀150倍。 这个时候,如果内存空间不够的话,GraphX为了容下所有的数据,会需要在节点之间,进行大量的Shuffle和Spill操作,使得后续的计算,变得非常慢。

其实这两个问题,在Spark的其它机器学习算法中,或多或少都会有,也是分布式计算系统中,经常面临的问题。但是在图计算中,它们是无法被忽略的问题,而且非常的严重。所以,这决定了GraphX需要大的内存,才能有良好的性能。

在正常情况下,128G内存,减掉8G的系统占用,剩下120G。这时配置每个Executor 60G内存,2个Core,每个Core分到30G的内存。这时不需要申请太多的Executor,经过合理的性能优化,全量关系链计算,可以运行成功。

性能优化

即便有了良好的模型和硬件保障,在面对QQ如此巨型的关系链时,依然需要熟练运用GraphX的技巧,并避开各种雷区,才能最终到达终点。简单总结两点:

  1. 图缓存:To Cache or not to Cache? That is a question Spark和GraphX原本设计的精妙之处,亮点之一,便在于Cache,也就Persist(MEMORY_ONLY),或者Persist(MEMORY_AND_DISK)。可以把RDD和Graph,Cache到内存中,方便多次调用而无需重新计算。那么是否对所有的RDD,或者图,都Cache一下,是最佳的选择呢?答案是未必。 判断是否要Cache一个Graph或RDD,最简单和重要的标准,就是 该Graph,是否会在后续的过程中,被直接使用多次,包括迭代。 如果会,那么这个Graph就要被Persist,然后通过action触发 如果不会,那么反过来,最好把这个Graph直接unpersist掉 一个Graph被Cache的话,一般最终体现为2个RDD的Cache,一个是Edge,一个是Vertex,其占用量是非常巨大的。在整体空间有限的情况下,cache会导致内存的使用量大大加剧,引发多次GC和重算,反而会拖慢速度。在QQ全量的关系链计算,一个全量图是非常大的,因此如果在一个图没被多次使用,那么先unpersist,再返回给下一个计算步骤,反而成了最佳实践。 示例代码如下: val oneNbrGraph = computeOneNbr(originalGraph) oneNbrGraph.unpersist() val resultRdd = oneNbrGraph.triplets.map { ………… } 当然,既然unpersist了,切记 它只能被再用一次了
  2. 分区策略:EdgePartition2D 对GraphX有所了解的人,应该都知道,有4种分区的策略,而其中性能最好的,莫过于EdgePartition2D这种边分区策略。但是由于QQ全量的关系链非常的大,所以,如果先用默认策略,构造了图,再调用partitionBy的方法来改变分区策略,那么会多一步代价非常高的计算。 因此,为了减少不必要的计算步骤,我们建议在构造图之前,先对Edge使用该策略进行划分,再用划分好的Edge RDD,进行图构建。 示例代码如下: ``` val edgesRepartitionRdd = edgeRdd.map { case (src, dst) => val pid = PartitionStrategy.EdgePartition2D.getPartition(src, dst, partitionNum) (pid, (src, dst)) }.partitionBy(new HashPartitioner(partitionNum)).map { case (pid, (src, dst)) => Edge(src, dst, 1) } val g = Graph.fromEdges( edgesRepartitionRdd, ... ) ``` 当然,有个非常重要的Hint别忘记:partitionNum必须是平方数,才能达到最佳的性能。

最终效果

经过反复多次优化之后,在高配置集群的资源充足情况下,使用如下配置项:

|配置项|配置值| |---|---| |executors | 360 | |executor-cores | 2 | |executor-memory | 50g | |并行度 | 10000 |

总共消耗18T的内存,可以在2个半小时左右,完成整个QQ关系链的计算。BTW:

  • 这个配置其实只是可运行配置,并非最佳配置。如果内存有512G,每个Executor配到80G,每个机器6个Executor,每个Executor 4个Core,应该能有更好的表现。可以进一步减少Executor的数目。
  • 集群硬盘是SATA盘,而非SSD硬盘。在整个计算过程中,由于QQ的关系链实在庞大,中间的过程也比较复杂,所以不推荐用MEMORYONLY的选项,一旦内存充不下,重算的代价非常高,所以建议用MEMORYAND_DISK。而但是用这种方式意味着和硬盘的通信是必不可少的,因此如果硬盘能换成SSD的高性能盘,对整体性能会有很大提升。
  • 在集群处于正常负荷的情况下,资源充足时,GraphX的任务不发生重跑时,作业可以在2小时10分之内,完成全量计算。但这是在运气最佳,没有任何Task发生重跑的情况下的表现。一旦有任务Task失败,Spark会自动重跑,但是整个计算过程会变得非常长,即便是很少的2-3个Task失败,也会将计算过程,延长到3个多小时甚至更多,这是因为GraphX的Failover没做好,而且在有多次迭代的时候,这个现象会更加严重。

总结和展望

整个的优化过程,貌似风轻云淡,但是中间经过了反复调优,多次在0.1的抽样数据和1.0的全量数据之间切换,优化每一步的操作,将硬件和GraphX的性能压榨到极致,才最终得到这个结果。

在这个过程中,我们发现无论应用层再怎么优化,核心层的软肋,始终制约着上层算法的性能。包括 Graph中最大限度的预创建RDD Cache的激进使用等问题,都会导致性能和稳定性不足,使得很多算法在腾讯级别的图数据下,显得捉襟见肘。其实这也难怪,GraphX的代码,从1.3版本开始,便已经一直没有变动,基本是在吃Core优化的红利,沾光提高性能,没有任何实质性的改进,如果要继续使用,在核心上必须有所提升才行。

腾讯作为拥有国内最大的关系链,在图计算的领域,无论是处于框架,模型,还是存储,都大有可为,可以做很多的事情。腾讯的数据平台部,作为公司的大数据支撑平台,欢迎在这方面有兴趣的业界同仁,和我们进行更多的合作和交流,共同在腾讯关系链上,玩出更多社交智能的火花。

原文发布于微信公众号 - 腾讯大数据(tencentbigdata)

原文发表时间:2016-07-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏SDNLAB

超大规模数据中心网络

一、计算模式的演进 图 1 计算模式的演进 计算纪年: 1、大型机时代:20世纪60~70年代,计算机体积大、价格高,支持成百上千用户同时操作。 2、个人电...

49760
来自专栏钱曙光的专栏

一周极客热文:200 行 C 代码编写你的第一个垃圾收集器

一名程序员在许多事物缠身,心里烦乱的情况下如何排解呢?Google Dart团队的一名工程师通过编写一个“垃圾收集器”来调整自己,而且起到了一个非常好的效果,但...

22390
来自专栏编程一生

入我新美大的Java后台开发面试题总结

22460
来自专栏数据派THU

独家 | 提速20倍!3个细节优化Tableau工作簿加载过程(附实例)

Katarzyna "Kasia" Gasiewska的Tableau Public主页上经常有一些精彩的可视化作品,她拥有100多位粉丝,如果你没有位列其中,...

33320
来自专栏性能与架构

Redis新增位置查询功能 - Redis Geo

移动互联网中基于位置信息的服务(Location Based Service,LBS)越来越重要。但是,目前位置信息的使用过程中存在诸多挑战如相邻计算不准确等。...

43170
来自专栏java一日一条

我对“Hello World”30年的爱恨情仇

我最近在4月1日的那一周休了一个假,因此有时间来回顾我的职业生涯。令我震惊的是,我已经写了近30年的代码了!于是,我决定好好利用这段额外的休息时间来创作一篇怀旧...

6010
来自专栏程序员互动联盟

jAVA发展历程

1991 绿色计划 (Green Project) 1991年1月 一个名为“Green Project”的项目启动。该项旨在为家用电器提供支持,使这些电器智能...

420110
来自专栏程序人生

高效能程序员的七个习惯

昨天收到一个读者留言,问作为程序员,有什么学习和工作上的好习惯可以借鉴?想了想,干脆附庸风雅一下,总结个『高效能程序员的七个习惯』吧。Disclaimer:一家...

36890
来自专栏程序人生

是时候想想该怎么删代码了

武林外传里秀才怼上姬无命,来了一段关于「我是谁」的精彩逼问。 我是谁?我生从何来,死往何处,我为何要出现在这个世界上?我的出现对这个世界来说意味着什么,是世界选...

367110
来自专栏云计算

云计算,迷你版线程同步

昨天发了那个吹牛的文章,一不注意把今天推送文章的机会用掉了,所以我现在(PM 8:50)虽然已准备好,但也发不出来,抱歉,说好的今天发线程同步的内容只等到明天凌...

22960

扫码关注云+社区

领取腾讯云代金券