Spark快速大数据分析(二)

楔子

Spark快速大数据分析 前3章内容,仅作为学习,有断章取义的嫌疑。如有问题参考原书

4 键值对操作

4.1 动机

Spark为包含键值对类型的RDD提供了一些专业的操作,这些RDD被称为pair RDD,Pair RDD是很多程序的构成要素,因为他们提供了并行操作各个键或跨节点重新进行数据分组的操作接口。例如:pair RDD提供了reduceByKey方法,可以分别归约每个键对应的数据,还有join方法,可以把两个RDD中键相同的元素组合到一起,合并为一个RDD,我们通常用一个RDD中提取某些字段,并使用这些字段作为pair RDD操作中的键。

4.2 创建Pair RDD

在Spark中很多种创建pair RDD的方法,很多存储键值对的数据会在读取时直接返回由键值对数据组成的pair RDD。此外,需要把一个普通的RDD转为pair RDD,可以调用map方法实现。

构建键值对对RDD的方法在不同的语言中会有所不同。

在Java中使用一个单词作为键创建一个pair RDD

/**
 * java中使用一个单词作为键创建出一个Pair RDD
 */
public static void getPairRDD() {
	PairFunction<String, String, String> keyData = new PairFunction<String, String, String>() {
		@Override
		public Tuple2<String, String> call(String t) throws Exception {
			return new Tuple2<String, String>(t.split(" ")[0], t);
		}

	};
	JavaSparkContext sc = SparkUtils.getContext();
	JavaRDD<String> lines = sc.parallelize(Arrays.asList("hello word", "spark good"));
	JavaPairRDD<String, String> javaPairRDD = lines.mapToPair(keyData);
	System.out.println(javaPairRDD.collect());
	// [(hello,hello word), (spark,spark good)]

}

/**
 * java中使用一个单词作为键创建出一个Pair RDD
 * https://blog.csdn.net/leen0304/article/details/78743715
 */
public static void getPairRDDLamdba() {
	JavaSparkContext sc = SparkUtils.getContext();
	JavaRDD<String> lines = sc.parallelize(Arrays.asList("hello word", "spark good"));
	// TODO 下面的语句 在new Tuple2 里面写 split 不自动提示,难道 lamdba有部分不提示吗?
	JavaPairRDD<String, String> counts = lines.mapToPair(t -> (new Tuple2<String, String>(t.split(" ")[0], t)));
	System.out.println(counts.collect());
	// [(hello,hello word),  (spark,spark good)]
}

当Scala和Python从一个内存中的数据集创建pair RDD时,只需要对这个由二元组组成的集合调用SparkContext.patallelize()方法。而要使用Java从内存数据集创建pair RDD的话,则需要使用SparkContext.parallelizePairs()。

4.3 Pair RDD的转化操作

Pair RDD可以使用所有标准RDD上的可用额转化操作,之前描述的有关传递函数的规则也适用于 pair RDD。由于 Pair RDD中包含二元组,所以需要传递的函数应当操作二元组而不是独立的元素。

Java中对第二个元素进行筛选

/**
 * 对第二个元素进行筛选
 */
public static void selectPairRDD() {
	JavaPairRDD<String, String> pairRDD = getPairRDDLamdba();
	JavaPairRDD<String, String> filter = pairRDD.filter(new Function<Tuple2<String, String>, Boolean>() {
		@Override
		public Boolean call(Tuple2<String, String> v1) throws Exception {
			return v1._2().length() < 20;
		}
	});
	System.out.println(filter.collect());
	// [(hello,hello word), (spark,spark good)]
}

/**
 * 对第二个元素进行筛选
 */
public static void selectPairRDDlamdba() {
	JavaPairRDD<String, String> pairRDD = getPairRDDLamdba();
	// TODO 下面的语句 eclipse没提示?
	JavaPairRDD<String, String> filter = pairRDD.filter(t -> t._2().length() < 20);

	// [(hello,hello word), (spark,spark good)]
	System.out.println(filter.collect());
}

有时,我们只是想访pair RDD的值部分,这是操作二组很麻烦。由于这是一种常见的使用模式,因此Spark提供了mapValues(func)函数,功能类似于map{case (x,y):x,func(y))}

