流式计算,spark streaming 之前有spark core开发的积累,直接使用spark streaming来进行流式计算开发是比较节省开发成本的。
业界同样还有优秀的流式计算框架,简单介绍一下
1、Storm
响应快,纯流式,底层全是无锁编程,想做汇聚,想搞个中间状态,需要借助外部存储。
2、Samza
kafka上接了MR,使用yarn来管理集群,Topic取下来,samza处理(MR),输出放入topic
3、Flink
为了抗衡spark streaming而诞生的。之后就是双方的相互借鉴,相互学习,相互进步了。
flink与spark的主要区别在与:
flink可以按条数来进行流式处理,spark暂时还是只能按照时间来进行流式处理;
flink是纯流式,spark straming是微批处理。
简单介绍下spark streaming
说了这么多,我们来介绍介绍我们今天的主角。这里不说谁好谁差,最终是要看业务场景和具体需求,和开发成本的。
通俗点说吧,spark streaming 就是给spark core加了个时间机制,将计算间隔缩短到了秒级别的微批处理。
光说也说体现不出什么,需要对比才能知道最终的需求。
我们与storm对比一下吧
1,处理速度方面,因为strom的底层是无锁编程,spark的底层是批处理。
2,Spark Streaming是Spark的核心子框架之一。
说到Spark核心,那就不得不说RDD了。
Spark Streaming作为核心的子框架,对RDD的操作支持肯定是杠杠的,这又说明了什么?
Spark Streaming可以通过RDD和Spark上的任何框架进行数据共享和交流,这就是Spark的野心,一个堆栈搞定所有场景
3,基本会了spark core 短暂学习就可以上手spark streaming,可以结合之前spark core的开发,保证有两套计算,在线计算,离线计算,节省开发成本。
4,Spark Streaming支持多语言编程,并且各个语言之间的编程模型也是类似的,strom是java主力开发,spark是scala主力开发。函数式与指令式自行百度。
5,Spark Streaming的容错机制。Spark Streaming在读取流数据进入内存的时候会保存两个副本,计算只用一个,当出现问题的时候可以快速的切换到另外一个副本;在规定的时间内进行数据的固化;由于支持RDD操作,所以RDD本身的容错处理机制也被继承(这算是spark的优势,也是劣势吧,劣势会占用太多资源)
spark streaming基本执行流程
1,以时间片为单位划分形成数据流形成RDD(DStream),这一点要细说一下。
在spark core中,常用sc.textFile去读取数据,在spark streaming中是将读取的数据创建Dstream数据流
例如代码spark是 val RDD = sc.textFile***
spark streaming见下图,例如创建一个socket的数据流
2,对每个划分数来的RDD以批处理的方式处理数据
3,每个划分出来的RDD地处理都会提交成Job
4,后面的流程基本与spark core的流程一样了。
第一步 思路解析
1.1简单实现
首先我们要实现,spark能从kafka中消费数据。因为我们这套流程使用的spark2.1.0(scala2.11.8)+ kafka0.10.2.0。
国内没有资源讲解spark2.1 + kafka010,先啃官方文档
http://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html
创建一个简单的流去消费kafka数据
几个注意的地方
引的kafka的包是kafka010,以前的版本都是直接kafka没有后面的数字
需要配置个kafkaParams参数
使用KafkaUtils.createDirectStream方式来创建数据流
1.2保证消费高可用
网上能查到的资料,基本是两极分化
第一种是开启spark的WAL机制,这种就是将数据读两份,一份计算,另一份进行容错重算使用,这样太耗资源了,暂不考虑
第二种是使用kafka的低级api,手动维护spark消费kafka数据的偏移度,来保证消费的高可用,但是网上能查到的版本全是kafka0.8 kafka0.9的。代码完全不能拿来用。
继续啃文档
文中有说到,可以根据偏移度,创建kafka的数据流。但是我怎么获得偏移度呢?
继续看
找到个,可以获得偏移度的方法
文档中差不多就这2个了。根据偏移度创建流,再获得偏移度。
我们来撸一撸思路。
思路1:
根据最后计算偏移度读取kafka数据
||
创建流
||
业务代码计
||
计算成功,保存偏移度
||
计算结果,存至外部
思路暂时这样没错,但是我怎么去实现,根据最后偏移度读取数据呢?我怎么知道是最后的偏移度?
保存偏移度,保存到哪呢?
我们先试试输出偏移度
这里使用的println,会在spark的标准输出stdout中出现。
我们去看看
偏移度打出来。
那我们可以把那个结束偏移度保存起来,每次让DStream从这个偏移度去读新的数据,进行计算,计算完之后再获取一次偏移度,将最新的偏移度保存起来。
突然想起个问题,如果是一个新的kafka topic,我都还没有计算过呀,怎么得到最后的偏移量呢?
那我们是不是可以,在创建数据流的之前做个判断,如果是个新的topic的时候,我们以默认为0的偏移度创建是否可行呢?
最后,我们这个外部存储用什么呢?这里我们用zk,看能不能实现,但是印象中我记得zk是不能很好的支持高并发的读写,如果流式处理数据量上来,秒级别的对zk的读写,肯定会出现问题。
不过我们可以先以zk来做测试,如果zk都可以读写,其他的外部存储肯定没问题。
修改后的思路2:
判断zk中是否有保存过该计算的偏移量
如果有就接着计算,没有的话创建个新的,从0开始算
||
创建流
||
业务代码计算
||
计算成功,保存偏移度
||
计算结果,存至外部
第二步 代码讲解
2.1 maven引包
2.11.8
2.1.0
2.6.0
org.apache.spark
spark-streaming_2.11
${spark.version}
org.scala-lang
scala-library
${scala.version}
org.apache.spark
spark-streaming-kafka-0-10_2.11
${spark.version}
org.apache.kafka
kafka_2.11
0.10.2.0
org.apache.kafka
kafka-clients
0.10.2.0
com.101tec
zkclient
0.10
org.apache.zookeeper
zookeeper
3.4.9
2.2 scala代码
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.spark.streaming.kafka010.{HasOffsetRanges, KafkaUtils, OffsetRange}
import org.apache.spark.{SparkConf, SparkContext, TaskContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object DealFlowBills2 {
/** ***************************************************************************************************************
* todo zookeeper 实例化,方便后面对zk的操作,zk的代码很简单,这里就不贴了,基本照抄官方api
*/
val zk = ZkWork
def main(args: Array[String]): Unit = {
/** ***************************************************************************************************************
* todo 输入参数
*/
val Array(output, topic, broker, group, sec) = args
/** ***************************************************************************************************************
* todo spark套路
*/
val conf = new SparkConf().setAppName("DealFlowBills2")
val sc = new SparkContext(conf)
val ssc = new StreamingContext(sc, Seconds(sec.toInt))
/** ***************************************************************************************************************
* todo 1 - 准备kafka参数
*/
val topics = Array(topic)
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> broker,
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> group,
"auto.offset.reset" -> "latest",
"enable.auto.commit" -> (false: java.lang.Boolean)
)
/** ***************************************************************************************************************
* todo 2 - 判断zk中是否有保存过该计算的偏移量
* 如果没有保存过,使用不带偏移量的计算,在计算完后保存
* 精髓就在于KafkaUtils.createDirectStream这个地方
* 默认是KafkaUtils.createDirectStream[String, String](ssc, PreferConsistent, Subscribe[String, String](topics, kafkaParams)),不加偏移度参数
* 实在找不到办法,最后啃了下源码。有个consumerStrategy消费者策略,看看里面是个什么套路;
*
* 原来可以执行topic,和offsets消费偏移度,这下派上用场了
*
*
*
*/
val stream = if (zk.znodeIsExists(s"${topic}offset")) {
val nor = zk.znodeDataGet(s"${topic}offset")
val newOffset = Map(new TopicPartition(nor(0).toString, nor(1).toInt) -> nor(2).toLong)//创建以topic,分区为k 偏移度为v的map
println(s"[ DealFlowBills2 ] --------------------------------------------------------------------")
println(s"[ DealFlowBills2 ] topic ${nor(0).toString}")
println(s"[ DealFlowBills2 ] Partition ${nor(1).toInt}")
println(s"[ DealFlowBills2 ] offset ${nor(2).toLong}")
println(s"[ DealFlowBills2 ] zk中取出来的kafka偏移量★★★ $newOffset")
println(s"[ DealFlowBills2 ] --------------------------------------------------------------------")
KafkaUtils.createDirectStream[String, String](ssc, PreferConsistent, Subscribe[String, String](topics, kafkaParams, newOffset))
} else {
println(s"[ DealFlowBills2 ] --------------------------------------------------------------------")
println(s"[ DealFlowBills2 ] 第一次计算,没有zk偏移量文件")
println(s"[ DealFlowBills2 ] 手动创建一个偏移量文件 ${topic}offset 默认从0号分区 0偏移度开始计算")
println(s"[ DealFlowBills2 ] --------------------------------------------------------------------")
zk.znodeCreate(s"${topic}offset", s"$topic,0,0")
val nor = zk.znodeDataGet(s"${topic}offset")
val newOffset = Map(new TopicPartition(nor(0).toString, nor(1).toInt) -> nor(2).toLong)
KafkaUtils.createDirectStream[String, String](ssc, PreferConsistent, Subscribe[String, String](topics, kafkaParams, newOffset))
}
/** ***************************************************************************************************************
* todo 3 - 业务代码部分
* 将流中的值取出来,用于计算
*/
val lines = stream.map(_.value())
lines.count().print()
val result = lines
.filter(_.split(",").length == 21)
.map {
mlines =>
val line = mlines.split(",")
(line(3), s"${line(4)},${line(2)}")
}
.groupByKey()
.map {
case (k, v) =>
val result = v
.flatMap {
fmlines =>
fmlines.split(",").toList.zipWithIndex
}
.groupBy(_._2)
.map {
case (v1, v2) =>
v2.map(_._1)
}
(k, result)
}
/** ***************************************************************************************************************
* todo 4 - 保存偏移度部分
* 计算成功后保存偏移度
*/
stream.foreachRDD {
rdd =>
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
rdd.foreachPartition {
iter =>
val o: OffsetRange = offsetRanges(TaskContext.get.partitionId)
println(s"[ DealFlowBills2 ] --------------------------------------------------------------------")
println(s"[ DealFlowBills2 ] topic: ${o.topic}")
println(s"[ DealFlowBills2 ] partition: ${o.partition} ")
println(s"[ DealFlowBills2 ] fromOffset 开始偏移量: ${o.fromOffset} ")
println(s"[ DealFlowBills2 ] untilOffset 结束偏移量: ${o.untilOffset} 需要保存的偏移量,供下次读取使用★★★")
println(s"[ DealFlowBills2 ] --------------------------------------------------------------------")
// 写zookeeper
zk.offsetWork(s"${o.topic}offset", s"${o.topic},${o.partition},${o.untilOffset}")
// 写本地文件系统
// val fw = new FileWriter(new File("/home/hadoop1/testjar/test.log"), true)
// fw.write(offsetsRangerStr)
// fw.close()
}
}
/** ***************************************************************************************************************
* todo 5 - 最后结果操作部分
*/
result.saveAsTextFiles(output + s"/output/" + "010")
/** ***************************************************************************************************************
* todo spark streaming 开始工作
*/
ssc.start()
ssc.awaitTermination()
}
}
第三步 调试实现
3.1 场景设想
回忆一下,我们之前需要实现的两个主要功能
1.flume文件采集数据不丢失,断点续采,根据崩溃时的索引,重启程序后能继续采集
2.spark streaming 消费kafka 实现数据零丢失,避免kafka重复消费,spark streaming 手动控制 kafka消费偏移量,将消费偏移量存到zookeeper,来保证消费高可用
现在文件采集的高可用是实现了,只要数据生成了。flume和kafka的启动谁先,谁后,都能保证数据不丢。
但是spark是在数据生成之前启,还是数据生成之后启,这就有点区别了。我们看看下面的两种场景。
暂时想到有2种场景
场景一
1,kafka创建topic
2,开启flume采集
3,开启spark streaming 计算
4,模拟数据源生成数据
5,开始计算
场景二
1,kafka创建topic
2,开启flume采集
3,模拟数据源生成数据
4,开启spark streamjing计算
5,开始计算
场景一是肯定没问题了,spark在数据生成之后,是肯定可以接收到数据开始计算的。
我们来模拟调试一下场景二
3.2 场景模拟
1.创建topic
2.开启flume采集
3.模拟生成数据
4.开启spark streaming 计算
5.开始计算,因为我们刚才只生成了1w,所以这里只有1w数据,但是为什么这里会多出5条,暂时没解决,环境大家批评斧正。
3.3 模拟spark宕机
造1000W数据
模式宕机
等个若干秒,再启动程序
停止生成数据,这里是生成了2416634条,我们看看spark那边有多少条
检查spark收到的条数
一共是
293434+13900+93200+101000+1217700+100100+131300+131600+129100+139000+66300 = 2416634
正好与生成数据的数量相符
3.4 待解决问题
这是实时在线计算,但是如果我们要考虑历史原始数据保留的话,该怎么操作比较合理?
怎么实现远程文件采集呢?
源代码已上传了github
https://github.com/feloxx/SparkStreaming-DirectKafka010