下一代大数据教程(二)

原文:Next-Generation Big Data

协议:CC BY-NC-SA 4.0

四、使用 Impala 和 Kudu 的高性能数据分析

Impala 是 Kudu 默认的 MPP SQL 引擎。Impala 允许您使用 SQL 与 Kudu 进行交互。如果您有使用 SQL 和存储引擎紧密集成的传统关系数据库的经验,您可能会发现 Kudu 和 Impala 相互解耦并不常见。Impala 旨在与其他存储引擎(如 HDFS、HBase 和 S3)一起工作,而不仅仅是 Kudu。将其他 SQL 引擎如 Apache Drill (DRILL-4241)和 Hive (HIVE-12971)与 Kudu 集成的工作也在进行中。在开源社区中,分离存储、SQL 和处理引擎是很常见的。

Impala-Kudu 的整合非常成功,但仍有工作要做。虽然它在性能和可伸缩性方面达到或超过了传统的数据仓库平台,但 Impala-Kudu 仍然缺乏大多数传统数据仓库平台中的一些企业特性。Kudu 是一个年轻的项目。我们将在本章后面讨论其中的一些限制。

主关键字

每个 Kudu 表都需要有一个主键。创建 Kudu 表时,必须首先列出用作主键的一列或多列。Kudu 的主键是作为聚集索引实现的。使用聚集索引,行以与索引相同的顺序物理存储在平板中。还要注意,Kudu 没有自动递增特性,所以在向 Kudu 表中插入行时,必须包含唯一的主键值。如果没有主键值,可以使用 Impala 内置的 uuid()函数或者更高效的生成唯一值的方法。

数据类型

和其他关系数据库一样,Kudu 支持各种数据类型(表 4-1 )。

您可能会注意到 Kudu 不支持 decimal 数据类型。这是 Kudu 的一个关键限制。float 和 double 数据类型仅存储非常接近的值,而不是 IEEE 754 规范中定义的精确值。

表 4-1

List of Data Types, with Available and Default Encoding

| 数据类型 | 编码 | 默认 | | :-- | :-- | :-- | | 布尔 | 普通,游程长度 | 运行长度 | | 8 位有符号整数 | 普通、位洗牌、游程长度 | 比特洗牌 | | 16 位有符号整数 | 普通、位洗牌、游程长度 | 比特洗牌 | | 32 位有符号整数 | 普通、位洗牌、游程长度 | 比特洗牌 | | 64 位有符号整数 | 普通、位洗牌、游程长度 | 比特洗牌 | | unixtime_micros(自 Unix 纪元以来的 64 位微秒) | 普通、位洗牌、游程长度 | 比特洗牌 | | 单精度(32 位)IEEE-754 浮点数 | 普通,位图 | 位混洗 | | 双精度(64 位)IEEE-754 浮点数 | 普通,位图 | 比特洗牌 | | UTF-8 编码字符串(未压缩时最大 64KB) | 普通,前缀,字典 | 词典 | | 二进制(最多 64KB 未压缩) | 普通,前缀,字典 | 词典 |

因此,行为 float 和 double 不适合存储财务数据。在撰写本文时,对十进制数据类型的支持仍在开发中(Apache Kudu 1.5 / CDH 5.13)。更多详情请查看 KUDU-721。周围有各种各样的工作。您可以将财务数据存储为 string,然后在每次需要读取数据时使用 Impala 将值转换为 decimal。因为 Parquet 支持小数,所以另一个解决方法是对事实表使用 Parquet,对维度表使用 Kudu。Kudu 提交者正在致力于增加十进制支持,并且可能会包含在 Kudu 的新版本中。

如表 4-1 所示,根据列的类型,Kudu 列可以使用不同的编码类型。支持的编码类型包括普通、位混洗、游程、字典和前缀。默认情况下,Kudu 列是未压缩的。Kudu 支持使用 Snappy、zlib 或 LZ4 压缩编解码器进行列压缩。压缩和编码可以显著减少空间开销并提高性能。有关编码和压缩的更多信息,请参考 Kudu 的在线文档。

Note

在 Kudu 的早期版本中,日期和时间被表示为 BIGINT。从 Impala 2.9/CDH 5.12 开始,可以在 Kudu 表中使用时间戳数据类型。然而,有几件事要记住。Kudu 使用 64 位值表示日期和时间列,而 Impala 使用 96 位值表示日期和时间。存储在 Kudu 中时,Impala 生成的纳秒值四舍五入。在读写时间戳列时,Kudu 的 64 位表示和 Impala 的 96 位表示之间存在转换开销。有两种解决方法:使用 Kudu 客户端 API 或 Spark 来插入数据,或者继续使用 BIGINT 来表示日期和时间。 ii

内部和外部 Impala 表

您可以在 Impala 中创建内部和外部表。

内部表格

内部表由 Impala 创建和管理。内部表一创建,Impala 就能立即看到。删除和重命名表等管理任务是使用 Impala 执行的。下面是一个如何在 Impala 中创建内部表的例子。

CREATE TABLE users
(
  id BIGINT,
  name STRING,
  age TINYINT,
  salary FLOAT,
  PRIMARY KEY(id)
)
PARTITION BY HASH PARTITIONS 8
STORED AS KUDU;

外部表格

通过 Kudu API 和 Spark 创建的 Kudu 表对 Impala 来说并不直接可见。这个表存在于 Kudu 中,但是因为它不是通过 Impala 创建的,所以它不知道这个表的任何信息。必须在 Impala 中创建一个引用 Kudu 表的外部表。删除外部表只会删除 Impala 和 Kudu 表之间的映射,而不会删除 Kudu 中的物理表。下面是一个关于如何在 Impala 中创建一个外部表到一个现有的 Kudu 表的例子。

CREATE EXTERNAL TABLE users
STORED AS KUDU
TBLPROPERTIES (
  'kudu.table_name' = 'kudu_users'
);

更改数据

您可以通过 Impala 使用 SQL 语句更改存储在 Kudu 表中的数据,就像传统的关系数据库一样。这就是为什么你会使用 Kudu 而不是不可变的存储格式,比如 ORC 或 Parquet 的主要原因之一。

插入行

您可以使用标准的 SQL insert 语句。

INSERT INTO users VALUES (100, "John Smith", 25, 50000);

插入具有多个值的行子子句。

INSERT INTO users VALUES (100, "John Smith", 25, 50000), (200, "Cindy Nguyen", 38, 120000), (300, "Steve Mankiw", 60, 75000);

也支持大容量插入。

INSERT INTO users SELECT * from users_backup;

更新行

标准 SQL 更新语句。

UPDATE users SET age=39 where name = 'Cindy Nguyen';

还支持批量更新。

UPDATE users SET salary=150000 where id > 100;

打乱行

支持向上插入。如果表中不存在主键,则插入整行。

UPSERT INTO users VALUES (400, "Mike Jones", 21, 80000);

但是,如果主键已经存在,则列将使用新值进行更新。

UPSERT INTO users VALUES (100, "John Smith", 27, 70000);

删除行

标准 SQL 删除语句。

DELETE FROM users WHERE id < 3;

还支持更复杂的 Delete 语句。

DELETE FROM users WHERE id in (SELECT id FROM users WHERE name = 'Steve Mankiw');

Note

如第二章所述,Kudu 不支持符合 ACID 的事务。如果更新中途失败,将不会回滚。更新后必须执行额外的数据验证,以确保数据完整性。

更改模式

Kudu 支持典型的数据库管理任务,比如重命名表、添加和删除范围分区以及删除表。更多信息,请查阅 Kudu 的在线文档。

分割

表分区是增强 Kudu 表的性能、可用性和可管理性的常用方法。分区允许将表细分成更小的部分,即片。分区使 Kudu 能够以更精细的粒度访问平板电脑,从而利用分区修剪。所有 Kudu 表都需要进行表分区,表分区对应用程序是完全透明的。Kudu 支持散列、范围、复合散列-范围和散列-散列分区。下面是 Kudu 中分区的几个例子。分区在第二章中有更详细的讨论。

哈希分区

散列分区使用散列键将行均匀地分布在不同的平板上。

CREATE TABLE myTable (
 id BIGINT NOT NULL,
 name STRING,
 PRIMARY KEY(id)
)
PARTITION BY HASH PARTITIONS 4
STORED AS KUDU;

范围划分

范围分区将数据范围分别存储在不同的平板电脑中。

CREATE TABLE myTable (
  year INT,
  deviceid INT,
  totalamt INT,
  PRIMARY KEY (deviceid, year)
)
PARTITION BY RANGE (year) (
  PARTITION VALUE = 2016,
  PARTITION VALUE = 2017,
  PARTITION VALUE = 2018
)
STORED AS KUDU;

哈希范围分区

哈希分区将写操作均匀地分布在平板电脑上。范围分区支持分区修剪,并允许添加和删除分区。复合分区结合了两种分区方案的优点,同时限制了它的缺点。对于物联网用例来说,这是一个很好的划分方案。

CREATE TABLE myTable (
 id BIGINT NOT NULL,
 sensortimestamp BIGINT NOT NULL,
 sensorid INTEGER,
 temperature INTEGER,
 pressure INTEGER,
 PRIMARY KEY(id, sensortimestamp)
)
PARTITION BY HASH (id) PARTITIONS 16,
RANGE (sensortimestamp)
(

PARTITION unix_timestamp('2017-01-01') <= VALUES < unix_timestamp('2018-01-01'),
PARTITION unix_timestamp('2018-01-01') <= VALUES < unix_timestamp('2019-01-01'),
PARTITION unix_timestamp('2019-01-01') <= VALUES < unix_timestamp('2020-01-01')
)
STORED AS KUDU;

哈希-哈希分区

您的表中可以有多个级别的散列分区。每个分区级别应该使用不同的散列列。

CREATE TABLE myTable (
  id BIGINT,
  city STRING,
  name STRING
  age TINYINT,
  PRIMARY KEY (id, city)
)
PARTITION BY HASH (id) PARTITIONS 8,
             HASH (city) PARTITIONS 8
STORED AS KUDU;

列表分区

如果您熟悉 Oracle 等其他关系数据库管理系统,您可能已经使用过列表分区。虽然 Kudu 在技术上没有列表分区,但是您可以使用范围分区来模仿它的行为。

CREATE TABLE myTable (
  city STRING
  name STRING,
  age TINYINT,
  PRIMARY KEY (city, name)
)
PARTITION BY RANGE (city)
(
  PARTITION VALUE = 'San Francisco',
  PARTITION VALUE = 'Los Angeles',
  PARTITION VALUE = 'San Diego',
  PARTITION VALUE = 'San Jose'
)
STORED AS KUDU;

Note

分区不是万能的。它只是优化 Kudu 的众多方法之一。如果使用默认的 Kudu 设置,在将数据摄取到 Kudu 中时,您仍可能会遇到性能问题。确保调整参数 maintenance _ manager _ num _ threads, iii ,这是维护线程的数量,有助于加速压缩和刷新。您可以监视 bloom_lookups_per_op 指标和内存压力拒绝,以查看压缩和刷新是否会影响性能。您可能需要调整的另一个设置是 memory_limit_hard_bytes,它控制分配给 Kudu 守护进程的内存总量。 iv 更多详情参考在线 Kudu 文档。

使用 JDBC 与阿帕奇黑斑羚和库杜

流行的 BI 和数据可视化工具,如 Power BI、Tableau、Qlik、OBIEE 和 MicroStrategy(仅举几个例子)可以使用 JDBC/ODBC 访问 Apache Impala 和 Kudu。黑斑羚 JDBC 的驱动程序可以从 Cloudera 的网站上下载。Progress DataDirect JDBC 驱动程序是另一种选择。 v 在某些情况下,来自不同公司的一些 JDBC 驱动程序具有额外的性能特征。你的经历可能会有所不同。图 4-1 显示了一个 Zoomdata 仪表板的样本截图,该仪表板通过 Impala JDBC/ODBC 可视化存储在 Kudu 中的数据。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-1

Kudu data accessible from ZoomData in real time

与 SQL Server 链接服务器和 Oracle 网关的联盟

您可以创建从 SQL Server 和 Oracle 到 Impala 的数据库链接。如果来回复制数据太麻烦并且数据相对较小,这有时会很有用。此外,如果您需要从 SQL Server 或 Oracle 接近实时地访问存储在 Kudu 中的最新数据,而 ETL 太慢,那么通过数据库链接访问数据是一种选择。下面是一个如何在 SQL Server 中创建数据库链接的示例。然后,用户可以访问存储在远程 Impala 环境中的数据(如清单 4-1 所示)。注意,通常建议在执行查询时使用 OpenQuery,以确保查询在远程服务器上执行。

EXEC master.dbo.sp_addlinkedserver
 @server = 'ClouderaImpala', @srvproduct='ClouderaImpala',
 @provider='MSDASQL', @datasrc='ClouderaImpala',
 @provstr='Provider=MSDASQL.1;Persist Security Info=True;User ID=;Password=';

SELECT * FROM OpenQuery(ClouderaImpala, 'SELECT * FROM order_items');

SELECT * FROM OpenQuery(ClouderaImpala, 'SELECT * FROM orders');

SELECT count(*)
FROM [ClouderaImpala].[IMPALA].[default].order_items

SELECT count(*)
FROM [ClouderaImpala].[IMPALA].[default].orders

create view OrderItems_v as
SELECT * FROM OpenQuery(ClouderaImpala, 'SELECT * from order_items');

create view Orders_v as
SELECT * FROM OpenQuery(ClouderaImpala, 'SELECT * from orders');

create view OrderDetail_v as
SELECT * FROM OpenQuery(ClouderaImpala, 'SELECT o.order_id,oi.order_item_id, o.order_date,o.order_status
FROM [IMPALA].[default].orders o, [IMPALA].[default].order_items oi
where o.order_id=oi.order_item_order_id')

Listing 4-1Creating and using linked server from SQL Server to Impala

您可以使用 Oracle 的 ODBC 异构网关创建从 Oracle 到 Impala 的数据库链接。有关更多详细信息,请参考 Oracle 文档。

如果访问中小型数据集,数据库链接是合适的。如果您需要一个成熟的数据虚拟化工具,您可能会考虑使用 Polybase、Denodo 或 TIBCO 数据虚拟化(以前由 Cisco 拥有)。

摘要

Impala 为 Kudu 提供了强大的 MPP SQL 引擎。总之,它们在性能和可伸缩性方面可以与传统的数据仓库平台相媲美。Kudu 提交者和贡献者正在努力增加更多的特性和功能。第二章提供了对 Kudu 的深入讨论,包括它的局限性。想了解更多关于黑斑羚的信息,我建议你参考第三章。第八章讲述了使用 Impala 和 Kudu 进行大数据仓储。第九章向用户展示如何使用 Impala 和 Kudu 进行实时数据可视化。

参考

  1. 微软;《理解 IEEE 浮点错误完整教程》,微软,2018, https://support.microsoft.com/en-us/help/42980/-complete-tutorial-to-understand-ieee-floating-point-errors
  2. 阿帕奇黑斑羚;“时间戳数据类型”,Apache Impala,2018, https://impala.apache.org/docs/build/html/topics/impala_timestamp.html
  3. Cloudera《阿帕奇库度后台维护任务》,Cloudera,2018, https://www.cloudera.com/documentation/kudu/latest/topics/kudu_background_tasks.html
  4. 托德·利普孔;“Re:如何计算维护 _ 管理器 _ 线程数'的最优值”,Todd Lipcon,2017,https://www.mail-archive.com/user@kudu.apache.org/msg00358.html`
  5. Saikrishna Teja Bobba《教程:用 Apache Kudu 使用 Impala JDBC 和 SQL》,《进度》2017, https://www.progress.com/blogs/tutorial-using-impala-jdbc-and-sql-with-apache-kudu

五、Spark 简介

Spark 是下一代大数据处理框架,用于处理和分析大型数据集。Spark 具有统一的处理框架,提供 Scala、Python、Java 和 R 的高级 API 以及强大的库,包括用于 SQL 支持的 Spark SQL、用于机器学习的 MLlib、用于实时流的 Spark Streaming 和用于图形处理的 GraphX。 i Spark 由马泰·扎哈里亚(Matei Zaharia)在加州大学伯克利分校的 AMPLab 创立,后捐赠给阿帕奇软件基金会,于 2014 年 2 月 24 日成为顶级项目。 ii 第一版于 2014 年 5 月 30 日发布。 iii

整本书都是关于 Spark 的。本章将向您简要介绍 Spark,足以让您掌握执行常见数据处理任务所需的技能。我的目标是让你尽快变得高效。对于更彻底的治疗,霍尔登·卡劳、安迪·孔温斯基、帕特里克·温德尔和马泰·扎哈里亚(O’Reilly,2015 年)的《学习 Spark》仍然是对 Spark 的最佳介绍。Sandy Ryza、Uri Laserson、Sean Owen 和 Josh Wills (O’Reilly,2015 年)撰写的《Spark 高级分析》第二版涵盖了高级 Spark 主题,也是强烈推荐的。我假设你以前没有 Spark 的知识。然而,了解一些 Scala 知识是有帮助的。Jason Swartz (O’Reilly,2014)的《学习 Scala》和 Martin Odersky、Lex Spoon 和 Bill Venners (Artima,2011)的《Scala 第二版编程》是帮助您学习 Scala 的好书。有关 Hadoop 和 Hadoop 组件(如 HDFS 和 YARN)的初级读本,请访问 Apache Hadoop 网站。第六章讨论了与 Kudu 的集成。

概观

Spark 的开发是为了解决 Hadoop 最初的数据处理框架 MapReduce 的局限性。Matei Zaharia 在加州大学伯克利分校和脸书大学(他在那里实习)看到了 MapReduce 的许多局限性,并试图创建一个更快、更通用、多用途的数据处理框架,可以处理迭代和交互式应用程序。 iv 马泰成功实现了他的目标,让 Spark 几乎在各个方面都优于 MapReduce。由于其简单而强大的 API,Spark 更容易访问和使用。Spark 提供了一个统一的平台(图 5-1 ),支持更多类型的工作负载,如流式、交互式、图形处理、机器学习和批处理。 v Spark 作业的运行速度比同等的 MapReduce 作业快 10-100 倍,这是因为它具有快速的内存功能和高级 DAG(定向非循环图)执行引擎。数据科学家和工程师通常使用 Spark 比使用 MapReduce 更有效率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-1

Apache Spark Ecosystem

集群管理器

群集管理器管理和分配群集资源应用程序。Spark 支持 Spark(独立调度程序)、YARN 和 Mesos 自带的独立集群管理器。有一个实验性的项目,为 Spark 提供本地支持,将 Kubernetes 用作集群管理器。查看 SPARK-18278 了解更多详情。

体系结构

在高层次上,Spark 将 Spark 应用程序任务的执行分布在集群节点上(图 5-2 )。每个 Spark 应用程序在其驱动程序中都有一个 SparkContext 对象。SparkContext 表示到集群管理器的连接,集群管理器为 Spark 应用程序提供计算资源。在连接到集群之后,Spark 在您的 worker 节点上获取执行器。然后 Spark 将您的应用程序代码发送给执行器。一个应用程序通常会运行一个或多个作业来响应一个 Spark 动作。然后 Spark 将每个任务划分成更小的有向无环图(Dag ),表示任务的各个阶段。然后,每个任务被分发并发送给工作节点上的执行器来执行。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-2

Apache Spark Architecture

每个 Spark 应用程序都有自己的一组执行器。因为来自不同应用程序的任务在不同的 JVM 中运行,所以一个 Spark 应用程序不会干扰另一个 Spark 应用程序。这也意味着,如果不使用像 HDFS 或 S3 这样的慢速外部数据源,Spark 应用程序很难共享数据[38]。使用 Tachyon(又名 Alluxio)等堆外内存存储可以使数据共享更快更容易。我在第十章讨论了 Alluxio。

执行 Spark 应用程序

我假设您正在使用 Cloudera Enterprise 作为您的大数据平台。在 CDH,Spark 1.x 和 2.x 可以在同一个集群上共存,不会出现任何问题。您可以使用交互式 shell (spark-shell 或 pyspark)或提交应用程序(spark-submit)来执行 Spark 1.x 应用程序。你需要使用不同的命令来访问 Spark 2.x(表 5-1 )。

表 5-1

Spark 1.x and Spark 2.x Commands

| Spark 1.x | Spark 2.x | | :-- | :-- | | Spark-提交 | Spark 2-提交 | | Spark 壳 | Spark 2-外壳 | | 派斯巴基派斯巴基派斯巴基派斯巴基派斯巴基派斯巴基派斯巴基派斯巴基派斯巴基派斯巴基派斯巴基派斯巴基 | 派斯巴基 2 号 |

纱线上的 Spark

YARN 是大多数基于 Hadoop 的平台(如 Cloudera 和 Hortonworks)的默认集群管理器。有两种部署模式可用于在 YARN 中启动 Spark 应用程序。

集群模式

在集群模式下,驱动程序运行在由 YARN 管理的主应用程序中。客户端可以退出而不影响应用程序的执行。以集群模式启动应用程序或 spark-shell:

spark-shell --master yarn --deploy-mode cluster

spark-submit --class mypath.myClass --master yarn --deploy-mode cluster

客户端模式

在客户端模式下,驱动程序在客户端上运行。应用程序主机仅用于向 YARN 请求资源。要在客户端模式下启动应用程序或 spark-shell:

spark-shell --master yarn --deploy-mode client

spark-submit --class mypath.myClass --master yarn --deploy-mode client

Spark 壳简介

您通常使用交互式 shell 进行特定的数据分析或探索。也是学习 Spark API 的好工具。Spark 的交互 shell 有 Scala 或 Python 版本。在下面的例子中,我们将创建一个城市列表,并将它们全部转换为大写。

spark2-shell

Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Spark context Web UI available at http://10.0.1.101:4040
Spark context available as 'sc' (master = yarn, app id = application_1513771857144_0002).
Spark session available as 'spark'.
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 2.2.0.cloudera1
      /_/

Using Scala version 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_151)
Type in expressions to have them evaluated.
Type :help for more information.