下面依次讨论pairRDD的各种操作


4.3.1聚合操作

当数据集以键值对形式组织的时候,聚合具有相同键的元素进行一些统计是很常见的操作。之前讲过在RDD上的fold、combine()、reduce()等操作,pair RDD上则有相应的针对键的转化操作,Spark有一组类似的操作,可以组合具有相同键的值。这些操作返回RDD,因此他们是转化而不是行动操作。

reduceByKey和reduce相当类似:他们都接收一个函数,并使用该函数对值进行合并。reduceByKey会为数据集中的每个键进行并行的归约操作,每个归约操作会将键相同的值合并起来。因为数据集中可能有大量的键,所以reduceByKey没有被实现为向用户程序返回一个值得行动操作。实际上,它会返回一个由键和值归约的结果组成新的RDD

foldByKey和fold相当类似:他们都使用一个与RDD和合并函数中的数据类型相同的零值作为初始值。与fold一样, foldByKey() 操作所使用的合并函数对零值与另一
个元素进行合并,结果仍为该元素。

Java中实现单词统计

/**
 * 单词统计
 * 
 * @throws IOException
 */
public static void wc() throws IOException {
	JavaSparkContext sc = SparkUtils.getContext();
	JavaRDD<String> inputRDD = sc.parallelize(Arrays.asList("hello word", "hello spark"));
	JavaRDD<String> words = inputRDD.flatMap(x -> Arrays.asList(x.split(" ")).iterator());
	JavaPairRDD<String, Integer> groupByKey = words.mapToPair(w -> new Tuple2<String, Integer>(w, 1)).reduceByKey((x, y) -> x + y);
	System.out.println(groupByKey.collect());
	// [(spark,1), (word,1), (hello,2)]

}

**事实上,我们可以对第一个 RDD 使用 countByValue() 函数,以更快地实现
单词计数: input.flatMap(x => x.split(" ")).countByValue() **

/**
 * 单词统计
 * 
 * @throws IOException
 */
public static void wc2() throws IOException {
	JavaSparkContext sc = SparkUtils.getContext();
	JavaRDD<String> inputRDD = sc.parallelize(Arrays.asList("hello word", "hello spark"));
	Map<String, Long> words = inputRDD.flatMap(x -> Arrays.asList(x.split(" ")).iterator()).countByValue();
	System.out.println(words);
	// {spark=1, word=1, hello=2}
}

combineByKey()是最常用的基于键进行聚合的函数。大多数基于键聚合的函数都是用它实现的。它可以让用户返回与输入类型不同的返回值。

未完待续,键值对怎么求平均值?


4.3.2数据分组

对于有键的数据,一个常见的用例是将数据根据键进行分组——比如查看一个客户的所有订单。

如果数据已经以预期的方式提供了键,groupByKey()就会使用RDD的键来对数据进行分组。对于一个由类型K的键和类型V的值组成的RDD,所得到的结果RDD类型会是[K,Interable[V]]。

groupBy()可以用于未成对的数据上,也可以根据键相同以外的条件进行分组。它可以接受一个函数,对源RDD中的每个元素使用该函数,将返回结果作为键在进行分组。

4.3.3 连接

将有键的数据与另一组有键的数据一起使用是对键值对数据执行最有用的操作之一。连接数据可能是pair RDD最常用的操作之一,连接方式多种多样:右外连接、左外连接、交叉连接、内连接。

普通的 join 操作符表示内连接 。只有在两个 pair RDD 中都存在的键才叫输出。当一个输 入对应的某个键有多个值时,生成的 pair RDD 会包括来自两个输入 RDD 的每一组相对应 的记录。 有时,我们不希望结果中的键必须在两个 RDD 中都存在。例如,在连接客户信息与推荐时,如果一些客户还没有收到推荐,我们仍然不希望丢掉这些顾客。 leftOuterJoin(other)和 rightOuterJoin(other) 都会根据键连接两个 RDD,但是允许结果中存在其中的一个pair RDD 所缺失的键。

