Spark每日半小时(12)——数据分区实现方式

在分布式程序中,通信的代价是很大的,因此控制数据分布以获得最少的网络传输可以极大地提升整体性能。和单节点的程序需要为记录集合选择合适的数据结构一样,Spark程序可以通过控制RDD分区方式来减少通信开销。分区并不是对所有应用都有好处——如果给定RDD只需要被扫描一次,我们完全没有必要对其预先进行分区处理。只有当数据集多次在诸如连接这种基于键的操作中使用时,分区才会有帮助。我们会给出一些小李子来说明这一点。

Spark中所有的键值对RDD都可以进行分区。系统会根据一个针对键的函数对元素进行分组。尽管Spark没有给出现实控制每个键具体落在哪一个工作节点上的方法,但Spark可以确保同一组的键出现在同一个节点上。比如,你可能使用哈希分区将一个RDD分成100个分区,此时键的哈希值对100取模的结果相同的记录会被放在一个节点上。你也可以使用范围分区法,将键在同一个范围区间内的记录都放在同一个节点上。

举个简单的例子,我们分析这样一个应用,它在内存中保存着一张很大的用户信息表——也就是一个由(UserID,UserInfo)对组成的RDD,其中UserInfo包含一个该用户所订阅的主题的列表。该应用会周期性地将这张表与一个小文件进行组合,这个小文件中存着过去五分钟内发生的事件——其实就是一个由()UserID,LinkInfo)对组成的表,存放着过去五分钟内某网站各用户的访问情况。例如,我们可能需要对用户访问其未订阅主题的页面情况进行统计。我们可以使用Spark的join()操作来实现这个组合操作,其中需要吧UserInfo和LinkInfo的有序对根据UserID进行分组。

// 初始化代码;从HDFS商的一个Hadoop SequenceFile中读取用户信息
// userData中的元素会根据它们被读取时的来源,即HDFS块所在的节点来分布
// Spark此时无法获知某个特定的UserID对应的记录位于哪个节点上
val sc = new SparkContext(...)
val userData = sc.sequenceFile[UserID, UserInfo]("hdfs://...").persist()
// 周期性调用函数来处理过去五分钟产生的事件日志
// 假设这是一个包含(UserID, LinkInfo)对的SequenceFile
def processNewLogs(logFileName: String) {
	val events = sc.sequenceFile[UserID, LinkInfo](logFileName)
	val joined = userData.join(events)// RDD of (UserID, (UserInfo, LinkInfo)) pairs
	val offTopicVisits = joined.filter {
		case (userId, (userInfo, linkInfo)) => // Expand the tuple into its components
		!userInfo.topics.contains(linkInfo.topic)
	}.count()
	println("Number of visits to non-subscribed topics: " + offTopicVisits)
}

还好这段Scala咱能看懂。看来之后有必要学学Scala语言了。

这段代码可以正确运行,但是不够高效。这是因为在每次调用processNewLogs()时都会用到join()操作,而我们对数据集是如何分区的却一无所知。默认情况下,连接操作会将两个数据集中的所有键的哈希值都求出来,将该哈希值相同的记录通过网络传到同一台机器上,然后在那台机器上对所有键相同的记录进行连接操作。因为userData表比每五分钟出现的访问日志表events要大得多,所以要浪费时间做很多额外工作:在每次调用时都对userData表进行哈希值计算和跨节点数据混洗,虽然这些数据从来都不会变化。

要解决这一问题也很简单:在程序开始时,对userData表使用partitionBy()转化操作,将这张表转为哈希分区。可以通过向partitionBy传递一个spark.HashPartitioner对象来实现该操作。

    public static void main(String[] args) {
        SparkConf conf = new SparkConf().setMaster("local").setAppName("sortByKey");
        JavaSparkContext sc = new JavaSparkContext(conf);

        JavaPairRDD<UserID, UserInfo> userData = sc.sequenceFile("hdfs://...", UserID.class, UserInfo.class)
                .partitionBy(new HashPartitioner(100))
                .persist(StorageLevels.MEMORY_AND_DISK);
    }

这个就用Java写吧,省的之后使用麻烦。。。

processNewLogs()方法可以保持不变:在processNewLogs()中,eventsRDD是本地变量,只在该方法中使用了一次,所以为events制定分区方式没有什么用处。由于在构建userData时调用了partitionBy(),Spark就知道了该RDD是根据键的哈希值来分区的,这样在调用join()时,Spark就会利用到这一点。具体来说,当调用userData.join(events)时,Spark只会对events进行数据混洗操作,将events中特定UserID的记录发送到userData的对应分区所在的那台机器上。这样,需要通过网络传输的数据就大大减少了,程序运行速度也可以显著提升。

注意,partitionBy()是一个转化操作,因此它的返回值总是一个新的RDD,但它不会改变原来的RDD。RDD一旦创建就无法修改。因此应该对partitionBy()的结果进行持久化,并保存为userData,而不是原来的sequenceFile()的输出。此外,传给partitionBy()的100表示分区数目,它会控制之后对这个RDD进行进一步操作时有多少任务会并行执行。总的来说,这个值至少应该和集群中的总核心数一样。

这里需要注意一点,如果没有将partitionBy()转化操作的结果持久化,那么后面每次用到这个RDD时都会重复地对数据进行分区操作。不进行持久化会导致整个RDD谱系图重新求值。那样的话,partitionBy()带来的好处就会被抵消,导致重复对数据进行分区以及跨节点的混洗,和没有指定分区方式时发生的情况十分相似。

事实上,许多其他Spark操作会自动为结果RDD设定已知的分区方式信息,而且除join()外还有很多操作也会利用到已有的分区信息。比如,sortByKey()和groupByKey()会分别生成范围分区的RDD和哈希分区的RDD。而另一方面,诸如map()这样的操作会导致新的RDD失去父RDD的分区信息,因为这样的操作理论上可能会修改每条记录的键。接下来我们会讨论如何获取RDD的分区信息,以及数据分区是如何影响各种Spark操作的。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值