RDD:分区器

目录

RDD 分区器

哈希分区器

范围分区器

自定义分区(Partitioner)


RDD 分区器

分区器(Partitioner)在前面章节中或多或少有所提及。我总结了 RDD 分区器的三个作用,而这三个影响在本质上其实是相互关联的。

  1. 决定 Shuffle 过程中 Reducer 的个数(实际上是子 RDD 的分区个数)以及 Map 端的一条数据记录应该分配给哪一个 Reducer。这个应该是最主要的作用。
  2. 决定 RDD 的分区数量。例如执行操作 groupByKey(new HashPartitioner(2)) 所生成的ShuffledRDD 中,分区的数目等于 2。
  3. 决定 CoGroupedRDD 与父 RDD 之间的依赖关系。这个在依赖小节说过。

由于分区器能够间接决定 RDD 中分区的数量和分区内部数据记录的个数,因此选择合适的分区器能够有效提高并行计算的性能(回忆下分区小节我们提及过的 spark.default.parallelism 配置参数)。Apache Spark 内置了两种分区器,分别是哈希分区器(Hash Partitioner)范围分区器(Range Partitioner)

开发者还可以根据实际需求,编写自己的分区器。分区器对应的源码实现是 Partitioner 抽象类,Partitioner 的子类(包括自定义分区器)需要实现自己的 getPartition 函数,用于确定对于某一特定键值的键值对记录,会被分配到子RDD中的哪一个分区。

/**
 * An object that defines how the elements in a key-value pair RDD are partitioned by key.
 * Maps each key to a partition ID, from 0 to `numPartitions - 1`.
 */
abstract class Partitioner extends Serializable {
  def numPartitions: Int
  def getPartition(key: Any): Int
}

哈希分区器

哈希分区器的实现在 HashPartitioner 中,其 getPartition 方法的实现很简单,取键值的 hashCode,除以子 RDD 的分区个数取余即可。

/**
 * A [[org.apache.spark.Partitioner]] that implements hash-based partitioning using
 * Java's `Object.hashCode`.
 *
 * Java arrays have hashCodes that are based on the arrays' identities rather than their contents,
 * so attempting to partition an RDD[Array[_]] or RDD[(Array[_], _)] using a HashPartitioner will
 * produce an unexpected or incorrect result.
 */
class HashPartitioner(partitions: Int) extends Partitioner {
  def numPartitions: Int = partitions

  def getPartition(key: Any): Int = key match {
    case null => 0
    case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
  }

  override def equals(other: Any): Boolean = other match {
    case h: HashPartitioner =>
      h.numPartitions == numPartitions
    case _ =>
      false
  }

  override def hashCode: Int = numPartitions
}

使用哈希分区器进行分区的一个示例 如下图所示。此例中整数的 hashCode 即其本身。

哈希分区器

范围分区器

哈希分析器的实现简单,运行速度快,但其本身有一明显的缺点:由于不关心键值的分布情况,其散列到不同分区的概率会因数据而异,个别情况下会导致一部分分区分配得到的数据多,一部分则比较少。范围分区器则在一定程度上避免这个问题,范围分区器争取将所有的分区尽可能分配得到相同多的数据,并且所有分区内数据的上界是有序的。使用范围分区器进行分区的一个示例 如下图所示。

如果你自己去测试下面这个例子的话,会发现键值 4 被分配到子 RDD 中的第一个分区,与下图并不一致,这是因为 Apache Spark 1.1 及之后的版本,划分分区边界时候用的是 > 而不是 >=,后文会细述相应的代码实现。我已经向 Apache Spark 提交了 JIRA 和 PR

哈希分区器

范围分区器需要做的事情有两个:根据父 RDD 的数据特征,确定子 RDD 分区的边界,以及给定一个键值对数据,能够快速根据键值定位其所应该被分配的分区编号。

如果之前有接触过 Apache Hadoop 的 TeraSort 排序算法的话,应该会觉得范围分区器解决的事情与 TeraSort 算法在 Map 端所需要完成的其实是一回事。两者解决问题的思路也是十分类似:对父 RDD 的数据进行采样(Sampling),将采样得到的数据排序,并分成 M 个数据块,分割点的键值作为后面快速定位的依据。尽管思路基本一致,但由于 RDD 的一些独有特性,在具体的实现细节上,范围分区器又与 TeraSort 算法有许多不同之处。

原文参考:https://ihainan.gitbooks.io/spark-source-code/content/section1/partitioner.html

自定义分区(Partitioner)

我们都知道Spark内部提供了HashPartitionerRangePartitioner两种分区策略(这两种分区的代码解析可以参见:《Spark分区器HashPartitioner和RangePartitioner代码详解》),这两种分区策略在很多情况下都适合我们的场景。但是有些情况下,Spark内部不能符合咱们的需求,这时候我们就可以自定义分区策略。为此,Spark提供了相应的接口,我们只需要扩展Partitioner抽象类,然后实现里面的三个方法:

package org.apache.spark

/**
 * An object that defines how the elements in a key-value pair RDD are partitioned by key.
 * Maps each key to a partition ID, from 0 to `numPartitions - 1`.
 */
abstract class Partitioner extends Serializable {
  def numPartitions: Int
  def getPartition(key: Any): Int
}

  假如我们想把来自同一个域名的URL放到一台节点上,比如:https://www.iteblog.comhttps://www.iteblog.com/archives/1368,如果你使用HashPartitioner,这两个URL的Hash值可能不一样,这就使得这两个URL被放到不同的节点上。所以这种情况下我们就需要自定义我们的分区策略,可以如下实现:  def numPartitions: Int:这个方法需要返回你想要创建分区的个数;
  def getPartition(key: Any): Int:这个函数需要对输入的key做计算,然后返回该key的分区ID,范围一定是0到numPartitions-1
  equals():这个是Java标准的判断相等的函数,之所以要求用户实现这个函数是因为Spark内部会比较两个RDD的分区是否一样。

package com.iteblog.utils

import org.apache.spark.Partitioner

/**
 * User: 过往记忆
 * Date: 2015-05-21
 * Time: 下午23:34
 * bolg: https://www.iteblog.com
 * 本文地址:https://www.iteblog.com/archives/1368
 * 过往记忆博客,专注于hadoop、hive、spark、shark、flume的技术博客,大量的干货
 * 过往记忆博客微信公共帐号:iteblog_hadoop
 */

class IteblogPartitioner(numParts: Int) extends Partitioner {
  override def numPartitions: Int = numParts

  override def getPartition(key: Any): Int = {
    val domain = new java.net.URL(key.toString).getHost()
    val code = (domain.hashCode % numPartitions)
    if (code < 0) {
      code + numPartitions
    } else {
      code
    }
  }

  override def equals(other: Any): Boolean = other match {
    case iteblog: IteblogPartitioner =>
      iteblog.numPartitions == numPartitions
    case _ =>
      false
  }

  override def hashCode: Int = numPartitions
}

 

因为hashCode值可能为负数,所以我们需要对他进行处理。然后我们就可以在partitionBy()方法里面使用我们的分区:

iteblog.partitionBy(new IteblogPartitioner(20))

 

  类似的,在Java中定义自己的分区策略和Scala类似,只需要继承org.apache.spark.Partitioner,并实现其中的方法即可。

  在Python中,你不需要扩展Partitioner类,我们只需要对iteblog.partitionBy()加上一个额外的hash函数,如下:

import urlparse

def iteblog_domain(url):
  return hash(urlparse.urlparse(url).netloc)

iteblog.partitionBy(20, iteblog_domain)

参考:https://www.iteblog.com/archives/1368.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值