在使用 leftOuterJoin() 产生的 pair RDD 中,源 RDD 的每一个键都有对应的记录。每个键相应的值是由一个源 RDD 中的值与一个包含第二个 RDD 的值的 Option (在 Java 中为Optional )对象组成的二元组。在 Python 中,如果一个值不存在,则使用 None 来表示;而数据存在时就用常规的值来表示,不使用任何封装。和 join() 一样,每个键可以得到多条记录;当这种情况发生时,我们会得到两个 RDD 中对应同一个键的两组值的笛尔积。

4.3.4 数据排序

如果键有已定义的顺序,就可以对这种键值对RDD进行排序。当把数据排序好之后,后续对数据形成collect()和save()等操作都会得到有序的数据。

我们经常要将 RDD 倒序排列,因此 sortByKey() 函数接收一个叫作 ascending 的参数,表示我们是否想要让结果按升序排序(默认值为 true )。有时我们也可能想按完全不同的排序依据进行排序。要支持这种情况,我们可以提供自定义的比较函数


/**
 * 排序
 * <p>
 * Java中以字符串顺序自定义排序
 */

public static void orderBy() {
	JavaSparkContext sc = SparkUtils.getContext();
	JavaRDD<String> inputRDD = sc.parallelize(Arrays.asList("hello word", "hello spark"));
	JavaRDD<String> words = inputRDD.flatMap(x -> Arrays.asList(x.split(" ")).iterator());
	JavaPairRDD<String, Integer> result = words.mapToPair(w -> new Tuple2<String, Integer>(w, 1)).reduceByKey((x, y) -> x + y);
	System.out.println(result.collect());
	// [(spark,1), (word,1), (hello,2)]

	class Compa implements Serializable, Comparator<String> {
		private static final long serialVersionUID = 1L;
		@Override
		public int compare(String a, String b) {
			return a.compareTo(b);
		}
	}
	Compa compa = new Compa();
	JavaPairRDD<String, Integer> sortByKey = result.sortByKey(compa);
	System.err.println(sortByKey.collect());
	// [(hello,2), (spark,1), (word,1)]
	sc.close();
}

4.4 Pair RDD行动操作

所有基础RDD支持的传统操作也都在pair RDD上可用。pair RDD还提供了一些额外的行动操作。

Pair RDD的行动操作(以键值对集合{(1,2),(3,4),(3,6)})为例

函数描述实例结果
countByKey()对每个键值对分别计数rdd.countByKey(){(1,1),)(3,2)}
collectAsMap()将结果以映射的形式返回,以便查询rdd.cokkectAsMapMap{(1,2),(3,4),(3,6)}
lookuo(key)返回给定键的所有值rdd.lookup(3)[4,6]

5 数据读取和保存

5.1 动机

到目前为止,所有的示例都是从本地集合或普通文件中进行数据读取和保存。但是有时候,数据量可能大到无法放在同一台机器中,这就需要探索别的数据读取和保存方法了。

Spark及其生态系统提供了很多可选方案。下面介绍常见的3中数据源

  • 文件格式和文件系统
  • Spark SQL中结构化数据源
  • 数据库与键值存储

1 文件格式和文件系统

对于存储在本地文件系统或者分布式文件系统(比如NFS、hdfs等)中的数据,Spark可以范文很多不同的文件格式,包括文本文件、JSON、SequenceFile以及protocol buff。Spark针对不同的文件系统的配置和压缩选项。

Spark SQL中结构化数据源

Spark SQL,它包括了JSON和Hive在内的结构化数据源,为我们提供了一套更加简洁高效地API。

数据库与键值存储

Spark自带的库和一些第三方库,可以用来连接 Cassandra、HBase、Elasticsearch 以及 JDBC 源。

5.2 文件格式

Spark对很多种文件格式的读取和保存方式都很简单。从诸如文本文件的非结构化文件、以及诸如JSON格式的半结构化文件,再到SequenceFile这样的结构化文件,Spark都支持。

Spark支持的一些常见格式

格式名称结构化备注
文本文件普通文本文件,每行一条记录
JSON半结构化常见的基于文本的格式,半结构化,多数数库都要求每行一条记录
CSV基于文本的格式,通常在电子表格中使用
SequenceFiles一种用于键值对数据的常见Hadoop文件格式
Protocol buffers一种快速、节约空间的跨语言格式
对象文件用来将Spark作业中的数据存储下来以让共享的代码读取,改变类的结构它会失效,因为它依赖Java序列化

