sparkstreaming注意要点
sparkstreaming是一个微对比准实时的计算框架
flink与sparkstreaming不同的是,sparkstreaming中的task执行完之后就会被释放掉,而flink不会,进去之后就不会释放,可以重复使用,spark的task是临时工,flink的是合同工
sparkstreaming执行程序时构建完rdd需要开启开启任务并挂起任务才会开始执行程序和一直在后台挂起
ssc.start
ssc.awaitTermination()
sparkstreaming在计算数据时如果想对数据进行汇总,那么每次计算完当前批次的数据之后都需要和上一次的汇总中间结果进行汇总,那么每次汇总出来的中间结果是非常重要的,因此将数据的最终中间结果放入内存的同时还放入磁盘中一份,checkpoint到磁盘中,但不会将每批数据都保存到磁盘中,只保存最后一批和上一批,所以想调用sparkstreaming自带的api进行汇总就需要指定checkpoint的存储目录,否则会报错
sparkstreaming对接kafka
在此我们采用新版的连接api,spark-streaming-kafka-0.10版
该版api采用的是直连的方式,效率更高,且内部拥有倍压机制,该机制在对接kafka时,如果kafka传输过来的数据过快导致spakr不能及时处理,就会关小阀门,减少数据的传输,数据变少时就继续开大阀门
spark-streaming在处理数据时执行需要两个进程:一个负责接收数据,一个负责执行处理数据
直连kafka时,kafka中topic的分区数量决定了spark的task数量,一个分区对应一个task,一个task对接一个分区
从kafka中获取到的数据并非只是value,它其实获得的是一个封装数据的对象ConsumerRecord,它里面封装了数据的key,value,topic,偏移量,分区编号等信息,后面在处理数据时可以单独拿出value进行处理,然后将偏移量抽出来写入到其他的数据库中等多种灵活处理
因为kafka自动记录偏移量默认是5秒记录一次,是有延迟的,这样容易造成数据丢失,那么我们可以将数据写入关系型数据库的同时,将偏移量也写入关系型数据库,同时要求该数据库还要能支持事务,因为我们写入数据时要求写入数据和记录偏移量要同时成功或同时失败,否则可能造成数据丢失或数据重复
自动更新偏移量是在executor端提交的,手动更新偏移量是在driver端提交的
在记录计算结果时,如果是聚合类数据,数据量不大,可以放入mysql等支持事务的数据库中,如果数据量大,可以放入hbase等会覆盖相同key的分布式数据库中
需要注意的是,每次程序启动的时候,或程序挂掉了重新启动的时候,都要先去数据库中读取偏移量再拉取数据去运算,因为我们设定的是手动提交偏移量,kafka中不会自动记录偏移量
将数据记录在mysql中时,以wordcount为例,往数据库中插入数据时,可以直接以单词为行键,因为统计单词的个数,单词肯定都是唯一的,那么每来一批数据进行插入数据时就需要判断该单词是否已经存在,如何存在了就和之前的个数进行累加,如果不存在,就以当前个数插入数据库
在设计记录偏移量的表时,可以将appname_gid,合起来作为一个字段,topic_partition合起来作为另一个字段,两个字段作为联合主键,联合主键任意一个字段都不可重复,也就是说这四个参数,改变了任意一个都要重新记录偏移量,记录偏移量时也只记录更新出最新的一个偏移量,所以偏移量的数据是非常少的,并非每批都要记录一个偏移量
代码实现:
package cn._51doit.sparkstreaming.day01
import java.sql.{Connection, DriverManager, PreparedStatement}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, HasOffsetRanges, KafkaUtils, LocationStrategies, OffsetRange}
object ExactlyOnceWordCountStoreInMySQL {
def main(args: Array[String]): Unit = {
val appName = "wc";
val groupId = "g999"
val conf = new SparkConf().setAppName(appName).setMaster("local[*]")
val ssc: StreamingContext = new StreamingContext(conf, Seconds(5))
ssc.sparkContext.setLogLevel("WARN")
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "node-1.51doit.cn:9092,node-2.51doit.cn:9092,node-3.51doit.cn:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> groupId,
"auto.offset.reset" -> "earliest",
"enable.auto.commit" -> (false: java.lang.Boolean)
)
val topics = List("wc2")
//跟Kafka整合创建DStream
//createDirectStream(使用Kafka底层高效的消费API,sparkstreaming的读数据的task(Kafka的消费者)直连Kafka的活跃分区,一一对应的)
//调用createDirectStream之前,将历史偏移量信息作为消费策略的参数传入
val historyOffset: Map[TopicPartition, Long] = OffsetUtils.queryHistoryOffsetFromMySQL(appName, groupId)
val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(
ssc,
LocationStrategies.PreferConsistent, //位置策略
ConsumerStrategies.Subscribe[String, String](topics, kafkaParams, historyOffset) //消费策略
)
//调用foreachRDD
kafkaDStream.foreachRDD(rdd => {
if(!rdd.isEmpty()) {
//获取偏移量
val offsetRanges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
val reduce: RDD[(String, Int)] = rdd.map(_.value()).flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
//将计算好的结果通过网络收集到Driver端
val result: Array[(String, Int)] = reduce.collect()
var connection: Connection = null
var pstm: PreparedStatement = null
var pstm2: PreparedStatement = null
try {
//创建一个连接并开启事务
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata", "root", "123456")
//开启事务
connection.setAutoCommit(false)
pstm = connection.prepareStatement("INSERT INTO t_wordcount (word, counts) VALUES (?, ?) ON DUPLICATE KEY UPDATE counts = counts + ?")
//将计算好的结果写入到MySQL
for (t <- result) {
pstm.setString(1, t._1)
pstm.setInt(2, t._2)
pstm.setInt(3, t._2)
pstm.executeUpdate()
}
pstm2 = connection.prepareStatement("INSERT INTO t_kafka_offset (app_gid, topic_partition, offset) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE offset = ?")
//更新偏移量
for (or <- offsetRanges) {
val topic = or.topic
val partition = or.partition
val offset: Long = or.untilOffset
pstm2.setString(1, appName + "_" + groupId)
pstm2.setString(2, topic + "_" + partition)
pstm2.setLong(3, offset)
pstm2.setLong(4, offset)
pstm2.executeUpdate()
}
//提交事务
connection.commit()
} catch {
case e: Exception => {
//回滚事务
connection.rollback()
//停掉application
ssc.stop(true)
}
} finally {
if (pstm2 != null) {
pstm2.close()
}
if (pstm != null) {
pstm.close()
}
if (connection != null) {
connection.close()
}
}
}
})
ssc.start()
ssc.awaitTermination()
}
}
package cn._51doit.sparkstreaming.day01
import java.sql.{Connection, DriverManager, ResultSet}
import java.util
import org.apache.kafka.common.TopicPartition
import org.apache.spark.streaming.kafka010.OffsetRange
import scala.collection.mutable
object OffsetUtils {
def queryHistoryOffsetFromMySQL(appName: String, groupId: String): Map[TopicPartition, Long] = {
val offsets = new mutable.HashMap[TopicPartition, Long]()
val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata", "root", "123456")
val ps = connection.prepareStatement("SELECT topic_partition, offset FROM t_kafka_offset WHERE app_gid = ?")
ps.setString(1, appName + "_" +groupId)
val rs = ps.executeQuery()
while (rs.next()) {
val topicAndPartition = rs.getString(1)
val offset = rs.getLong(2)
val fields = topicAndPartition.split("_")
val topic = fields(0)
val partition = fields(1).toInt
val topicPartition = new TopicPartition(topic, partition)
//将构建好的TopicPartition放入map中
offsets(topicPartition) = offset
}
offsets.toMap
}
}