一、Spark SQL
Spark SQL的核心是把已有的RDD,带上Schema信息,然后注册成类似sql里的”Table”,对其进行sql查询。这里面主要分两部分,一是生成SchemaRD,二是执行查询。
如果是spark-hive项目,那么读取metadata信息作为Schema、读取hdfs上数据的过程交给Hive完成,然后根据这俩部分生成SchemaRDD,在HiveContext下进行hql()查询。
对于Spark SQL来说,数据方面,RDD可以来自任何已有的RDD,也可以来自支持的第三方格式,如json file、parquet file。
SQLContext下会把带case class的RDD隐式转化为SchemaRDD
ExsitingRdd单例里会反射出case class的attributes,并把RDD的数据转化成Catalyst的GenericRow,最后返回RDD[Row],即一个SchemaRDD。这里的具体转化逻辑可以参考ExsitingRdd的productToRowRdd和convertToCatalyst方法。
之后可以进行SchemaRDD提供的注册table操作、针对Schema复写的部分RDD转化操作、DSL操作、saveAs操作等等。
Row和GenericRow是Catalyst里的行表示模型
Row用Seq[Any]来表示values,GenericRow是Row的子类,用数组表示values。Row支持数据类型包括Int, Long, Double, Float, Boolean, Short, Byte, String。支持按序数(ordinal)读取某一个列的值。读取前需要做isNullAt(i: Int)的判断。
各自都有Mutable类,提供setXXX(i: int, value: Any)修改某序数上的值。
下图大致对比了Pig,Spark SQL,Shark在实现层次上的区别,仅做参考。
SQLContext里对sql的一个解析和执行流程:
1. 第一步parseSql(sql: String),simple sql parser做词法语法解析,生成LogicalPlan。
2. 第二步analyzer(logicalPlan),把做完词法语法解析的执行计划进行初步分析和映射,
目前SQLContext内的Analyzer由Catalyst提供,定义如下:
new Analyzer(catalog, EmptyFunctionRegistry, caseSensitive =true)
catalog为SimpleCatalog,catalog是用来注册table和查询relation的。
而这里的FunctionRegistry不支持lookupFunction方法,所以该analyzer不支持Function注册,即UDF。
Analyzer内定义了几批规则:
3. 从第二步得到的是初步的logicalPlan,接下来第三步是optimizer(plan)。
Optimizer里面也是定义了几批规则,会按序对执行计划进行优化操作。
4. 优化后的执行计划,还要丢给SparkPlanner处理,里面定义了一些策略,目的是根据逻辑执行计划树生成最后可以执行的物理执行计划树,即得到SparkPlan。
5. 在最终真正执行物理执行计划前,最后还要进行两次规则,SQLContext里定义这个过程叫prepareForExecution,这个步骤是额外增加的,直接new RuleExecutor[SparkPlan]进行的。
6. 最后调用SparkPlan的execute()执行计算。这个execute()在每种SparkPlan的实现里定义,一般都会递归调用children的execute()方法,所以会触发整棵Tree的计算。
内存列存储
SQLContext下cache/uncache table的时候会调用列存储模块。
该模块借鉴自Shark,目的是当把表数据cache在内存的时候做行转列操作,以便压缩。
实现类
InMemoryColumnarTableScan类是SparkPlan LeafNode的实现,即是一个物理执行计划。传入一个SparkPlan(确认了的物理执行计)和一个属性序列,内部包含一个行转列、触发计算并cache的过程(且是lazy的)。
ColumnBuilder针对不同的数据类型(boolean, byte, double, float, int, long, short, string)由不同的子类把数据写到ByteBuffer里,即包装Row的每个field,生成Columns。与其对应的ColumnAccessor是访问column,将其转回Row。
CompressibleColumnBuilder和CompressibleColumnAccessor是带压缩的行列转换builder,其ByteBuffer内部存储结构如下
CompressionScheme子类是不同的压缩实现
都是scala实现的,未借助第三方库。不同的实现,指定了支持的column data类型。在build()的时候,会比较每种压缩,选择压缩率最小的(若仍大于0.8就不压缩了)。
这里的估算逻辑,来自子类实现的gatherCompressibilityStats方法。
Cache逻辑
cache之前,需要先把本次cache的table的物理执行计划生成出来。
在cache这个过程里,InMemoryColumnarTableScan并没有触发执行,但是生成了以InMemoryColumnarTableScan为物理执行计划的SparkLogicalPlan,并存成table的plan。
其实在cache的时候,首先去catalog里寻找这个table的信息和table的执行计划,然后会进行执行(执行到物理执行计划生成),然后把这个table再放回catalog里维护起来,这个时候的执行计划已经是最终要执行的物理执行计划了。但是此时Columner模块相关的转换等操作都是没有触发的。
真正的触发还是在execute()的时候,同其他SparkPlan的execute()方法触发场景是一样的。
Uncache逻辑
UncacheTable的时候,除了删除catalog里的table信息之外,还调用了InMemoryColumnarTableScan的cacheColumnBuffers方法,得到RDD集合,并进行了unpersist()操作。cacheColumnBuffers主要做了把RDD每个partition里的ROW的每个Field存到了ColumnBuilder内。
UDF(暂不支持)
如前面对SQLContext里Analyzer的分析,其FunctionRegistry没有实现lookupFunction。
在spark-hive项目里,HiveContext里是实现了FunctionRegistry这个trait的,其实现为HiveFunctionRegistry,实现逻辑见org.apache.spark.sql.hive.hiveUdfs
JSON支持
SQLContext下,增加了jsonFile的读取方法,而且目前看,代码里实现的是hadoop textfile的读取,也就是这份json文件应该是在HDFS上的。具体这份json文件的载入,InputFormat是TextInputFormat,key class是LongWritable,value class是Text,最后得到的是value部分的那段String内容,即RDD[String]。
除了jsonFile,还支持jsonRDD,读取json文件之后,转换成SchemaRDD。JsonRDD.inferSchema(RDD[String])里有详细的解析json和映射出schema的过程,最后得到该json的LogicalPlan。
Json的解析使用的是FasterXML/jackson-databind库,GitHub地址,wiki
把数据映射成Map[String, Any]
Json的支持丰富了Spark SQL数据接入场景。
JDBC支持
Jdbc support branchis under going
SQL92
Spark SQL目前的SQL语法支持情况见SqlParser类。目标是支持SQL92
1. 基本应用上,sql server 和oracle都遵循sql 92语法标准。
2. 实际应用中大家都会超出以上标准,使用各家数据库厂商都提供的丰富的自定义标准函数库和语法。
3. 微软sql server的sql 扩展叫T-SQL(Transcate SQL).
4. Oracle 的sql 扩展叫PL-SQL.
二、 Spark StreamingSpark是一个类似于MapReduce的分布式计算框架,其核心是弹性分布式数据集,提供了比MapReduce更丰富的模型,可以在快速在内存中对数据集进行多次迭代,以支持复杂的数据挖掘算法和图形计算算法。Spark Streaming是一种构建在Spark上的实时计算框架,它扩展了Spark处理大规模流式数据的能力。
Spark Streaming的优势在于:
基于云梯Spark on Yarn的Spark Streaming总体架构如图1所示。其中Spark on Yarn的启动流程我的另外一篇文章(《程序员》2013年11月期刊《深入剖析阿里巴巴云梯Yarn集群》)有详细描述,这里不再赘述。Spark on Yarn启动后,由Spark AppMaster把Receiver作为一个Task提交给某一个Spark Executor;Receive启动后输入数据,生成数据块,然后通知Spark AppMaster;Spark AppMaster会根据数据块生成相应的Job,并把Job的Task提交给空闲Spark Executor 执行。图中蓝色的粗箭头显示被处理的数据流,输入数据流可以是磁盘、网络和HDFS等,输出可以是HDFS,数据库等。
图1 云梯Spark Streaming总体架构
Spark Streaming的基本原理是将输入数据流以时间片(秒级)为单位进行拆分,然后以类似批处理的方式处理每个时间片数据,其基本原理如图2所示。
图2 Spark Streaming基本原理图
首先,Spark Streaming把实时输入数据流以时间片Δt (如1秒)为单位切分成块。Spark Streaming会把每块数据作为一个RDD,并使用RDD操作处理每一小块数据。每个块都会生成一个Spark Job处理,最终结果也返回多块。
下面介绍Spark Streaming内部实现原理。
使用Spark Streaming编写的程序与编写Spark程序非常相似,在Spark程序中,主要通过操作RDD(Resilient Distributed Datasets弹性分布式数据集)提供的接口,如map、reduce、filter等,实现数据的批处理。而在Spark Streaming中,则通过操作DStream(表示数据流的RDD序列)提供的接口,这些接口和RDD提供的接口类似。图3和图4展示了由Spark Streaming程序到Spark jobs的转换图。
图3 Spark Streaming程序转换为DStream Graph
图4 DStream Graph转换为Spark jobs
在图3中,Spark Streaming把程序中对DStream的操作转换为DStream Graph,图4中,对于每个时间片,DStream Graph都会产生一个RDD Graph;针对每个输出操作(如print、foreach等),Spark Streaming都会创建一个Spark action;对于每个Spark action,Spark Streaming都会产生一个相应的Spark job,并交给JobManager。JobManager中维护着一个Jobs队列, Spark job存储在这个队列中,JobManager把Spark job提交给Spark Scheduler,Spark Scheduler负责调度Task到相应的Spark Executor上执行。
Spark Streaming的另一大优势在于其容错性,RDD会记住创建自己的操作,每一批输入数据都会在内存中备份,如果由于某个结点故障导致该结点上的数据丢失,这时可以通过备份的数据在其它结点上重算得到最终的结果。
正如Spark Streaming最初的目标一样,它通过丰富的API和基于内存的高速计算引擎让用户可以结合流式处理,批处理和交互查询等应用。因此Spark Streaming适合一些需要历史数据和实时数据结合分析的应用场合。当然,对于实时性要求不是特别高的应用也能完全胜任。另外通过RDD的数据重用机制可以得到更高效的容错处理。
三、 Spark MllibMLlib 是Spark对常用的机器学习算法的实现库,同时包括相关的测试和数据生成器。MLlib 目前支持四种常见的机器学习问题:二元分类,回归,聚类以及协同过滤,同时也包括一个底层的梯度下降优化基础算法。本指南将会简要介绍 MLlib 中所支持的功能,并给出相应的调用 MLlib 的例子。
MLlib 将会调用 jblas 线性代数库,这个库本身依赖于原生的 Fortran 程序。如果你的节点中没有这些库,你也许会需要安装 gfortran runtime library 。如果程序没有办法自动检测到这些库,MLlib 将会抛出链接错误的异常。
如果想用 Python 调用 MLlib,你需要安装 NumPy 1.7 或者更新的版本。
二元分类是一个监督学习问题。在这个问题中,我们希望将实体归类到两个独立的类别或标签的其中一个中,例如判断一个邮件是否是垃圾邮件。这个问题涉及在一组被 打过标签 的样例运行一个学习 算法 ,例如一组由(数字)特征和(相关的)类别标签所代表的实体。这个算法将会返回一个训练好的模型,该模型能够对标签未知的新个体进行潜在标签预测。
MLlib 目前支持两个适用于二元分类的标准模型家族: 线性支持向量机(SVMs) 和 逻辑回归 ,同时也包括分别适用与这两个模型家族的 L1 和 L2 正则化 变体。这些训练算法都利用了一个底层的梯度下降基础算法(描述如下)。二元分类算法的输入值是一个正则项参数(regParam) 和多个与梯度下降相关的参数( stepSize, numIterations, miniBatchFraction ) 。
目前可用的二元分类算法:
线性回归是另一个经典的监督学习问题。在这个问题中,每个个体都有一个与之相关联的实数标签(而在二元分类中个体的标签都是二元的),并且我们希望在给出用于表示这些实体的数值特征后,所预测出的标签值可以尽可能接近实际值。MLlib支持线性回归和与之相关的 L1 ( lasso )和 L2 ( ridge ) 正则化的变体。MLlib中的回归算法也利用了底层的梯度下降基础算法(描述如下),输入参数与上述二元分类算法一致。
目前可用的线性回归算法:
聚类是一个非监督学习问题,在这个问题上,我们的目标是将一部分实体根据某种意义上的相似度和另一部分实体聚在一起。聚类通常被用于探索性的分析,或者作为层次化监督学习管道网(hierarchical supervised learning pipeline) 的一个组件(其中每一个类簇都会用与训练不同的分类器或者回归模型)。 MLlib 目前已经支持作为最被广泛使用的聚类算法之一的 k-means 聚类算法,根据事先定义的类簇个数,这个算法能对数据进行聚类。MLlib 的实现中包含一个 k-means++ 方法的并行化变体 kmeans||。 MLlib 里面的实现有如下的参数:
目前可用的聚类算法:
协同过滤 常被应用于推荐系统。这些技术旨在补充用户-商品关联矩阵中所缺失的部分。MLlib当前支持基于模型的协同过滤,其中用户和商品通过一小组隐语义因子进行表达,并且这些因子也用于预测缺失的元素。为此,我们实现了 交替最小二乘法(ALS) 来学习这些隐性语义因子。在 MLlib 中的实现有如下的参数:
numBlocks 是用于并行化计算的分块个数 (设置为-1为自动配置)。
rank 是模型中隐语义因子的个数。
iterations 是迭代的次数。
lambda 是ALS的正则化参数。
implicitPrefs 决定了是用 显性反馈 ALS的版本还是用适用 隐性反馈 数据集的版本。
alpha 是一个针对于 隐性反馈 ALS 版本的参数,这个参数决定了偏好行为强度的 基准。
基于矩阵分解的协同过滤的标准方法一般将用户商品矩阵中的元素作为用户对商品的显性偏好。
在许多的现实生活中的很多场景中,我们常常只能接触到 隐性的反馈 (例如游览,点击,购买,喜欢,分享等等)在 MLlib 中所用到的处理这种数据的方法来源于文献:Collaborative Filtering for Implicit Feedback Datasets 。 本质上,这个方法将数据作为二元偏好值和 偏好强度 的一个结合,而不是对评分矩阵直接进行建模。因此,评价就不是与用户对商品的显性评分而是和所观察到的用户偏好强度关联了起来。然后,这个模型将尝试找到隐语义因子来预估一个用户对一个商品的偏好。
目前可用的协同过滤的算法:
梯度下降 (及其随机的变种)是非常适用于大型分布式计算的一阶优化方案。梯度下降旨在通过向一个函数当前点(当前的参数值)的负梯度方向移动的方式迭代地找到这个函数的本地最优解。MLlib 以梯度下降作为一个底层的基础算法,在上面开发了各种机器学习算法。梯度下降算法有如下的参数:
目前可用的梯度下降算法:
下面的代码段可以在 spark-shell 中运行。
下面的代码段演示了如何导入一份样本数据集,使用算法对象中的静态方法在训练集上执行训练算法,在所得的模型上进行预测并计算训练误差。
import org.apache.spark.SparkContext
import org.apache.spark.mllib.classification.SVMWithSGD
import org.apache.spark.mllib.regression.LabeledPoint
// Load and parse the data file
val data = sc.textFile("mllib/data/sample_svm_data.txt")
val parsedData = data.map { line =>
val parts = line.split(' ')
LabeledPoint(parts(0).toDouble, parts.tail.map(x => x.toDouble).toArray)
}
// Run training algorithm to build the model
val numIterations = 20
val model = SVMWithSGD.train(parsedData, numIterations)
// Evaluate model on training examples and compute training error
val labelAndPreds = parsedData.map { point =>
val prediction = model.predict(point.features)
(point.label, prediction)
}
val trainErr = labelAndPreds.filter(r => r._1 != r._2).count.toDouble / parsedData.count
println("Training Error = " + trainErr)
默认情况下,这个 SVMWithSGD.train() 方法使用正则参数为 1.0 的 L2 正则项。如果我们想配置这个算法,我们可以通过直接新建一个新的对象,并调用setter的方法,进一步个性化设置 SVMWithSGD 。所有其他的 MLlib 算法也是通过这样的方法来支持个性化的设置。比如,下面的代码给出了一个正则参数为0.1的 L1 正则化SVM变体,并且让这个训练算法迭代200遍。
import org.apache.spark.mllib.optimization.L1Updater val svmAlg = new SVMWithSGD() svmAlg.optimizer.setNumIterations(200) .setRegParam(0.1) .setUpdater(new L1Updater) val modelL1 = svmAlg.run(parsedData)
下面这个例子演示了如何导入训练集数据,将其解析为带标签点的RDD。然后,使用 LinearRegressionWithSGD 算法来建立一个简单的线性模型来预测标签的值。最后我们计算了均方差来评估预测值与实际值的 吻合度 。
import org.apache.spark.mllib.regression.LinearRegressionWithSGD import org.apache.spark.mllib.regression.LabeledPoint // Load and parse the data val data = sc.textFile("mllib/data/ridge-data/lpsa.data") val parsedData = data.map { line => val parts = line.split(',') LabeledPoint(parts(0).toDouble, parts(1).split(' ').map(x => x.toDouble).toArray) } // Building the model val numIterations = 20 val model = LinearRegressionWithSGD.train(parsedData, numIterations) // Evaluate model on training examples and compute training error val valuesAndPreds = parsedData.map { point => val prediction = model.predict(point.features) (point.label, prediction) } val MSE = valuesAndPreds.map{ case(v, p) => math.pow((v - p), 2)}.reduce(_ + _)/valuesAndPreds.count println("training Mean Squared Error = " + MSE)
类似的,你也可以使用 RidgeRegressionWithSGD 和 LassoWithSGD 这两个算法,并比较这些算法在训练集上的均方差。
在下面的例子中,在载入和解析数据之后,我们使用 KMeans 对象来将数据聚类到两个类簇当中。所需的类簇个数会被传递到算法中。然后我们将计算集内均方差总和 (WSSSE). 你可以通过增加类簇的个数 k 来减小误差。 实际上,最优的类簇数通常是 1,因为这一点通常是WSSSE图中的 “低谷点”。
import org.apache.spark.mllib.clustering.KMeans
// Load and parse the data
val data = sc.textFile("kmeans_data.txt")
val parsedData = data.map( _.split(' ').map(_.toDouble))
// Cluster the data into two classes using KMeans
val numIterations = 20
val numClusters = 2
val clusters = KMeans.train(parsedData, numClusters, numIterations)
// Evaluate clustering by computing Within Set Sum of Squared Errors
val WSSSE = clusters.computeCost(parsedData)
println("Within Set Sum of Squared Errors = " + WSSSE)
在下面的例子中,我们导入的训练集中,数据每一行由一个用户,一个商品和相应的评分组成。假设评分是显性的,在这种情况下我们使用默认的 ALS.train() 方法。我们通过计算预测出的评分的均方差来评估这个推荐模型。
import org.apache.spark.mllib.recommendation.ALS
import org.apache.spark.mllib.recommendation.Rating
// Load and parse the data
val data = sc.textFile("mllib/data/als/test.data")
val ratings = data.map(_.split(',') match {
case Array(user, item, rate) => Rating(user.toInt, item.toInt, rate.toDouble)
})
// Build the recommendation model using ALS
val numIterations = 20
val model = ALS.train(ratings, 1, 20, 0.01)
// Evaluate the model on rating data
val usersProducts = ratings.map{ case Rating(user, product, rate) => (user, product)}
val predictions = model.predict(usersProducts).map{
case Rating(user, product, rate) => ((user, product), rate)
}
val ratesAndPreds = ratings.map{
case Rating(user, product, rate) => ((user, product), rate)
}.join(predictions)
val MSE = ratesAndPreds.map{
case ((user, product), (r1, r2)) => math.pow((r1- r2), 2)
}.reduce(_ + _)/ratesAndPreds.count
println("Mean Squared Error = " + MSE)
如果这个评分矩阵是通过其他的信息来源(如从其他的信号中提取出来的)所获得,你也可以使用 trainImplicit 的方法来得到更好的结果。
val model = ALS.trainImplicit(ratings, 1, 20, 0.01)
val model = ALS . trainImplicit ( ratings , 1 , 20 , 0.01 ) |
所有 MLlib 中的算法都是对Java友好的,因此你可以用在 Scala 中一样的方法来导入和调用这些算法。唯一要注意的是,这些算法的输入值是 Scala RDD 对象,而在 Spark Java API 中用了分离的 JavaRDD 类。你可以在你的 JavaRDD对象中调用 .rdd()的方法来将 Java RDD 转化成 Scala RDD 。
下面的列子可以在 PySpark shell 中得到测试。
下面的代码段表明了如何导入一份样本数据集,使用算法对象中的静态方法在训练集上执行训练算法,在所得的模型上进行预测并计算训练误差。
from pyspark.mllib.classification import LogisticRegressionWithSGD from numpy import array # Load and parse the data data = sc.textFile("mllib/data/sample_svm_data.txt") parsedData = data.map(lambda line: array([float(x) for x in line.split(' ')])) model = LogisticRegressionWithSGD.train(parsedData) # Build the model labelsAndPreds = parsedData.map(lambda point: (int(point.item(0)), model.predict(point.take(range(1, point.size))))) # Evaluating the model on training data trainErr = labelsAndPreds.filter(lambda (v, p): v != p).count() / float(parsedData.count()) print("Training Error = " + str(trainErr))
下面这个例子给出了如何导入训练集数据,将其解析为带标签点的RDD。然后,这个例子使用了 LinearRegressionWithSGD 算法来建立一个简单的线性模型来预测标签的值。我们在最后计算了均方差来评估预测值与实际值的 吻合度 。
from pyspark.mllib.regression import LinearRegressionWithSGD from numpy import array # Load and parse the data data = sc.textFile("mllib/data/ridge-data/lpsa.data") parsedData = data.map(lambda line: array([float(x) for x in line.replace(',', ' ').split(' ')])) # Build the model model = LinearRegressionWithSGD.train(parsedData) # Evaluate the model on training data valuesAndPreds = parsedData.map(lambda point: (point.item(0), model.predict(point.take(range(1, point.size))))) MSE = valuesAndPreds.map(lambda (v, p): (v - p)**2).reduce(lambda x, y: x + y)/valuesAndPreds.count() print("Mean Squared Error = " + str(MSE))
类似的,你也可以使用 RidgeRegressionWithSGD 和 LassoWithSGD 这两个算法,并比较这些算法在训练集上的均方差。
在下面的例子中,在载入和解析数据之后,我们使用 KMeans对象来将数据聚类到两个类簇当中。所需的类簇个数被传递到算法中。然后我们将计算集内均方差总和(WSSSE). 你可以通过增加类簇的个数 k 来减小误差。 实际上,最优的类簇数通常是 1,因为这一点通常是WSSSE图中的”低谷点”。
from pyspark.mllib.clustering import KMeans from numpy import array from math import sqrt # Load and parse the data data = sc.textFile("kmeans_data.txt") parsedData = data.map(lambda line: array([float(x) for x in line.split(' ')])) # Build the model (cluster the data) clusters = KMeans.train(parsedData, 2, maxIterations=10, runs=30, initialization_mode="random") # Evaluate clustering by computing Within Set Sum of Squared Errors def error(point): center = clusters.centers[clusters.predict(point)] return sqrt(sum([x**2 for x in (point - center)])) WSSSE = parsedData.map(lambda point: error(point)).reduce(lambda x, y: x + y) print("Within Set Sum of Squared Error = " + str(WSSSE))
在下面的例子中,我们导入的训练集中,数据每一行由一个用户,一个商品和相应的评分组成。假设评分是显性的,在这种情况下我们使用默认的> ALS.train() 方法。我们通过计算预测出的评分的均方差来评估这个推荐模型。
from pyspark.mllib.recommendation import ALS from numpy import array # Load and parse the data data = sc.textFile("mllib/data/als/test.data") ratings = data.map(lambda line: array([float(x) for x in line.split(',')])) # Build the recommendation model using Alternating Least Squares model = ALS.train(ratings, 1, 20) # Evaluate the model on training data testdata = ratings.map(lambda p: (int(p[0]), int(p[1]))) predictions = model.predictAll(testdata).map(lambda r: ((r[0], r[1]), r[2])) ratesAndPreds = ratings.map(lambda r: ((r[0], r[1]), r[2])).join(predictions) MSE = ratesAndPreds.map(lambda r: (r[1][0] - r[1][1])**2).reduce(lambda x, y: x + y)/ratesAndPreds.count() print("Mean Squared Error = " + str(MSE))
如果这个评分矩阵是通过其他的信息来源(如从其他的信号中提取出来的)所获得,你也可以使用 trainImplicit 的方法来得到更好的结果。
# Build the recommendation model using Alternating Least Squares based on implicit ratings
model = ALS.trainImplicit(ratings, 1, 20)
Spark GraphX是一个分布式图处理框架,Spark GraphX基于Spark平台提供对图计算和图挖掘简洁易用的而丰富多彩的接口,极大的方便了大家对分布式图处理的需求。
大家都知道,社交网络中人与人之间有很多关系链,例如Twitter、Facebook、微博、微信,这些都是大数据产生的地方,都需要图计算,现在的图处理基本都是分布式的图处理,而并非单机处理,Spark GraphX由于底层是基于Spark来处理的,所以天然就是一个分布式的图处理系统。
图的分布式或者并行处理其实是把这张图拆分成很多的子图,然后我们分别对这些子图进行计算,计算的时候可以分别迭代进行分阶段的计算,即对图进行并行计算。
下面我们看一下图计算的简单示例:
从图中我们可以看出:拿到Wikipedia的文档以后,可以变成Table形式的视图,然后基于Table形式的视图我们可以分析Hyperlinks超链接,也可以分析Term-Doc Graph,然后经过LDA之后进入WordTopics,之于上面的Hyperlinks,我们可以使用PageRank去分析,在下面的Editor Graph到Community,这个过程可以称之为Triangle Computation,这是计算三角形的一个算法,基于此,会发现一个社区,从上面的分析中我们可以发现图计算有很多的做法和算法,同时也发现图和表格可以做互相的转换,不过并非所有的图计算框架都支持图与表格的互相转换。
Spark GraphX的优势在于能够把表格和图进行互相转换,这一点可以带来非常多的优势,现在很多框架也在渐渐的往这方面发展,例如GraphLib已经实现了可以读取Graph中的Data,也可以读取Table中的Data,也可以读取Text总的data即文本中的内容等,与此同时Spark GraphX基于Spark也为GraphX增添了额外的很多优势,例如和mllib、Spark SQL协作等。
当今图计算领域对图的计算大多数只考虑邻居节点的计算,也就是说一个节点计算的时候只会考虑其邻居节点,对于非邻居节点是不关心的,如下图所示:
目前基于图的并行计算框架已经有很多,比如来自Google的Pregel、来自Apache开源的图计算框架Giraph,以及我们最为著名的GraphLab,当然也包含HAMA,其中Pregel、HAMA、Giraph都是非常类似的,都是基于BSP模型,BSP模型实现了SuperStep即超步,BSP首先进行本地计算,然后进行全局的通信,然后进行全局的Barrier;BSP最大的好处是编程简单,而其问题在于一些情况下BSP运算的性能非常差, 因为我们有一个全局Barrier的存在,所以系统速度取决于最慢的计算,也就把木桶原理体现无遗,另外一方面,很多现实生活中的网络是符合幂律分布的,也就是定点、边等分布式很不均匀,所以在这种情况下BSP的木桶原理导致了性能问题会得到很大的放大,对这个问题的解决,以GraphLab为例,使用了一种异步的概念而没有全部的Barrier;最后,不得不提的一点是在Spark Graphx中可以用极为简洁的代码非常方便的使用Pregel的API。
基于图的计算框架的共同特点是抽象出了一批API来简化基于图的编程,这往往比一般的data-parellel系统的性能高出很多倍。
传统的图计算,往往需要不同的系统支持不同的View,例如在Table View这种视图下可能需要Spark的支持或者Hadoop的支持,而在Graph View这种视图下可能需要Pregel或者GraphLab的支持,也就是把图和表分别在不同的系统中进行拉练处理,如下图所示:
上面所描述的图计算处理方式是传统的计算方式,当然现在除了Spark GraphX之外的图计算框架也在考虑这个问题;不同系统带来的问题是之一是需要学习、部署和管理不同的系统,例如要同时学习、部署和管理Hadoop、Hive、Spark、Giraph、GraphLab等:
大家都知道“Detail is evil”,如果我们能够用更少的框架解决更多的问题那是更好的。
其实最关键的问题还是效率问题,因为在不同的转换中间每步都要落地的话,数据转换和复制带来的开销也非常大,包括序列化带来的开销,同时中间结果和相应的结构无法重用,特别是一些结构性的东西,譬如说顶点或者边的结构一直没有变,这种情况下结构内部的Structure是不需要改变的,而如果每次都重新构建的话,就算不变也无法重用,这回导致非常差的性能:
解决方案就是Spark GraphX,GarphX实现了Unified Representation,GraphX统一了Table View和Graph View,基于Spark可以非常轻松的做pipeline的操作:
如果和Spark SQL结合,我们可以用SQL语句来进行ETL,然后放入GraphX来处理,是非常方便的。
在Spark GraphX中的Graph其实是Property Graph,也就是说图的每个顶点和边都是有属性的,如下图所示:
例如为3的顶点的名称为rxin,是学生stu.,5这个顶点是franlin,是一个prof.,5到3表明5是3的Advisor,上图中蓝色的表示的是相应顶点的Property,而黄色橙黄色部分表示的边的Property,边和顶点都是有ID的,对于顶点而言有自身的ID,而对于边来说有SourceID和DestinationID,即对于边而言会有两个ID来表达从哪个顶点出发到哪个顶点结束,来表明边的方向,这就是Property Graph的表示方法;如果把Property反映到表上的话,例如我们在Vertex Table中Id为的3的Property就是(rxin, student),而在Edge Table中3到7表明的边的Property是Collaborator的关系,2到5是Colleague的关系;更为重要的是Property Graph和Table之间是可以相互转换的,在GraphX中所有操作的基础是table operator和graph operator,,其继承自Spark中的RDD,都是针对集合进行操作。
五、Spark RDataFrame是数据组织成一个带有列名称的分布式数据集。在概念上和关系型数据库中的表类似,或者和R语言中的data frame类似,但是这个提供了很多的优化措施。构造DataFrame的方式有很多:可以通过结构化文件中构造;可以通过Hive中的表构造;可以通过外部数据库构造或者是通过现有R的data frame构造等等。
SparkContext是SparkR的切入点,它使得你的R程序和Spark集群互通。你可以通过sparkR.init来构建SparkContext,然后可以传入类似于应用程序名称的选项给它。如果想使用DataFrames,我们得创建SQLContext,这个可以通过SparkContext来构造。如果你使用SparkR shell, SQLContext 和SparkContext会自动地构建好。
1 |
sc <- sparkR.init () |
2 |
sqlContext <- sparkRSQL.init (sc) |
如果有SQLContext实例,那么应用程序就可以通过本地的R data frame(或者是Hive表;或者是其他数据源)来创建DataFrames。下面将详细地介绍。
通过本地data frame构造
最简单地创建DataFrames是将R的data frame转换成SparkR DataFrames,我们可以通过createDataFrame来创建,并传入本地R的data frame以此来创建SparkR DataFrames,下面例子就是这种方法:
1 |
df <- createDataFrame (sqlContext, faithful) |
2 |
3 |
# Displays the content of the DataFrame to stdout |
4 |
head (df) |
5 |
## eruptions waiting |
6 |
##1 3.600 79 |
7 |
##2 1.800 54 |
8 |
##3 3.333 74 |
通过Data Sources构造
通过DataFrame接口,SparkR支持操作多种数据源,本节将介绍如何通过Data Sources提供的方法来加载和保存数据。你可以阅读Spark SQL编程指南来了解更多的options选项.
Data Sources中创建DataFrames的一般方法是使用read.df,这个方法需要传入SQLContext,需要加载的文件路径以及数据源的类型。SparkR内置支持读取JSON和Parquet文件,而且通过Spark Packages你可以读取很多类型的数据,比如CSV和Avro文件。
下面是介绍如何JSON文件,注意,这里使用的文件不是典型的JSON文件。每行文件必须包含一个分隔符、自包含有效的JSON对象:
01 |
people <- read.df (sqlContext, "./examples/src/main/resources/people.json" , "json" ) |
02 |
head (people) |
03 |
## age name |
04 |
##1 NA Michael |
05 |
##2 30 Andy |
06 |
##3 19 Justin |
07 |
08 |
# SparkR automatically infers the schema from the JSON file |
09 |
printSchema (people) |
10 |
# root |
11 |
# |-- age: integer (nullable = true) |
12 |
# |-- name: string (nullable = true) |
Data sources API还可以将DataFrames保存成多种的文件格式,比如我们可以通过write.df将上面的DataFrame保存成Parquet文件:
1 |
write.df (people, path= "people.parquet" , source= "parquet" , mode= "overwrite" ) |
通过Hive tables构造
我们也可以通过Hive表来创建SparkR DataFrames,为了达到这个目的,我们需要创建HiveContext,因为我们可以通过它来访问Hive MetaStore中的表。注意,Spark内置就对Hive提供了支持,SQLContext和HiveContext 的区别可以参见SQL编程指南。
01 |
# sc is an existing SparkContext. |
02 |
hiveContext <- sparkRHive.init (sc) |
03 |
04 |
sql (hiveContext, "CREATE TABLE IF NOT EXISTS src (key INT, value STRING)" ) |
05 |
sql (hiveContext, "LOAD DATA LOCAL INPATH 'examples/src/main/resources/kv1.txt' INTO TABLE src" ) |
06 |
07 |
# Queries can be expressed in HiveQL. |
08 |
results <- hiveContext.sql ( "FROM src SELECT key, value" ) |
09 |
10 |
# results is now a DataFrame |
11 |
head (results) |
12 |
## key value |
13 |
## 1 238 val_238 |
14 |
## 2 86 val_86 |
15 |
## 3 311 val_311 |
SparkR DataFrames中提供了大量操作结构化数据的函数,这里仅仅列出其中一小部分,详细的API可以参见SparkR编程的API文档。
01 |
# Create the DataFrame |
02 |
df <- createDataFrame (sqlContext, faithful) |
03 |
04 |
# Get basic information about the DataFrame |
05 |
df |
06 |
## DataFrame[eruptions:double, waiting:double] |
07 |
08 |
# Select only the "eruptions" column |
09 |
head ( select (df, df$eruptions)) |
10 |
## eruptions |
11 |
##1 3.600 |
12 |
##2 1.800 |
13 |
##3 3.333 |
14 |
15 |
# You can also pass in column name as strings |
16 |
head ( select (df, "eruptions" )) |
17 |
18 |
# Filter the DataFrame to only retain rows with wait times shorter than 50 mins |
19 |
head ( filter (df, df$waiting < 50)) |
20 |
## eruptions waiting |
21 |
##1 1.750 47 |
22 |
##2 1.750 47 |
23 |
##3 1.867 48 |
01 |
# We use the `n` operator to count the number of times each waiting time appears |
02 |
head ( summarize ( groupBy (df, df$waiting), count = n (df$waiting))) |
03 |
## waiting count |
04 |
##1 81 13 |
05 |
##2 60 6 |
06 |
##3 68 1 |
07 |
08 |
# We can also sort the output from the aggregation to get the most common waiting times |
09 |
waiting_counts <- summarize ( groupBy (df, df$waiting), count = n (df$waiting)) |
10 |
head ( arrange (waiting_counts, desc (waiting_counts$count))) |
11 |
12 |
## waiting count |
13 |
##1 78 15 |
14 |
##2 83 14 |
15 |
##3 81 13 |
SparkR提供了大量的函数用于直接对列进行数据处理的操作。
1 |
# Convert waiting time from hours to seconds. |
2 |
# Note that we can assign this to a new column in the same DataFrame |
3 |
df$waiting_secs <- df$waiting * 60 |
4 |
head (df) |
5 |
## eruptions waiting waiting_secs |
6 |
##1 3.600 79 4740 |
7 |
##2 1.800 54 3240 |
8 |
##3 3.333 74 4440 |
SparkR DataFrame也可以在Spark SQL中注册成临时表。将DataFrame 注册成表可以允许我们在数据集上运行SQL查询。sql函数可以使得我们直接运行SQL查询,而且返回的结构是DataFrame。
01 |
# Load a JSON file |
02 |
people <- read.df (sqlContext, "./examples/src/main/resources/people.json" , "json" ) |
03 |
04 |
# Register this DataFrame as a table. |
05 |
registerTempTable (people, "people" ) |
06 |
07 |
# SQL statements can be run by using the sql method |
08 |
teenagers <- sql (sqlContext, "SELECT name FROM people WHERE age >= 13 AND age <= 19" ) |
09 |
head (teenagers) |
10 |
## name |
11 |
##1 Justin |