5.2.1 文本文件

在Spark中读写文本文件很容易。当我们将一个文本文件读取为RDD时,输入的每一行都会成为RDD中的一个元素。也可以将多个完整的文本文件一次性读取为一个pair RDD,其中键是文件名,值是文本内容。

1 读取文本文件

只需要SparkContext 中textFile函数,如果要控制分区数的话,可以指定 minPartitions。

sc.textFile("c:/info.txt");//支持统配符

如果多个输入文件以一个包含数据所有部分的目录的形式出现,可以用两种方式来处理。可以仍使用textFile函数,传递目录作为参数,这样它会把各部分都读取到RDD中。有时候必要知道数据的各部分分别来自哪个文件,有时候则希望同时处理整个文件。如果文件足够小,那么可以使用SparkContext.wholeTextFiles()方法,该方法会返回一个pair RDD,其中键是输入文件的文件名。

wholeTextFiles()在每个文件表示一个特定时间段内的数据是非常有用。

2 保存文本文件

输出文本文件也相当简单。saveAsTextFile()方法接收一个路劲,并将RDD中的内容输入到路劲对应的文件中。Spark将传入的路劲作为目录对待,会在那个目录输出多个文件。这样,Spark就可以从多个节点上并行输出了。在这个方法中,我们不能控制数据的那一部分输出到哪个文件中,不过有些输出格式支持控制。

5.2.2 JSON

读取JSON数据的最简单的方式是将数据作为文本文件读取,然后使用JSON解析器来对RDD中的值进行映射操作。

1 读取JSON

这种方法假设文件中每一行都是一条JSON记录。如果你有跨行的JSON数据,你就只能读入整个文件,然后对每个文件进行解析。如果在你使用的语言中构建一个JSON解析器的开销较大,你可以使用mapPartitions()来重用解析器。

5.2.3 逗号分隔值与制表符分隔值

在Scala和Java中则使用opencsv库

5.3 文件系统

5.4 Spark SQL中的结构化数据

在各种情况下,我们把一条SQL查询给Spark SQL,让它对于一个数据源执行查询(选出一些字段或者对字段使用一些函数),然后得到有ROW对象组成的RDD,每个row对象表示一条记录,在Java和Scala中,row对象的访问时基于下标的。每个Row都有一个get方法,返回一个一般类型都我们进行类型转换。

5.4.1 Apache Hive

Spark SQL可以读取到Hive支持的任何表。

要把Spark SQL连接到已有的Hive上,你需要提供Hive的配置文件,需要将hive-site.xml文件复制到Saprk的conf目录下,这样之后,在创建出HiveContext对象,也就是Saprk SQL的入口,然后可以用Hive查询语句来对你的表进行查询,并由行组成的RDD的形式拿到返回的数据。

/**
 * Java创建hiveContext并查询数据
 */
public static void connHive() {
	// 我使用到这个是spark 2.1.1版本,HiveContext已经过时了
	HiveContext hiveContext = new HiveContext(SparkUtils.getContext());
	Dataset<Row> rows = hiveContext.sql("select name,age from user");
	Row first = rows.first();
	System.out.println(first.getString(0));// 字段0是name字段
}

5.4.2 JSON

如果记录结构一致的JSON数据,Spark SQL也可以自动推断出他们的结构信息,并将这些数据读取为记录,这样就可以使得提取字段操作变得简单。要读取JSON数据,首先要和HIVE一样创建一个HiveContext。(不过这种情况不需要我们安装好hive,也就是说你不需要hive-site.xml文件)。然后使用HiveContext.jsonFile方法从整个文件中获取ow对象组成的RDD,除了整个RDD对象,你也可以将RDD注册为一张表,然后从中选出特定的字段。

JSONS示例
在这里插入图片描述

5.5 数据库

通过数据库提供的 Hadoop 连接器或者自定义的 Spark 连接器,Spark 可以访问一些常用的数据库系统。本节来展示四种常见的连接器。

5.5.1 Java数据库连接器

Spark可以从任何支持Java数据库连接JDBC关系型数据库中读取数据,包括MySQL、postgre等系统。要访问这些数据。需要构建一个org.apache.spark.rdd.JdbcRDD,将SparkContext和其他参数一起传给它。

