我们先看官网的定义:
Task | A unit of work that will be sent to one executor |
Job | A parallel computation consisting of multiple tasks that gets spawned in response to a Spark action (e.g. save , collect ); you'll see this term used in the driver's logs. |
Stage | Each job gets divided into smaller sets of tasks called stages that depend on each other (similar to the map and reduce stages in MapReduce); you'll see this term used in the driver's logs. |
stage是把每个job又拆分成stage,这些stage有对应的依赖关系,在spark里面实际上是需要经过shuffle的过程就会划分为一个stage, 比如map,它不需要经过shuffle,那么它就不会被拆分为stage, 比如reduceByKey需要经过shuffle,那么一个JOB就会根据reduceByKey来拆分,前面作为一个stage, reduce之后又最为一个stage,并且前后依赖。
task: task实际上对应partition, partition是 spark的最小处理集,简单理解partition就是数据集,一个很大的文件,你定义100个分区,那么这100个分区就是100个数据集,也对应100个task, task和partition是一一对应的。 虽然是一一对应,但是这里要说明清楚,有时候你定义100个分区,实际上数据只有90个分区,剩余10个分区是没有数据的,这个时候仍然会有100个task,但是很显然会有10个task没有数据处理。所以在定义partition的时候,要搞清楚自己的分区方式,默认是spark提供了hash, range,你也可以自己定义partition方法。
上面实际有一个新概念就是shuffle, 到底什么是shuffle ? 官网的解释其实很清楚,这里稍作翻译及解释,比如reduceByKey, 我们知道一个task对应一个partition, 现在我们需要根据key做reduce操作,那么就意味着所有相同的key必须在一个partition里,否者一个task怎么来处理所有的key ? 因此spark必须查询reduce之前所有的partition数据,然后把这些相同的key扔到新的partition , 这2个操作成为shuffle write / shuffle read , 整个过程就是shuffle, 这也是为什么spark会根据shuffle来划分stage, reduce依赖之前的所有处理,比如map, 你map处理完成之后的数据并不适合直接提交给reduce,因为这个时候的数据还是乱的,partition并不会保证相同key在一个partition, 只有在经过shuffle之后才可用。 从我们人为的思考也很清晰了,shuffle前作为一个stage,可以理解为准备工作,而shuffle后作为一个stage,可以理解为真正处理了,要准备出结果了。
接下来我们做一些测试:
1)
代码如下:
val line = sc.textFile("/tmp/mapreduce_dir/spark/a",3)
val rdd = line.map { x => (x.split(",")(1), x.split(",")(0)) }.foreach(x => println(x))
task是4个,因为我分了4个区(0-3),2个操作,一个map转化原始文件,然后输出,此处就是一个JOB,只有一个foreach action,没有shuffle,那么整个JOB也就一个stage.
从结果看,也是毫无顺序可言。
(0,panda)
(3,pink)
(3,pirate)
(1,panda)
(4,pink)
(3,coffee)
(2,dog)
(3,dog)
代码如下,添加了reduceByKey:
val rdd = line.map { x => (x.split(",")(1), x.split(",")(0)) }.reduceByKey((a,b) => a + b).foreach(x => println(x))
上面还是一个JOB, 因为只有一个action, 但是有2个stage, 因为reduceByKey是一个shuffle操作, 2个stage的task是4个。 这里要阐述一下,map肯定是4个task,因为我们的文件分区是4个partition, 但是reduceByKey我默认没有去设置partition ,按照理论来说,默认是2个task, 实际测试会发现,有时候多个stage的之间如果没有去设置task数量,会根据上一个stage task的数字。 也就是说因为map是4个,那么接下来的操作也是4个。
看上面的task数量,我换成了6,再看shuffle read/shuffle write,他们的数据大小相等。
3.
代码如下,添加了sortByKey, 这里要仔细看,有很多新东西出来了:
val rdd = line.map { x => (x.split(",")(1), x.split(",")(0)) }.reduceByKey((a,b) => a + b,6).sortByKey().foreach(x => println(x))
添加了sortByKey, 已经有2个JOB了,不是一个,根据之前的理论,一个action一个job,那么添加的sortByKey是action ? 再看看stage01 的截图:
stage02的截图:
大家看明白了吗? 2个stage,如果说sortBey是action, 那么2个stage就是正常的了,2个JOB也就是正常的。 之前提到过,从表面上很难区分什么是action什么是shuffle就是想表达这个意思,我之前一直以为sortByKey就是shuffle. 另外,整个spark的执行顺序好像乱了,我的代码明明是先map => reduce => sort => foreach , 但是从stage的顺序来看,变成了map => sort => reduce => foreach. 而且还有一个skip task. 为什么spark要自动改变我们的顺序呢?非要先sort再reduce ? 为什么还有一个skip比较容易理解,之前我们是map=>reduce,现在是map=> sort, 所以spark明显的标记出,stage 02是不需要执行map的,因为stage 01已经执行了。
这个地方如果非要说能够理解,也很简单的能自圆其说,我可以认为,因为reduce本身就需要查询所有的数据,然后扔到相同的partition , sort本身就是排序,先排序,把结果再给reduce也符合情理。
我一直是这么认为的,很多东西的研究是不一定要去看源代码的,在我做ORACLE DBA的时候,哪里有源码给你看? 但是ORACLE大师级的人还是很多,测试的过程你能够从结果看出一个产品的实际表现,就好比说sortByKey到底是action还是shuffle, 不需要去看代码,结合官方文档的解释,再测试一下就明白了,它是action . 现在网上很多人动不动就贴源代码,很烦。
顺带发一些spark常用的一些函数。
1. map, mapVlues
一个是针对key来做操作,一个是针对value来做操作。
文件:
/tmp/mapreduce_dir/spark/a
数据:
panda,0
pink,3
pirate,3
panda,1
pink,4
coffee,3
dog,2
dog,3
根据key求value的平均值
line.map { line => (line.split(",")(0), line.split(",")(1))}.mapValues { x => (x.toDouble,1) }
.reduceByKey((a,b) => (a._1 + b._1 , a._2 + b._2 )).mapValues(x => x._1/x._2).foreach(x => println(x._1 + "," + x._2))
2. foreachPartition, mapparition
一个RDD可以针对单个元素操作,也可以针对parition来操作,优势在于,比如你要把RDD的每个元素插入到数据库,如果针对单个元素操作,每个元素会建立一个连接,这对数据库来说不可思议,如果针对partition来操作就会少很多了。
foreachPartition:
bankRDD.foreachPartition { x =>
var count = 0
val hbaseconf = HBaseConfiguration.create()
hbaseconf.set("hbase.zookeeper.quorum", "datanode01.isesol.com,datanode02.isesol.com,datanode03.isesol.com,datanode04.isesol.com,cmserver.isesol.com")
hbaseconf.set("hbase.zookeeper.property.clientPort", "2181")
hbaseconf.set("maxSessionTimeout", "6")
val myTable = new HTable(hbaseconf, TableName.valueOf(table))
// myTable.setAutoFlush(true)
myTable.setWriteBufferSize(3 * 1024 * 1024)
x.foreach { y =>
{
val rowkey = System.currentTimeMillis().toString()
val p = new Put(Bytes.toBytes(machine_tool + "#" + cur_time(3) + "#" + fileNum + "#" + rowkey))
for (i <- 0 to hbaseCols_scala.size - 1) {
p.add(Bytes.toBytes("cf"), Bytes.toBytes(hbaseCols_scala(i)), Bytes.toBytes(y(i)))
}
myTable.put(p)
}
}
myTable.flushCommits()
myTable.close()
}
} catch {
case ex: Exception => println("can not connect hbase")
}
}
val line = sc.textFile("/tmp/mapreduce_dir/spark/a",10)
val rdd = line.map { x => (x.split(",")(0),x.split(",")(1))}.mapPartitions { x =>
{
var res = List[(String,Int)]()
while(x.hasNext){
val b = x.next()
res.::=(b._1, b._2.toInt.*(2))
}
res.iterator
}
}
rdd.foreach { x => println(x) }
}
3. sortByKey, groupByKey
根据key排序和分组
sortByKey:
object mappartition {
def main(args: Array[String]) {
val conf = new SparkConf()
conf.setMaster("local").setAppName("this is for spark SQL")
val sc = new SparkContext(conf)
val line = sc.textFile("/tmp/mapreduce_dir/spark/a",10)
line.map { x => (x.split(",")(0),x.split(",")(1)) }.sortByKey(true, 1).foreach(x => println(x))
}
}
结果已经根据key排序了:
(coffee,3)
(dog,2)
(dog,3)
(panda,0)
(panda,1)
(pink,3)
(pink,4)
(pirate,3)
groupByKey:
object mappartition {
def main(args: Array[String]) {
val conf = new SparkConf()
conf.setMaster("local").setAppName("this is for spark SQL")
val sc = new SparkContext(conf)
val line = sc.textFile("/tmp/mapreduce_dir/spark/a",1)
line.map { x => (x.split(",")(0),x.split(",")(1))}.groupByKey().foreach(x => println(x))
}
}
结果如下:
(coffee,CompactBuffer(3))
(panda,CompactBuffer(0, 1))
(dog,CompactBuffer(2, 3))
(pirate,CompactBuffer(3))
(pink,CompactBuffer(3, 4))