scala> val myCities = sc.parallelize(List("tokyo","new york","sydney","san francisco"))
myCities: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[0] at parallelize at <console>:24

scala> val uCities = myCities.map {x => x.toUpperCase}
uCities: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[1] at map at <console>:26

scala> uCities.collect.foreach(println)

TOKYO
NEW YORK
SYDNEY
SAN FRANCISCO

Listing 5-1Introduction to spark-shell

你将在整章中使用 Spark 壳。当您启动如清单 5-1 所示的 spark2-shell 时,会自动创建一个名为“spark”的 SparkSession。

Spark 会议

如图 5-2 所示,SparkContext 支持访问所有 Spark 特性和功能。驱动程序使用 SparkContext 来访问其他上下文,如 StreamingContext、SQLContext 和 HiveContext。从 Spark 2.0 开始,SparkSession 提供了与 Spark 交互的单一入口点。SparkContext 提供的所有功能,如 Spark 1.x 中的 SQLContext、HiveContext 和 StreamingContext,现在都可以通过 SparkSession 访问。VI

在 Spark 1.x 中,您可以编写类似这样的代码。

val sparkConf = new SparkConf().setAppName("MyApp").setMaster("local")

val sc = new SparkContext(sparkConf).set("spark.executor.cores", "4")

val sqlContext = new org.apache.spark.sql.SQLContext(sc)

在 Spark 2.x 中,您不必显式创建 SparkConf、SparkContext 或 SQLContext,因为它们的所有功能都已经包含在 SparkSession 中。

val spark = SparkSession.
builder().
appName("MyApp").
config("spark.executor.cores", "4").
getOrCreate()

蓄电池

累加器是只被“添加”的变量。它们通常用于实现计数器。在示例中,我使用累加器将数组的元素相加:

val accum = sc.longAccumulator("Accumulator 01")

sc.parallelize(Array(10, 20, 30, 40)).foreach(x => accum.add(x))

accum.value
res2: Long = 100

广播变量

广播变量是存储在每个执行器节点内存中的只读变量。Spark 使用高速广播算法来减少复制广播变量的网络延迟。使用广播变量在每个节点上存储数据集的副本是一种更快的方法,而不是将数据存储在 HDFS 或 S3 这样的慢速存储引擎中。

val broadcastVar = sc.broadcast(Array(10, 20, 30))

broadcastVar.value
res0: Array[Int] = Array(10, 20, 30)

放射性散布装置

RDD 是一个有弹性的不可变分布式对象集合,跨集群中的一个或多个节点进行分区。RDD 可以通过两种类型的操作并行处理和操作:转换和动作。

Note

RDD 是 Spark 1.x 中 Spark 的主要编程接口。从 Spark 2.0 开始,数据集已经取代 RDD 成为主要的 API。由于更丰富的编程界面和更好的性能,强烈建议用户从 RDD 切换到数据集。我们将在本章后面讨论数据集和数据帧。

创建 RDD

创建 RDD 非常简单。你可以从现有的 Scala 集合中创建一个 RDD,或者从存储在 HDFS 或 S3 的外部文件中读取。

平行放置

并行化从 Scala 集合创建一个 RDD。

val myList = (1 to 5).toList
val myRDD = sc.parallelize(myList)
val myCitiesRDD = sc.parallelize(List("tokyo","new york","sydney","san francisco"))

文本文件

文本文件从储存在 HDFS 或 S3 的文本文件创建 RDD。

val myRDD = sc.textFile("hdfs://master01:9000/files/mydirectory")

val myRDD = sc.textFile("s3a://mybucket/files/mydata.csv")

请注意,RDD 是不可变的。需要执行任何类型的数据转换的操作将需要创建另一个 RDD。RDD 操作可以分为两类:转换和行动。

转换

转换是创建新 RDD 的操作。我描述了一些最常见的转换。有关完整的列表,请参考在线 Spark 文档。

地图

Map 对 RDD 中的每个元素执行一个函数。它创建并返回结果的新 RDD。地图的返回类型不必与原始 RDD 的类型相同。

val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
val uCities = myCities.map {x => x.toUpperCase}
uCities.collect.foreach(println)
TOKYO
NEW YORK
PARIS
SAN FRANCISCO

让我们展示另一个地图的例子。

val lines = sc.parallelize(List("Michael Jordan", "iPhone"))

val words = lines.map(line => line.split(" "))

words.collect

res2: Array[Array[String]] = Array(Array(Michael, Jordan), Array(iPhone))  

平面地图

Flatmap 对 RDD 中的每个元素执行一个函数,然后对结果进行拼合。

val lines = sc.parallelize(List("Michael Jordan", "iPhone"))

val words = lines.flatMap(line => line.split(" "))

words.collect

res3: Array[String] = Array(Michael, Jordan, iPhone)

过滤器

返回仅包含符合指定条件的元素的 RDD。

val lines = sc.parallelize(List("Michael Jordan", "iPhone","Michael Corleone"))

val words = lines.map(line => line.split(" "))

val results = words.filter(w => w.contains("Michael"))

results.collect

res9: Array[Array[String]] = Array(Array(Michael, Jordan), Array(Michael, Corleone))

明显的

仅返回不同的值。

val myCities1 = sc.parallelize(List("tokyo","tokyo","paris","sydney"))

val myCities2 = sc.parallelize(List("perth","tokyo","canberra","sydney"))

使用 union 合并结果。

val citiesFromBoth = myCities1.union(myCities2)

仅显示不同的值。

citiesFromBoth.distinct.collect.foreach(println)

sydney
perth
canberra
tokyo
paris

ReduceByKey

用同一个键组合值。使用指定的 reduce 函数。

val pairRDD = sc.parallelize(List(("a", 1), ("b",2), ("c",3), ("a", 30), ("b",25), ("a",20)))
val sumRDD = pairRDD.reduceByKey((x,y) => x+y)
sumRDD.collect
res15: Array[(String, Int)] = Array((b,27), (a,51), (c,3))   

返回一个只包含键的 RDD。

val myRDD = sc.parallelize(List(("a", "Larry"), ("b", "Curly"), ("c", "Moe")))

val keysRDD = myRDD.keys

keysRDD.collect.foreach(println)

a
b
c

价值观念

返回只包含值的 RDD。

val myRDD = sc.parallelize(List(("a", "Larry"), ("b", "Curly"), ("c", "Moe")))

val valRDD = myRDD.values

valRDD.collect.foreach(println)

Larry
Curly
Moe

内部连接

基于连接谓词返回两个 RDD 中所有元素的 RDD。

val employee = Array((100,"Jim Hernandez"), (101,"Shane King"))
val employeeRDD = sc.parallelize(employee)

val employeeCity = Array((100,"Glendale"), (101,"Burbank"))
val employeeCityRDD = sc.parallelize(employeeCity)

val employeeState = Array((100,"CA"), (101,"CA"), (102,"NY"))
val employeeStateRDD = sc.parallelize(employeeState)

val employeeRecordRDD = employeeRDD.join(employeeCityRDD).join(employeeStateRDD)

employeeRecordRDD.collect.foreach(println)

(100,((Jim Hernandez,Glendale),CA))
(101,((Shane King,Burbank),CA))

RightOuterJoin / LeftOuterJoin

返回右 RDD 中所有元素的 RDD,即使左 RDD 中没有匹配的行。左外连接相当于右外连接,只是列的顺序不同。

val employeeRecordRDD = employeeRDD.join(employeeCityRDD).rightOuterJoin(employeeStateRDD)

employeeRecordRDD.collect.foreach(println)

(100,(Some((Jim Hernandez,Glendale)),CA))
(102,(None,NY))
(101,(Some((Shane King,Burbank)),CA))

联盟

返回包含两个或更多 RDD 组合的 RDD。

val  employee2 = Array((103,"Mark Choi","Torrance","CA"), (104,"Janet Reyes","Rolling Hills","CA"))
val employee2RDD = sc.parallelize(employee2)
val  employee3 = Array((105,"Lester Cruz","Van Nuys","CA"), (106,"John White","Inglewood","CA"))
val employee3RDD = sc.parallelize(employee3)
employeesRDD.collect.foreach(println)

(103,Mark Choi,Torrance,CA)
(104,Janet Reyes,Rolling Hills,CA)
(105,Lester Cruz,Van Nuys,CA)
(106,John White,Inglewood,CA)

减去

返回仅包含第一个 RDD 中的元素的 RDD。

val  listEmployees = Array((103,"Mark Choi","Torrance","CA"), (104,"Janet Reyes","Rolling Hills","CA"),(105,"Lester Cruz","Van Nuys","CA"))
val listEmployeesRDD = sc.parallelize(listEmployees)

val  exEmployees = Array((103,"Mark Choi","Torrance","CA"))
val exEmployeesRDD = sc.parallelize(exEmployees)

val currentEmployeesRDD = listEmployeesRDD.subtract(exEmployeesRDD)

currentEmployeesRDD.collect.foreach(println)

(105,Lester Cruz,Van Nuys,CA)
(104,Janet Reyes,Rolling Hills,CA)

联合

联合减少了 RDD 中的分区数量。在大型 RDD 上执行过滤后,您可能需要使用合并。虽然过滤减少了新 RDD 消耗的数据量,但它继承了原始 RDD 的分区数量。如果新的 RDD 比原来的 RDD 小得多,它可能会有成百上千个小分区,这可能会导致性能问题。

当您希望在写入 HDFS 时减少 Spark 生成的文件数量,防止可怕的“小文件”问题时,使用 coalesce 也很有用。每个分区作为单独的文件写入 HDFS。请注意,使用 coalesce 时,您可能会遇到性能问题,因为在写入 HDFS 时,您会有效地降低并行度。如果发生这种情况,请尝试增加分区的数量。这也适用于数据帧,我将在后面讨论。在下面的例子中,我们只写了一个拼花文件给 HDFS。

DF.coalesce(1).write.mode("append").parquet("/user/hive/warehouse/Mytable")

再分

重新分区可以减少或增加 RDD 中的分区数量。减少分区时通常会使用联合,因为它比重新分区更有效。在写入 HDFS 时,增加分区数量可能有助于提高并行度。这也适用于数据帧,我将在后面讨论。在下面的例子中,我们写了 6 个拼花文件给 HDFS。

DF.repartition (6).write.mode("append").parquet("/user/hive/warehouse/Mytable")

Note

合并通常比重新分区快。重新分区将执行完全洗牌,创建新分区并在工作节点之间平均分配数据。通过使用现有分区,联合最大限度地减少了数据移动并避免了完全洗牌。

行动

动作是向驱动程序返回值的 RDD 操作。我列出了一些最常见的动作。有关 RDD 行动的完整列表,请参考在线 Spark 文档。

收集

将 RDD 的全部内容返回给驱动程序。不适合大型数据集。

val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
myCities.collect
res2: Array[String] = Array(tokyo, new york, paris, san francisco)

将 RDD 的子集返回给驱动程序。

val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
myCities.take(2)
res4: Array[String] = Array(tokyo, new york)

数数

返回 RDD 中的项目数。

val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
myCities.count
res3: Long = 4   

为每一个

对 RDD 的每一项执行所提供的功能。

val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))

myCities.collect.foreach(println)

tokyo
new york
paris
san Francisco

懒惰评估

Spark 支持惰性求值,这对于大数据处理至关重要。Spark 中的所有转换都是延迟计算的。Spark 不会立即执行转换。您可以继续定义更多的转换。当您最终想要最终结果时,您执行一个动作,这将导致转换被执行。

贮藏

默认情况下,每次运行操作时,都会重新执行每个转换。您可以使用 cache 或 persist 方法在内存中缓存 RDD,以避免多次重复执行转换。有几个持久性级别可供选择,如 MEMORY_ONLY、MEMORY_ONLY_SER、MEMORY_AND_DISK、MEMORY_AND_DISK_SER。和仅磁盘。有关缓存的更多细节,请参考 Apache Spark 的在线文档。使用 Alluxio(以前称为 Tachyon)的堆外缓存在第十章中讨论。

Spark SQL、数据集和数据帧 API

虽然目前大多数令人兴奋的事情都集中在涉及非结构化和半结构化数据(视频分析、图像处理和文本挖掘等)的用例上,但大多数实际的数据分析和处理仍然是在结构化的关系数据上完成的。公司的大多数重要商业决策仍然基于对关系数据的分析。

SparkSQL 的开发是为了让 Spark 数据工程师和数据科学更容易处理和分析结构化数据。数据集类似于 RDD,因为它支持强类型,但是在它的内部有一个更高效的引擎。从 Spark 2.0 开始,数据集 API 现在是主要的编程接口。DataFrame 只是一个带有命名列的数据集,非常类似于关系表。Spark SQL 和 DataFrames 一起为处理和分析结构化数据提供了强大的编程接口。这里有一个关于如何使用 DataFrames API 的简单例子。稍后我将更详细地讨论 DataFrames API。

val jsonDF = spark.read.json("/jsondata")

jsonDF.show
+---+------+--------------+-----+------+-----+
|age|  city|          name|state|userid|  zip|
+---+------+--------------+-----+------+-----+
| 35|Frisco| Jonathan West|   TX|   200|75034|
| 28|Dallas|Andrea Foreman|   TX|   201|75001|
| 69| Plano|  Kirsten Jung|   TX|   202|75025|
| 52| Allen|Jessica Nguyen|   TX|   203|75002|
+---+------+--------------+-----+------+-----+

jsonDF.select ("age", "city").show

+---+------+
|age|  city|
+---+------+
| 35|Frisco|
| 28|Dallas|
| 69| Plano|
| 52| Allen|
+---+------+

jsonDF.filter($"userid" < 202).show()
+---+------+--------------+-----+------+-----+
|age|  city|          name|state|userid|  zip|
+---+------+--------------+-----+------+-----+
| 35|Frisco| Jonathan West|   TX|   200|75034|
| 28|Dallas|Andrea Foreman|   TX|   201|75001|

+---+------+--------------+-----+------+-----+

jsonDF.createOrReplaceTempView("jsonDF")

val uzDF = spark.sql("SELECT userid, zip FROM jsonDF")

uzDF.show
+------+-----+
|userid|  zip|
+------+-----+
|   200|75034|
|   201|75001|
|   202|75025|
|   203|75002|
+------+-----+

Note

Spark 2.0 中统一了数据帧和数据集 API。DataFrame 现在只是行数据集的类型别名,其中行是通用的非类型化对象。相比之下,Dataset 是强类型对象 Dataset[T]的集合。Scala 支持强类型和非类型 API,而在 Java 中,Dataset[T]是主要的抽象。DataFrames 是 R 和 Python 的主要编程接口,因为它缺乏对编译时类型安全的支持。

Spark 数据源

数据处理中一些最常见的任务是读取和写入不同的数据源。在接下来的几页中,我提供了几个例子。我在第六章中介绍了 Spark 和 Kudu integration。

战斗支援车

Spark 为您提供了从 CSV 文件中读取数据的不同方法。您可以先将数据读入 RDD,然后将其转换为 DataFrame。

val dataRDD = sc.textFile("/sparkdata/customerdata.csv")
val parsedRDD = dataRDD.map{_.split(",")}
case class CustomerData(customerid: Int, name: String, city: String, state: String, zip: String)
val dataDF = parsedRDD.map{ a => CustomerData (a(0).toInt, a(1).toString, a(2).toString,a(3).toString,a(4).toString) }.toDF

您也可以使用 Databricks CSV 包。该方法直接读取数据帧中的数据。

spark-shell --packages com.databricks:spark-csv_2.10:1.5.0

val dataDF = sqlContext.read.format("csv")
  .option("header", "true")
  .option("inferSchema", "true")
  .load("/sparkdata/customerdata.csv")

从 Spark 2.0 开始,CSV 连接器已经内置,因此无需使用 Databrick 的第三方包。

val dataDF = spark.read
              .option("header", "true")
              .option("inferSchema", "true")    
              .csv("/sparkdata/customerdata.csv")

可扩展置标语言

Databricks 有一个 Spark xml 包,可以轻松读取 xml 数据。

cat users.xml

<userid>100</userid><name>Wendell Ryan</name><city>San Diego</city><state>CA</state><zip>92102</zip>
<userid>101</userid><name>Alicia Thompson</name><city>Berkeley</city><state>CA</state><zip>94705</zip>
<userid>102</userid><name>Felipe Drummond</name><city>Palo Alto</city><state>CA</state><zip>94301</zip>
<userid>103</userid><name>Teresa Levine</name><city>Walnut Creek</city><state>CA</state><zip>94507</zip>

hadoop fs -mkdir /xmldata
hadoop fs -put users.xml /xmldata

spark-shell --packages  com.databricks:spark-xml:2.10:0.4.1

使用 Spark XML 创建一个数据帧。在本例中,我们指定了行标记和 XML 文件所在的 HDFS 路径。

val xmlDF = sqlContext.read.format("com.databricks.spark.xml").option("rowTag", "user").load("/xmldata/");

xmlDF: org.apache.spark.sql.DataFrame = [city: string, name: string, state: string, userid: bigint, zip: bigint]

我们也来看看数据。

xmlDF.show

+------------+---------------+-----+------+-----+
|        city|           name|state|userid|  zip|
+------------+---------------+-----+------+-----+
|   San Diego|   Wendell Ryan|   CA|   100|92102|
|    Berkeley|Alicia Thompson|   CA|   101|94705|
|   Palo Alto|Felipe Drummond|   CA|   102|94301|
|Walnut Creek|  Teresa Levine|   CA|   103|94507|
+------------+---------------+-----+------+-----+

数据

我们将创建一个 JSON 文件作为这个例子的样本数据。确保该文件位于 HDFS 名为/jsondata 的文件夹中。

cat users.json

{"userid": 200, "name": "Jonathan West", "city":"Frisco", "state":"TX", "zip": "75034", "age":35}
{"userid": 201, "name": "Andrea Foreman", "city":"Dallas", "state":"TX", "zip": "75001", "age":28}
{"userid": 202, "name": "Kirsten Jung", "city":"Plano", "state":"TX", "zip": "75025", "age":69}
{"userid": 203, "name": "Jessica Nguyen", "city":"Allen", "state":"TX", "zip": "75002", "age":52}

从 JSON 文件创建一个数据帧。

val jsonDF = sqlContext.read.json("/jsondata")

jsonDF: org.apache.spark.sql.DataFrame = [age: bigint, city: string, name: string, state: string, userid: bigint, zip: string]

检查日期

jsonDF.show

+---+------+--------------+-----+------+-----+
|age|  city|          name|state|userid|  zip|
+---+------+--------------+-----+------+-----+
| 35|Frisco| Jonathan West|   TX|   200|75034|
| 28|Dallas|Andrea Foreman|   TX|   201|75001|
| 69| Plano|  Kirsten Jung|   TX|   202|75025|
| 52| Allen|Jessica Nguyen|   TX|   203|75002|
+---+------+--------------+-----+------+-----+

使用 JDBC 的关系数据库

我们在这个例子中使用 MySQL,但是也支持其他关系数据库,例如 Oracle、SQL Server、Teradata 和 PostgreSQL 等等。只要关系数据库有 JDBC/ODBC 驱动程序,就应该可以从 Spark 访问它。性能取决于您的 JDBC/ODBC 驱动程序对批处理操作的支持。请查看您的 JDBC 驱动程序文档以了解更多详细信息。

mysql -u root -pmypassword

create databases salesdb;

use salesdb;

create table customers (
customerid INT,
name VARCHAR(100),
city VARCHAR(100),
state CHAR(3),
zip  CHAR(5));

spark-shell --driver-class-path mysql-connector-java-5.1.40-bin.jar --jars mysql-connector-java-5.1.40-bin.jar

Note

在某些版本的 Spark - jars 中,没有在驱动程序的类路径中添加 JAR。 vii 建议您将 JDBC 驱动程序包含在您的- jars 和 Spark 类路径中。VIII

