有的时候,需要判断rdd.isEmpty()
,以决定是否需要后续操作。而这个isEmpty
方法是个action算子。也就是说如果rdd不为空,需要做后续操作的话,那么这个rdd的创建过程可能就执行了两遍。那么rdd需要cache
吗?
进入isEmpty方法
def isEmpty(): Boolean = withScope {
partitions.length == 0 || take(1).length == 0
}
如果这个rdd是从kafka读出来的,那么partitions.length == 0
这个判断就为false
,会进入take(num = 1)
方法,
def take(num: Int): Array[T] = withScope {
// 扫描范围扩大因子
val scaleUpFactor = Math.max(conf.getInt("spark.rdd.limit.scaleUpFactor", 4), 2)
if (num == 0) {
new Array[T](0)
} else {
// 保存take的结果
val buf = new ArrayBuffer[T]
// 总共的分区个数
val totalParts = this.partitions.length
// 浏览过的分区个数
var partsScanned = 0
// 结果中的记录数小于take需要的num,并且浏览过的分区数小于总分区数
while (buf.size < num && partsScanned < totalParts) {
// 应该浏览的分区个数
// 最开始为1,也就是先尝试从第0个分区取记录,如果一个这个分区的记录数不够,再浏览其他分区
var numPartsToTry = 1L
val left = num - buf.size
if (partsScanned > 0) {
// 进入到这个判断里说明不是第一次循环,上次浏览的分区取出来的记录数量还不够num,这时就需要扩大应该本次应该浏览的分区数了
if (buf.isEmpty) {
numPartsToTry = partsScanned * scaleUpFactor
} else {
// 已经浏览过的分区个数 * (剩余要访问的记录数与已经访问过的记录数的比值),再扩大50%,得出还需要浏览的分区个数
numPartsToTry = Math.ceil(1.5 * left * partsScanned / buf.size).toInt
numPartsToTry = Math.min(numPartsToTry, partsScanned * scaleUpFactor)
}
}
// p是应该浏览的分区索引数组,标明哪些分区应该被浏览
val p = partsScanned.until(math.min(partsScanned + numPartsToTry, totalParts).toInt)
// 按指定的分区执行“小规模”job
// 这里it.take(left)会让各分区的迭代器只迭代当前buf所需要的记录数。根据迭代器模式,可知这里并不会遍历整个分区的数据再从中拿出left条记录
val res = sc.runJob(this, (it: Iterator[T]) => it.take(left).toArray, p)
// 将job的结果塞进buf中
// 这里使用_.take(num - buf.size),保证buf的记录数量不会超过num
res.foreach(buf ++= _.take(num - buf.size))
partsScanned += p.size
}
buf.toArray
从源码中可见,
如果take的num不超过第0个分区里的记录数,那么会发生一次“小规模job”,总共访问过的记录数=num;
如果超过了,就会再在更大的范围(更多分区中)查找更少的剩余需要take出来的记录数,从而产生一个“中等规模job”,可能使总共访问过的记录数>num;
举个例子
val rdd = sc.makeRDD(Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 5)
rdd.cache().take(5).foreach(println)
1.取出第0个分区的(1,2),buf的size=2,left=3
2.算出还需要从2.25≈3个分区(第1,2,3号分区)中各取3条记录(但各分区只有两条,所以取了2条)
3.第2步取出的(3,4),(5,6),(7,8)这三组共6条数据,塞入buf中。buf只还需要3条,所以只塞进(3,4,5)
4.返回结果buf(1,2,3,4,5)
由图可见,总共执行了两次job,第一次1个分区,第二次3个分区。并且缓存了4个分区。
顺便提一点,cache()
是以分区为最小单位的,如果只需要遍历的某个分区的一小部分数据,用了cache,也会把整个分区都遍历一次缓存起来。
回答最初的问题
1.isEmpty()
,只会从rdd产生的源头中遍历第一条数据,如果不cache()
,它只会从数据源访问一条数据。如果cache了,会遍历第0个分区的所有数据并缓存;
2.这个rdd如果在后面是的代码中不被使用的话,就不要cache,否则可以cache。