Spark开发小笔记:从0开始的Spark建图生活
持续更新中……
0.开发平台Zeppelin
支持多种语言,默认是scala(背后是spark shell),SparkSQL, Markdown 和 Shell。
是一个基于web的笔记本,支持交互式数据分析。你可以用SQL、Scala等做出数据驱动的、交互、协作的文档。
1.RDD
Spark里的计算都是操作RDD进行,那么学习RDD的第一个问题就是如何构建RDD,构建RDD从数据来源角度分为两类:第一类是从内存里直接读取数据,第二类就是从文件系统里读取,当然这里的文件系统种类很多常见的就是HDFS以及本地文件系统了。
/* 使用makeRDD创建RDD */
val rdd01 = sc.makeRDD(List(1,2,3,4,5,6))
val r01 = rdd01.map { x => x * x }
println(r01.collect().mkString(","))
/*通过文件系统构造RDD*/
val rdd:RDD[String] = sc.textFile("file:///D:/sparkdata.txt", 1)
val r:RDD[String] = rdd.flatMap { x => x.split(",") }
println(r.collect().mkString(","))
/*通过调用SparkContext的parallelize方法,
在一个已经存在的Scala集合上创建的(一个Seq对象)。
集合的对象将会被拷贝,创建出一个可以被并行操作的分布式数据集。*/
data = [1, 2, 3, 4, 5]
distData = sc.parallelize(data)
/*一旦分布式数据集(distData)被创建好,它们将可以被并行操作。*/
/*并行集合的一个重要参数是slices,表示数据集切分的份数。
Spark将会在集群上为每一份数据起一个任务。典型地,你可以
在集群的每个CPU上分布2-4个slices. 一般来说,Spark会尝
试根据集群的状况,来自动设定slices的数目。然而,你也可以
通过传递给parallelize的第二个参数来进行手动设置。*/
RDD的操作分为转化操作(transformation)和行动操作(action),RDD之所以将操作分成这两类这是和RDD惰性运算有关,当RDD执行转化操作时候,实际计算并没有被执行,只有当RDD执行行动操作时候才会促发计算任务提交,执行相应的计算操作。区别转化操作和行动操作也非常简单,转化操作就是从一个RDD产生一个新的RDD操作,而行动操作就是进行实际的计算。
2.import
Hive + sql(数据类型) + 参数配置(SparkConf, SparkContext)
import org.apache.spark.sql.hive.HiveContext
import org.apache.spark.sql._
import org.apache.spark.{SparkConf, SparkContext}
- 为了让Spark能够访问Hive,必须为Spark添加Hive支持。Spark官方提供的预编译版本,通常是不包含Hive支持的,需要采用源码编译,编译得到一个包含Hive支持的Spark版本。
- Spark的所有数据类型都定义在包org.apache.spark.sql中,你可以通过import org.apache.spark.sql._访问它们。
- 每个Spark程序都是需要导入SparkContext的. SparkContext使得Spark驱动的程序access the cluster through a resource manager(YARN, or Spark’s cluster manager)。为了建立SparkContext首先需要创建SparkConf.SparkConf存储构造器参数,这些参数将由你编写的程序传入SparkContext。
- '. _ '用于隐式导入包中全部内容
3.package
package定义: 文件顶部package定义
package com.sunny.scala.service
package特性:
- 同一个包定义,可以在不同的scala源文件中的; 一个scala源文件内,可以包含两个包。
- 子包中的类,可以访问父包中的类。
- 相对包名与绝对包名,使用_root_,引用绝对包名。
4.class,Object,Trait区别
- class:在scala中,类名可以和对象名为同一个名字,该对象称为该类的伴生对象,类和伴生对象可以相互访问他们的私有属性,但是他们必须在同一个源文件内。类只会被编译,不能直接被执行,类的申明和主构造器在一起被申明,在一个类中,主构造器只有一个所有必须在内部申明主构造器或者是其他申明主构造器的辅构造器,主构造器会执行类定义中的所有语句。scala对每个字段都会提供getter和setter方法,同时也可以显示的申明,但是针对val类型,只提供getter方法,默认情况下,字段为公有类型,可以在setter方法中增加限制条件来限定变量的变化范围,在scala中方法可以访问改类所有对象的私有字段。
- object:在scala中没有静态方法和静态字段,所以在scala中可以用object来实现这些功能,直接用对象名调用的方法都是采用这种实现方式,例如Array.toString。对象的构造器在第一次使用的时候会被调用,如果一个对象从未被使用,那么他的构造器也不会被执行;对象本质上拥有类(scala中)的所有特性,除此之外,object还可以一扩展类以及一个或者多个特质:例如,
abstract class ClassName(val parameter){}
object Test extends ClassName(val parameter){}
注意:object不能提供构造器参数,也就是说object必须是无参的 - trait:在java中可以通过interface实现多重继承,在Scala中可以通过特征(trait)实现多重继承,不过与java不同的是,它可以定义自己的属性和实现方法体,在没有自己的实现方法体时可以认为它时java interface是等价的,在Scala中也是一般只能继承一个父类,可以通过多个with进行多重继承。
trait TraitA{}
trait TraitB{}
trait TraitC{}
object Test1 extends TraitA with TraitB with TraitC{}
5.初始化配置
object GenGraph {
def main(args: Array[String]) {
/** 初始化配置 */
val conf = new SparkConf().setAppName("example")
val sc = new SparkContext(conf)
val sqlContext = new HiveContext(sc)
6.创建字段结构并添加描述
val vertexSchema = new StructType()
.add("p_id", LongType)
.add("label_id", LongType)
- StructType(fields):表示一个拥有StructFields (fields)序列结构的值,StructType(fields) ,注意fields是一个StructField序列,相同名字的两个StructField不被允许。
- StructField(name, dataType, nullable):代表StructType中的一个字段,字段的名字通过name指定,dataType指定field的数据类型,nullable表示字段的值是否有null值。StructField(name, dataType, nullable)。
7. HashMap
import scala.collection.mutable.HashMap
/**Initializing*/
/**3元素法*/
val hashMap1: HashMap[String, String] = HashMap(("PD","Plain Donut"),("SD","Strawberry Donut"),("CD","Chocolate Donut"))
/**key->value法*/
val hashMap2: HashMap[String, String] = HashMap("VD"-> "Vanilla Donut", "GD" -> "Glazed Donut")
/**EMPTY ONE*/
val emptyMap: HashMap[String,String] = HashMap.empty[String,String]
/**Access*/
println(s"Element by key VD = ${hashMap2("VD")}")
hashMap1 += ("KD" -> "Krispy Kreme Donut")
hashMap1 -= "CD"/**加减元素*/
hashMap1 ++= hashMap2 /**一个hashMap加到另一个上*/
8.定义函数
/**规范化写法,scala函数的返回值是最后一行代码*/
def addInt(a:Int,b:Int) : Int = {
var total : Int = a + b
return total
}
/**Unit,是Scala语言中数据类型的一种,表示无值,用作不返回任何结果的方法*/
def returnUnit(): Unit = {
println("shows!")
}
/**不写明返回值的类型,程序会自行判断,最后一行代码的执行结果为返回值*/
def addInt(a:Int,b:Int) = {
a+b
}
/**只有一行的写法*/
def addInt (a:Int,b:Int) = x + y
/**最简单写法:def ,{ },返回值都可以省略,此方法在spark编程中经常使用。*/
val addInt = (x: Int,y: Int) => x + y
9.条件判断和循环
for循环:是不断的循环一个集合,然后for循环后面的{}代码块部分会根据for循环()里面提取的集合的item来作为{}的输入进行流程控制。
- for循环中加入的if叫做条件守卫,用于限制for循环,
- 想跳出for循环,除了加入if守卫以外,还可以使用return关键字
for(i<-0 to 5 if i==5){
println(i)
}
10.sql取数文本
val sql=
"""
|select user_id,
| user_type,
| city_id,
| bank_type,
| edge_weight,
| edge_count
| from $sourceTable
| where datekey = $datekey
""".stripMargin
11.match-case模式匹配
match到的case即进行对应case的操作:
def getSalary(name:String,age:Int){
name match{
//从前往后匹配
case "Spark"=>println("$150000/year")
case "Hadoop"=>println("$100000/year")
//加入判断条件(用变量接受参数)
case _name if age>=5 =>println(name+":"+age+" $140000/year")
case _ =>println("$90000/year")//都不匹配时
}
}
//对类型进行匹配
def getMatchType(msg:Any){
msg match{
case i : Int=>println("Integer")
case s : String=>println("String")
case d : Double=>println("Double")
case _=>println("Unknow type")
}
}
12.map()和flatMap()
map()将原数据的每个元素传给函数func进行格式化,返回一个新的分布式数据集。
flatMap(func)跟map(func)类似,但是每个输入项可成为0个或多个输出项(所以func函数应该返回的是一个序列化的数据而不是单个数据项)。flatMap(func)也会对每一条输入进行执行的func操作,然后每一条输入返回一个对象,但是最后会将所有的对象再合成为一个对象。
map返回的数据对象的个数和原来的输入数据是相同的,而flatMap返回的个数则是不同的。
var mapResult = textFile.map(line => line.split("\\s+"))
13. parse()
13.1 java.text.SimpleDateFormat的parse()
import java.text.SimpleDateFormat
val simpleDateFormat = new SimpleDateFormat("yyyyMMdd")
val date = simpleDateFormat.parse(datekey);
*容易踩坑注目
Q1:Date formats 是线程不安全的。推荐为每个线程创建单独的format实例。如果多线程并发访问同一个format实例,必须加同步操作,正确写法如下:
class DateUtils {
public static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
private static final Object LOCK = new Object();
// OK
public Date parseString(String datetime) throws Exception {
synchronized (LOCK) {
return format.parse(datetime);
}
}
// OK
public Date parseStringV2(String datetime) throws Exception {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
return format.parse(datetime);
}
}
Q2:在很多时候程序使用SimpleDateFormat都能正常执行,并不会报错;但有时发现日志出现java.text.ParseException: Unparseable date: "2017-03-20 02:10"异常,为什么还抛出这种异常呢?
当使用format方法将Date转成String时,SimpleDateFormat可实例化为任意期望的时间格式;但是使用parse方法将String转为Date时,SimpleDateFormat定义的格式与参数String的格式必须完全一致,不然就会出现Unparseable date。
14.字符串插值
字符串插值允许使用者将变量引用直接插入处理过的字面字符中。编译器会对它做额外的工作。待处理字符串字面通过“号前的字符来标示。
def getHeterEdgeSql(sourceTable:String, datekey: Int) :String=
s"""
|select user_id,
| from $sourceTable
| where datekey = $datekey
""".stripMargin
Scala 提供了三种创新的字符串插值方法:s,f 和 raw:
- 在任何字符串前加上s,就可以直接在串中使用变量了。
字符串插值器也可以处理任意的表达式:
println(s"1+1=${1+1}") /*将会输出字符串1+1=2。*/
- 在任何字符串字面前加上 f,就可以生成简单的格式化串,功能相似于其他语言中的 printf 函数。当使用 f 插值器的时候,所有的变量引用都应当后跟一个printf-style格式的字符串,如%d。f 插值器利用了java中的字符串数据格式。这种以%开头的格式在 [Formatter javadoc] 中有相关概述。如果在具体变量后没有%,则格式化程序默认使用 %s(串型)格式。
val height=1.9d
val name="James"
println(f"$name%s is $height%2.2f meters tall")
/*James is 1.90 meters tall f 插值器是类型安全的。*/
/*如果试图向只支持 int 的格式化串传入一个double 值,编译器则会报错.*/
val height:Double=1.9d
- 除了对字面值中的字符不做编码外,raw 插值器与 s 插值器在功能上是相同的。
s"a\nb"
res0:String=
a
b
/*这里,s 插值器用回车代替了\n。而raw插值器却不会如此处理。*/
raw"a\nb"
res1:String=a\nb /*当不想输入\n被转换为回车的时候,raw 插值器是非常实用的。*/
spark图计算GraphX
GraphX的核心抽象是Resilient Distributed Property Graph,一种点和边都带属性的有向多重图。它扩展了Spark RDD的抽象,有Table和Graph两种视图,而只需要一份物理存储。两种视图都有自己独有的操作符,从而获得了灵活操作和执行效率。
对Graph视图的所有操作,最终都会转换成其关联的Table视图的RDD操作来完成。这样对一个图的计算,最终在逻辑上,等价于一系列RDD的转换过程。因此,Graph最终具备了RDD的3个关键特性:Immutable、Distributed和Fault-Tolerant,其中最关键的是Immutable(不变性)。逻辑上,所有图的转换和操作都产生了一个新图;物理上,GraphX会有一定程度的不变顶点和边的复用优化,对用户透明。
*.override in Scala
override是覆盖的意思,在很多语言中都有,在scala中,override是非常常见的。
当一个类extends另外一个类的时候,override的规则基本如下:
子类中的方法要覆盖父类中的方法,必须写override。
子类中的属性val要覆盖父类中的属性,必须写override。
父类中的变量不可以覆盖。