STEP 0 搭建Spark集群
开发Spark客户端Java程序前,需要搭建一个可用的Spark集群。具体搭建过程参见 http://spark.apache.org/docs/0.8.0/spark-standalone.html 。
下面给出一个示例Spark集群的环境信息,后面的开发步骤都针对该示例Spark集群。
Java版本:Sun JDK 1.6.0
Scala版本: Scala 2.9.3
Spark:版本为 Spark 0.8.0 ,1个Master结点,4个Worker结点,每个Worker结点可用24 Cores和32GB Memory
另外,开发Spark程序通常需要从外部存储系统输入待处理的数据,而HDFS往往是首选。因此,这里再给出示例Spark集群使用的HDFS环境信息。
Hadoop版本为 CDH 4.2.0,1个NameNode(运行于Spark Master结点),4个DataNode(分别运行于4个Spark Worker结点)。
下面给出一个示例Spark集群的环境信息,后面的开发步骤都针对该示例Spark集群。
Java版本:Sun JDK 1.6.0
Scala版本: Scala 2.9.3
Spark:版本为 Spark 0.8.0 ,1个Master结点,4个Worker结点,每个Worker结点可用24 Cores和32GB Memory
另外,开发Spark程序通常需要从外部存储系统输入待处理的数据,而HDFS往往是首选。因此,这里再给出示例Spark集群使用的HDFS环境信息。
Hadoop版本为 CDH 4.2.0,1个NameNode(运行于Spark Master结点),4个DataNode(分别运行于4个Spark Worker结点)。
STEP 1 创建Maven Project
创建Maven Project可以使用Maven工具,也可以手工创建,创建的结果是生成一个初始的Project目录结构,其中包含若干特定用途的目录和文件,这一步重点关注Project根目录下的pom.xml文件。
/ Project根目录
----src/
----main/
----java/ Java代码存储位置
----resources/ 资源文件存储位置
----test/
----java/ 测试用途的Java代码存储位置
----resources/ 测试用途的资源文件存储位置
----pom.xml Project配置文件
在pom.xml文件中可以设定Project的基本信息,指定代码库和对第三方代码的依赖等。在本例中,将对Spark代码的依赖和对Hadoop代码的依赖(访问HDFS需要)添加进pom.xml文件,再配置必要的代码库。下面为示例配置。
/ Project根目录
----src/
----main/
----java/ Java代码存储位置
----resources/ 资源文件存储位置
----test/
----java/ 测试用途的Java代码存储位置
----resources/ 测试用途的资源文件存储位置
----pom.xml Project配置文件
在pom.xml文件中可以设定Project的基本信息,指定代码库和对第三方代码的依赖等。在本例中,将对Spark代码的依赖和对Hadoop代码的依赖(访问HDFS需要)添加进pom.xml文件,再配置必要的代码库。下面为示例配置。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--配置Project的基本信息-->
<groupId>audaque.app</groupId>
<artifactId>SparkApp</artifactId>
<version>1.0</version>
<!--配置代码库:从akka.releases.repo库可以获取对Spark代码的依赖,从cdh.releases.repo库可以获取对Hadoop代码的依赖,从central库可以获取对其他第三方代码的依赖-->
<repositories>
<repository>
<id>central</id>
<name>Central Repository</name>
<url>http://repo.maven.apache.org/maven2</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>akka.releases.repo</id>
<name>Akka repository</name>
<url>http://repo.akka.io/releases</url>
</repository>
<repository>
<id>cdh.releases.repo</id>
<url>https://repository.cloudera.com/content/groups/cdh-releases-rcs</url>
<name>CDH Releases Repository</name>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<!--配置对第三方代码的依赖:对Spark的依赖和对Hadoop的依赖-->
<dependencies>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.9.3</artifactId>
<version>0.8.0-incubating</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.0.0-mr1-cdh4.2.0</version>
</dependency>
</dependencies>
<!--配置Java源码基于Java 1.6编译器-->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
STEP 2 编写Java源程序
编写Java源程序访问Spark集群,首先是获取表示Spark上下文的JavaSparkContext对象,然后利用JavaSparkContext对象生成初始RDD,取得相应的JavaRDD对象,之后就可以调用JavaRDD和JavaPairRDD的各个方法在集群上对RDD实施各种Transformation和Action操作了。作为示例,以下给出一个完整的词频统计程序,其中需要重点关注的是main方法中各条语句的作用。
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
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.PairFlatMapFunction;
import scala.Tuple2;
public class App {
static final String SPARK_MASTER_ADDRESS = "spark://hadoop01:7077";
static final String SPARK_HOME = "/home/ARCH/spark";
static final String APP_LIB_PATH = "lib";
public static void main(String[] args) throws Exception {
/************************ 以下代码片段可被所有App共用 ****************************/
// 设置App访问Spark使用的用户名:ARCH
System.setProperty("user.name", "ARCH");
// 设置App访问Hadoop使用的用户名:ARCH
System.setProperty("HADOOP_USER_NAME", "ARCH");
// 在将要传递给Executor的环境中设置Executor访问Hadoop使用的用户名:ARCH
Map<String, String> envs = new HashMap<String, String>();
envs.put("HADOOP_USER_NAME", "ARCH");
// 为App的每个Executor配置最多可以使用的内存量:2GB
System.setProperty("spark.executor.memory", "2g");
// 为App的所有Executor配置共计最多可以使用的Core数量(最大并行任务数):20
System.setProperty("spark.cores.max", "20");
// 获取要分发到集群各结点的Jar文件
// 此例策略:若指定路径为文件,则返回该文件;若指定路径为目录,则列举目录下所有文件
String[] jars = getApplicationLibrary();
// 获取Spark上下文对象——访问Spark的起点。构造方法各参数的意义分别为:
// 1 Spark Master结点的地址;2 App的名称;
// 3 Spark各Worker结点的Spark部署目录,各结点相同;4 待分发到集群各结点的Jar文件;
// 5 待传递给Executor环境(仅Map中的部分Key有效)
JavaSparkContext context = new JavaSparkContext(SPARK_MASTER_ADDRESS,
"Spark App 0", SPARK_HOME, jars, envs);
/************************ 以上代码片段可被所有App共用 ****************************/
// Spark上的词频统计
countWords(context);
}
private static String[] getApplicationLibrary()
throws IOException {
List<String> list = new LinkedList<String>();
File lib = new File(APP_LIB_PATH);
if (lib.exists()) {
if (lib.isFile() && lib.getName().endsWith(".jar")) {
list.add(lib.getCanonicalPath());
} else {
for (File file : lib.listFiles()) {
if (file.isFile()&& file.getName().endsWith(".jar"))
list.add(file.getCanonicalPath());
}
}
}
String[] ret = new String[list.size()];
int i = 0;
for (String s : list)
ret[i++] = s;
return ret;
}
private static void countWords(JavaSparkContext context)
throws Exception {
String input = "hdfs://hadoop01:8020/user/ARCH/a.txt";
JavaRDD<String> data = context.textFile(input).cache();
JavaPairRDD<String, Integer> pairs;
pairs = data.flatMap(new SplitFunction());
pairs = pairs.reduceByKey(new ReduceFunction());
String output = "hdfs://hadoop01:8020/user/ARCH/output";
pairs.saveAsTextFile(output);
}
private static class SplitFunction extends
PairFlatMapFunction<String, String, Integer> {
private static final long serialVersionUID = 41959375063L;
public Iterable<Tuple2<String, Integer>> call(String line)
throws Exception {
List<Tuple2<String, Integer>> list;
list = new LinkedList<Tuple2<String, Integer>>();
for (String word : line.split(" "))
list.add(new Tuple2<String, Integer>(word, 1));
return list;
}
}
private static class ReduceFunction extends
Function2<Integer, Integer, Integer> {
private static final long serialVersionUID = 5446148657508L;
public Integer call(Integer a, Integer b) throws Exception {
return a + b;
}
}
}
STEP 3 运行
将STEP 2的示例源程序文件加入STEP 1创建的Maven Project(Java源码目录为src/main/java),然后使用Maven工具组建Project(在Project根目录下执行Maven命令 mvn package),如果是第一次组建,Maven需要从配置的代码库下载程序对第三方的依赖,过程的持续时间可能较长。组建成功后,在Project根目录下的target目录下会生成程序的Jar文件,对于本例Jar文件的名称是SparkApp-1.0.jar。
在HDFS上为示例程序准备好待统计词频的输入文件(hdfs://hadoop01:8020/user/ARCH/a.txt),然后在Project根目录下执行以下Maven命令尝试运行示例程序。
mvn exec:java -Dexec.mainClass=App
以上命令实际发生的过程是将示例程序的Jar文件及示例程序对第三方的依赖添加到类路径,然后运行指定的Main类,即App。这与以Standalone方式运行普通Java程序的方式无异。结果,程序运行了,但是马上又出错退出了。
回过头再看看示例程序的源码,在countWords方法中调用JavaRDD的flatMap方法和JavaPairRDD的reduceByKey方法时使用了两个自定义类型SplitFunction和ReduceFunction的实例作为调用参数,这两个参数将以序列化的方式传递到在Worker结点运行的各个Executor,但是现在Executor在反序列化这两个参数时加载不到相应的自定义类型,于是出错了。
对此,Spark允许客户端程序指定要分发到集群各结点的Jar文件,示例程序被设计为将lib目录中的所有Jar文件指定为要分发到集群各结点的Jar文件。Executor需要的自定义类型SplitFunction和ReduceFunction包含在程序的Jar文件(SparkApp-1.0.jar)中,因此将该Jar文件拷贝到lib目录,然后再次执行上面同样的命令。结果,程序运行了,运行完成后正常退出。可以到输出目录(hdfs://hadoop01:8020/user/ARCH/output)检查词频统计的结果。
在HDFS上为示例程序准备好待统计词频的输入文件(hdfs://hadoop01:8020/user/ARCH/a.txt),然后在Project根目录下执行以下Maven命令尝试运行示例程序。
mvn exec:java -Dexec.mainClass=App
以上命令实际发生的过程是将示例程序的Jar文件及示例程序对第三方的依赖添加到类路径,然后运行指定的Main类,即App。这与以Standalone方式运行普通Java程序的方式无异。结果,程序运行了,但是马上又出错退出了。
回过头再看看示例程序的源码,在countWords方法中调用JavaRDD的flatMap方法和JavaPairRDD的reduceByKey方法时使用了两个自定义类型SplitFunction和ReduceFunction的实例作为调用参数,这两个参数将以序列化的方式传递到在Worker结点运行的各个Executor,但是现在Executor在反序列化这两个参数时加载不到相应的自定义类型,于是出错了。
对此,Spark允许客户端程序指定要分发到集群各结点的Jar文件,示例程序被设计为将lib目录中的所有Jar文件指定为要分发到集群各结点的Jar文件。Executor需要的自定义类型SplitFunction和ReduceFunction包含在程序的Jar文件(SparkApp-1.0.jar)中,因此将该Jar文件拷贝到lib目录,然后再次执行上面同样的命令。结果,程序运行了,运行完成后正常退出。可以到输出目录(hdfs://hadoop01:8020/user/ARCH/output)检查词频统计的结果。