关注公众号:登峰大数据,阅读Spark实战第二版(完整中文版),系统学习Spark3.0大数据框架!
如果您觉得作者翻译的内容有帮助,请分享给更多人。您的分享,是作者翻译的动力!
本章涵盖了
构建一个不需要数据接入的简单应用程序
使用Java lambdas语法和Spark
构建使用或不使用lambdas语法的应用程序
在本地模式、集群模式和交互方式下与Spark交互
用Spark计算圆周率π的近似值
在前几章中,了解了Apache Spark是什么,以及如何构建简单的应用程序,理解了包括dataframe和惰性在内的关键概念。第5章和第6章是相关联的:您将在本章构建一个应用程序,并在第6章中部署它。
在本章中,将从零开始构建一个应用程序。在本书之前构建了应用程序,但它们总是需要在过程的最开始就接入数据。本章实验本身会通过Spark产生数据,避免接入数据的需要。在集群中接入数据比创建自生成的数据集要复杂一些。这个应用程序的目标是近似地求出圆周率π的值。
然后您将了解与Spark互动的三种方式:
本地模式,通过前几章中的示例,您已经熟悉该模式
集群模式
交互模式
实验
本章中的例子可以在GitHub中获得:https://github .com/jgperrin/net.jgp.books.spark.ch05。
5.1 一个无数据接入的例子
在本节中,您将处理一个不需要接入数据的示例。正如你在这本书中看到的许多例子一样,接入是整个大数据处理的关键部分。但是,本章将介绍部署,包括集群上的部署,所以我不希望您因为过多地了解而分心。
在集群上工作意味着数据对所有节点都可用。要理解集群上的部署,您需要花大量时间关注数据分布。你将在第6章中学习更多关于数据分布的知识。
因此,为了简化对部署的理解,我跳过了数据接入,而将重点放在理解所有组件、数据流和实际部署上。Spark将生成一个大的随机数据集(自生成),可以用来计算圆周率π。
5.1.1 计算π
在这个小小的理论部分中,我解释了如何通过使用darts(投掷飞镖)来计算π,以及如何在Spark中实现这个过程。如果你讨厌数学,你会发现这部分并不是那么糟糕(如果你对数学过敏,你可以跳过这部分)。不过,我还是想把这一部分献给我的大儿子皮埃尔-尼古拉,他肯定比我更欣赏这种算法的美。
你还在阅读吗?好啊!在这个简短的部分中,您将了解如何通过投掷飞镖来获得π近似的表达式,然后在Spark中用代码实现。结果和实际代码在下一节中。
有很多方法可以计算出π(参见Wikipedia:https://en.wikipedia.org/wiki/Approximations_of_%CF%80)。最适合我们场景的一种方法称为圆的面积求和,如图5.1所示。
图5.1用圆圈的面积投掷飞镖来近似地模拟环境
这个代码通过向一个圆圈“投掷飞镖”来估计π:当点(飞镖的冲击)随机散落在单位正方形内时,一些点落在单位圆内。随着点的增加,圆内积分的分数接近于π /4。
你将模拟投掷数百万个飞镖。将随机生成每次投掷的横坐标(x)和纵坐标(y)。根据这些坐标,使用勾股定理,可以计算出飞镖是圆内还是圆外。图5.2说明了这种方法。
图5.2 使用勾股定理,可以很容易地确定掷出的点是否在圈内。
根据图5.2,您可以看到两次投掷,t1和t2,可以看到第一次投掷,t1,在圆外。它的坐标是x1 = 0.75 y1 = 0.9。距离d1为t1到原点的距离,其坐标为(0,0),表示为:
第二次也可以做同样的练习,其中d2表示原点到t2的距离:
这意味着第二次投掷是在圆圈内。
Spark将创建数据、应用转换(transformation),然后应用action操作——这是Spark的一种经典操作方式。过程(如图5.3所示)如下:
打开一个Spark会话。
Spark创建一个数据集,其中包含每次飞镖投掷的一行数据。投掷的次数越多,你的近似值就会越精确。
创建包含每次抛出结果的数据集。
对有助于计算圆的面积的投掷次数求和。
计算两个区域投掷的比率,并将其乘以4,这近似于π。
在总结此过程时,图5.3介绍了所使用的方法以及所涉及的组件。您在第2章中了解了其中一些组件。第6章还将进一步详细说明每个组件。
图5.3 模拟仿真的过程,演示了Spark组件(驱动程序driver和执行程序executor)以及方法。executor由worker控制。
足够多的数学知识——让我们看看Java代码,好吗?
5.1.2 求圆周率π的近似值的代码
在本节中,您将浏览这些代码,这些代码将在本章的各个示例中使用。您将首先以本地模式运行代码。然后将修改该版本的代码以使用Java lambda函数。
Java 8引入了lambda函数,它可以不属于类而存在,可以作为参数传递,并按需执行。您将了解lambda函数如何帮助您(或不帮助)编写转换代码。
让我们首先看一下应用程序的输出,如下面的清单所示。
//清单5.1 投掷飞镖来求π的近似值的结果About to throw 1000000 darts, ready? Stay away from the target!Session initialized in 1685 msInitial dataframe built in 5083 msThrowing darts done in 21 ms//你在21毫秒内投了100万次飞镖。100000 darts thrown so far//只有当你调用action时,投掷才会开始。200000 darts thrown so far...900000 darts thrown so far1000000 darts thrown so farAnalyzing result in 6337 ms Pi is roughly 3.143304
该应用程序告诉您,在21毫秒内投掷了100万次飞镖。然而,Spark只有在您要求它时才会抛出飞镖,因为您调用一个action来分析结果。这是来自Spark的懒惰特点,你在第4章学到的;记住那些需要action来提醒的讨厌的孩子们!
在这个实验里,你将把加工过程分成不同的批次。我将其称为slices,在运行实验之后,可以在不同位置处理不同的slices值,以便更好地理解Spark如何处理这个过程。
实验
实验100的代码在net.jgp.books.spark.ch05.lab100_pi_compute .PiComputeApp类中,也在下面的清单中。
package net.jgp.books.spark.ch05.lab100_pi_compute; import java.io.Serializable;import java.util.ArrayList;import java.util.List;import org.apache.spark.api.java.function.MapFunction;import org.apache.spark.api.java.function.ReduceFunction;import org.apache.spark.sql.Dataset;import org.apache.spark.sql.Encoders;import org.apache.spark.sql.Row;import org.apache.spark.sql.SparkSession;... private void start( int slices ) { int numberOfThrows = 100000 * slices ;//可以用slices(切片)的数量作为乘数;稍后在集群上运行它时,它将非常有用。 System. out .println( "About to throw " + numberOfThrows + " darts, ready? Stay away from the target!" ); long t0 = System.currentTimeMillis(); SparkSession spark = SparkSession .builder() .appName( "Spark Pi" ) .master( "local[*]" )//使用系统上所有可用的线程 .getOrCreate(); long t1 = System.currentTimeMillis(); System. out .println( "Session initialized in " + ( t1 - t0 ) + " ms" );
到目前为止,代码是相当标准的:它使用导入的Spark相关类,创建一个会话。你会注意到调用currentTimeMillis()来测量时间的花费:
List<Integer> listOfThrows = new ArrayList<>( numberOfThrows ); for ( int i = 0; i < numberOfThrows ; i ++) { listOfThrows .add( i ); } Dataset<Row> incrementalDf = spark .createDataset( l , Encoders.INT()) .toDF(); long t2 = System.currentTimeMillis(); System. out .println( "Initial dataframe built in " + ( t2 - t1 ) + " ms" );
在这个代码片段中,从一个整数列表创建一个数据集,并将其转换为一个dataframe。当从列表创建数据集时,需要提供关于数据类型的提示——因此使用encodes.INT()参数。
这个dataframe的目的仅仅是在一个名为map的操作中将处理分派到尽可能多的节点上。在传统的编程中,如果你想抛出一百万个darts,你需要在单个线程,单个节点上使用一个循环。这不能扩展。在高度分布式的环境中,您可以在所有节点上映射投掷飞镖的过程。图5.4比较了这些过程。
图5.4 比较在迭代过程中投掷100万次飞镖的过程与将它们映射到四个节点上的过程(但可能更多)
换句话说,每一行incrementalDf都被传递给一个DartMapper的实例,它位于集群的所有物理节点上:
Dataset<Integer> dartsDs = incrementalDf .map( new DartMapper(), Encoders.INT()); long t3 = System.currentTimeMillis(); System. out .println( "Throwing darts done in " + ( t3 - t2 ) + " ms" );
您将在清单5.3中看到DartMapper()。
reduce操作带来的结果:圆圈中飞镖的数量。以类似的方式,reduce操作在你的应用程序中是透明的,并且只包含一行代码:
int dartsInCircle = dartsDs .reduce( new DartReducer()); long t4 = System.currentTimeMillis(); System. out .println( "Analyzing result in " + ( t4 - t3 ) + " ms" ); System. out .println( "Pi is roughly " + 4.0 * dartsInCircle / numberOfThrows );
您将在清单5.3中看到DartReducer()。
我可以将处理过程总结如下:
创建一个列表,它将用于映射数据。
map数据(投掷飞镖)。
reduce结果。
让我们看一下清单5.3中的map和reduce操作的代码。
更进一步
您可以通过将numberOfThrows类型从int改为long来练习这个实验。如果使用Eclipse之类的IDE,您将直接看到它对代码其余部分的影响。这个示例的另一个变化是:在调用master时尝试合并slices变量(或它的一部分),如.master(“local[]”),以查看它如何影响性能(当使用更多核时,这一点会更明显)。*
//清单5.3计算π的代码:map和reduce类的代码private final class DartMapper implements MapFunction<Row, Integer> { private static final long serialVersionUID = 38446L; @Override public Integer call(Row r ) throws Exception { double x = Math.random() * 2 - 1;//你随意扔飞镖;x和y是坐标。 double y = Math.random() * 2 - 1; counter ++;//简单的计数器,看看发生了什么;不要重置它。 if ( counter % 100000 == 0) { System. out .println( "" + counter + " darts thrown so far" ); } return ( x * x + y * y <= 1) ? 1 : 0;//如果它在圆中,则返回1;如果它不在圆中,则返回0(参见后面的平方根说明) } } //reduce将计算相加的结果;注意,类型是匹配的。泛型异常来自方法的签名。 private final class DartReducer implements ReduceFunction<Integer> { private static final long serialVersionUID = 12859L; @Override public Integer call(Integer x , Integer y ) throws Exception { return x + y ;//返回每次投掷结果的和 } }
为什么不返回平方根呢?
看一下mapper中的call()方法的返回值。如果遵循勾股定理,就需要返回x和y的平方和的平方根。但是,由于这些值小于1,这并不重要:我们关心的是飞镖是否在圆中,我们不关心从原点到投掷点的确切距离。因此,我们可以省去昂贵的开根号操作。
(未完待续......) 欢迎关注公众号,及时获得最新翻译内容: