Spark Streaming在接收数据的时候有两种模式,第一种是基于Receiver模式,第二种是Kafka Direct模式,两者不丢数据的处理方式不一样,下面我们就来聊聊这两种模式不丢数据的处理策略
在这种模式下,我们可以使用checkpoint + WAL + ReliableReceiver的方式保证不丢失数据,就是说在driver端打开chechpoint,用于定期的保存driver端的状态信息到HDFS上,保证driver端的状态信息不会丢失;在接收数据Receiver所在的Executor上打开WAL,使得接收到的数据保存在HDFS中,保证接收到的数据不会丢失;因为我们使用的是ReliableReceiver,所以在Receiver挂掉的期间,是不会接收数据,当这个Receiver重启的时候,会从上次消费的地方开始消费。
所以Spark Streaming的checkpoint机制包括driver端元数据的checkpoint以及Executor端的数据的checkpoint(WAL以及updateStateByKey等也需要checkpint),Executor端的checkpoint机制除了保证数据写到HDFS之外,还有切断很长的RDD依赖的功效(因为RDD是一个很长的依赖链,如果有checkpoint机制,那么在某一个依赖链断的时候,就不会从头到尾的去再去计算一次了,只需要从checkpoint的位置重新计算即可)
这种模式下,因为数据源都是存储在Kafka中的,所以一般不会丢数据,但是有一种情况下可能会丢失数据,就是当Spark Streaming应用失败后或者升级重启的时候因为没有记住重启之前消费的topic的offset,使得重启后Spark Streaming从topic的最新的offset开始消费(这个是默认的行为),这样就导致Spark Streaming消费不到失败或者重启过程中Kafka接收到的消息,解决这个问题的办法有三个:
1、使用Spark Streaming自带的Driver端checkpoint机制,因为Driver端checkpoint机制会定期的保存Driver端的状态信息,当然也包括当前批次消费的Kafka中topic的offset信息,这样下次重启的时候就可以从checkpoint文件中直接读取上次消费到的offset信息,然后从这个offset开始消费。但是Driver端的checkpoint机制有一个很明显的缺陷,因为Driver端的checkpoint机制保存的Driver端的状态信息还包含DStreamGraph的状态信息,意思就是将Driver端的代码序列化到checkpoint文件中,这样的话,如果我们对代码做了很大的改动或者升级的话,那么升级后的代码和checkpoint文件中的代码不兼容,这样的话会导致重启失败,解决这个问题的方法就是每次升级的时候将checkpoint文件清除掉,但是这样做的话也清除了保存在checkpoint文件中上次消费到的offset信息,这个不是我们想要的,所以这种方式不可取。
2、我们可以在每一个批次开始之前将我们消费到的offset手动的保存到其他第三方存储系统中,可以是zookeeper或者Hbase,如下:
import org.apache.curator.framework.CuratorFrameworkFactory
import org.apache.curator.retry.ExponentialBackoffRetry
import org.apache.kafka.common.TopicPartition
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.kafka010.HasOffsetRanges
import org.slf4j.LoggerFactory
class Stopwatch {
// 记录当前时间和开始时间的时间差
private val start = System.currentTimeMillis()
override def toString() = (System.currentTimeMillis() - start) + " ms"
}
trait OffsetsStore {
// 每一个分区消费的数据信息,从数据库中读取出来
def readOffsets(topic: String): Option[Map[TopicPartition, Long]]
// 把消费了的offset保存到数据库中去
def saveOffsets(topic: String, rdd: RDD[_]): Unit
}
/**
* 将Spark Streaming消费的kafka的offset信息保存到zookeeper中
* @param zkHosts zookeeper的主机信息
* @param zkPath offsets存储在zookeeper的路径
*/
class ZooKeeperOffsetsStore(zkHosts: String, zkPath: String) extends OffsetsStore with Serializable {
private val logger = LoggerFactory.getLogger("ZooKeeperOffsetsStore")
@transient // transient意思是在写磁盘的时候会忽略掉这个属性
private val client = CuratorFrameworkFactory.builder() // 创建ZK的客户端
.connectString(zkHosts)
.connectionTimeoutMs(10000) // 超时时间
.sessionTimeoutMs(10000) // 重试间隔时间
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build()
client.start()
/**
* 从zookeeper上读取Spark Streaming消费的指定topic的所有的partition的offset信息
* @param topic
* @return
*/
override def readOffsets(topic: String): Option[Map[TopicPartition, Long]] = {
logger.info("Reading offsets from ZooKeeper")
val stopwatch = new Stopwatch()
val offsetsRangesStrOpt = Some(new String(client.getData.forPath(zkPath))) // 从zkpath去读消费情况的数据
offsetsRangesStrOpt match {
case Some(offsetsRangesStr) =>
logger.info(s"Read offset ranges: ${offsetsRangesStr}")
if (offsetsRangesStr.isEmpty) {
None
} else { // 不是空的话就对数据进行解析
val offsets = offsetsRangesStr.split(",")
.map(s => s.split(":"))
.map { case Array(partitionStr, offsetStr) => (new TopicPartition(topic, partitionStr.toInt) -> offsetStr.toLong) }
.toMap
logger.info("Done reading offsets from ZooKeeper. Took " + stopwatch)
Some(offsets)
}
case _ =>
logger.info("No offsets found in ZooKeeper. Took " + stopwatch)
None
}
}
/**
* 将指定的topic的所有的partition的offset信息保存到zookeeper中
* @param topic
* @param rdd
*/
override def saveOffsets(topic: String, rdd: RDD[_]): Unit = {
logger.info("Saving offsets to ZooKeeper")
val stopwatch = new Stopwatch()
val offsetsRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
offsetsRanges.foreach(offsetRange => logger.info(s"Using $offsetRange"))
// 分区和offset进行拼接
val offsetsRangesStr = offsetsRanges.map(offsetRange => s"${offsetRange.partition}:${offsetRange.fromOffset}")
.mkString(",") //partition1:220,partition2:320,partition3:10000
logger.info(s"Writing offsets to ZooKeeper: $offsetsRangesStr")
client.setData().forPath(zkPath, offsetsRangesStr.getBytes())
//ZkUtils.updatePersistentPath(zkClient, zkPath, offsetsRangesStr)
logger.info("Done updating offsets in ZooKeeper. Took " + stopwatch)
}
}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategy}
import scala.collection.JavaConversions._
object KafkaSource {
def createDirectStream[K, V](ssc: StreamingContext, locationStrategy: LocationStrategy,
kafkaParams: Map[String, Object], zkHosts: String, zkPath: String, topic: String
): DStream[ConsumerRecord[K, V]] = {
val offsetsStore = new ZooKeeperOffsetsStore(zkHosts, zkPath)
val storedOffsets = offsetsStore.readOffsets(topic) // 读出数据
val topicSet = Set(topic)
val kafkaStream = storedOffsets match {
case None => // 没有数据的话就是第一次启动
KafkaUtils.createDirectStream[K, V](ssc, locationStrategy,
ConsumerStrategies.Subscribe[K, V](topicSet, kafkaParams))
case Some(fromOffsets) => // 如果有值的话就把之前的offset告诉程序
KafkaUtils.createDirectStream[K, V](ssc, locationStrategy,
ConsumerStrategies.Subscribe[K, V](util.Arrays.asList(topic), kafkaParams, fromOffsets))
}
// 保存消费数据,读出来了就直接保存
kafkaStream.foreachRDD(rdd => offsetsStore.saveOffsets(topic, rdd))
kafkaStream
}
}
这样就是实现了手动的保存我们每一个批次消费到的topic的offset信息,数据也不会丢失了
3、也可以直接调用Kafka中高级的API,将消费的offset信息保存到zookeeper中,如下:
当重启Spark Streaming应用的时候,Spark Streaming会自动的从zookeeper中拿到上次消费的offset信息
在项目初期就应该按照需求设计,参考http://blog.cloudera.com/blog/2015/03/how-to-tune-your-apache-spark-jobs-part-2,比如:
num_executors取决于如下因素:
1:每一秒接收到的events,尤其是在高峰时间
2:数据源的缓冲能力
3:可以忍受的最大的滞后时间
executor_memory取决于如下因素:
1:每一个batch需要处理的数据的大小
2:transformations API的种类,如果使用的transformations(比如:groupByKey)需要shuffle的话,则需要的内存更大一点
3:使用状态Api的话(updateStateByKey,mapWithState,注意mapWithState虽然性能更好(因为有timeout 超时时间,不需要加更多的判断,会自动的清除超时数据),但是目前还处于试验版本,因此实际中更多的是用updateStateByKey),需要的内存更加大一点,因为需要内存缓存每一个key的状态
executor_cores:
每一个executor配置3到5个cores是比较好的,因为3到5个并发写HDFS是最优的
backpressure 方向调节接收速率:
receiver_max_rate=1000
receiver_initial_rate=30
–conf spark.streaming.kafka.maxRatePerPartition=${receiver_max_rate} #direct模式读取kafka每一个分区数据的最大速度
参考http://xuyangyang.club/articles/2018/07/23/1532348839398.html