启动 Spark 壳。请注意,我必须将 MySQL 驱动程序作为参数包含在–driver-class-path 和–jar 中。在较新版本的 Spark 中,您可能不需要这样做。

将 csv 文件读入数据帧

val dataRDD = sc.textFile("/home/hadoop/test.csv")
val parsedRDD = dataRDD.map{_.split(",")}
case class CustomerData(customerid: Int, name: String, city: String, state: String, zip: String)

val dataDF = parsedRDD.map{ a => CustomerData (a(0).toInt, a(1).toString, a(2).toString,a(3).toString,a(4).toString)}.toDF

将数据帧注册为临时表,以便我们可以对其运行 SQL 查询。在 Spark 2.x 中,使用 createOrReplaceTempView。

dataDF.registerTempTable("dataDF")

让我们设置连接属性。

val jdbcUsername = "myuser"
val jdbcPassword = "mypass"
val jdbcHostname = "10.0.1.112"
val jdbcPort = 3306
val jdbcDatabase ="salesdb"
val jdbcrewriteBatchedStatements = "true"
val jdbcUrl = s"jdbc:mysql://${jdbcHostname}:${jdbcPort}/${jdbcDatabase}?user=${jdbcUsername}&password=${jdbcPassword}&rewriteBatchedStatements=${jdbcrewriteBatchedStatements}"

val connectionProperties = new java.util.Properties()

这将允许我们指定正确的保存模式——追加、覆盖等。

import org.apache.spark.sql.SaveMode

将 SELECT 语句返回的数据插入到 MySQL salesdb 数据库中存储的 customer 表中。

sqlContext.sql("select * from dataDF").write.mode(SaveMode.Append).jdbc(jdbcUrl, "customers", connectionProperties)

让我们用 JDBC 读一个表格。让我们用一些测试数据填充 MySQL 中的 users 表。确保 salesdb 数据库中存在 users 表。

mysql -u root -pmypassword

use salesdb;

describe users;
+--------+--------------+------+-----+---------+-------+
| Field  | Type         | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| userid | bigint(20)   | YES  |     | NULL    |       |
| name   | varchar(100) | YES  |     | NULL    |       |
| city   | varchar(100) | YES  |     | NULL    |       |
| state  | char(3)      | YES  |     | NULL    |       |
| zip    | char(5)      | YES  |     | NULL    |       |
| age    | tinyint(4)   | YES  |     | NULL    |       |
+--------+--------------+------+-----+---------+-------+

select * from users;
Empty set (0.00 sec)

insert into users values (300,'Fred Stevens','Torrance','CA',90503,23);

insert into users values (301,'Nancy Gibbs','Valencia','CA',91354,49);

insert into users values (302,'Randy Park','Manhattan Beach','CA',90267,21);

insert into users values (303,'Victoria Loma','Rolling Hills','CA',90274,75);

 select * from users;

+--------+---------------+-----------------+-------+-------+------+
| userid | name          | city            | state | zip   | age  |
+--------+---------------+-----------------+-------+-------+------+
|    300 | Fred Stevens  | Torrance        | CA    | 90503 |   23 |
|    301 | Nancy Gibbs   | Valencia        | CA    | 91354 |   49 |
|    302 | Randy Park    | Manhattan Beach | CA    | 90267 |   21 |
|    303 | Victoria Loma | Rolling Hills   | CA    | 90274 |   75 |
+--------+---------------+-----------------+-------+-------+------+

spark-shell --driver-class-path mysql-connector-java-5.1.40-bin.jar --jars mysql-connector-java-5.1.40-bin.jar

让我们设置 jdbc url 和连接属性。

val jdbcURL = s"jdbc:mysql://10.0.1.101:3306/salesdb?user=myuser&password=mypass"

val connectionProperties = new java.util.Properties()

我们可以从整个表中创建一个数据帧。

val mysqlDF = sqlContext.read.jdbc(jdbcURL, "users", connectionProperties)

mysqlDF.show
+------+-------------+---------------+-----+-----+---+
|userid|         name|           city|state|  zip|age|
+------+-------------+---------------+-----+-----+---+
|   300| Fred Stevens|       Torrance|   CA|90503| 23|
|   301|  Nancy Gibbs|       Valencia|   CA|91354| 49|
|   302|   Randy Park|Manhattan Beach|   CA|90267| 21|
|   303|Victoria Loma|  Rolling Hills|   CA|90274| 75|
+------+-------------+---------------+-----+-----+---+

镶木地板

在拼花地板上读写很简单。

val employeeDF = spark.read.load("/sparkdata/employees.parquet")

employeeDF.select("id","firstname","lastname","salary").write.format("parquet").save("/sparkdata/myData.parquet")

您可以直接在 Parquet 文件上运行 SELECT 语句。

val myDF = spark.sql("SELECT * FROM parquet.`/sparkdata/myData.parquet`")

巴什

从 Spark 访问 HBase 有不同的方法。如前所述,Scala 可以访问所有 Java 库,包括 HBase 客户端 API。这并不是从 Spark 访问 HBase 的首选方式,但一些开发人员可能会发现它们很方便。另一种访问 HBase 的方式是通过 Impala,我将在第六章中讨论。

Note

HBase 上的 Spark 是从 Spark 访问 HBase 的首选方式。然而,在撰写本文时,它只能在 Spark 1.x 上运行。HBase 上的 Spark 在 Spark 2.x 上不受支持。

您可以使用 SaveAsHadoopDataset 将数据写入 HBase。

启动 HBase shell。创建一个 HBase 表并用测试数据填充它。

hbase shell

create 'users', 'cf1'

启动 Spark 壳。

spark-shell

导入所有必需的包。

import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.{HBaseConfiguration, HTableDescriptor}
import org.apache.hadoop.hbase.client.HBaseAdmin
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.HColumnDescriptor
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;

val hconf = HBaseConfiguration.create()
val jobConf = new JobConf(hconf, this.getClass)
jobConf.setOutputFormat(classOf[TableOutputFormat])
jobConf.set(TableOutputFormat.OUTPUT_TABLE,"users")

val num = sc.parallelize(List(1,2,3,4,5,6))

val theRDD = num.filter.map(x=>{

        val rowkey = "row" + x

        val put = new Put(Bytes.toBytes(rowkey))

        put.add(Bytes.toBytes("cf1"), Bytes.toBytes("fname"), Bytes.toBytes("my fname" + x))

    (new ImmutableBytesWritable, put)
})
theRDD.saveAsHadoopDataset(jobConf)

您还可以使用 Spark 中的 HBase 客户端 API 来读写 HBase 中的数据。

启动 HBase shell。创建另一个 HBase 表,并用测试数据填充它。

hbase shell

create 'employees', 'cf1'

put 'employees','400','cf1:name', 'Patrick Montalban'
put 'employees','400','cf1:city', 'Los Angeles'
put 'employees','400','cf1:state', 'CA'
put 'employees','400','cf1:zip', '90010'
put 'employees','400','cf1:age', '71'
put 'employees','401','cf1:name', 'Jillian Collins'
put 'employees','401','cf1:city', 'Santa Monica'
put 'employees','401','cf1:state', 'CA'
put 'employees','401','cf1:zip', '90402'
put 'employees','401','cf1:age', '45'

put 'employees','402','cf1:name', 'Robert Sarkisian'
put 'employees','402','cf1:city', 'Glendale'

put 'employees','402','cf1:state', 'CA'
put 'employees','402','cf1:zip', '91204'
put 'employees','402','cf1:age', '29'

put 'employees','403','cf1:name', 'Warren Porcaro'
put 'employees','403','cf1:city', 'Burbank'
put 'employees','403','cf1:state', 'CA'
put 'employees','403','cf1:zip', '91523'
put 'employees','403','cf1:age', '62'

让我们验证数据是否成功地插入到我们的 HBase 表中。

scan 'employees'

ROW                    COLUMN+CELL
 400                   column=cf1:age, timestamp=1493105325812, value=71
 400                   column=cf1:city, timestamp=1493105325691, value=Los Angeles
 400                   column=cf1:name, timestamp=1493105325644, value=Patrick Montalban
 400                   column=cf1:state, timestamp=1493105325738, value=CA
 400                   column=cf1:zip, timestamp=1493105325789, value=90010
 401                   column=cf1:age, timestamp=1493105334417, value=45
 401                   column=cf1:city, timestamp=1493105333126, value=Santa Monica
 401                   column=cf1:name, timestamp=1493105333050, value=Jillian Collins
 401                   column=cf1:state, timestamp=1493105333145, value=CA
 401                   column=cf1:zip, timestamp=1493105333165, value=90402
 402                   column=cf1:age, timestamp=1493105346254, value=29
 402                   column=cf1:city, timestamp=1493105345053, value=Glendale
 402                   column=cf1:name, timestamp=1493105344979, value=Robert Sarkisian
 402                   column=cf1:state, timestamp=1493105345074, value=CA
 402                   column=cf1:zip, timestamp=1493105345093, value=91204
 403                   column=cf1:age, timestamp=1493105353650, value=62
 403                   column=cf1:city, timestamp=1493105352467, value=Burbank
 403                   column=cf1:name, timestamp=1493105352445, value=Warren Porcaro
 403                   column=cf1:state, timestamp=1493105352513, value=CA
 403                   column=cf1:zip, timestamp=1493105352549, value=91523

启动 Spark 壳。

spark-shell

导入所有必需的包。

val configuration = HBaseConfiguration.create()

指定 HBase 表和行键。

val table = new HTable(configuration, "employees");
val g = new Get(Bytes.toBytes("401"))
val result = table.get(g);

从表中提取值。

val val2 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("name"));
val val3 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("city"));
val val4 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("state"));
val val5 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("zip"));
val val6 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("age"));

将这些值转换为适当的数据类型。

val id = Bytes.toString(result.getRow())
val name = Bytes.toString(val2);
val city = Bytes.toString(val3);
val state = Bytes.toString(val4);
val zip = Bytes.toString(val5);
val age = Bytes.toShort(val6);

打印数值。

println("employee id: " + id + "name: " + name + "city: " + city + "state: " + state + "zip: " + zip + "age: " + age);

employee id: 401 name: Jillian Collins city: Santa Monica state: CA zip: 90402 age: 13365

让我们使用 HBase API 写入 HBase。

val configuration = HBaseConfiguration.create()
val table = new HTable(configuration, "employees");

指定新的行键。

val p = new Put(new String("404").getBytes());

用新值填充单元格。

p.add("cf1".getBytes(), "name".getBytes(), new String("Denise Shulman").getBytes());
p.add("cf1".getBytes(), "city".getBytes(), new String("La Jolla").getBytes());
p.add("cf1".getBytes(), "state".getBytes(), new String("CA").getBytes());
p.add("cf1".getBytes(), "zip".getBytes(), new String("92093").getBytes());
p.add("cf1".getBytes(), "age".getBytes(), new String("56").getBytes());

写入 HBase 表。

table.put(p);
table.close();

确认值已成功插入 HBase 表中。

启动 HBase shell。

hbase shell

scan 'employees'

ROW                COLUMN+CELL
 400               column=cf1:age, timestamp=1493105325812, value=71
 400               column=cf1:city, timestamp=1493105325691, value=Los Angeles
 400               column=cf1:name, timestamp=1493105325644, value=Patrick Montalban
 400               column=cf1:state, timestamp=1493105325738, value=CA
 400               column=cf1:zip, timestamp=1493105325789, value=90010
 401               column=cf1:age, timestamp=1493105334417, value=45
 401               column=cf1:city, timestamp=1493105333126, value=Santa Monica
 401               column=cf1:name, timestamp=1493105333050, value=Jillian Collins
 401               column=cf1:state, timestamp=1493105333145, value=CA
 401               column=cf1:zip, timestamp=1493105333165, value=90402
 402               column=cf1:age, timestamp=1493105346254, value=29
 402               column=cf1:city, timestamp=1493105345053, value=Glendale
 402               column=cf1:name, timestamp=1493105344979, value=Robert Sarkisian
 402               column=cf1:state, timestamp=1493105345074, value=CA
 402               column=cf1:zip, timestamp=1493105345093, value=91204
 403               column=cf1:age, timestamp=1493105353650, value=62
 403               column=cf1:city, timestamp=1493105352467, value=Burbank
 403               column=cf1:name, timestamp=1493105352445, value=Warren Porcaro
 403               column=cf1:state, timestamp=1493105352513, value=CA
 403               column=cf1:zip, timestamp=1493105352549, value=91523
 404               column=cf1:age, timestamp=1493123890714, value=56
 404               column=cf1:city, timestamp=1493123890714, value=La Jolla
 404               column=cf1:name, timestamp=1493123890714, value=Denise Shulman
 404               column=cf1:state, timestamp=1493123890714, value=CA
 404               column=cf1:zip, timestamp=1493123890714, value=92093

亚马逊 S3

亚马逊 S3 是一个流行的对象存储,经常被用作临时集群的数据存储。它还是备份和冷数据的经济高效的存储方式。从 S3 读取数据就像从 HDFS 或任何其他文件系统读取数据一样。

阅读来自亚马逊 S3 的 CSV 文件。请确保您已经配置了 S3 凭据。

val myCSV = sc.textFile("s3a://mydata/customers.csv")

将 CSV 数据映射到 RDD。

import org.apache.spark.sql.Row

val myRDD = myCSV.map(_.split(',')).map(e ⇒ Row(r(0).trim.toInt, r(1), r(2).trim.toInt, r(3)))

创建一个模式。

import org.apache.spark.sql.types.{StructType, StructField, StringType, IntegerType};

val mySchema = StructType(Array(
StructField("customerid",IntegerType,false),
StructField("customername",StringType,false),
StructField("age",IntegerType,false),
StructField("city",StringType,false)))

val myDF = sqlContext.createDataFrame(myRDD, mySchema)

使用

Solr 是一个流行的搜索平台,提供全文搜索和实时索引功能。您可以使用 SolrJ 从 Spark 与 Solr 进行交互。 ix

import java.net.MalformedURLException;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrServer;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocumentList;

val solr = new HttpSolrServer("http://master02:8983/solr/mycollection");

val query = new SolrQuery();

query.setQuery("*:*");
query.addFilterQuery("userid:3");
query.setFields("userid","name","age","city");
query.setStart(0);    
query.set("defType", "edismax");

val response = solr.query(query);
val results = response.getResults();

println(results);

从 Spark 访问 Solr 集合的一个更好的方法是使用 spark-solr 包。Lucidworks 启动了 spark-solr 项目来提供 Spark-Solr 集成。与 solrJ 相比,使用 spark-solr 要简单和强大得多,它允许你从 Solr 集合中创建数据帧,并使用 SparkSQL 与它们进行交互。

首先从 spark-shell 导入 jar 文件。您可以从 Lucidworks 的网站下载 jar 文件。

spark-shell --jars spark-solr-3.0.1-shaded.jar

指定集合和连接信息。

val myOptions = Map( "collection" -> "mycollection","zkhost" -> "{ master02:8983/solr}")

创建一个数据帧。

val solrDF = spark.read.format("solr")
  .options(myOptions)
  .load

微软优越试算表

我遇到过几个关于如何从 Spark 访问 Excel 工作表的请求。虽然这不是我通常会做的事情,但使用 Excel 是几乎每个企业 IT 环境中的现实。

一家名为 Crealytics 的公司开发了一个用于与 Excel 交互的 Spark 插件。该库需要 Spark 2.x。可以使用- packages 命令行选项添加该包。Xi

spark-shell --packages com.crealytics:spark-excel_2.11:0.9.12

从 Excel 工作表创建数据帧。

val ExcelDF = spark.read
    .format("com.crealytics.spark.excel")
    .option("sheetName", "sheet1")
    .option("useHeader", "true")
    .option("inferSchema", "true")
    .option("treatEmptyValuesAsNulls", "true")
    .load("budget.xlsx")

将数据帧写入 Excel 工作表。

ExcelDF2.write
  .format("com.crealytics.spark.excel")
  .option("sheetName", "sheet1")
  .option("useHeader", "true")
  .mode("overwrite")
  .save("budget2.xlsx")

你可以在他们的 github 页面上找到更多的细节:github.com/crealytics.

安全 FTP

从 SFTP 下载文件并将数据帧写入 SFTP 的服务器也是一个流行的请求。SpringML 提供了一个 Spark SFTP 连接器库。该库需要 Spark 2.x 并利用 jsch,这是 SSH2 的一个 Java 实现。对 SFTP 服务器的读写将作为单个进程执行。

可以使用- packages 命令行选项添加软件包。XII

spark-shell --packages com.springml:spark-sftp_2.11:1.1.1

从 SFTP 服务器中的文件创建一个数据帧。

val SftpDF = spark.read.
            format("com.springml.spark.sftp").
            option("host", "sftpserver.com").
            option("username", "myusername").
            option("password", "mypassword").
            option("inferSchema", "true").
            option("fileType", "csv").
            option("delimiter", ",").
            load("/myftp/myfile.csv")

将数据帧作为 CSV 文件写入 FTP 服务器。

SftpDF2.write.
      format("com.springml.spark.sftp").
      option("host", "sftpserver.com").
      option("username", "myusername").
      option("password", "mypassword").
      option("fileType", "csv").
      option("delimiter", ",").
      save("/myftp/myfile.csv")

你可以在他们的 github 页面上找到更多的细节:github.com/springml/spark-sftp.

Spark MLlib(基于数据帧的 API)

机器学习是 Spark 的主要应用之一。基于数据帧的 API (Spark ML 或 Spark ML 管道)现在是 Spark 的主要 API。基于 RDD 的 API (Spark MLlib)正在进入维护模式。我们不会讨论旧的基于 RDD 的 API。以前,基于数据帧的 API 被非正式地称为 Spark ML 和 Spark ML 管道(spark.ml 包),以区别于基于 RDD 的 API,后者是基于原始 spark.mllib 包命名的。一旦基于数据帧的 API 达到特性对等,基于 RDD 的 API 在 Spark 2.3 中将被弃用。 xiii 基于 RDD 的 API 将在 Spark 3.0 中被移除。目前,Spark MLlib 包含这两种 API。

基于 DataFrames 的 API 比基于 RDD 的 API 更快更容易使用,允许用户使用 SQL 并利用 Catalyst 和钨优化。基于 DataFrames 的 API 通过提供更高级别的抽象来表示类似于关系数据库表的表格数据,使转换功能变得容易,这使它成为实现管道的自然选择。

在典型的机器学习流水线中执行的大多数操作是特征转换。如图 5-3 所示,DataFrames 便于执行特征转换。 xiv 记号赋予器将文本分解成单词包,将单词附加到输出的第二数据帧中。TF-IDF 将第二个数据帧作为输入,将单词包转换为特征向量,并将它们添加到第三个数据帧中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-3

Example Feature Transformation in a typical Spark ML pipeline

管道

流水线只是创建机器学习工作流的一系列相连的阶段。一个阶段可以是一个转换器或估计器。

变压器

转换器将一个数据帧作为输入,并输出一个新的数据帧,其中附加了附加列。新数据帧包括来自输入数据帧的列和附加列。

估计量

估计是一种机器学习算法,适合训练数据的模型。估计器估计器接受训练数据并产生机器学习模型。

ParamGridBuilder

ParamGridBuilder 用于构建参数网格。CrossValidator 执行网格搜索,并用参数网格中用户指定的超参数组合来训练模型。

交叉验证器

CrossValidator 交叉评估拟合的机器学习模型,并通过尝试用用户指定的超参数组合拟合底层估计器来输出最佳模型。

求值程序

评估者计算你的机器学习模型的性能。评估器输出精度或召回率等指标来衡量拟合模型的表现。

例子

让我们来看一个例子。我们将使用来自 UCI 机器学习库中的心脏病数据集【XV】来预测心脏病的存在。这些数据是由罗伯特德特拉诺,医学博士,博士,从弗吉尼亚州医学中心,长滩和克利夫兰诊所基金会收集的。历史上,克利夫兰数据集一直是众多研究的主题,因此我们将使用该数据集。原始数据集有 76 个属性,但其中只有 14 个是 ML 研究人员传统使用的(表 5-2 )。我们将简单地执行二项式分类,并确定患者是否患有心脏病。

表 5-2

Cleveland Heart Disease Data Set Attribute Information

| 属性 | 描述 | | :-- | :-- | | 年龄 | 年龄 | | 性 | 性 | | 丙酸纤维素 | 胸痛型 | | treatbps | 静息血压 | | 胆固醇 | 血清胆固醇(毫克/分升) | | 前沿系统 | 空腹血糖> 120 毫克/分升 | | 尊重 | 静息心电图结果 | | 塔尔巴赫 | 达到最大心率 | | 考试 | 运动诱发的心绞痛 | | 旧峰 | 相对于静息运动诱发的 ST 段压低 | | 倾斜 | 运动 ST 段峰值的斜率 | | 大约 | 荧光镜染色的主要血管数量(0-3) | | 塔尔 | 唐松草压力测试结果 | | 数字 | 预测属性——心脏病的诊断 |

