【问题标题】:Recursive method call in Apache SparkApache Spark 中的递归方法调用
【发布时间】:2016-02-17 18:07:07
【问题描述】:

我正在从 Apache Spark 上的数据库构建家谱,使用递归搜索来查找数据库中每个人的最终父项(即位于家谱顶部的人)。

假设搜索id时第一个返回的人是正确的父母

val peopleById = peopleRDD.keyBy(f => f.id)
def findUltimateParentId(personId: String) : String = {

    if((personId == null) || (personId.length() == 0))
        return "-1"

    val personSeq = peopleById.lookup(personId)
    val person = personSeq(0)
    if(person.personId == "0 "|| person.id == person.parentId) {

        return person.id

    }
    else {

        return findUltimateParentId(person.parentId)

    }

}

val ultimateParentIds = peopleRDD.foreach(f => f.findUltimateParentId(f.parentId))

出现以下错误

"Caused by: org.apache.spark.SparkException: RDD 转换和动作只能由驱动调用,不能在其他转换内部调用;例如,rdd1.map(x => rdd2.values.count() * x) 无效,因为值转换和计数动作不能在rdd1.map 转换内部执行。有关详细信息,请参阅 SPARK-5063。"

我从阅读其他类似问题中了解到,问题在于我在 foreach 循环中调用了 findUltimateParentId,如果我从 shell 中使用人的 id 调用该方法,它会返回正确的最终 @987654325 @

但是,其他建议的解决方案都不适合我,或者至少我看不到如何在我的程序中实现它们,有人可以帮忙吗?

【问题讨论】:

  • 你在这里采取了错误的方法。目前尚不清楚 Spark 是否对您有用,但您是否考虑使用 GraphX API。
  • 对不起,我的手被绑在了这个上面。我必须使用 Spark。
  • GraphX 是 Spark。一种或另一种方式,您至少应该首先学习 Spark API :) 至少有一些没有意义的瘦,包括您使用查找和 foreach 的方式。
  • 我已经阅读了一些关于 GraphX 的教程,我将如何建立人与人之间的关系 Edge 集合?

标签: scala recursion apache-spark rdd


【解决方案1】:

如果我理解正确 - 这是一个适用于任何输入大小的解决方案(尽管性能可能不是很好) - 它在 RDD 上执行 N 次迭代,其中 N 是“最深的族”(从祖先到的最大距离)孩子)在输入中:

// representation of input: each person has an ID and an optional parent ID
case class Person(id: Int, parentId: Option[Int])

// representation of result: each person is optionally attached its "ultimate" ancestor,
// or none if it had no parent id in the first place
case class WithAncestor(person: Person, ancestor: Option[Person]) {
  def hasGrandparent: Boolean = ancestor.exists(_.parentId.isDefined)
}

object RecursiveParentLookup {
  // requested method
  def findUltimateParent(rdd: RDD[Person]): RDD[WithAncestor] = {

    // all persons keyed by id
    def byId = rdd.keyBy(_.id).cache()

    // recursive function that "climbs" one generation at each iteration
    def climbOneGeneration(persons: RDD[WithAncestor]): RDD[WithAncestor] = {
      val cached = persons.cache()
      // find which persons can climb further up family tree
      val haveGrandparents = cached.filter(_.hasGrandparent)

      if (haveGrandparents.isEmpty()) {
        cached // we're done, return result
      } else {
        val done = cached.filter(!_.hasGrandparent) // these are done, we'll return them as-is
        // for those who can - join with persons to find the grandparent and attach it instead of parent
        val withGrandparents = haveGrandparents
          .keyBy(_.ancestor.get.parentId.get) // grandparent id
          .join(byId)
          .values
          .map({ case (withAncestor, grandparent) => WithAncestor(withAncestor.person, Some(grandparent)) })
        // call this method recursively on the result
        done ++ climbOneGeneration(withGrandparents)
      }
    }

    // call recursive method - start by assuming each person is its own parent, if it has one:
    climbOneGeneration(rdd.map(p => WithAncestor(p, p.parentId.map(i => p))))
  }

}

这里有一个测试可以更好地理解它是如何工作的:

/**
  *     Example input tree:
  *
  *            1             5
  *            |             |
  *      ----- 2 -----       6
  *      |           |
  *      3           4
  *
  */

val person1 = Person(1, None)
val person2 = Person(2, Some(1))
val person3 = Person(3, Some(2))
val person4 = Person(4, Some(2))
val person5 = Person(5, None)
val person6 = Person(6, Some(5))

test("find ultimate parent") {
  val input = sc.parallelize(Seq(person1, person2, person3, person4, person5, person6))
  val result = RecursiveParentLookup.findUltimateParent(input).collect()
  result should contain theSameElementsAs Seq(
    WithAncestor(person1, None),
    WithAncestor(person2, Some(person1)),
    WithAncestor(person3, Some(person1)),
    WithAncestor(person4, Some(person1)),
    WithAncestor(person5, None),
    WithAncestor(person6, Some(person5))
  )
}

应该很容易将您的输入映射到这些Person 对象,并将输出WithAncestor 对象映射到您需要的任何对象。请注意,此代码假定如果任何人具有 parentId X - 另一个具有该 id 的人实际上存在于输入中

【讨论】:

  • 正是我需要的!感谢负载!
  • 无论如何也可以获得中间父母,而不仅仅是根父母?例如(3, (2, 1)), (4, (2, 1), (6, (5))) ?
  • 我相信这是可能的,您必须更改 WithAncestor 以包含一些有序的祖先列表并在每次迭代时更新它......虽然无法说明所需的确切更改.
【解决方案2】:

使用 SparkContext.broadcast 解决了这个问题:

val peopleById = peopleRDD.keyBy(f => f.id)
val broadcastedPeople = sc.broadcast(peopleById.collectAsMap())

def findUltimateParentId(personId: String) : String = {

    if((personId == null) || (personId.length() == 0))
        return "-1"

    val personOption = broadcastedPeople.value.get(personId)
    if(personOption.isEmpty) {

        return "0";

    }
    val person = personOption.get
    if(person.personId == 0 || person.orgId == person.personId) {

        return person.id

    }
    else {

        return findUltimateParentId(person.parentId)

    }

}

val ultimateParentIds = peopleRDD.foreach(f => f.findUltimateParentId(f.parentId))

现在工作得很好!

【讨论】:

  • 请注意,此解决方案仅限于 peopleById 小到足以放入驱动程序内存(单机)的情况,在这种情况下,您根本不需要 Spark...如果这样如果集合变大,您很可能会在第二行收到 OutOfMemoryError,它将所有数据从集群收集到驱动程序机器。
猜你喜欢
  • 2015-06-16
  • 2021-09-07
  • 2023-04-10
  • 2015-06-08
  • 1970-01-01
  • 1970-01-01
  • 2014-07-07
  • 1970-01-01
相关资源
最近更新 更多