5.5.2 Cassandra

Cassandra

5.5.4 HBase

6 Spark编程进阶

介绍两种类型的共享变量:累加器和广播变量

累加器用来对信息进行聚合,广播变量用来高效分发较大的对象

在已有的 RDD 转化操作的基础上,我们为类似查询数据库这样需要很大配置代价的任务引入了批操作。为了扩展可用的工具范围,本章会介绍 Spark 与外部程序交互的方式,比如如何与用 R 语言编写的脚本进行交互。

6.2 累加器

通过在向Spark传递函数是,比如使用map函数或者filter传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中对应的变量。Spark的两个共享变量,累加器和广播变量,分别为结果聚合与广播这两种常见的同学模式突破了这一限制。

累加器,提供了将工作节点中的值聚合到驱动器程序中的简单语法。累加器的一个常见用途是在调试时对作业执行过程中的事件进行计数。

/**
 * 累加器,计算空白行
 */
public static void callamdba() {
	JavaSparkContext sc = SparkUtils.getContext();
	JavaRDD<String> file = sc.textFile("c://info.txt");
	Accumulator<Integer> blankLines = sc.accumulator(0);// 创建并初始化为0
	JavaRDD<String> call = file.flatMap(line -> {
		if (line.length() == 0) {
			// 累加器加1
			blankLines.add(1);
		}
		System.out.println("======|" + line + "|");
		return Arrays.asList(line.split(" ")).iterator();
	});

	System.out.println(call.count());
	System.out.println("空白行有:" + blankLines);
	sc.close();
}

/**
 * 累加器,计算空白行
 */
public static void cal() {
	JavaSparkContext sc = SparkUtils.getContext();
	JavaRDD<String> file = sc.textFile("c://info.txt");
	Accumulator<Integer> blankLines = sc.accumulator(0);// 创建并初始化为0

	JavaRDD<String> call = file.flatMap(new FlatMapFunction<String, String>() {
		public Iterator<String> call(String t) throws Exception {
			if (t.length() == 0) {
				blankLines.add(1);
			}
			return Arrays.asList(t.split(" ")).iterator();
		}
	});

	System.out.println(call.count());
	System.out.println("空白行有:" + blankLines);
	sc.close();
}

上述代码中只有使用或者保存了结果RDD,才能看到空白行输出,因为flatMap是惰性的。

累加器如法如下:

  • 通过在驱动器中调用SparkContext.accumulator()方法,创建出存有初始值的累加器。
  • Saprk闭包里的执行器代码可以使用累加器的+=方法增加累加器的值
  • 驱动器程序可以调用累加器的value的属性来访问累加器的值。

6.2.1 累加器与容错性

Spark会自动重新执行失败的或者较慢的任务来应对有错误的或者比较慢的机器。即使该节点没有崩溃,而只是处理的比其他节点慢很多,Spark也可以抢占式德在另一节点上启动一个投机型的任务副本,如果该任务更早结束就可以直接获取结果。即使没有节点失败,Spark 有时也需要重新运行任务来获取缓存中被移除出内存的数据。因此最终结果就是同一个函数可能对同一个数据运行了多次,这取决于集群发生了什么。

如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,我们必须把它放在foreach这样的行动操作中。

6.2.2 自定义累加器

自定义累加器需要扩展 AccumulatorParam

6.3 广播变量

Spark 的第二种共享变量类型是广播变量,它可以让程序高效地向所有工作节点发送一个较大的只读值,以供一个或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。

广播变量使用的过程

  • 通过一个类型T的对象调用 SparkContext.broadcast 创建出一个 Broadcast[T] 对象。任何可序列化的类型都可以这么实现。
  • 通过value属性访问该对象的值
  • 变量只会被发到各个节点一次,应作为只读值处理

6.6 数值RDD的操作

Spark 的数值操作是通过流式算法实现的,允许以每次一个元素的方式构建出模型。这些统计数据都会在调用 stats() 时通过一次遍历数据计算出来,并以 StatsCounter 对象返回.

StatsCounter 中可用的汇总统计数据

方法含义
countRDD元素个数
mean元素平均值
sum/max/min总和/最大、最小
variance元素的方差
stdev标准差
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值