我们开始吧。开始之前,将列名添加到 CSV 文件中。我们需要下载文件并拷贝到 HDFS。

wget http://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/cleveland.data

head -n 10 processed.cleveland.data

63.0,1.0,1.0,145.0,233.0,1.0,2.0,150.0,0.0,2.3,3.0,0.0,6.0,0
67.0,1.0,4.0,160.0,286.0,0.0,2.0,108.0,1.0,1.5,2.0,3.0,3.0,2
67.0,1.0,4.0,120.0,229.0,0.0,2.0,129.0,1.0,2.6,2.0,2.0,7.0,1
37.0,1.0,3.0,130.0,250.0,0.0,0.0,187.0,0.0,3.5,3.0,0.0,3.0,0
41.0,0.0,2.0,130.0,204.0,0.0,2.0,172.0,0.0,1.4,1.0,0.0,3.0,0
56.0,1.0,2.0,120.0,236.0,0.0,0.0,178.0,0.0,0.8,1.0,0.0,3.0,0
62.0,0.0,4.0,140.0,268.0,0.0,2.0,160.0,0.0,3.6,3.0,2.0,3.0,3
57.0,0.0,4.0,120.0,354.0,0.0,0.0,163.0,1.0,0.6,1.0,0.0,3.0,0
63.0,1.0,4.0,130.0,254.0,0.0,2.0,147.0,0.0,1.4,2.0,1.0,7.0,2
53.0,1.0,4.0,140.0,203.0,1.0,2.0,155.0,1.0,3.1,3.0,0.0,7.0,1

hadoop fs -put processed.cleveland.data /tmp/data

然后使用 spark-shell 通过 Spark MLlib 交互式地创建我们的模型,如清单 5-2 所示。

spark-shell --packages com.databricks:spark-csv_2.10:1.5.0

import org.apache.spark._
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SQLContext
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types._
import org.apache.spark.sql._
import org.apache.spark.ml.classification.RandomForestClassifier
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
import org.apache.spark.ml.feature.StringIndexer
import org.apache.spark.ml.feature.VectorAssembler
import org.apache.spark.ml.tuning.{ParamGridBuilder, CrossValidator}
import org.apache.spark.ml.{Pipeline, PipelineStage}
import org.apache.spark.mllib.evaluation.RegressionMetrics
import org.apache.spark.ml.param.ParamMap
import org.apache.kudu.spark.kudu._

val dataDF = sqlContext.read.format("csv")
  .option("header", "true")
  .option("inferSchema", "true")
  .load("/tmp/data/processed.cleveland.data")

dataDF.printSchema
root
 |-- id: string (nullable = false)
 |-- age: float (nullable = true)
 |-- sex: float (nullable = true)
 |-- cp: float (nullable = true)
 |-- trestbps: float (nullable = true)
 |-- chol: float (nullable = true)
 |-- fbs: float (nullable = true)
 |-- restecg: float (nullable = true)
 |-- thalach: float (nullable = true)
 |-- exang: float (nullable = true)
 |-- oldpeak: float (nullable = true)
 |-- slope: float (nullable = true)
 |-- ca: float (nullable = true)
 |-- thal: float (nullable = true)
 |-- num: float (nullable = true)

val myFeatures = Array("age", "sex", "cp", "trestbps", "chol", "fbs",
      "restecg", "thalach", "exang", "oldpeak", "slope",
      "ca", "thal", "num")

val myAssembler = new VectorAssembler().setInputCols(myFeatures).setOutputCol("features")

val dataDF2 = myAssembler.transform(dataDF)

val myLabelIndexer = new StringIndexer().setInputCol("num").setOutputCol("label")

val dataDF3 = mylabelIndexer.fit(dataDF2).transform(dataDF2)

val dataDF4 = dataDF3.where(dataDF3("ca").isNotNull).where(dataDF3("thal").isNotNull).where(dataDF3("num").isNotNull)

val Array(trainingData, testData) = dataDF4.randomSplit(Array(0.8, 0.2), 101)

val myRFclassifier = new RandomForestClassifier().setFeatureSubsetStrategy("auto").setSeed(101)

val myEvaluator = new BinaryClassificationEvaluator().setLabelCol("label")

val myParamGrid = new ParamGridBuilder()
      .addGrid(myRFclassifier.maxBins, Array(10, 20, 30))
      .addGrid(myRFclassifier.maxDepth, Array(5, 10, 15))
      .addGrid(myRFclassifier.numTrees, Array(20, 30, 40))
      .addGrid(myRGclassifier.impurity, Array("gini", "entropy"))
      .build()

val myPipeline = new Pipeline().setStages(Array(myRFclassifier))

val myCV = new CrosValidator()
      .setEstimator(myPipeline)
      .setEvaluator(myEvaluator)
      .setEstimatorParamMaps(myParamGrid)
      .setNumFolds(3)

Listing 5-2Performing binary classification using Random Forest

我们现在可以拟合模型了

val myCrossValidatorModel = myCV.fit(trainingData)

我们来评价一下模型。

val myEvaluatorParamMap = ParamMap(myEvaluator.metricName -> "areaUnderROC")

val aucTrainingData = myEvaluator.evaluate(CrossValidatorPrediction, myEvaluatorParamMap)

你现在可以根据我们的数据做一些预测。

val myCrossValidatorPrediction = myCrossValidatorModel.transform(testData)

Spark MLlib 提供了构建管道、特征化和流行的机器学习算法的功能,用于回归、分类、聚类和协作过滤。Sandy Ryza、Uri Laserson、Sean Owen 和 Josh Wills (O’Reilly,2017 年)的《Spark 高级分析第二版》对 Spark 的机器学习进行了更深入的处理。我们将在第六章中使用 Kudu 作为功能商店。

图形 x

Spark 包括一个名为 GraphX 的图形处理框架。有一个名为 GraphFrames 的基于 DataFrames 的独立包。GraphFrames 目前不是核心 Apache Spark 的一部分。在撰写本文时,GraphX 和 GraphFrames 仍被认为是不成熟的,不被 Cloudera Enterprise 支持。 xvi 我不会在本书中涉及它们,但可以随意访问 Spark 的在线文档了解更多细节。

Spark 流

我在第六章中介绍了 Spark 流。Spark 2.0 包括一个新的流处理框架,称为结构化流,这是一个构建在 Spark SQL 之上的高级流 API。撰写本文时,Cloudera 还不支持结构化流。XVII

Spark 上的蜂巢

Cloudera 支持 Hive on Spark,以实现更快的批处理。早期基准测试显示,在 MapReduce 上,性能比 Hive 平均快 3 倍。 xviii 对于那些希望利用 Spark 的性能而无需学习 Scala 或 Python 的组织来说,Hive on Spark 非常有用。由于 HiveQL 查询的数量和复杂性,有些人可能会发现重构数据处理管道并不容易。Hive for Spark 非常适合这些场景。

Spark 1.x vs Spark 2.x

尽管大量代码仍然在 Spark 1.x 上运行,但现在您的大部分开发应该在 Spark 2.x 上进行。Spark 2.x API 的大部分与 1.x 相似,但 2.x 中有一些改变破坏了 API 兼容性。Spark 2 不兼容 Scala 2.10 仅支持 Scala 2.11。JDK 8 也是 Spark 2.2 的一个要求。更多细节请参考 Spark 的在线文档。

监控和配置

有几个工具可以用来监控和配置 Apache Spark。Cloudera Manager 是 Cloudera Enterprise 事实上的管理工具。Spark 还包括系统管理和监控功能。

Cloudera 经理

