先介绍官网提交的例子,我用的是spark 0.9.0 hadoop2.2.0
一.使用脚本提交
1.使用spark脚本提交到yarn,首先需要将spark所在的主机和hadoop集群之间hosts相互配置(也就是把spark主机的ip和主机名配置到hadoop所有节点的/etc/hosts里面,再把集群所有节点的ip和主机名配置到spark所在主机的/etc/hosts里面)。
2.然后需要把hadoop目录etc/hadoop下面的*-sit.xml复制到${SPARK_HOME}的conf下面.
3.确保hadoop集群配置了 HADOOP_CONF_DIR or YARN_CONF_DIR
1.yarn-standalone方式提交到yarn
在${SPARK_HOME}下面执行:
1
2
3
4
5
6
7
8
9
|
SPARK_JAR=.
/assembly/target/scala-2
.10.4
/spark-assembly-0
.9.0-incubating-hadoop2.2.0.jar \
.
/bin/spark-class
org.apache.spark.deploy.yarn.Client \
--jar .
/examples/target/scala-2
.10
/spark-examples_2
.10-assembly-0.9.0-incubating.jar \
--class org.apache.spark.examples.SparkPi \
--args yarn-standalone \
--num-workers 3 \
--master-memory 2g \
--worker-memory 2g \
--worker-cores 1
|
在${SPARK_HOME}下面执行:
1
2
3
|
SPARK_JAR=.
/assembly/target/scala-2
.10.4
/spark-assembly-0
.9.0-incubating-hadoop2.2.0.jar \
SPARK_YARN_APP_JAR=examples
/target/scala-2
.10
/spark-examples_2
.10-assembly-0.9.0-incubating.jar \
.
/bin/run-example
org.apache.spark.examples.SparkPi yarn-client
|
1.必须使用linux主机提交任务,使用windows提交到linux hadoop集群会报
1
|
org.apache.hadoop.util.Shell$ExitCodeException: /bin/bash: 第
0
行: fg: 无任务控制
|
错误。hadoop2.2.0不支持windows提交到linux hadoop集群,网上搜索发现这是hadoop的bug。
2.提交任务的主机和hadoop集群主机名需要在hosts相互配置。
3.因为使用程序提交是使用yarn-client方式,所以必须像上面脚本那样设置环境变量SPARK_JAR 和 SPARK_YARN_APP_JAR
比如我的设置为向提交任务主机~/.bashrc里面添加:
1
2
|
export
SPARK_JAR=
file
:
///home/ndyc/software/sparkTest/lib/spark-assembly-0
.9.0-incubating-hadoop2.2.0.jar
export
SPARK_YARN_APP_JAR=
file
:
///home/ndyc/software/sparkTest/ndspark-0
.0.1.jar
|
其中SPARK_JAR是${SPARK_HOME}/assembly/target/scala-2.10.4/spark-assembly-0.9.0-incubating-hadoop2.2.0.jar
SPARK_YARN_APP_JAR是自己程序打的jar包,包含自己的测试程序。
4.程序中加入hadoop、yarn、依赖。
注意,如果引入了hbase依赖,需要这样配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-thrift</artifactId>
<version>${hbase.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-jobclient</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
</exclusion>
</exclusions>
</dependency>
|
然后再加入
1
2
3
4
5
|
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-all</artifactId>
<version>
4.0
</version>
</dependency>
|
1
|
IncompatibleClassChangeError has
interface
org.objectweb.asm.ClassVisitor as
super
class
|
异常是因为Hbase jar hadoop-mapreduce-client-jobclient.jar里面使用到了asm3.1 而spark需要的是asm-all-4.0.jar
5. hadoop conf下的*-site.xml需要复制到提交主机的classpath下,或者说maven项目resources下面。
6.编写程序
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
package
com.sdyc.ndspark.sys;
import
org.apache.spark.SparkConf;
import
org.apache.spark.api.java.JavaPairRDD;
import
org.apache.spark.api.java.JavaRDD;
import
org.apache.spark.api.java.JavaSparkContext;
import
org.apache.spark.api.java.function.Function2;
import
org.apache.spark.api.java.function.PairFunction;
import
scala.Tuple2;
import
java.util.ArrayList;
import
java.util.List;
/**
* Created with IntelliJ IDEA.
* User: zarchary
* Date: 14-1-19
* Time: 下午6:23
* To change this template use File | Settings | File Templates.
*/
public
class
ListTest {
public
static
void
main(String[] args)
throws
Exception {
SparkConf sparkConf =
new
SparkConf();
sparkConf.setAppName(
"listTest"
);
//使用yarn模式提交
sparkConf.setMaster(
"yarn-client"
);
JavaSparkContext sc =
new
JavaSparkContext(sparkConf);
List<String> listA =
new
ArrayList<String>();
listA.add(
"a"
);
listA.add(
"a"
);
listA.add(
"b"
);
listA.add(
"b"
);
listA.add(
"b"
);
listA.add(
"c"
);
listA.add(
"d"
);
JavaRDD<String> letterA = sc.parallelize(listA);
JavaPairRDD<String, Integer> letterB = letterA.map(
new
PairFunction<String, String, Integer>() {
@Override
public
Tuple2<String, Integer> call(String s)
throws
Exception {
return
new
Tuple2<String, Integer>(s,
1
);
}
});
letterB = letterB.reduceByKey(
new
Function2<Integer, Integer, Integer>() {
public
Integer call(Integer i1, Integer i2) {
return
i1 + i2;
}
});
//颠倒顺序
JavaPairRDD<Integer, String> letterC = letterB.map(
new
PairFunction<Tuple2<String, Integer>, Integer, String>() {
@Override
public
Tuple2<Integer, String> call(Tuple2<String, Integer> stringIntegerTuple2)
throws
Exception {
return
new
Tuple2<Integer, String>(stringIntegerTuple2._2, stringIntegerTuple2._1);
}
});
JavaPairRDD<Integer, List<String>> letterD = letterC.groupByKey();
// //false说明是降序
JavaPairRDD<Integer, List<String>> letterE = letterD.sortByKey(
false
);
System.out.println(
"========"
+ letterE.collect());
System.exit(
0
);
}
}
|
关于spark需要依赖的jar的配置可以参考我的博客spark安装和远程调用。
以上弄完之后就可以运行程序了。
运行后会看到yarn的ui界面出现:
正在执行的过程中会发现hadoop yarn 有的nodemanage会有下面这个进程:
1
|
13247 org.apache.spark.deploy.yarn.WorkerLauncher
|
这是spark的工作进程。
如果接收到异常为:
1
|
WARN YarnClientClusterScheduler: Initial job has not accepted any resources; check your cluster UI to ensure that workers are registered and have sufficient memory
|
出现这个错误是因为提交任务的节点不能和spark工作节点交互,因为提交完任务后提交任务节点上会起一个进程,展示任务进度,大多端口为4044,工作节点需要反馈进度给该该端口,所以如果主机名或者IP在hosts中配置不正确,就会报
WARN YarnClientClusterScheduler: Initial job has not accepted any resources; check your cluster UI to ensure that workers are registered and have sufficient memory错误。
所以请检查主机名和IP是否配置正确。
我自己的理解为,程序提交任务到yarn后,会上传SPARK_JAR和SPARK_YARN_APP_JAR到hadoop节点, yarn根据任务情况来分配资源,在nodemanage节点上来启动org.apache.spark.deploy.yarn.WorkerLauncher工作节点来执行spark任务,执行完成后退出。
Spark自定义分区(Partitioner)
我们都知道Spark内部提供了HashPartitioner
和RangePartitioner
两种分区策略,这两种分区策略在很多情况下都适合我们的场景。但是有些情况下,Spark内部不能符合咱们的需求,这时候我们就可以自定义分区策略。为此,Spark提供了相应的接口,我们只需要扩展Partitioner
抽象类,然后实现里面的三个方法:
01 | package org.apache.spark |
02 |
03 | /** |
04 | * An object that defines how the elements in a key-value pair RDD are partitioned by key. |
05 | * Maps each key to a partition ID, from 0 to `numPartitions - 1`. |
06 | */ |
07 | abstract class Partitioner extends Serializable { |
08 | def numPartitions : Int |
09 | def getPartition(key : Any) : Int |
10 | } |
def numPartitions: Int
:这个方法需要返回你想要创建分区的个数;
def getPartition(key: Any): Int
:这个函数需要对输入的key做计算,然后返回该key的分区ID,范围一定是0到numPartitions-1
;
equals()
:这个是Java标准的判断相等的函数,之所以要求用户实现这个函数是因为Spark内部会比较两个RDD的分区是否一样。
假如我们想把来自同一个域名的URL放到一台节点上,比如:http://www.iteblog.com
和http://www.iteblog.com/archives/1368
,如果你使用HashPartitioner
,这两个URL的Hash值可能不一样,这就使得这两个URL被放到不同的节点上。所以这种情况下我们就需要自定义我们的分区策略,可以如下实现:
01 | package com.iteblog.utils |
02 |
03 | import org.apache.spark.Partitioner |
04 |
05 | /** |
06 | * User: 过往记忆 |
07 | * Date: 2015-05-21 |
08 | * Time: 下午23:34 |
09 | * bolg: http://www.iteblog.com |
10 | * 本文地址:http://www.iteblog.com/archives/1368 |
11 | * 过往记忆博客,专注于hadoop、hive、spark、shark、flume的技术博客,大量的干货 |
12 | * 过往记忆博客微信公共帐号:iteblog_hadoop |
13 | */ |
14 |
15 | class IteblogPartitioner(numParts : Int) extends Partitioner { |
16 | override def numPartitions : Int = numParts |
17 |
18 | override def getPartition(key : Any) : Int = { |
19 | val domain = new java.net.URL(key.toString).getHost() |
20 | val code = (domain.hashCode % numPartitions) |
21 | if (code < 0 ) { |
22 | code + numPartitions |
23 | } else { |
24 | code |
25 | } |
26 | } |
27 |
28 | override def equals(other : Any) : Boolean = other match { |
29 | case iteblog : IteblogPartitioner = > |
30 | iteblog.numPartitions == numPartitions |
31 | case _ = > |
32 | false |
33 | } |
34 |
35 | override def hashCode : Int = numPartitions |
36 | } |
因为hashCode
值可能为负数,所以我们需要对他进行处理。然后我们就可以在partitionBy()
方法里面使用我们的分区:
1 | iteblog.partitionBy( new IteblogPartitioner( 20 )) |
类似的,在Java中定义自己的分区策略和Scala类似,只需要继承org.apache.spark.Partitioner
,并实现其中的方法即可。
在Python中,你不需要扩展Partitioner类,我们只需要对iteblog.partitionBy()
加上一个额外的hash函数,如下:
1 | import urlparse |
2 |
3 | def iteblog_domain(url): |
4 | return hash (urlparse.urlparse(url).netloc) |
5 |
6 | iteblog.partitionBy( 20 , iteblog_domain) |
spark中的SparkContext实例的textFile使用的小技巧
网上很多例子,包括官网的例子,都是用textFile来加载一个文件创建RDD,类似sc.textFile("hdfs://n1:8020/user/hdfs/input")
textFile的参数是一个path,这个path可以是:
1. 一个文件路径,这时候只装载指定的文件
2. 一个目录路径,这时候只装载指定目录下面的所有文件(不包括子目录下面的文件)
3. 通过通配符的形式加载多个文件或者加载多个目录下面的所有文件
第三点是一个使用小技巧,现在假设我的数据结构为先按天分区,再按小时分区的,在hdfs上的目录结构类似于:
/user/hdfs/input/dt=20130728/hr=00/
/user/hdfs/input/dt=20130728/hr=01/
...
/user/hdfs/input/dt=20130728/hr=23/
具体的数据都在hr等于某个时间的目录下面,现在我们要分析20130728这一天的数据,我们就必须把这个目录下面的所有hr=*的子目录下面的数据全部装载进RDD,于是我们可以这样写:sc.textFile("hdfs://n1:8020/user/hdfs/input/dt=20130728/hr=*/"),注意到hr=*,是一个模糊匹配的方式。
spark的kafka的低阶API createDirectStream的一些总结。
大家都知道在spark1.3版本后,kafkautil里面提供了两个创建dstream的方法,一个是老版本中有的createStream方法,还有一个是后面新加的createDirectStream方法。关于这两个方法的优缺点,官方已经说的很详细(http://spark.apache.org/docs/latest/streaming-kafka-integration.html),总之就是createDirectStream性能会更好一点,通过新方法创建出来的dstream的rdd partition和kafka的topic的partition是一一对应的,通过低阶API直接从kafka的topic消费消息,但是它不再往zookeeper中更新consumer offsets,使得基于zk的consumer offsets的监控工具都会失效。
官方只是蜻蜓点水般的说了一下可以在foreachRDD中更新zookeeper上的offsets:
对应 Exactly-once semantics要自己去实现了,大致的实现思路就是在driver启动的时候先从zk上获得consumer offsets信息,createDirectStream有两个重载方法,其中一个可以设置从任意offsets位置开始消费,部分代码如下:
这里会有几个问题,就是在一个group是新的consumer group时,即首次消费,zk上海没有相应的group offsets目录,这时要先初始化一下zk上的offsets目录,或者是zk上记录的offsets已经过时,由于kafka有定时清理策略,直接从zk上的offsets开始消费会报ArrayOutofRange异常,即找不到offsets所属的index文件了,针对这两种情况,做了以下处理:
这里又碰到了一个问题,从consumer offsets到leader latest offsets中间延迟了很多消息,在下一次启动的时候,首个batch要处理大量的消息,会导致spark-submit设置的资源无法满足大量消息的处理而导致崩溃。因此在spark-submit启动的时候多加了一个配置:--conf spark.streaming.kafka.maxRatePerPartition=10000。限制每秒钟从topic的每个partition最多消费的消息条数,这样就把首个batch的大量的消息拆分到多个batch中去了,为了更快的消化掉delay的消息,可以调大计算资源和把这个参数调大。
OK,driver启动的问题解决了,那么接下来处理处理完消息后更新zk offsets的工作,这里要注意是在处理完之后再更新,想想如果你消费了消息先更新zk offset在去处理消息将处理好的消息保存到其他地方去,如果后一步由于处理消息的代码有BUG失败了,前一步已经更新了zk了,会导致这部分消息虽然被消费了但是没被处理,等你把处理消息的BUG修复再重新提交后,这部分消息在下次启动的时候不会再被消费了,因为你已经更新了ZK OFFSETS,针对这些因素考虑,部分代码实现如下:
仔细想一想,还是没有实现精确一次的语义,写入mongo和更新ZK由于不是一个事务的,如果更新mongo成功,然后更新ZK失败,则下次启动的时候这个批次的数据就被重复计算,对于UV由于是addToSet去重操作,没什么影响,但是PV是inc操作就会多算这一个批次的的数据,其实如果batch time比较短的话,其实都还是可以接受的。