offset管理:
checkpoint
zk、nosql、rdbms
kafka
一、
CheckPoint:对于具有以下要求的应用程序,必须启用检查点:
1.有状态转换的使用——如果在应用程序中使用updateStateByKey或reduceByKeyAnd.(具有逆函数),那么必须提供CheckPoint以允许周期性的RDD检查。
2.从运行应用程序的驱动程序的故障中恢复——元数据检查点用于利用进度信息进行恢复。
注意:没有上述有状态转换的简单流应用程序可以在不启用检查点的情况下运行。在这种情况下,从driver故障恢复也将是部分的(一些接收的但未处理的数据可能丢失)。这通常是可以接受的,并且许多人以这种方式运行SparkStreaming应用程序。
CheckPoint的定义与使用:可以通过在容错、可靠的文件系统(例如,HDFS、S3等)中设置目录来启用检查点功能,检查点信息将被保存到其中。这是通过使用流式上下文检查点(检查点目录)来完成的。这将允许您使用前面提到的状态转换。此外,如果希望使应用程序从Driver程序故障中恢复,则应该重写流应用程序,使其具有以下行为。
1.当程序第一次启动时,它将创建一个新的StreamingContext,设置所有流,然后调用start()。
2.当程序在失败后重新启动时,它将从检查点目录中的检查点数据重新创建StreamingContext。
// Function to create and setup a new StreamingContext
def functionToCreateContext(): StreamingContext = {
val ssc = new StreamingContext(...) // new context
val lines = ssc.socketTextStream(...) // create DStreams
...
ssc.checkpoint(checkpointDirectory) // set checkpoint directory
ssc
}
// Get StreamingContext from checkpoint data or create a new one
val context = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _)
// Do additional setup on context that needs to be done,
// irrespective of whether it is being started or restarted
context. ...
// Start the context
context.start()
context.awaitTermination()
注意:RDDS的checkpoints需考虑保存到可靠存储的成本。这可能导致RDDS得到checkpoints的批处理时间的增加。因此,需要仔细设置checkpoints的间隔。在小批量(例如1秒)中,每个批次的checkpoints可以显著地减少操作吞吐量。相反,checkpoints太少会导致lineage和task大小增长,这可能有不利影响。对于状态转换,需要RDD checkpoints,默认间隔是批处理间隔的倍数,至少为10秒。它可以通过使用dstream.checkpoint(checkpointInterval)来设置。通常,一个checkpoint interval为5到10个DStream的滑动间隔较好。
示例:
package com.ruozedata.spark.streaming.day04
import kafka.serializer.StringDecoder
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Duration, Seconds, StreamingContext}
/**
* Created by ruozedata on 2018/9/15.
*/
object OffsetApp1 {
def main(args: Array[String]) {
val sparkConf = new SparkConf().setMaster("local[2]").setAppName("OffsetApp1")
val kafkaParams = Map[String, String](
"metadata.broker.list"->"ip:port",
"auto.offset.reset" -> "smallest"
)
val topics = "ruoze_g3_offset".split(",").toSet
val checkpointDirectory = "hdfs://hadoop000:8020/g3_offset/"
// Function to create and setup a new StreamingContext
def functionToCreateContext(): StreamingContext = {
val ssc = new StreamingContext(sparkConf,Seconds(10)) // new context
val messages = KafkaUtils.createDirectStream[String,String,StringDecoder,StringDecoder](ssc,kafkaParams,topics)
ssc.checkpoint(checkpointDirectory)
messages.checkpoint(Duration(8*10*1000))
messages.foreachRDD(rdd=>{
if(!rdd.isEmpty()) {
println("若泽数据统计记录为:"+ rdd.count())
}
})
ssc
}
val ssc = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _)
ssc.start()
ssc.awaitTermination()
}
}
二、
Kafka的Offset管理:offset是consumer position,Topic的每个Partition都有各自的offset.消费者需要自己保留一个offset,从kafka 获取消息时,只拉去当前offset 以后的消息。Kafka 的scala/java 版的client 已经实现了这部分的逻辑,将offset 保存到zookeeper上.
详见:Kafka的offset管理
示例:
配置文件:
# MySQL example
//db.default.driver="com.mysql.jdbc.Driver"
//db.default.url="jdbc:mysql://hadoop000:3306/ruozedata?characterEncoding=utf-8"
//db.default.user="root"
//db.default.password="root"
metadata.broker.list="ip:port"
auto.offset.reset="smallest"
group.id="ruoze_offset_group_pk"
kafka.topics="ruoze_g3_offset"
db.default.driver="com.mysql.jdbc.Driver"
db.default.url="jdbc:mysql://hadoop000:3306/g3?characterEncoding=utf-8"
db.default.user="root"
db.default.password="root"
从配置文件通过key获取value:
package com.ruozedata.spark.streaming.day04
import com.typesafe.config.ConfigFactory
import org.apache.commons.lang3.StringUtils
/**
* Created by ruozedata on 2018/9/15.
*/
object ValueUtils {
val load = ConfigFactory.load()
def getStringValue(key:String, defaultValue:String="") = {
val value = load.getString(key)
if(StringUtils.isNotEmpty(value)){
value
}else{
defaultValue
}
}
}
通过MySQL管理Offset:
package com.ruozedata.spark.streaming.day04
import kafka.common.TopicAndPartition
import kafka.message.MessageAndMetadata
import kafka.serializer.StringDecoder
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka.{HasOffsetRanges, KafkaUtils}
import org.apache.spark.streaming.{Duration, Seconds, StreamingContext}
import scalikejdbc.config.DBs
import scalikejdbc._
/**
* 1) StreamingContext
* 2) 从kafka中获取数据 (获取offset)
* 3)根据业务进行逻辑处理
* 4)将处理结果存到外部存储中 (保存offset)
* 5)启动程序,等待程序结束
*
*/
object JdbcOffsetApp {
def main(args: Array[String])
{
val sparkConf = new SparkConf().setMaster("local[2]").setAppName("JdbcOffsetApp")
val kafkaParams = Map[String, String](
"metadata.broker.list"-> ValueUtils.getStringValue("metadata.broker.list"),
"auto.offset.reset" -> ValueUtils.getStringValue("auto.offset.reset"),
"group.id" -> ValueUtils.getStringValue("group.id")
)
val topics = ValueUtils.getStringValue("kafka.topics").split(",").toSet
val ssc = new StreamingContext(sparkConf,Seconds(10)) // new context
DBs.setup()
val fromOffsets = DB.readOnly{ implicit session => {
sql"select * from offsets_storage".map(rs => {
(TopicAndPartition(rs.string("topic"),rs.int("partitions")), rs.long("offset"))
}).list().apply()
}
}.toMap
/**
* 每次重启应用,由于采用了smallest,都是从头开始,这就有问题了
*
* true 变活
*/
val messages = if(fromOffsets.size == 0) {
println("~~~~~~从头开始消费~~~~~~~~~")
KafkaUtils.createDirectStream[String,String,StringDecoder,StringDecoder](ssc,kafkaParams,topics)
} else { // 从已保存的offset开始消费
println("~~~~~~从以保存的offset开始消费~~~~~~~~~")
// val fromOffsets = Map[TopicAndPartition, Long]()
val messageHandler = (mm:MessageAndMetadata[String,String]) => (mm.key(),mm.message())
KafkaUtils.createDirectStream[String,String,StringDecoder,StringDecoder,(String,String)](ssc,kafkaParams,fromOffsets,messageHandler)
}
messages.foreachRDD(rdd=>{
println("若泽数据统计记录为:"+ rdd.count())
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
offsetRanges.foreach(x =>{
// TODO... 需要保存到MySQL中的值
println(s"---${x.topic},${x.partition},${x.fromOffset},${x.untilOffset}--")
DB.autoCommit{
implicit session => {
sql"replace into offsets_storage(topic,groupid,partitions,offset) values(?,?,?,?)"
.bind(x.topic,ValueUtils.getStringValue("group.id"),x.partition,x.untilOffset).update().apply()
}
}
})
// if(!rdd.isEmpty()) {
// println("若泽数据统计记录为:"+ rdd.count())
// }
})
ssc.start()
ssc.awaitTermination()
}
}
MySQL表:
create table offsets_storage(
topic varchar(32),
groupid varchar(50),
partitions int,
offset bigint,
primary key(topic,groupid,partitions)
);