Cloudera Manager 是 Cloudera Enterprise 附带的集群管理工具。您可以使用 Cloudera Manager 执行各种管理任务,例如更新配置(图 5-4 )和监控其性能(图 5-5 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-5

Monitoring Spark Jobs

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-4

Using Cloudera Manager to configure Spark

Spark Web 用户界面

Spark 提供了几种监控 Spark 应用程序的方法。对于当前正在运行的 Spark 应用程序,您可以在端口 4040 上访问其性能信息。如果在同一节点上运行多个作业,您可以通过端口 4041、4042 等访问 web 用户界面。

Spark 历史服务器提供关于已经完成执行的 Spark 应用程序的详细信息。Spark 历史服务器可以通过端口 18088 访问(图 5-6 )。可以查看详细的性能指标(图 5-7 和图 5-8 )和关于 Spark 环境的信息(图 5-9 )以帮助监控和故障排除。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-9

Information about the current Spark environment

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-8

Performance metrics on Spark executors

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-7

Detailed performance information about a particular Spark job

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-6

Spark History Server

摘要

Apache Spark 已经取代 MapReduce 成为事实上的大数据处理框架。数据工程师和科学家欣赏 Spark 简单易用的 API、处理多种工作负载的能力以及快速的性能。它对 SparkSQL、Dataset 和 DataFrame APIs 的关注是受欢迎的改进,使 Spark 更容易访问和使用。Spark 是 Kudu 的理想数据处理引擎。我在第六章讨论 Spark 和 Kudu 集成。

参考

  1. 阿帕奇 Spark《Spark 概览》,阿帕奇 Spark,2018, https://spark.apache.org/docs/2.2.0/
  2. “阿帕奇软件基金会”Apache 软件基金会宣布 Apache Spark 为顶级项目,“Apache 软件基金会,2018, https://blogs.apache.org/foundation/entry/the_apache_software_foundation_announces50
  3. 阿帕奇 Spark《阿帕奇星火新闻》,阿帕奇星火,2018, https://spark.apache.org/news/
  4. 马泰·扎哈里亚;“我是 Matei Zaharia,Spark 的创始人,Databricks 的首席技术官。AMA!,“Reddit,2018, https://www.reddit.com/r/IAmA/comments/31bkue/im_matei_zaharia_creator_of_spark_and_cto_at/?st=j1svbrx9&sh=a8b9698e
  5. 数据块;“什么是阿帕奇 Spark?,“Databricks,2018, https://databricks.com/spark/about
  6. 朱尔斯·丹吉;《如何在 Apache Spark 2.0 中使用 SparkSession》,Databricks,2018, https://databricks.com/blog/2016/08/15/how-to-use-sparksession-in-apache-spark-2-0.html
  7. 霍尔登·卡劳,雷切尔·沃伦;“高性能 Spark”,奥莱利,2017 年 6 月, https://www.safaribooksonline.com/library/view/high-performance-spark/9781491943199/
  8. 阿帕奇 Spark《JDBC 到其他数据库》,阿帕奇 Spark,2018, https://spark.apache.org/docs/latest/sql-programming-guide.html#jdbc-to-other-databases
  9. 阿帕奇 Lucene《使用 SolrJ》,阿帕奇·卢斯,2018, https://lucene.apache.org/solr/guide/6_6/using-solrj.html
  10. Lucidworks“从 Solr 中读取数据作为 Spark RDD 并使用 SolrJ 将 Spark 中的对象索引到 Solr 中的工具”,Lucidworks,2018, https://github.com/lucidworks/spark-solr
  11. Crealytics“一个通过 Apache POI 读取 Excel 文件的 Spark 插件”,Crealytics,2018, https://github.com/crealytics/spark-excel
  12. SpringML《星火 SFTP 连接器库》,SpringML,2018, https://github.com/springml/spark-sftp
  13. 阿帕奇 Spark《机器学习库(MLlib)指南》,Apache Spark,2018, https://spark.apache.org/docs/latest/ml-guide.html
  14. 孟祥瑞、约瑟夫·布拉德利、埃文·斯帕克斯和希瓦拉姆·文卡塔拉曼;《ML Pipelines:ML lib 的一种新的高层 API》,Databricks,2018, https://databricks.com/blog/2015/01/07/ml-pipelines-a-new-high-level-api-for-mllib.html
  15. 大卫啊哈;《心脏病数据集》,UCI 机器学习资源库,1988 年, https://archive.ics.uci.edu/ml/datasets/heart%2BDisease
  16. Cloudera“不支持 GraphX”,Cloudera,2018, https://www.cloudera.com/documentation/spark2/latest/topics/spark2_known_issues.html#ki_graphx
  17. Cloudera“不支持结构化流”,Cloudera,2018, https://www.cloudera.com/documentation/spark2/latest/topics/spark2_known_issues.html#ki_structured_streaming
  18. 桑托什·库马尔;“利用 Hive-on-Spark 实现更快的批处理”,Cloudera,2016, https://vision.cloudera.com/faster-batch-processing-with-hive-on-spark/

六、使用 Spark 和 Kudu 的高性能数据处理

Kudu 只是一个存储引擎。你需要一种方法将数据输入输出。作为 Cloudera 默认的大数据处理框架,Spark 是 Kudu 理想的数据处理和摄取工具。Spark 不仅提供了出色的可伸缩性和性能,Spark SQL 和 DataFrame API 使得与 Kudu 的交互变得很容易。

如果您来自数据仓库背景,或者熟悉 Oracle 和 SQL Server 等关系数据库,那么您可以考虑 Spark,它是 SQL(如 PL/SQL 和 T-SQL)的过程扩展的更强大、更通用的等价物。

Spark 和酷都

您可以使用数据源 API 将 Spark 与 Kudu 结合使用。您可以使用 spark-shell 或 spark-submit 中的 packages 选项来包含 kudu-spark 依赖项。您也可以从 central.maven.org 手动下载 jar 文件,并将其包含在—jars 选项中。有关如何使用 sbt 和 Maven 作为项目构建工具的更多细节,请参考 Apache Spark 在线文档。

Spark 1.6.x

如果您在 Scala 2.10 中使用 spark,请使用 kudu-spark_2.10 工件。例如:

spark-shell -packages org.apache.kudu:kudu-spark_2.10:1.1.0
spark-shell -jars kudu-spark_2.10-1.1.0.jar

Spark 2.x

如果您在 Scala 2.11 中使用 spark2,请使用 kudu-spark2_2.11 工件。例如:

spark-shell –-packages org.apache.kudu:kudu-spark2_2.11:1.1.0
spark-shell -jars kudu-spark2_2.11-1.1.0.jar

酷都语境

您使用 Kudu 上下文来对 Kudu 表执行 DML 语句。 i 你要指定 Kudu 主服务器和端口。在下面的例子中,我们只有一个 Kudu 上下文。一个生产环境中通常有多个 Kudu masters 在这种情况下,您必须指定一个逗号分隔的列表,列出所有 Kudu 主机的主机名。我给出了如何使用 Spark 和 Kudu 的例子。我还展示了如何使用 Spark 将 Kudu 与其他数据源集成的例子。

import org.apache.kudu.spark.kudu._
val kuduContext = new KuduContext("kudumaster01:7051")

在开始之前,我们需要创建我们的表。

impala-shell

CREATE TABLE customers
(
 id BIGINT PRIMARY KEY,
 name STRING,
 age SMALLINT
)
PARTITION BY HASH PARTITIONS 4
STORED AS KUDU;

创建一个 case 类,为我们的示例数据提供一个模式。

case class CustomerData(id: Long, name: String, age: Short)

插入数据

创建一些示例数据。

val data = Array(CustomerData(101,"Lisa Kim",60), CustomerData(102,"Casey Fernandez",45))

val insertRDD = sc.parallelize(data)
val insertDF = sqlContext.createDataFrame(insertRDD)

insertDF.show

+----------+---------------+---+
|customerid|           name|age|
+----------+---------------+---+
|       101|       Lisa Kim| 60|
|       102|Casey Fernandez| 45|
+----------+---------------+---+

插入数据帧。记下表的名称。如果表是在 Impala 中创建的,就需要这种格式。在本例中,default 是数据库的名称,customers 是表的名称。采用这种约定是为了防止在 Impala 中创建的表和使用 Kudu API 或 Spark 本地创建的表之间的表名冲突。

kuduContext.insertRows(insertDF, "impala::default.customers")

确认数据已成功插入。

val df = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.customers")).kudu
df.select("id","name","age").show()

+---+---------------+---+
| id|           name|age|
+---+---------------+---+
|102|Casey Fernandez| 45|
|101|       Lisa Kim| 60|
+---+---------------+---+

更新 Kudu 表

创建更新的数据集。请注意,我们修改了姓氏和年龄。

val data = Array(CustomerData(101,"Lisa Kim",120), CustomerData(102,"Casey Jones",90))

val updateRDD = sc.parallelize(data)
val updateDF = sqlContext.createDataFrame(updateRDD)

updateDF.show

+--+------+---------+
| id|       name|age|
+--+------+---------+
|101|   Lisa Kim|120|
|102|Casey Jones| 90|
+--+------+---------+

更新表格。

kuduContext.updateRows(updateDF, "impala::default.customers");

确认该表已成功更新。

val df = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.customers")).kudu

df.select("id","name","age").show()

+--+------+---------+
| id|       name|age|
+--+------+---------+
|102|Casey Jones| 90|
|101|   Lisa Kim|120|
+--+------+---------+

令人不安的数据

创建一些示例数据。

val data = Array(CustomerData(101,"Lisa Kim",240), CustomerData(102,"Casey Cullen",90),CustomerData(103,"Byron Miller",25))

val upsertRDD = sc.parallelize(data)
val upsertDF = sqlContext.createDataFrame(upsertRDD)

upsertDF.show

+--+------+----------+
| id|        name|age|
+--+------+----------+
|101|    Lisa Kim|240|
|102|Casey Cullen| 90|
|103|Byron Miller| 25|
+--+------+----------+

up sert data–如果主键存在,则更新所有列,如果主键不存在,则插入行。

kuduContext.upsertRows(upsertDF, "impala::default.customers")

确认数据是否成功更新。

val df = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.customers")).kudu

df.select("id","name","age").show()

+--+------+----------+
| id|        name|age|
+--+------+----------+
|102|Casey Cullen| 90|
|103|Byron Miller| 25|
|101|    Lisa Kim|240|
+--+------+----------+

删除数据

检查表中的数据。

val df = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.customers")).kudu

df.select("id","name","age").show()

+--+------+----------+
| id|        name|age|
+--+------+----------+
|102|Casey Cullen| 90|
|103|Byron Miller| 25|
|101|    Lisa Kim|240|
+--+------+----------+

注册该表,以便我们可以在 SQL 查询中使用该表。

df.registerTempTable("customers")

根据我们的查询删除数据。

kuduContext.deleteRows(sqlContext.sql("select id from customers where name like 'Casey%'"), "impala::default.customers")

确认数据已成功删除。

val df = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.customers")).kudu

df.select("id","name","age").show()

+--+------+----------+
| id|        name|age|
+--+------+----------+
|103|Byron Miller| 25|
|101|    Lisa Kim|240|
+--+------+----------+

选择数据

选择表格中的数据。

val df = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.customers")).kudu

df.select("id","name","age").show()

+--+------+----------+
| id|        name|age|
+--+------+----------+
|103|Byron Miller| 25|
|101|    Lisa Kim|240|
+--+------+----------+

还可以通过注册该表并在 SQL 查询中使用它来运行查询。注意,如果使用 Spark 2.x,应该使用 createOrReplaceTempView。

df.registerTempTable("customers")

val df2 = sqlContext.sql("select * from customers where age=240")
df2.show

+--+----+--------+
| id|    name|age|
+--+----+--------+
|101|Lisa Kim|240|
+--+----+--------+

创建 Kudu 表

Impala 看不到这个表,因为这个表是在 Spark 中创建的。你必须在 Impala 中创建一个外部表,并引用这个表。

Import org.apache.kudu.client.CreateTableOptions;

kuduContext.createTable("customer2", df.schema, Seq("customerid"), new CreateTableOptions().setRangePartitionColumns(List("customerid").asJava).setNumReplicas(1))

将 CSV 插入 Kudu

让我们从 CSV 文件中插入多行。

val dataRDD = sc.textFile("/sparkdata/customerdata.csv")

val parsedRDD = dataRDD.map{_.split(",")}

case class CustomerData(customerid: Long, name: String, age: Short)

val dataDF = parsedRDD.map{a => CustomerData (a(0).toLong, a(1), a(2).toShort) }.toDF

kuduContext.insertRows(dataDF, "customer2");

因为我们通过 Spark 创建了 customer2 表,所以我们只需要指定表名(customer2)而不是 impala::default.customer2。

使用 spark-csv 包将 CSV 插入 Kudu

我们还可以使用 spark-csv 包来处理样本 csv 数据。下面的命令将自动下载 spark-csv 依赖项,因此请确保您的集群节点中有 Internet 连接。请注意使用逗号分隔软件包列表。

spark-shell -packages com.databricks:spark-csv_2.10:1.5.0,org.apache.kudu:kudu-spark_2.10:1.1.0

val dataDF = sqlContext.read.format("csv")
  .option("header", "true")
  .option("inferSchema", "true")
  .load(/sparkdata/customerdata.csv ")

kuduContext.insertRows(dataDF, "customer2");

因为我们通过 Spark 创建了 customer2 表,所以我们只需要指定表名(customer2)而不是 impala::default.customer2。

通过以编程方式指定模式,将 CSV 插入 Kudu

您可以使用 StructType 为您的数据集定义一个架构。当无法提前确定模式时,以编程方式指定模式很有帮助。

阅读来自 HDFS 的 CSV 文件。

val myCSV = sc.textFile("/tmp/mydata/customers.csv")

将 CSV 数据映射到 RDD

import org.apache.spark.sql.Row

val myRDD = myCSV.map(_.split(',')).map(e ⇒ Row(r(0).trim.toInt, r(1), r(2).trim.toInt, r(3)))

创建一个模式。

import org.apache.spark.sql.types.{StructType, StructField, StringType, IntegerType};

val mySchema = StructType(Array(
StructField("customerid",IntegerType,false),
StructField("customername",StringType,false),
StructField("age",IntegerType,false),
StructField("city",StringType,false)))

val myDF = sqlContext.createDataFrame(myRDD, mySchema)

将数据帧插入 Kudu。

kuduContext.insertRows(myDF, "impala::default.customers")

还记得我们通过 Impala 创建了 customers 表。因此,在引用 customers 表时,我们需要使用格式 impala::database.table。

使用 spark-xml 包将 XML 插入 Kudu

我们将创建一个 XML 文件作为这个例子的样本数据。我们需要把文件复制到 HDFS。

cat users.xml

<userid>100</userid><name>Wendell Ryan</name><city>San Diego</city><state>CA</state><zip>92102</zip>
<userid>101</userid><name>Alicia Thompson</name><city>Berkeley</city><state>CA</state><zip>94705</zip>
<userid>102</userid><name>Felipe Drummond</name><city>Palo Alto</city><state>CA</state><zip>94301</zip>
<userid>103</userid><name>Teresa Levine</name><city>Walnut Creek</city><state>CA</state><zip>94507</zip>

hadoop fs -mkdir /xmldata
hadoop fs -put users.xml /xmldata

我们将使用 spark-xml 包来处理样本 xml 数据。这个包的工作方式类似于 spark-csv 包。下面的命令将自动下载 spark-xml 包,因此请确保您的集群节点中有 Internet 连接。包括 kudu-spark 依赖性。

spark-shell -packages  com.databricks:spark-xml:2.10:0.4.1,org.apache.kudu:kudu-spark_2.10:1.1.0

使用 Spark XML 创建一个数据帧。在本例中,我们指定了行标记和 XML 文件所在的 HDFS 路径。

val xmlDF = sqlContext.read.format("com.databricks.spark.xml").option("rowTag", "user").load("/xmldata/");

xmlDF: org.apache.spark.sql.DataFrame = [city: string, name: string, state: string, userid: bigint, zip: bigint]

我们也来看看数据。

xmlDF.show

+------------+---------------+-----+------+-----+
|        city|           name|state|userid|  zip|
+------------+---------------+-----+------+-----+
|   San Diego|   Wendell Ryan|   CA|   100|92102|
|    Berkeley|Alicia Thompson|   CA|   101|94705|
|   Palo Alto|Felipe Drummond|   CA|   102|94301|
|Walnut Creek|  Teresa Levine|   CA|   103|94507|
+------------+---------------+-----+------+-----+

让我们检查一下模式。

xmlDF.printSchema

root
 |- age: long (nullable = true)
 |- city: string (nullable = true)
 |- name: string (nullable = true)
 |- state: string (nullable = true)
 |- userid: long (nullable = true)
 |- zip: long (nullable = true)

现在让我们回到 impala-shell,将模式与 users 表的结构进行比较。如您所见,用户表中年龄和邮政编码列的数据类型不同于数据帧中的相应列。如果我们试图将这个数据帧插入到 Kudu 表中,我们会得到一个错误消息。

describe users;
+--------+---------+---------+-------------+
| name   | type    | comment | primary_key |
+--------+---------+---------+-------------+
| userid | bigint  |         | true        |
| name   | string  |         | false       |
| city   | string  |         | false       |
| state  | string  |         | false       |
| zip    | string  |         | false       |
| age    | tinyint |         | false       |
+--------+---------+---------+-------------+

在将数据帧插入 Kudu 表之前,我们需要转换数据类型。我们在这里介绍使用 selectExpr 方法来转换数据类型,但是另一种选择是使用 StructType 以编程方式指定模式。

val convertedDF = xmlDF.selectExpr("userid","name","city","state","cast(zip as string) zip","cast(age as tinyint) age");

convertedDF: org.apache.spark.sql.DataFrame = [usersid: bigint, name: string, city: string, state: string, zip: string, age: tinyint]

创建 kudu 上下文并将数据帧插入目标表。

import org.apache.kudu.spark.kudu._

val kuduContext = new KuduContext("kudumaster01:7051")

kuduContext.insertRows(convertedDF, "impala::default.users")

看起来 DataFrame 已经成功地插入到 Kudu 表中。使用 impala-shell,检查表格中的数据进行确认。

select * from users;
+--------+-----------------+--------------+-------+-------+-----+
| userid | name            | city         | state | zip   | age |
+--------+-----------------+--------------+-------+-------+-----+
| 102    | Felipe Drummond | Palo Alto    | CA    | 94301 | 33  |
| 103    | Teresa Levine   | Walnut Creek | CA    | 94507 | 47  |
| 100    | Wendell Ryan    | San Diego    | CA    | 92102 | 24  |
| 101    | Alicia Thompson | Berkeley     | CA    | 94705 | 52  |
+--------+-----------------+--------------+-------+-------+-----+

将 JSON 插入 Kudu

我们将创建一个 JSON 文件作为这个例子的样本数据。确保该文件位于 HDFS 名为/jsondata 的文件夹中。

cat users.json

{"userid": 200, "name": "Jonathan West", "city":"Frisco", "state":"TX", "zip": "75034", "age":35}
{"userid": 201, "name": "Andrea Foreman", "city":"Dallas", "state":"TX", "zip": "75001", "age":28}
{"userid": 202, "name": "Kirsten Jung", "city":"Plano", "state":"TX", "zip": "75025", "age":69}
{"userid": 203, "name": "Jessica Nguyen", "city":"Allen", "state":"TX", "zip": "75002", "age":52}

从 JSON 文件创建一个数据帧。

val jsonDF = sqlContext.read.json("/jsondata")

jsonDF: org.apache.spark.sql.DataFrame = [age: bigint, city: string, name: string, state: string, userid: bigint, zip: string]

检查日期

jsonDF.show

+---+------+--------------+-----+------+-----+
|age|  city|          name|state|userid|  zip|
+---+------+--------------+-----+------+-----+
| 35|Frisco| Jonathan West|   TX|   200|75034|
| 28|Dallas|Andrea Foreman|   TX|   201|75001|
| 69| Plano|  Kirsten Jung|   TX|   202|75025|
| 52| Allen|Jessica Nguyen|   TX|   203|75002|
+---+------+--------------+-----+------+-----+

请检查架构。

jsonDF.printSchema

root
 |- age: long (nullable = true)
 |- city: string (nullable = true)
 |- name: string (nullable = true)
 |- state: string (nullable = true)
 |- userid: long (nullable = true)
 |- zip: string (nullable = true)

将 age 列的数据类型转换为 tinyint,以匹配表的数据类型。

val convertedDF = jsonDF.selectExpr("userid","name","city","state","zip","cast(age as tinyint) age");

convertedDF: org.apache.spark.sql.DataFrame = [userid: bigint, name: string, city: string, state: string, zip: string, age: tinyint]

创建 kudu 上下文并将数据帧插入目标表。

import org.apache.kudu.spark.kudu._

val kuduContext = new KuduContext("kudumaster01:7051")

kuduContext.insertRows(convertedDF, "impala::default.users")

使用 impala-shell,检查行是否成功插入。

select * from users order by userid;

+--------+-----------------+--------------+-------+-------+-----+
| userid | name            | city         | state | zip   | age |
+--------+-----------------+--------------+-------+-------+-----+
| 100    | Wendell Ryan    | San Diego    | CA    | 92102 | 24  |
| 101    | Alicia Thompson | Berkeley     | CA    | 94705 | 52  |
| 102    | Felipe Drummond | Palo Alto    | CA    | 94301 | 33  |
| 103    | Teresa Levine   | Walnut Creek | CA    | 94507 | 47  |
| 200    | Jonathan West   | Frisco       | TX    | 75034 | 35  |
| 201    | Andrea Foreman  | Dallas       | TX    | 75001 | 28  |
| 202    | Kirsten Jung    | Plano        | TX    | 75025 | 69  |
| 203    | Jessica Nguyen  | Allen        | TX    | 75002 | 52  |
+--------+-----------------+--------------+-------+-------+-----+

从 MySQL 插入到 Kudu

让我们用一些测试数据填充 MySQL 中的 users 表。确保 salesdb 数据库中存在 users 表。我们将把这些数据插入到 Kudu 中的一个表中。

mysql -u root –p mypassword

use salesdb;

describe users;

+--------+--------------+------+-----+---------+-------+
| Field  | Type         | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| userid | bigint(20)   | YES  |     | NULL    |       |
| name   | varchar(100) | YES  |     | NULL    |       |
| city   | varchar(100) | YES  |     | NULL    |       |
| state  | char(3)      | YES  |     | NULL    |       |
| zip    | char(5)      | YES  |     | NULL    |       |
| age    | tinyint(4)   | YES  |     | NULL    |       |
+--------+--------------+------+-----+---------+-------+

select * from users;
Empty set (0.00 sec)

insert into users values (300,'Fred Stevens','Torrance','CA',90503,23);

insert into users values (301,'Nancy Gibbs','Valencia','CA',91354,49);

insert into users values (302,'Randy Park','Manhattan Beach','CA',90267,21);

insert into users values (303,'Victoria Loma','Rolling Hills','CA',90274,75);

 select * from users;

+--------+---------------+-----------------+-------+-------+------+
| userid | name          | city            | state | zip   | age  |
+--------+---------------+-----------------+-------+-------+------+
|    300 | Fred Stevens  | Torrance        | CA    | 90503 |   23 |
|    301 | Nancy Gibbs   | Valencia        | CA    | 91354 |   49 |
|    302 | Randy Park    | Manhattan Beach | CA    | 90267 |   21 |
|    303 | Victoria Loma | Rolling Hills   | CA    | 90274 |   75 |
+--------+---------------+-----------------+-------+-------+------+

Note

在某些版本的 Spark -jars 中,没有在驱动程序的类路径中添加 JAR。 ii 建议您将 JDBC 驱动程序包含在您的-jars 和 Spark 类路径中。 iii

启动 Spark 壳。请注意,我必须将 MySQL 驱动程序作为参数包含在–driver-class-path 和–jar 中。

spark-shell -packages org.apache.kudu:kudu-spark_2.10:1.1.0 -driver-class-path mysql-connector-java-5.1.40-bin.jar -jars mysql-connector-java-5.1.40-bin.jar

让我们设置 jdbc url 和连接属性。

val jdbcURL = s"jdbc:mysql://10.0.1.101:3306/salesdb?user=root&password=cloudera"

val connectionProperties = new java.util.Properties()

我们可以从整个表中创建一个数据帧。

val mysqlDF = sqlContext.read.jdbc(jdbcURL, "users", connectionProperties)

mysqlDF.show
+------+-------------+---------------+-----+-----+---+
|userid|         name|           city|state|  zip|age|
+------+-------------+---------------+-----+-----+---+
|   300| Fred Stevens|       Torrance|   CA|90503| 23|
|   301|  Nancy Gibbs|       Valencia|   CA|91354| 49|
|   302|   Randy Park|Manhattan Beach|   CA|90267| 21|
|   303|Victoria Loma|  Rolling Hills|   CA|90274| 75|
+------+-------------+---------------+-----+-----+---+

让我们利用下推优化,在数据库中运行查询,只返回所需的结果。

val mysqlDF = sqlContext.read.jdbc(jdbcURL, "users", connectionProperties).select("userid", "city", "state","age").where("age < 25")

 mysqlDF.show
+------+---------------+-----+---+
|userid|           city|state|age|
+------+---------------+-----+---+
|   300|       Torrance|   CA| 23|
|   302|Manhattan Beach|   CA| 21|
+------+---------------+-----+---+

让我们指定一个完整的查询。这是一种更方便、更灵活的方法。此外,与前面的方法不同,如果在 WHERE 子句中指定了列,则不需要在选择列表中指定列。

val query = "(SELECT userid,name FROM users WHERE city IN ('Torrance','Rolling Hills')) as users"  

val mysqlDF = sqlContext.read.jdbc(jdbcURL, query, connectionProperties)

 mysqlDF.show
+------+-------------+
|userid|         name|
+------+-------------+
|   300| Fred Stevens|
|   303|Victoria Loma|
+------+-------------+

我们刚刚尝试了从 MySQL 表中选择数据的不同方法。

让我们直接将整个表插入到 Kudu 中。

val mysqlDF = sqlContext.read.jdbc(jdbcURL, "users", connectionProperties)

mysqlDF: org.apache.spark.sql.DataFrame = [userid: bigint, name: string, city: string, state: string, zip: string, age: int]

mysqlDF.show
+------+-------------+---------------+-----+-----+---+
|userid|         name|           city|state|  zip|age|
+------+-------------+---------------+-----+-----+---+
|   300| Fred Stevens|       Torrance|   CA|90503| 23|
|   301|  Nancy Gibbs|       Valencia|   CA|91354| 49|
|   302|   Randy Park|Manhattan Beach|   CA|90267| 21|
|   303|Victoria Loma|  Rolling Hills|   CA|90274| 75|
+------+-------------+---------------+-----+-----+---+

请验证架构。

mysqlDF.printSchema

root
 |- userid: long (nullable = true)
 |- name: string (nullable = true)
 |- city: string (nullable = true)
 |- state: string (nullable = true)
 |- zip: string (nullable = true)
 |- age: integer (nullable = true)

但是首先让我们把年龄从整数转换成 TINYINT。否则,您将无法将此数据帧插入 Kudu。同样,我们可以使用 StructType 定义一个模式。

val convertedDF = mysqlDF.selectExpr("userid","name","city","state","zip","cast(age as tinyint) age");

convertedDF: org.apache.spark.sql.DataFrame = [userid: bigint, name: string, city: string, state: string, zip: string, age: tinyint]

如您所见,age 列的数据类型现在是 TINYINT。让我们继续将数据插入到 Kudu 中。

import org.apache.kudu.spark.kudu._

val kuduContext = new KuduContext("kudumaster01:7051")

kuduContext.insertRows(convertedDF, "impala::default.users")

现在转到 impala-shell,检查数据是否成功插入。

select * from users order by userid

+--------+-----------------+-----------------+-------+-------+-----+
| userid | name            | city            | state | zip   | age |
+--------+-----------------+-----------------+-------+-------+-----+
| 100    | Wendell Ryan    | San Diego       | CA    | 92102 | 24  |
| 101    | Alicia Thompson | Berkeley        | CA    | 94705 | 52  |
| 102    | Felipe Drummond | Palo Alto       | CA    | 94301 | 33  |
| 103    | Teresa Levine   | Walnut Creek    | CA    | 94507 | 47  |
| 200    | Jonathan West   | Frisco          | TX    | 75034 | 35  |
| 201    | Andrea Foreman  | Dallas          | TX    | 75001 | 28  |
| 202    | Kirsten Jung    | Plano           | TX    | 75025 | 69  |
| 203    | Jessica Nguyen  | Allen           | TX    | 75002 | 52  |
| 300    | Fred Stevens    | Torrance        | CA    | 90503 | 23  |
| 301    | Nancy Gibbs     | Valencia        | CA    | 91354 | 49  |
| 302    | Randy Park      | Manhattan Beach | CA    | 90267 | 21  |
| 303    | Victoria Loma   | Rolling Hills   | CA    | 90274 | 75  |
+--------+-----------------+-----------------+-------+-------+-----+

或者,您也可以使用 Spark 进行检查。

import org.apache.kudu.spark.kudu._

val kuduDF = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.users")).kudu

kuduDF.select("userid","name","city","state","zip","age").sort($"userid".asc).show()

+------+---------------+---------------+-----+-----+---+
|userid|           name|           city|state|  zip|age|
+------+---------------+---------------+-----+-----+---+
|   100|   Wendell Ryan|      San Diego|   CA|92102| 24|
|   101|Alicia Thompson|       Berkeley|   CA|94705| 52|
|   102|Felipe Drummond|      Palo Alto|   CA|94301| 33|
|   103|  Teresa Levine|   Walnut Creek|   CA|94507| 47|
|   200|  Jonathan West|         Frisco|   TX|75034| 35|
|   201| Andrea Foreman|         Dallas|   TX|75001| 28|
|   202|   Kirsten Jung|          Plano|   TX|75025| 69|
|   203| Jessica Nguyen|          Allen|   TX|75002| 52|
|   300|   Fred Stevens|       Torrance|   CA|90503| 23|
|   301|    Nancy Gibbs|       Valencia|   CA|91354| 49|
|   302|     Randy Park|Manhattan Beach|   CA|90267| 21|
|   303|  Victoria Loma|  Rolling Hills|   CA|90274| 75|
+------+---------------+---------------+-----+-----+---+

从 SQL Server 插入到 Kudu

您需要做的第一件事是下载用于 SQL Server 的微软 JDBC 驱动程序。你可以在这里下载 JDBC 驱动: https://docs.microsoft.com/en-us/sql/connect/jdbc/microsoft-jdbc-driver-for-sql-server

您应该会看到类似于图 6-1 中的页面。单击“下载 JDBC 驱动程序”链接。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-1

Microsoft JDBC Driver for SQL Server

选择语言并点击“下载”

解开并拉开 Tarball。根据 JRE 的版本选择驱动程序。

tar zxvf sqljdbc_6.0.8112.100_enu.tar.gz

sqljdbc_6.0/enu/auth/x64/sqljdbc_auth.dll
sqljdbc_6.0/enu/auth/x86/sqljdbc_auth.dll
sqljdbc_6.0/enu/install.txt
sqljdbc_6.0/enu/jre7/sqljdbc41.jar
sqljdbc_6.0/enu/jre8/sqljdbc42.jar
sqljdbc_6.0/enu/license.txt
sqljdbc_6.0/enu/release.txt
sqljdbc_6.0/enu/samples/adaptive/executeStoredProcedure.java
sqljdbc_6.0/enu/samples/adaptive/readLargeData.java
sqljdbc_6.0/enu/samples/adaptive/updateLargeData.java
sqljdbc_6.0/enu/samples/alwaysencrypted/AlwaysEncrypted.java
sqljdbc_6.0/enu/samples/connections/connectDS.java
sqljdbc_6.0/enu/samples/connections/connectURL.java
sqljdbc_6.0/enu/samples/datatypes/basicDT.java
sqljdbc_6.0/enu/samples/datatypes/sqlxmlExample.java
sqljdbc_6.0/enu/samples/resultsets/cacheRS.java
sqljdbc_6.0/enu/samples/resultsets/retrieveRS.java
sqljdbc_6.0/enu/samples/resultsets/updateRS.java
sqljdbc_6.0/enu/samples/sparse/SparseColumns.java
sqljdbc_6.0/enu/xa/x64/sqljdbc_xa.dll
sqljdbc_6.0/enu/xa/x86/sqljdbc_xa.dll
sqljdbc_6.0/enu/xa/xa_install.sql

我将在本书中通篇使用 SQL Server 2016。您还需要单独安装 SQL Server Management Studio(图 6-2 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-2

SQL Server Management Studio

我们将创建 salesdb 数据库和 users 表。在对象资源管理器中,右键单击数据库节点,然后单击“新建数据库”(图 6-3 )。将显示一个窗口,您可以在其中指定数据库名称和其他数据库配置选项。输入数据库名称“salesdb ”,然后单击确定。出于测试目的,我们将其他选项保留默认值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-3

Create new database

展开 salesdb 节点。右键单击“表格”,单击“新建”,然后单击“表格”填写列名和数据类型。为了便于您理解书中的示例,请确保列名和数据类型与 MySQL 和 Kudu 表相同。点击窗口右上角附近的“保存”图标,然后输入表名“用户”(图 6-4 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-4

Create new table

让我们将一些测试数据插入到刚刚创建的 users 表中。点击标准工具栏上的“新建查询”按钮,打开一个新的编辑器窗口(图 6-5 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-5

Insert test data

我们将把这四行从 SQL Server 复制到 Kudu。

启动 Spark 壳。不要忘记在-driver-class-path 和-jar 中将 SQL Server 驱动程序作为参数传递。

spark-shell -packages org.apache.kudu:kudu-spark_2.10:1.1.0 -driver-class-path sqljdbc41.jar -jars sqljdbc41.jar

让我们设置 jdbc url 和连接属性。

val jdbcURL = "jdbc:sqlserver://192.168.56.102;databaseName=salesdb;user=salesuser;password=salespassword"
val connectionProperties = new java.util.Properties()

我们可以从整个表中创建一个数据帧。

val sqlDF = sqlContext.read.jdbc(jdbcURL, "users", connectionProperties)

sqlDF.show
+------+-------------+--------+-----+-----+---+
|userid|         name|    city|state|  zip|age|
+------+-------------+--------+-----+-----+---+
|   500|  Eric Steele| Seattle|  WA |98109| 91|
|   501|Brian Ambrose|Portland|  OR |97035| 53|
|   502|  Tim Parsons|  Tucson|  AZ |85704| 49|
|   503|   Lee Greene|   Miami|  FL |33018| 30|
+------+-------------+--------+-----+-----+---+

让我们利用下推优化在数据库中运行查询,并且只返回结果。

val sqlDF = sqlContext.read.jdbc(jdbcURL, "users", connectionProperties).select("userid", "city", "state","age").where("age < 50")

sqlDF.show
+------+------+-----+---+
|userid|  city|state|age|
+------+------+-----+---+
|   502|Tucson|  AZ | 49|
|   503| Miami|  FL | 30|
+------+------+-----+---+

我们可以指定整个查询。

val query = "(SELECT userid,name FROM users WHERE city IN ('Seattle','Portland')) as users"  

val sqlDF = sqlContext.read.jdbc(jdbcURL, query, connectionProperties)

sqlDF.show
+------+-------------+
|userid|         name|
+------+-------------+
|   500|  Eric Steele|
|   501|Brian Ambrose|
+------+-------------+

让我们直接将整个表插入到 Kudu 中。

val sqlDF = sqlContext.read.jdbc(jdbcURL, "users", connectionProperties)

sqlDF.show
+------+-------------+--------+-----+-----+---+
|userid|         name|    city|state|  zip|age|
+------+-------------+--------+-----+-----+---+
|   500|  Eric Steele| Seattle|  WA |98109| 91|
|   501|Brian Ambrose|Portland|  OR |97035| 53|
|   502|  Tim Parsons|  Tucson|  AZ |85704| 49|
|   503|   Lee Greene|   Miami|  FL |33018| 30|
+------+-------------+--------+-----+-----+---+

检查模式,看起来年龄被转换为整数。

sqlDF.printSchema
root
 |- userid: long (nullable = true)
 |- name: string (nullable = true)
 |- city: string (nullable = true)
 |- state: string (nullable = true)
 |- zip: string (nullable = true)
 |- age: integer (nullable = true)

我们需要将年龄从整数转换为整数。否则,您将无法将此数据帧插入 kudu。

val convertedDF = sqlDF.selectExpr("userid","name","city","state","zip","cast(age as tinyint) age");

convertedDF: org.apache.spark.sql.DataFrame = [userid: bigint, name: string, city: string, state: string, zip: string, age: tinyint]

如您所见,age 列的数据类型现在是 TINYINT。让我们将数据帧插入到 Kudu 中。

import org.apache.kudu.spark.kudu._

val kuduContext = new KuduContext("kudumaster01:7051")

kuduContext.insertRows(convertedDF, "impala::default.users")

现在让我们转到 impala-shell 并确认数据是否被成功插入。

select * from users order by userid

+--------+-------------------+-----------------+-------+-------+-----+
| userid | name              | city            | state | zip   | age |
+--------+-------------------+-----------------+-------+-------+-----+
| 100    | Wendell Ryan      | San Diego       | CA    | 92102 | 24  |
| 101    | Alicia Thompson   | Berkeley        | CA    | 94705 | 52  |
| 102    | Felipe Drummond   | Palo Alto       | CA    | 94301 | 33  |
| 103    | Teresa Levine     | Walnut Creek    | CA    | 94507 | 47  |
| 200    | Jonathan West     | Frisco          | TX    | 75034 | 35  |
| 201    | Andrea Foreman    | Dallas          | TX    | 75001 | 28  |
| 202    | Kirsten Jung      | Plano           | TX    | 75025 | 69  |
| 203    | Jessica Nguyen    | Allen           | TX    | 75002 | 52  |
| 300    | Fred Stevens      | Torrance        | CA    | 90503 | 23  |
| 301    | Nancy Gibbs       | Valencia        | CA    | 91354 | 49  |
| 302    | Randy Park        | Manhattan Beach | CA    | 90267 | 21  |
| 303    | Victoria Loma     | Rolling Hills   | CA    | 90274 | 75  |
| 400    | Patrick Montalban | Los Angeles     | CA    | 90010 | 71  |
| 401    | Jillian Collins   | Santa Monica    | CA    | 90402 | 45  |
| 402    | Robert Sarkisian  | Glendale        | CA    | 91204 | 29  |
| 403    | Warren Porcaro    | Burbank         | CA    | 91523 | 62  |
| 500    | Eric Steele       | Seattle         | WA    | 98109 | 91  |
| 501    | Brian Ambrose     | Portland        | OR    | 97035 | 53  |
| 502    | Tim Parsons       | Tucson          | AZ    | 85704 | 49  |
| 503    | Lee Greene        | Miami           | FL    | 33018 | 30  |
+--------+-------------------+-----------------+-------+-------+-----+

或者,您也可以使用 Spark 进行检查。

import org.apache.kudu.spark.kudu._

val kuduDF = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.users")).kudu

kuduDF.select("userid","name","city","state","zip","age").sort($"userid".asc).show()

+------+-----------------+---------------+-----+-----+---+
|userid|             name|           city|state|  zip|age|
+------+-----------------+---------------+-----+-----+---+
|   100|     Wendell Ryan|      San Diego|   CA|92102| 24|
|   101|  Alicia Thompson|       Berkeley|   CA|94705| 52|
|   102|  Felipe Drummond|      Palo Alto|   CA|94301| 33|
|   103|    Teresa Levine|   Walnut Creek|   CA|94507| 47|
|   200|    Jonathan West|         Frisco|   TX|75034| 35|
|   201|   Andrea Foreman|         Dallas|   TX|75001| 28|
|   202|     Kirsten Jung|          Plano|   TX|75025| 69|
|   203|   Jessica Nguyen|          Allen|   TX|75002| 52|
|   300|     Fred Stevens|       Torrance|   CA|90503| 23|
|   301|      Nancy Gibbs|       Valencia|   CA|91354| 49|
|   302|       Randy Park|Manhattan Beach|   CA|90267| 21|
|   303|    Victoria Loma|  Rolling Hills|   CA|90274| 75|
|   400|Patrick Montalban|    Los Angeles|   CA|90010| 71|
|   401|  Jillian Collins|   Santa Monica|   CA|90402| 45|
|   402| Robert Sarkisian|       Glendale|   CA|91204| 29|
|   403|   Warren Porcaro|        Burbank|   CA|91523| 62|
|   500|      Eric Steele|        Seattle|  WA |98109| 91|
|   501|    Brian Ambrose|       Portland|  OR |97035| 53|
|   502|      Tim Parsons|         Tucson|  AZ |85704| 49|
|   503|       Lee Greene|          Miami|  FL |33018| 30|

+------+-----------------+---------------+-----+-----+---+

从 HBase 插入 Kudu

有几种方法可以将数据从 HBase 传输到 Kudu。我们可以使用 HBase 客户端 API。有 Hortonworks 开发的 Spark-HBase 连接器。 iv Astro,由虎娃微开发,使用 Spark SQL 提供 SQL 层 HBase。来自 Cloudera 的 SparkOnHBase 项目最近被集成到 HBase 中,但在它成为 HBase 的一个版本之前,可能还需要一段时间。 vi

我们将使用最简单的方法,通过 JDBC。这可能不是将数据从 HBase 移动到 Kudu 的最快方式,但对于大多数任务来说应该足够了。我们将在 HBase 表的顶部创建一个 Hive 表,然后我们将通过 Impala 使用 JDBC 创建一个 Spark 数据帧。一旦我们有了数据帧,我们可以很容易地将其插入到 Kudu 中。

我们需要做的第一件事是下载黑斑羚 JDBC 驱动程序。将浏览器指向 https://www.cloudera.com/downloads.html (图 6-6 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-6

Cloudera Enterprise download page

点击页面右下角附近的黑斑羚 JDBC 驱动程序下载链接,您将进入黑斑羚 JDBC 驱动程序最新版本的下载页面。下载并解压缩文件。

ls -l

-rw-r-r- 1 hadoop hadoop  693530 Mar  9 10:04 Cloudera-JDBC-Driver-for-Impala-Install-Guide.pdf
-rw-r-r- 1 hadoop hadoop   43600 Mar  8 11:11 Cloudera-JDBC-Driver-for-Impala-Release-Notes.pdf
-rw-r-r- 1 hadoop hadoop   46725 Mar  4 13:12 commons-codec-1.3.jar
-rw-r-r- 1 hadoop hadoop   60686 Mar  4 13:12 commons-logging-1.1.1.jar
-rw-r-r- 1 hadoop hadoop 7670596 Mar  4 13:16 hive_metastore.jar
-rw-r-r- 1 hadoop hadoop  596600 Mar  4 13:16 hive_service.jar
-rw-r-r- 1 hadoop hadoop  352585 Mar  4 13:12 httpclient-4.1.3.jar
-rw-r-r- 1 hadoop hadoop  181201 Mar  4 13:12 httpcore-4.1.3.jar
-rw-r-r- 1 hadoop hadoop 1562600 Mar  4 13:19 ImpalaJDBC41.jar
-rw-r-r- 1 hadoop hadoop  275186 Mar  4 13:12 libfb303-0.9.0.jar
-rw-r-r- 1 hadoop hadoop  347531 Mar  4 13:12 libthrift-0.9.0.jar
-rw-r-r- 1 hadoop hadoop  367444 Mar  4 13:12 log4j-1.2.14.jar
-rw-r-r- 1 hadoop hadoop  294796 Mar  4 13:16 ql.jar
-rw-r-r- 1 hadoop hadoop   23671 Mar  4 13:12 slf4j-api-1.5.11.jar
-rw-r-r- 1 hadoop hadoop    9693 Mar  4 13:12 slf4j-log4j12-1.5.11.jar
-rw-r-r- 1 hadoop hadoop 1307923 Mar  4 13:16 TCLIServiceClient.jar
-rw-r-r- 1 hadoop hadoop  792964 Mar  4 13:12 zookeeper-3.4.6.jar

我们需要做的第一件事是启动“hbase shell”并创建 hbase 表。我们还需要将测试数据添加到 HBase 表中。如果您不熟悉 HBase 命令,请在线查阅 Apache HBase 参考指南。

hbase shell

create 'hbase_users', 'cf1'

put 'hbase_users','400','cf1:name', 'Patrick Montalban'
put 'hbase_users','400','cf1:city', 'Los Angeles'
put 'hbase_users','400','cf1:state', 'CA'
put 'hbase_users','400','cf1:zip', '90010'
put 'hbase_users','400','cf1:age', '71'

put 'hbase_users','401','cf1:name', 'Jillian Collins'
put 'hbase_users','401','cf1:city', 'Santa Monica'
put 'hbase_users','401','cf1:state', 'CA'
put 'hbase_users','401','cf1:zip', '90402'
put 'hbase_users','401','cf1:age', '45'

put 'hbase_users','402','cf1:name', 'Robert Sarkisian'
put 'hbase_users','402','cf1:city', 'Glendale'
put 'hbase_users','402','cf1:state', 'CA'
put 'hbase_users','402','cf1:zip', '91204'
put 'hbase_users','402','cf1:age', '29'

put 'hbase_users','403','cf1:name', 'Warren Porcaro'
put 'hbase_users','403','cf1:city', 'Burbank'
put 'hbase_users','403','cf1:state', 'CA'
put 'hbase_users','403','cf1:zip', '91523'
put 'hbase_users','403','cf1:age', '62'

在 HBase 表的顶部创建配置单元外部表。

create external table hbase_users
(userid bigint,
name string,
city string,
state string,
zip string,
age tinyint)
stored by
'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
with
SERDEPROPERTIES ('hbase.columns.mapping'=':key, cf1:name, cf1:city, cf1:state, cf1:zip, cf1:age')
TBLPROPERTIES ('hbase.table.name'='hbase_users');

使用 impala-shell,验证您可以看到 Hive 外部表。

show tables;

+-----------+
| name      |
+-----------+
| customers |
| sample_07 |
| sample_08 |
| users     |
| web_logs  |
+-----------+

它没有出现。我们需要使元数据无效来刷新 Impala 的内存。

invalidate metadata;

show tables;

+-------------+
| name        |
+-------------+
| customers   |
| hbase_users |
| sample_07   |
| sample_08   |
| users       |
| web_logs    |
+-------------+

select * from hbase_users;

+--------+-----+--------------+-------------------+-------+-------+
| userid | age | city         | name              | state | zip   |
+--------+-----+--------------+-------------------+-------+-------+
| 400    | 71  | Los Angeles  | Patrick Montalban | CA    | 90010 |
| 401    | 45  | Santa Monica | Jillian Collins   | CA    | 90402 |
| 402    | 29  | Glendale     | Robert Sarkisian  | CA    | 91204 |
| 403    | 62  | Burbank      | Warren Porcaro    | CA    | 91523 |
+--------+-----+--------------+-------------------+-------+-------+

启动 Spark 壳。

spark-shell -driver-class-path ImpalaJDBC41.jar -jars ImpalaJDBC41.jar -packages org.apache.kudu:kudu-spark_2.10:1.1.0

从 HBase 表创建一个数据帧

val jdbcURL = s"jdbc:impala://10.0.1.101:21050;AuthMech=0"

val connectionProperties = new java.util.Properties()

val hbaseDF = sqlContext.read.jdbc(jdbcURL, "hbase_users", connectionProperties)

hbaseDF: org.apache.spark.sql.DataFrame = [userid: bigint, age: int, city: string, name: string, state: string, zip: string]

hbaseDF.show

+------+---+------------+-----------------+-----+-----+
|userid|age|        city|             name|state|  zip|
+------+---+------------+-----------------+-----+-----+
|   400| 71| Los Angeles|Patrick Montalban|   CA|90010|
|   401| 45|Santa Monica|  Jillian Collins|   CA|90402|
|   402| 29|    Glendale| Robert Sarkisian|   CA|91204|
|   403| 62|     Burbank|   Warren Porcaro|   CA|91523|
+------+---+------------+-----------------+-----+-----+

在将数据插入 Kudu users 表之前,我们仍然需要将 age 转换为 TINYINT。使用 StructType 定义模式是这里的一个选项。

val convertedDF = hbaseDF.selectExpr("userid","name","city","state","zip","cast(age as tinyint) age");

convertedDF: org.apache.spark.sql.DataFrame = [userid: bigint, name: string, city: string, state: string, zip: string, age: tinyint]

我们现在可以将数据插入到 Kudu 中。

import org.apache.kudu.spark.kudu._

val kuduContext = new KuduContext("kudumaster01:7051")

kuduContext.insertRows(convertedDF, "impala::default.users")

确认数据是否成功插入。

val kuduDF = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.users")).kudu

kuduDF.select("userid","name","city","state","zip","age").sort($"userid".asc).show()
+------+-----------------+---------------+-----+-----+---+
|userid|             name|           city|state|  zip|age|
+------+-----------------+---------------+-----+-----+---+
|   100|     Wendell Ryan|      San Diego|   CA|92102| 24|
|   101|  Alicia Thompson|       Berkeley|   CA|94705| 52|
|   102|  Felipe Drummond|      Palo Alto|   CA|94301| 33|
|   103|    Teresa Levine|   Walnut Creek|   CA|94507| 47|
|   200|    Jonathan West|         Frisco|   TX|75034| 35|
|   201|   Andrea Foreman|         Dallas|   TX|75001| 28|
|   202|     Kirsten Jung|          Plano|   TX|75025| 69|
|   203|   Jessica Nguyen|          Allen|   TX|75002| 52|
|   300|     Fred Stevens|       Torrance|   CA|90503| 23|
|   301|      Nancy Gibbs|       Valencia|   CA|91354| 49|
|   302|       Randy Park|Manhattan Beach|   CA|90267| 21|
|   303|    Victoria Loma|  Rolling Hills|   CA|90274| 75|
|   400|Patrick Montalban|    Los Angeles|   CA|90010| 71|
|   401|  Jillian Collins|   Santa Monica|   CA|90402| 45|
|   402| Robert Sarkisian|       Glendale|   CA|91204| 29|
|   403|   Warren Porcaro|        Burbank|   CA|91523| 62|
+------+-----------------+---------------+-----+-----+---+

行已成功插入。

从 Solr 插入 Kudu

正如第五章所讨论的,你可以使用 SolrJ 从 Spark 访问 Solr。VII

import java.net.MalformedURLException;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrServer;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocumentList;

val solr = new HttpSolrServer("http://master02:8983/solr/mycollection");

val query = new SolrQuery();

query.setQuery("*:*");
query.addFilterQuery("userid:3");
query.setFields("userid","name","age","city");
query.setStart(0);    
query.set("defType", "edismax");

val response = solr.query(query);
val results = response.getResults();

println(results);

从 Spark 访问 Solr 集合的一个更好的方法是使用 spark-solr 包。Lucidworks 启动了 spark-solr 项目来提供 Spark-Solr 集成。与 solrJ 相比,使用 spark-solr 要简单和强大得多,它允许你从 Solr 集合中创建数据帧,并使用 SparkSQL 与它们进行交互。您可以从 Lucidworks 的网站下载 jar 文件。

首先从 spark-shell 导入 jar 文件。

spark-shell -jars spark-solr-3.0.1-shaded.jar

指定集合和连接信息。

val myOptions = Map("collection" -> "mycollection","zkhost" -> "{master02:8983/solr}")

创建一个数据帧。

val solrDF = spark.read.format("solr")
  .options(myOptions)
  .load

将数据插入 Kudu。

kuduContext.insertRows(solrDF, "impala::default.users")

从亚马逊 S3 插入到 Kudu

亚马逊 S3 是一个流行的对象存储,经常被用作临时集群的数据存储。它还是备份和冷数据的经济高效的存储方式。从 S3 读取数据就像从 HDFS 或任何其他文件系统读取数据一样。

阅读来自亚马逊 S3 的 CSV 文件。请确保您已经配置了 S3 凭据。

val myCSV = sc.textFile("s3a://mydata/customers.csv")

将 CSV 数据映射到 RDD。

import org.apache.spark.sql.Row
val myRDD = myCSV.map(_.split(,)).map(e ⇒ Row(r(0).trim.toInt, r(1), r(2).trim.toInt, r(3)))

创建一个模式。

import org.apache.spark.sql.types.{StructType, StructField, StringType, IntegerType};

val mySchema = StructType(Array(
StructField("customerid",IntegerType,false),
StructField("customername",StringType,false),
StructField("age",IntegerType,false),
StructField("city",StringType,false)))

val myDF = sqlContext.createDataFrame(myRDD, mySchema)

将数据帧插入 Kudu。

kuduContext.insertRows(myDF, "impala::default.customers")

您已经成功地将 S3 的数据插入到 Kudu 中。

我们已经将不同数据源的数据插入到 Kudu 中。现在让我们将 Kudu 中的数据插入到不同的数据源中。

从 Kudu 插入 MySQL

启动 Spark 壳。

spark-shell -packages org.apache.kudu:kudu-spark_2.10:1.1.0 -driver-class-path mysql-connector-java-5.1.40-bin.jar -jars mysql-connector-java-5.1.40-bin.jar

连接到 Kudu master 并检查 users 表中的数据。我们将同步这个 Kudu 表和一个 MySQL 表。

import org.apache.kudu.spark.kudu._

val kuduDF = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.users")).kudu

kuduDF.select("userid","name","city","state","zip","age").sort($"userid".asc).show()

+------+---------------+---------------+-----+-----+---+
|userid|           name|           city|state|  zip|age|
+------+---------------+---------------+-----+-----+---+
|   100|   Wendell Ryan|      San Diego|   CA|92102| 24|
|   101|Alicia Thompson|       Berkeley|   CA|94705| 52|
|   102|Felipe Drummond|      Palo Alto|   CA|94301| 33|
|   103|  Teresa Levine|   Walnut Creek|   CA|94507| 47|
|   200|  Jonathan West|         Frisco|   TX|75034| 35|
|   201| Andrea Foreman|         Dallas|   TX|75001| 28|
|   202|   Kirsten Jung|          Plano|   TX|75025| 69|
|   203| Jessica Nguyen|          Allen|   TX|75002| 52|
|   300|   Fred Stevens|       Torrance|   CA|90503| 23|
|   301|    Nancy Gibbs|       Valencia|   CA|91354| 49|
|   302|     Randy Park|Manhattan Beach|   CA|90267| 21|
|   303|  Victoria Loma|  Rolling Hills|   CA|90274| 75|
+------+---------------+---------------+-----+-----+---+

注册数据帧,以便我们可以对其运行 SQL 查询。

kuduDF.registerTempTable("kudu_users")

设置 MySQL 数据库的 JDBC URL 和连接属性。

val jdbcURL = s"jdbc:mysql://10.0.1.101:3306/salesdb?user=root&password=cloudera"

val connectionProperties = new java.util.Properties()
import org.apache.spark.sql.SaveMode

Check the data in the MySQL table using the MySQL command-line tool.

select * from users;
+--------+---------------+-----------------+-------+-------+------+
| userid | name          | city            | state | zip   | age  |
+--------+---------------+-----------------+-------+-------+------+
|    300 | Fred Stevens  | Torrance        | CA    | 90503 |   23 |
|    301 | Nancy Gibbs   | Valencia        | CA    | 91354 |   49 |
|    302 | Randy Park    | Manhattan Beach | CA    | 90267 |   21 |
|    303 | Victoria Loma | Rolling Hills   | CA    | 90274 |   75 |
+--------+---------------+-----------------+-------+-------+------+

让我们通过将 userid < 300 的所有行从 Kudu 插入 MySQL 来同步这两个表。

sqlContext.sql("select * from kudu_users where userid < 300").write.mode(SaveMode.Append).jdbc(jdbcUrl, "users", connectionProperties)

再次检查 MySQL 表,验证是否添加了行。

select * from users order by userid;
+--------+-----------------+-----------------+-------+-------+------+
| userid | name            | city            | state | zip   | age  |
+--------+-----------------+-----------------+-------+-------+------+
|    100 | Wendell Ryan    | San Diego       | CA    | 92102 |   24 |
|    101 | Alicia Thompson | Berkeley        | CA    | 94705 |   52 |
|    102 | Felipe Drummond | Palo Alto       | CA    | 94301 |   33 |
|    103 | Teresa Levine   | Walnut Creek    | CA    | 94507 |   47 |
|    200 | Jonathan West   | Frisco          | TX    | 75034 |   35 |
|    201 | Andrea Foreman  | Dallas          | TX    | 75001 |   28 |
|    202 | Kirsten Jung    | Plano           | TX    | 75025 |   69 |
|    203 | Jessica Nguyen  | Allen           | TX    | 75002 |   52 |
|    300 | Fred Stevens    | Torrance        | CA    | 90503 |   23 |

|    301 | Nancy Gibbs     | Valencia        | CA    | 91354 |   49 |
|    302 | Randy Park      | Manhattan Beach | CA    | 90267 |   21 |
|    303 | Victoria Loma   | Rolling Hills   | CA    | 90274 |   75 |
+--------+-----------------+-----------------+-------+-------+------+

看起来行已成功插入。

从 Kudu 插入 SQL Server

启动 Spark 壳。

spark-shell -packages org.apache.kudu:kudu-spark_2.10:1.1.0 -driver-class-path sqljdbc41.jar -jars sqljdbc41.jar

从默认数据库中的 users 表创建一个 DataFrame。

import org.apache.kudu.spark.kudu._

val kuduDF = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.users")).kudu

验证数据帧的内容。

kuduDF.select("userid","name","city","state","zip","age").sort($"userid".asc).show()

+------+---------------+------------+-----+-----+---+
|userid|           name|        city|state|  zip|age|
+------+---------------+------------+-----+-----+---+
|   100|   Wendell Ryan|   San Diego|   CA|92102| 24|
|   101|Alicia Thompson|    Berkeley|   CA|94705| 52|
|   102|Felipe Drummond|   Palo Alto|   CA|94301| 33|
|   103|  Teresa Levine|Walnut Creek|   CA|94507| 47|
+------+---------------+------------+-----+-----+---+

注册数据帧,以便我们可以对其运行 SQL 查询。

kuduDF.registerTempTable("kudu_users")

设置 SQL Server 数据库的 JDBC URL 和连接属性。

val jdbcURL = "jdbc:sqlserver://192.168.56.103;databaseName=salesdb;user=sa;password=cloudera"

val connectionProperties = new java.util.Properties()

import org.apache.spark.sql.SaveMode

为了确保我们的测试是一致的,使用 SQL Server Management Studio 确保 SQL Server 中的 users 表是空的(图 6-7 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-7

Make sure the table is empty

将 Kudu 中的数据插入 SQL Server。

sqlContext.sql("select * from kudu_users").write.mode(SaveMode.Append).jdbc(jdbcURL, "users", connectionProperties)

再次检查 SQL Server 表,验证是否添加了行(图 6-8 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-8

Check the table

恭喜你!数据已成功插入。

从 Kudu 插入 Oracle

我们需要做的第一件事是设置 Oracle 环境。我们将在名为 EDWPDB 的现有可插拔数据库中创建用户表。以 sysdba 身份登录并启动实例。如果您不熟悉 Oracle,请参考联机 Oracle 文档。

sqlplus / as sysdba

SQL*Plus: Release 12.2.0.1.0 Production on Sat May 6 18:12:45 2017

Copyright (c) 1982, 2016, Oracle.  All rights reserved.

Connected to an idle instance.

SQL> startup
ORACLE instance started.

Total System Global Area 1845493760 bytes
Fixed Size                  8793976 bytes
Variable Size             553648264 bytes
Database Buffers         1275068416 bytes
Redo Buffers                7983104 bytes
Database mounted.
Database opened.

SELECT name, open_mode FROM v$pdbs;

NAME                 OPEN_MODE
-------------------- ---------
PDB$SEED             READ ONLY
ORCLPDB              MOUNTED
EDWPDB               MOUNTED

打开 EDWPDB 可插拔数据库,并将其设置为当前容器。

ALTER PLUGGABLE DATABASE EDWPDB OPEN;

SELECT name, open_mode FROM v$pdbs;

NAME                 OPEN_MODE
-------------------- -----
PDB$SEED             READ ONLY
ORCLPDB              MOUNTED
EDWPDB               READ WRITE

ALTER SESSION SET container = EDWPDB;

创建 Oracle 表。

CREATE TABLE users (
userid NUMBER(19),
name VARCHAR(50),
city VARCHAR(50),
state VARCHAR (50),
zip VARCHAR(50),
age NUMBER(3));

启动 Spark 壳。不要忘记包括 oracle 驱动程序。在这个例子中,我使用的是 ojdbc6.jar。

Note

使用 ojdbc6.jar 驱动程序连接到 Oracle 12c R2 时,您可能会遇到错误“ORA-28040:没有匹配的认证协议异常”。这很可能是由 Oracle12c 中的一个错误导致的,错误 14575666。解决方法是设置 SQLNET。Oracle/network/admin/sqlnet . ora 文件中的 ALLOWED_LOGON_VERSION=8。

spark-shell -packages org.apache.kudu:kudu-spark_2.10:1.1.0 -driver-class-path ojdbc6.jar -jars ojdbc6.jar

从默认数据库中的 users 表创建一个 DataFrame。

import org.apache.kudu.spark.kudu._

val kuduDF = sqlContext.read.options(Map("kudu.master" ->
"kudumaster01:7051","kudu.table" -> "impala::default.users")).kudu

验证数据帧的内容。

kuduDF.select("userid","name","city","state","zip","age").sort($"userid".asc).show()

+------+---------------+------------+-----+-----+---+
|userid|           name|        city|state|  zip|age|
+------+---------------+------------+-----+-----+---+
|   100|   Wendell Ryan|   San Diego|   CA|92102| 24|
|   101|Alicia Thompson|    Berkeley|   CA|94705| 52|
|   102|Felipe Drummond|   Palo Alto|   CA|94301| 33|
|   103|  Teresa Levine|Walnut Creek|   CA|94507| 47|
+------+---------------+------------+-----+-----+---+

注册数据帧,以便我们可以对其运行 SQL 查询。

kuduDF.registerTempTable("kudu_users")

设置 Oracle 数据库的 JDBC URL 和连接属性。

val jdbcURL = "jdbc:oracle:thin:sales/cloudera@//192.168.56.30:1521/EDWPDB"

val connectionProperties = new java.util.Properties()

import org.apache.spark.sql.SaveMode

使用 Oracle SQL Developer 确保 Oracle 中的用户表为空(图 6-9 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-9

Make sure Oracle table is empty

将 Kudu 中的数据插入 Oracle。

sqlContext.sql("select * from kudu_users").write.mode(SaveMode.Append).jdbc(jdbcURL, "users", connectionProperties)

再次检查 Oracle 表,验证是否添加了行(图 6-10 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-10

Check Oracle table

恭喜你!您已成功地将行从 Kudu 复制到 Oracle。

从 Kudu 插入到 HBase

我们将通过 Impala 插入数据 HBase,以便使用 SQL。这不是写入 HBase 的最快方法。如果性能很关键,我建议你使用 saveAsHadoopDataset 方法或者 HBase Java 客户端 API 来写入 HBase。将数据导入 HBase 还有其他各种方法。 ix

启动 spark-shell 并从 kudu 表创建一个数据帧。

spark-shell -driver-class-path ImpalaJDBC41.jar -jars ImpalaJDBC41.jar -packages org.apache.kudu:kudu-spark_2.10:1.1.0

import org.apache.kudu.client.CreateTableOptions;
import org.apache.kudu.spark.kudu._

val kuduDF = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.users")).kudu

验证表格的内容。

kuduDF.sort($"userid".asc).show()

+------+-----------------+---------------+-----+-----+---+
|userid|             name|           city|state|  zip|age|
+------+-----------------+---------------+-----+-----+---+
|   100|     Wendell Ryan|      San Diego|   CA|92102| 24|
|   101|  Alicia Thompson|       Berkeley|   CA|94705| 52|
|   102|  Felipe Drummond|      Palo Alto|   CA|94301| 33|
|   103|    Teresa Levine|   Walnut Creek|   CA|94507| 47|

|   200|    Jonathan West|         Frisco|   TX|75034| 35|
|   201|   Andrea Foreman|         Dallas|   TX|75001| 28|
|   202|     Kirsten Jung|          Plano|   TX|75025| 69|
|   203|   Jessica Nguyen|          Allen|   TX|75002| 52|
|   300|     Fred Stevens|       Torrance|   CA|90503| 23|
|   301|      Nancy Gibbs|       Valencia|   CA|91354| 49|
|   302|       Randy Park|Manhattan Beach|   CA|90267| 21|
|   303|    Victoria Loma|  Rolling Hills|   CA|90274| 75|
|   400|Patrick Montalban|    Los Angeles|   CA|90010| 71|
|   401|  Jillian Collins|   Santa Monica|   CA|90402| 45|
|   402| Robert Sarkisian|       Glendale|   CA|91204| 29|
|   403|   Warren Porcaro|        Burbank|   CA|91523| 62|
+------+-----------------+---------------+-----+-----+---+

让我们注册该表,以便在查询中使用它。

kuduDF.registerTempTable("kudu_users")

使用 impala-shell,验证目标 HBase 表的内容。

select * from hbase_users order by userid;

+--------+-----+--------------+-------------------+-------+-------+
| userid | age | city         | name              | state | zip   |
+--------+-----+--------------+-------------------+-------+-------+
| 400    | 71  | Los Angeles  | Patrick Montalban | CA    | 90010 |
| 401    | 45  | Santa Monica | Jillian Collins   | CA    | 90402 |
| 402    | 29  | Glendale     | Robert Sarkisian  | CA    | 91204 |
| 403    | 62  | Burbank      | Warren Porcaro    | CA    | 91523 |
+--------+-----+--------------+-------------------+-------+-------+

回到 Spark 壳,建立黑斑羚连接。

val jdbcURL = s"jdbc:impala://10.0.1.101:21050;AuthMech=0"

val connectionProperties = new java.util.Properties()

仅将所选行插入目标 HBase 表。

import org.apache.spark.sql.SaveMode

sqlContext.sql("select * from kudu_users where userid in (300,301,302,303)").write.mode(SaveMode.Append).jdbc(jdbcURL, "hbase_users", connectionProperties)

回到 impala-shell,确认这些行已经添加到目标 HBase 表中。

select * from hbase_users order by userid;

+--------+-----+-----------------+-------------------+-------+-------+
| userid | age | city            | name              | state | zip   |
+--------+-----+-----------------+-------------------+-------+-------+
| 300    | 23  | Torrance        | Fred Stevens      | CA    | 90503 |
| 301    | 49  | Valencia        | Nancy Gibbs       | CA    | 91354 |
| 302    | 21  | Manhattan Beach | Randy Park        | CA    | 90267 |
| 303    | 75  | Rolling Hills   | Victoria Loma     | CA    | 90274 |
| 400    | 71  | Los Angeles     | Patrick Montalban | CA    | 90010 |
| 401    | 45  | Santa Monica    | Jillian Collins   | CA    | 90402 |
| 402    | 29  | Glendale        | Robert Sarkisian  | CA    | 91204 |
| 403    | 62  | Burbank         | Warren Porcaro    | CA    | 91523 |
+--------+-----+-----------------+-------------------+-------+-------+

数据已成功插入 HBase 表。

将 Kudu 中的行插入拼花地板

读表格。

spark-shell -packages org.apache.kudu:kudu-spark_2.10:1.1.0

import org.apache.kudu.client.CreateTableOptions;
import org.apache.kudu.spark.kudu._

val df = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.customers")).kudu

df.show

+---+------------+---+
| id|        name|age|
+---+------------+---+
|103|Byron Miller| 25|
|101|    Lisa Kim|240|
+---+------------+---+

注册该表,然后根据查询结果创建另一个数据帧。

df.registerTempTable("customers")

val df2 = sqlContext.sql("select * from customers where age=240")

检查数据。

df2.show

+---+--------+---+
| id|    name|age|
+---+--------+---+
|101|Lisa Kim|240|
+---+--------+---+

将数据帧附加到镶木地板桌子上。您也可以使用关键词“覆盖”而不是“追加”来覆盖目的位置

df2.write.mode("SaveMode.Append").parquet("/user/hive/warehouse/Mytable")

你会发现 Spark 在给 HDFS 写信时会生成几十或几百个小文件。这就是所谓的“小文件”问题。 x 这最终会导致集群出现各种性能问题。如果发生这种情况,您可能需要使用联合或重新分区来指定要写入 HDFS 的文件数量。例如,您可能希望 Spark 向 HDFS 写一个拼花文件。

df2.coalesce(1).write.mode("SaveMode.Append").parquet("/user/hive/warehouse/Mytable")

使用合并和重新分区可能会导致性能问题,因为写入数据时实际上是降低了并行度。根据您正在处理的数据量,合并和重新分区还会触发可能导致性能问题的洗牌。您需要平衡生成文件的数量和处理性能。一段时间后,您可能仍然需要定期压实镶木地板。这是一个你在 Kudu 身上不会遇到的问题。

将 SQL Server 和 Oracle 数据帧插入 Kudu

我们将连接来自 SQL 和 Oracle 的数据,并将其插入 Kudu。

启动 Spark 壳。不要忘记包括必要的驱动程序和依赖项。

spark-shell -packages org.apache.kudu:kudu-spark_2.10:1.1.0 -driver-class-path ojdbc6.jar:sqljdbc41.jar -jars ojdbc6.jar,sqljdbc41.jar

设置 Oracle 连接。

val jdbcURL = "jdbc:oracle:thin:sales/cloudera@//192.168.56.30:1521/EDWPDB"
val connectionProperties = new java.util.Properties()

从 Oracle 表创建一个数据帧。

val oraDF = sqlContext.read.jdbc(jdbcURL, "users", connectionProperties)

oraDF.show

+------+---------------+------------+-----+-----+---+
|USERID|           NAME|        CITY|STATE|  ZIP|AGE|
+------+---------------+------------+-----+-----+---+
|   102|Felipe Drummond|   Palo Alto|   CA|94301| 33|
|   103|  Teresa Levine|Walnut Creek|   CA|94507| 47|
|   100|   Wendell Ryan|   San Diego|   CA|92102| 24|
|   101|Alicia Thompson|    Berkeley|   CA|94705| 52|
+------+---------------+------------+-----+-----+---+

注册该表,以便我们可以对其运行 SQL。

oraDF.registerTempTable("ora_users")

设置 SQL Server 连接。

val jdbcURL = "jdbc:sqlserver://192.168.56.103;databaseName=salesdb;user=sa;password=cloudera"

val connectionProperties = new java.util.Properties()

从 SQL Server 表创建数据帧。

val sqlDF = sqlContext.read.jdbc(jdbcURL, "userattributes", connectionProperties)

sqlDF.show

+------+------+------+------------------+
|userid|height|weight|        occupation|
+------+------+------+------------------+
|   100|   175|   170|       Electrician|
|   101|   180|   120|         Librarian|
|   102|   180|   215|    Data Scientist|
|   103|   178|   132|Software Developer|
+------+------+------+------------------+

注册该表,以便我们可以将其连接到 Oracle 表。

sqlDF.registerTempTable("sql_userattributes")

连接两张桌子。我们将把结果插入到 Kudu 表中。

val joinDF = sqlContext.sql("select ora_users.userid,ora_users.name,ora_users.city,ora_users.state,ora_users.zip,ora_users.age,sql_userattributes.height,sql_userattributes.weight,sql_userattributes.occupation from ora_users  INNER JOIN sql_userattributes ON ora_users.userid=sql_userattributes.userid")

joinDF.show

+------+---------------+------------+-----+-----+---+------+------+-----------+
|userid|           name|        city|state|  zip|age|height|weight| occupation|
+------+---------------+------------+-----+-----+---+------+------+-----------+
|   100|   Wendell Ryan|   San Diego|   CA|92102| 24|   175|   170|Electrician|
|   101|Alicia Thompson|    Berkeley|   CA|94705| 52|   180|   120|  Librarian|
|   102|Felipe Drummond|   Palo Alto|   CA|94301| 33|   180|   215|  Data                                                                     Scientist|
|   103|  Teresa Levine|Walnut Creek|   CA|94507| 47|   178|   132|  Software Developer|
+------+---------------+------------+-----+-----+---+------+------+-----------+

您也可以使用此方法连接两个数据帧。

val joinDF2 = oraDF.join(sqlDF,"userid")

joinDF2.show

+------+---------------+------------+-----+-----+---+------+------+------------+
|userid|           NAME|        CITY|STATE|  ZIP|AGE|height|weight|  occupation|
+------+---------------+------------+-----+-----+---+------+------+------------+
|   100|   Wendell Ryan|   San Diego|   CA|92102| 24|   175|   170| Electrician|
|   101|Alicia Thompson|    Berkeley|   CA|94705| 52|   180|   120|   Librarian|
|   102|Felipe Drummond|   Palo Alto|   CA|94301| 33|   180|   215|   Data                                                                       Scientist|
|   103|  Teresa Levine|Walnut Creek|   CA|94507| 47|   178|   132|   Software                                                                       Developer|
+------+---------------+------------+-----+-----+---+------+------+------------+

在 Impala 中创建目标 Kudu 表。

impala-shell

create table users2 (
userid BIGINT PRIMARY KEY,
name STRING,
city STRING,
state STRING,
zip STRING,
age STRING,
height STRING,
weight STRING,
occupation STRING
)
PARTITION BY HASH PARTITIONS 16
STORED AS KUDU;

回到 spark-shell 并建立 Kudu 连接

import org.apache.kudu.spark.kudu._

val kuduContext = new KuduContext("kudumaster01:7051")

将数据插入 Kudu。

kuduContext.insertRows(JoinDF, "impala::default.users2")

确认数据已成功插入 Kudu 表。

impala-shell

select * from users2;

+------+---------------+------------+-----+------+---+------+------+----------+
|userid|name           |city        |state|zip   |age|height|weight|occupation|
+------+---------------+------------+-----+------+---+------+------+----------+
|102   |Felipe Drummond|Palo Alto   |CA   |94301 |33 |180   |215   | Data                                                                      Scientist|
|103   |Teresa Levine  |Walnut Creek|CA   |94507 |47 |178   |132   | Software                                                                      Developer|
|100   |Wendell Ryan   |San Diego   |CA   |92102 |24 |175   |170   |Electrician|
|101   |Alicia Thompson|Berkeley    |CA   |94705 |52 |180   |120   |Librarian |
+------+---------------+------------+-----+------+---+------+------+----------+

我觉得不错。

将 Kudu 和 SQL Server 数据帧插入 Oracle

使用 Oracle SQL Developer 在 Oracle 中创建目标表(图 6-11 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-11

Create an Oracle table

启动 Spark 壳。不要忘记包括必要的驱动程序和依赖项。

spark-shell -packages org.apache.kudu:kudu-spark_2.10:1.1.0 -driver-class-path ojdbc6.jar:sqljdbc41.jar -jars ojdbc6.jar,sqljdbc41.jar

从默认数据库中的 Kudu users 表创建一个 DataFrame。

import org.apache.kudu.spark.kudu._

val kuduDF = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.users")).kudu

验证数据帧的内容。

kuduDF.select("userid","name","city","state","zip","age").sort($"userid".asc).show()

+------+---------------+------------+-----+-----+---+
|userid|           name|        city|state|  zip|age|
+------+---------------+------------+-----+-----+---+
|   100|   Wendell Ryan|   San Diego|   CA|92102| 24|
|   101|Alicia Thompson|    Berkeley|   CA|94705| 52|
|   102|Felipe Drummond|   Palo Alto|   CA|94301| 33|
|   103|  Teresa Levine|Walnut Creek|   CA|94507| 47|
+------+---------------+------------+-----+-----+---+

注册数据帧,以便我们可以对其运行 SQL 查询。

kuduDF.registerTempTable("kudu_users")

val jdbcURL = "jdbc:sqlserver://192.168.56.103;databaseName=salesdb;user=sa;password=cloudera"

val connectionProperties = new java.util.Properties()

从 SQL Server 表创建数据帧。

val sqlDF = sqlContext.read.jdbc(jdbcURL, "userattributes", connectionProperties)

sqlDF.show

+------+------+------+------------------+
|userid|height|weight|        occupation|
+------+------+------+------------------+
|   100|   175|   170|       Electrician|
|   101|   180|   120|         Librarian|
|   102|   180|   215|    Data Scientist|
|   103|   178|   132|Software Developer|
+------+------+------+------------------+

将数据帧注册为临时表。

sqlDF.registerTempTable("sql_userattributes")

连接两张桌子。我们将把结果插入 Oracle 数据库。

val joinDF = sqlContext.sql("select
kudu_users.userid,kudu_users.name,kudu_users.city,kudu_users.state,kudu_users.zip,kudu_users.age,sql_userattributes.height,sql_userattributes.weight,sql_userattributes.occupation from kudu_users  INNER JOIN sql_userattributes ON kudu_users.userid=sql_userattributes.userid")
joinDF.show

+------+---------------+------------+-----+-----+---+------+------+-----------+
|userid|           name|        city|state|  zip|age|height|weight|occupation |
+------+---------------+------------+-----+-----+---+------+------+-----------+
|   100|   Wendell Ryan|   San Diego|   CA|92102| 24|   175|   170|Electrician|
|   101|Alicia Thompson|    Berkeley|   CA|94705| 52|   180|   120|Librarian  |
|   102|Felipe Drummond|   Palo Alto|   CA|94301| 33|   180|   215|  Data                                                                      Scientist|
|   103|  Teresa Levine|Walnut Creek|   CA|94507| 47|   178|   132|Software                                                                      Developer|
+------+---------------+------------+-----+-----+---+------+------+-----------+

使用这种方法可以获得相同的结果。

val joinDF2 = kuduDF.join(sqlDF,"userid")

joinDF2.show

+------+---------------+------------+-----+-----+---+------+------+-----------+
|userid|           name|        city|state|  zip|age|height|weight| occupation|
+------+---------------+------------+-----+-----+---+------+------+-----------+
|   100|   Wendell Ryan|   San Diego|   CA|92102| 24|   175|   170|Electrician|
|   101|Alicia Thompson|    Berkeley|   CA|94705| 52|   180|   120|  Librarian|
|   102|Felipe Drummond|   Palo Alto|   CA|94301| 33|   180|   215|  Data                                                                      Scientist|
|   103|  Teresa Levine|Walnut Creek|   CA|94507| 47|   178|   132|  Software                                                                      Developer|
+------+---------------+------------+-----+-----+---+------+------+-----------+

设置 Oracle 数据库的 JDBC URL 和连接属性。

val jdbcURL = "jdbc:oracle:thin:sales/cloudera@//192.168.56.30:1521/EDWPDB"
val connectionProperties = new java.util.Properties()

import org.apache.spark.sql.SaveMode

将数据帧插入 Oracle。

joinDF.write.mode(SaveMode.Append).jdbc(jdbcURL, "users2", connectionProperties)

验证这些行是否已成功添加到 Oracle 数据库中(图 6-12 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-12

Verify rows

Spark 流和 Kudu

在一些用例中,您需要近乎实时地将数据接收到 Kudu 中。例如,这是物联网(IoT)用例的要求。在清单 6-1 中,我们展示了一个从 Flume 获取传感器数据的样本 Spark 流应用程序。我们执行基本的事件流处理,根据传感器返回的温度将每个事件的状态标记为正常、警告或严重。然后,状态与其余数据一起保存在 Kudu 表中。

用户可以在向 Kudu 表中插入数据时对其进行查询。在第九章,我们讨论一个叫做 Zoomdata 的实时数据可视化工具。您可以使用 Zoomdata 实时可视化存储在 Kudu 中的数据。

import java.io.IOException;
import org.apache.spark._
import org.apache.spark.rdd.NewHadoopRDD
import org.apache.spark.SparkConf
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.flume._
import org.apache.spark.streaming.Seconds
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.util.IntParam
import org.apache.spark.sql.SQLContext

object FlumeStreaming {

   case class SensorData(tableid: String, deviceid: String, thedate: String, thetime: String, temp: Short, status: String)

    def parseSensorData(str: String): SensorData = {
      val myData = str.split(",")

      val myTableid = myData(0)
      val myDeviceid = myData(1)
      val myDate = myData(2)
      val myTime = myData(3)
      val myTemp = myData(4)
      val myStatus = myData(5)

      SensorData(myTableid, myDeviceid, myDate, myTime, myTemp.toShort, myStatus)
    }

  def main(args: Array[String]) {

    val sparkConf = new SparkConf().setMaster("local[2]").setAppName("FlumeStreaming")
    val sc = new SparkContext(sparkConf)
    val ssc = new StreamingContext(sc, Seconds(1))

    val flumeStream = FlumeUtils.createPollingStream(ssc,args(0),args(1).toInt)

    val sensorDStream = flumeStream.map (x => new String(x.event.getBody.array)).map(parseSensorData)

    sensorDStream.foreachRDD { rdd =>

        val sqlContext = new SQLContext(sc)
        import sqlContext.implicits._

             val kuduContext = new KuduContext("kudumaster01:7051")

// convert the RDD into a DataFrame and insert it into the Kudu table

             val DataDF = rdd.toDF
             kuduContext.insertRows(DataDF, "impala::default.sensortable")

             DataDF.registerTempTable("currentDF")

             // Update the table based on the thresholds

             val WarningFilteredDF = sqlContext.sql("select * from currentDF where temp > 50 and temp <= 60")
             WarningFilteredDF.registerTempTable("warningtable")

             val UpdatedWarningDF = sqlContext.sql("select tableid,deviceid,thedate,thetime,temp,'WARNING' as status from warningtable")

             kuduContext.updateRows(UpdatedWarningDF, "impala::default.sensortable")

             val CriticalFilteredDF = sqlContext.sql("select * from currentDF where temp > 61")
             CriticalFilteredDF.registerTempTable("criticaltable")

             val UpdatedCriticalDF = sqlContext.sql("select tableid,deviceid,thedate,thetime,temp,'CRITICAL' as status from criticaltable")

             kuduContext.updateRows(UpdatedCriticalDF, "impala::default.sensortable")

     }

    ssc.start()
    ssc.awaitTermination()

  }

}

Listing 6-1Spark Streaming with Kudu

这是我的 SBT 档案。有关建筑应用的更多信息,请参考第五章。如果您正在使用 Maven,请查阅 Maven 的在线文档。

name := "My Test App"
version := "1.0"

scalaVersion := "2.10.5"

resolvers ++= Seq(
  "Apache Repository" at "https://repository.apache.org/content/repositories/releases/",
  "Cloudera repo" at "https://repository.cloudera.com/artifactory/cloudera-repos/"
)

libraryDependencies ++= Seq (
        "org.apache.spark" % "spark-core_2.10" % "1.5.0",
        "org.apache.spark" % "spark-streaming_2.10" % "1.5.0",
        "org.apache.spark" % "spark-streaming-flume_2.10" % "1.5.0",
        "org.apache.spark" % "spark-sql_2.10" % "1.5.0"
)

在执行 sbt package 来打包 spark 应用程序之后,现在可以使用 spark 附带的 spark-submit 工具启动应用程序。这些参数仅用于测试目的。根据您的数据接收要求更改参数。注意,我使用 jar 文件来包含 kudu-spark 依赖项。参数 localhost 和 9999 将被用作 Flume sink 目的地。

spark-submit \
-class FlumeStreaming \
-jars kudu-spark_2.10-0.10.0.jar \
-master yarn-client \
-driver-memory=512m \
-executor-memory=512m \
-executor-cores 4  \
/mydir/spark/flume_streaming_kudu/target/scala-2.10/butch-app_2.10-1.0.jar \ localhost 9999

这是一个使用 spooldir 作为数据源的样例 flume.conf 文件。这些设置足以满足测试目的。

agent1.sources  = source1
agent1.channels = channel1
agent1.sinks = spark

agent1.sources.source1.type = spooldir
agent1.sources.source1.spoolDir = /tmp/streaming
agent1.sources.source1.channels = channel1

agent1.channels.channel1.type = memory
agent1.channels.channel1.capacity = 10000
agent1.channels.channel1.transactionCapacity = 1000

agent1.sinks.spark.type = org.apache.spark.streaming.flume.sink.SparkSink
agent1.sinks.spark.hostname = 127.0.0.1
agent1.sinks.spark.port =  9999
agent1.sinks.spark.channel = channel1
agent1.sinks.spark.batchSize=5

Kudu 作为 Spark MLlib 的特性库

Kudu 让数据科学家更容易准备和清理数据。Kudu 可以作为机器学习应用程序的快速、高度可扩展和可变的特征库。它还可以与 Spark SQL 和 DataFrame API 顺利集成。

让我们来看一个例子。我们将使用来自 UCI 机器学习知识库的心脏病数据集【Xi】来预测心脏病的存在。这些数据是由罗伯特德特拉诺,医学博士,博士,从弗吉尼亚州医学中心,长滩和克利夫兰诊所基金会收集的。历史上,克利夫兰数据集一直是众多研究的主题,因此我们将使用该数据集。原始数据集有 76 个属性,但其中只有 14 个是 ML 研究人员传统上使用的(表 6-1 )。我们将简单地执行二项式分类,并确定患者是否患有心脏病(列表 6-2 )。这是我们在第五章中使用的同一个例子,但是这次我们将使用 Kudu 来存储我们的特性。

表 6-1

Cleveland Heart Disease Data Set Attribute Information

| 属性 | 描述 | | :-- | :-- | | 年龄 | 年龄 | | 性 | 性 | | 丙酸纤维素 | 胸痛型 | | treatbps | 静息血压 | | 胆固醇 | 血清胆固醇(毫克/分升) | | 前沿系统 | 空腹血糖> 120 毫克/分升 | | 尊重 | 静息心电图结果 | | 塔尔巴赫 | 达到最大心率 | | 考试 | 运动诱发的心绞痛 | | 旧峰 | 相对于静息运动诱发的 ST 段压低 | | 倾斜 | 运动 ST 段峰值的斜率 | | 大约 | 荧光镜染色的主要血管数量(0-3) | | 塔尔 | 唐松草压力测试结果 | | 数字 | 预测属性——心脏病的诊断 |

我们开始吧。我们需要下载该文件,将其复制到 HDFS,并在其上创建一个外部表。然后,我们将数据复制到 Kudu 表中。

wget http://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/cleveland.data

head -n 10 processed.cleveland.data

63.0,1.0,1.0,145.0,233.0,1.0,2.0,150.0,0.0,2.3,3.0,0.0,6.0,0
67.0,1.0,4.0,160.0,286.0,0.0,2.0,108.0,1.0,1.5,2.0,3.0,3.0,2
67.0,1.0,4.0,120.0,229.0,0.0,2.0,129.0,1.0,2.6,2.0,2.0,7.0,1
37.0,1.0,3.0,130.0,250.0,0.0,0.0,187.0,0.0,3.5,3.0,0.0,3.0,0
41.0,0.0,2.0,130.0,204.0,0.0,2.0,172.0,0.0,1.4,1.0,0.0,3.0,0
56.0,1.0,2.0,120.0,236.0,0.0,0.0,178.0,0.0,0.8,1.0,0.0,3.0,0
62.0,0.0,4.0,140.0,268.0,0.0,2.0,160.0,0.0,3.6,3.0,2.0,3.0,3
57.0,0.0,4.0,120.0,354.0,0.0,0.0,163.0,1.0,0.6,1.0,0.0,3.0,0
63.0,1.0,4.0,130.0,254.0,0.0,2.0,147.0,0.0,1.4,2.0,1.0,7.0,2
53.0,1.0,4.0,140.0,203.0,1.0,2.0,155.0,1.0,3.1,3.0,0.0,7.0,1

hadoop fs -put processed.cleveland.data /tmp/data
impala-shell

CREATE EXTERNAL TABLE cleveland_csv (
               age float,
               sex float,
               cp float,
               trestbps float,
               chol float,
               fbs float,
               restecg float,
               thalach float,
               exang float,
               oldpeak float,
               slope float ,
               ca float,
               thal float,
               num float
               )
               ROW FORMAT
          DELIMITED FIELDS TERMINATED BY ','
          LINES TERMINATED BY '\n' STORED AS TEXTFILE
          LOCATION '/tmp/data';

CREATE TABLE cleveland_kudu (
               id string,
               age float,
               sex float,
               cp float,
               trestbps float,
               chol float,
               fbs float,
               restecg float,
               thalach float,
               exang float,
               oldpeak float,
               slope float ,
               ca float,
               thal float,
               num float,
               primary key(id)

               )
        PARTITION BY HASH PARTITIONS 4
        STORED AS KUDU
        TBLPROPERTIES ('kudu.num_tablet_replicas' = '1');

INSERT INTO cleveland_kudu
SELECT
         uuid(),
               age,
               sex
               cp,
               trestbps,
               chol,
               fbs,
               restecg,
               thalach,
               exang,
               oldpeak,
               slope,
               ca,
               thal,
               num
FROM
cleveland_csv;

注意,我们使用了 Impala 函数 uuid()为我们的 Kudu 表生成一个惟一的主键。Kudu 表现在应该有一些数据了。让我们使用 spark-shell 来拟合使用 Spark MLlib 的模型。

spark-shell -packages org.apache.kudu:kudu-spark_2.10:1.1.0

import org.apache.spark._
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SQLContext
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types._
import org.apache.spark.sql._
import org.apache.spark.ml.classification.RandomForestClassifier
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
import org.apache.spark.ml.feature.StringIndexer
import org.apache.spark.ml.feature.VectorAssembler
import org.apache.spark.ml.tuning.{ParamGridBuilder, CrossValidator}
import org.apache.spark.ml.{Pipeline, PipelineStage}
import org.apache.spark.mllib.evaluation.RegressionMetrics
import org.apache.spark.ml.param.ParamMap
import org.apache.kudu.spark.kudu._

val kuduDF = sqlContext.read.options(Map("kudu.master" -> "kudumaster01:7051","kudu.table" -> "impala::default.cleveland_kudu")).kudu

kuduDF.printSchema
root
 |- id: string (nullable = false)
 |- age: float (nullable = true)
 |- sex: float (nullable = true)
 |- cp: float (nullable = true)
 |- trestbps: float (nullable = true)
 |- chol: float (nullable = true)
 |- fbs: float (nullable = true)
 |- restecg: float (nullable = true)
 |- thalach: float (nullable = true)
 |- exang: float (nullable = true)
 |- oldpeak: float (nullable = true)
 |- slope: float (nullable = true)
 |- ca: float (nullable = true)
 |- thal: float (nullable = true)
 |- num: float (nullable = true)

val myFeatures = Array("age", "sex", "cp", "trestbps", "chol", "fbs",
      "restecg", "thalach", "exang", "oldpeak", "slope",
      "ca", "thal", "num")

val myAssembler = new VectorAssembler().setInputCols(myFeatures).setOutputCol("features")

val kuduDF2 = myAssembler.transform(kuduDF)

val myLabelIndexer = new StringIndexer().setInputCol("num").setOutputCol("label")

val kuduDF3 = mylabelIndexer.fit(kuduDF2).transform(kuduDF2)

val kuduDF4 = kuduDF3.where(kuduDF3("ca").isNotNull).where(kuduDF3("thal").isNotNull).where(kuduDF3("num").isNotNull)

val Array(trainingData, testData) = kuduDF4.randomSplit(Array(0.8, 0.2), 101)

val myRFclassifier = new RandomForestClassifier().setFeatureSubsetStrategy("auto").setSeed(101)

val myEvaluator = new BinaryClassificationEvaluator().setLabelCol("label")

val myParamGrid = new ParamGridBuilder()
      .addGrid(myRFclassifier.maxBins, Array(10, 20, 30))
      .addGrid(myRFclassifier.maxDepth, Array(5, 10, 15))
      .addGrid(myRFclassifier.numTrees, Array(20, 30, 40))
      .addGrid(myRGclassifier.impurity, Array("gini", "entropy"))
      .build()

val myPipeline = new Pipeline().setStages(Array(myRFclassifier))

val myCV = new CrosValidator()
      .setEstimator(myPipeline)

      .setEvaluator(myEvaluator)
      .setEstimatorParamMaps(myParamGrid)
      .setNumFolds(3)

Listing 6-2Performing binary classifcation using Kudu as a feature store

我们现在可以拟合模型了。

val myCrossValidatorModel = myCV.fit(trainingData)

我们来评价一下模型。

val myEvaluatorParamMap = ParamMap(myEvaluator.metricName -> "areaUnderROC")

val aucTrainingData = myEvaluator.evaluate(CrossValidatorPrediction, myEvaluatorParamMap)

你现在可以根据我们的数据做一些预测。

val myCrossValidatorPrediction = myCrossValidatorModel.transform(testData)

在我们的例子中,我们使用了一个非常小的数据集(300 多个观察值),但是想象一下,如果数据集包含数十亿行。如果需要添加或更新训练数据,只需对 Kudu 表运行 DML 语句。更新高度可扩展的存储引擎(如 Kudu)的能力极大地简化了数据准备和功能工程。

Note

Kudu 允许每个表最多 300 列。如果需要存储 300 个以上的特性,HBase 是更合适的存储引擎。HBase 表可以包含成千上万的列。使用 HBase 的缺点是,与 Kudu 相比,它在处理全表扫描时效率不高。Apache Kudu 社区正在讨论在 Kudu 的未来版本中解决 300 列的限制。

严格来说,可以通过设置不安全标志来绕过 Kudu 的 300 列限制。例如,如果您需要创建一个包含 1000 列的 Kudu 表,您可以使用以下标志启动 Kudu master:-unlock-unsafe-flags-max-num-columns = 1000。这还没有经过 Kudu 开发团队的彻底测试,因此不建议用于生产。

摘要

Spark,Impala,和 Kudu 是完美的搭配。Spark 提供了高度可扩展的数据处理框架,而 Kudu 提供了高度可扩展的存储引擎。有了 Impala 提供的快速 SQL 接口,您就拥有了实现经济高效的企业数据管理和分析平台所需的一切。当然,您并不局限于这三个 Apache 开源项目。在随后的章节中,我将介绍可以进一步增强您的大数据平台的其他开源和商业应用程序。

参考

  1. 阿帕奇库杜;《Kudu 与 Spark 的融合》,阿帕奇 Kudu,2018, https://kudu.apache.org/docs/developing.html#_kudu_integration_with_spark
  2. 霍尔登·卡劳,雷切尔·沃伦;“高性能 Spark”,奥莱利,2017 年 6 月 https://www.safaribooksonline.com/library/view/high-performance-spark/9781491943199/
  3. 阿帕奇 Spark《JDBC 到其他数据库》,阿帕奇 Spark,2018, http://spark.apache.org/docs/latest/sql-programming-guide.html#jdbc-to-other-databases
  4. 张占;《HBASE 星火:基于数据帧的 HBASE 连接器》,霍顿作品,2016, https://hortonworks.com/blog/spark-hbase-dataframe-based-hbase-connector/
  5. 华为;《Astro:通过使用 Spark SQL 框架在 HBase 上实现高性能 SQL 层》,华为,2018, http://huaweibigdata.github.io/astro/
  6. 泰德·马拉斯卡;“Apache Spark 携 HBase-Spark 模块来到 Apache HBase”,Cloudera,2018 https://blog.cloudera.com/blog/2015/08/apache-spark-comes-to-apache-hbase-with-hbase-spark-module/
  7. 阿帕奇 Lucene《使用 SolrJ》,阿帕奇·卢斯,2018, https://lucene.apache.org/solr/guide/6_6/using-solrj.html
  8. Lucidworks《Lucidworks Spark/Solr 集成》,Lucidworks,2018, https://github.com/lucidworks/spark-solr
  9. Cloudera《将数据导入 HBase》,Cloudera,2018, https://www.cloudera.com/documentation/enterprise/latest/topics/admin_hbase_import.html
  10. 汤姆·怀特;《小文件问题》,Cloudera,2009, https://blog.cloudera.com/blog/2009/02/the-small-files-problem/
  11. 大卫·w·阿哈;“心脏病数据集”,加州大学欧文分校,1988 年, http://archive.ics.uci.edu/ml/datasets/heart+Disease
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值