一、 整合版本说明

这是一种流式数据处理中最常见的方式之一,使用SparkStreaming去从kafka中拉取数据有两大主要的版本。主要在spark2.0开始之后进行区分。

spark stream和kafka整合 spark和kafka的整合_A

  • SparkStremaing-kafka-0-8版本
    在此版本中有两种方式来消费kafka中的数据,receiver的方式(已经被淘汰);最早出现的拉取kafka数据的方式,在1.2开始出现。direct的方式是1.3版本出现才出现的。
  • SparkStremaing-kafka-0-10版本
    只有一种处理方式——那就是direct的方式

二、以SparkStremaing-kafka-0-8版本来讲解两种整合方式

1、SparkStreaming基于Receiver的方式整合Kafka

这种方式使用Receiver来获取数据。Receiver是使用Kafka的高层次Consumer API来实现的。receiver从Kafka中获取的数据都是存储在Spark Executor的内存中的,然后Spark Streaming启动的job会去处理那些数据。
然而,在默认的配置下,这种方式可能会因为底层的失败而丢失数据。如果要启用高可靠机制,让数据零丢失,就必须启用Spark Streaming的预写日志机制(Write Ahead Log,WAL)。该机制会同步地将接收到的Kafka数据写入分布式文件系统(比如HDFS)上的预写日志中。所以,即使底层节点出现了失败,也可以使用预写日志中的数据进行恢复。

1.1代码整合实现

/**
  * 使用Reciever的方式整合kafka
  *
  * 二者整合的入口类就是KafkaUtils
  *
  * kafka-topics.sh --create --topic spark --zookeeper bigdata01:2181/kafka --partitions 3 --replication-factor 3
  */
object  SparkStreamingWithReceiver2KafkaOps {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf()
                .setAppName("SparkStreamingWithReceiver2Kafka")
                .setMaster("local[*]")
        val ssc = new StreamingContext(conf, Seconds(2))

//        readFromKafka1(ssc)
        val kafkaParams = Map[String, String](
            "zookeeper.connect" -> "bigdata01:2181,bigdata02:2181,bigdata03:2181/kafka",
            "group.id" -> "test-group-2",
            "auto.offset.reset" -> "smallest"
        )
        val topics = Map[String, Int](
            "spark" -> 3
        )
        val messages:ReceiverInputDStream[(String, String)] = KafkaUtils
            .createStream[String, String, StringDecoder, StringDecoder](ssc,
                        kafkaParams, topics, StorageLevel.MEMORY_ONLY)

 messages.print()
 ssc.start()
 ssc.awaitTermination()
 }

 private def readFromKafka1(ssc: StreamingContext) = {
 //接收kafka中的数据
 val zkQuorum = "bigdata01:2181,bigdata02:2181,bigdata03:2181/kafka"
 val groupId = "test-group-1"
 val topics = MapString, Int
 /*
 返回值的key:kafka中每一条record对应的key
 返回值的value:kafka中每一条record对应的value
 这种方式只能从offset最开始的位置消费数据
 */
 val inputStream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(ssc, zkQuorum, groupId, topics)
 inputStream.print()
 }

1.2receiver原理说明

spark stream和kafka整合 spark和kafka的整合_A_02

1.3receiver方式需要注意的点

  • Kafka的topic的partition和RDD的partition没有任何关系,增加topic的partition数量,只是增加receiver中用于读取数据的线程个数。
  • 如果我们要想提升消费topic数据的能力,我们可以多创建几个receiver,最后将多个receiver读取到得数据经union联合之后进行处理
  • 如果开启了基于hdfs的wal机制,就不要在创建流的时候指定StorageLevel的时候备份了。

开启wal日志,只需要配置参数spark.streaming.receiver.writeAheadLog.enable=true即可。同时需要执行checkpoint目录,将数据保存起来。

ssc.checkpoint(hdfs-path)

2、SparkStreaming基于Direct的方式整合kafka

2.1代码整合实现

object SparkStreamingWithDirectOps {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf()
            .setAppName("SparkStreamingWithDirect")
            .setMaster("local[*]")
        val ssc = new StreamingContext(conf, Seconds(2))

        //kafkautils
        val kafkaParams = Map[String, String](
            "bootstrap.servers" -> "bigdata01:9092,bigdata02:9092,bigdata03:9092",
            "auto.offset.reset" -> "largest",
            "group.id" -> "test-group-3"
        )

        val topics = "spark".split(",").toSet
        val messages:InputDStream[(String, String)] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topics)

        messages.print()

        ssc.start()
        ssc.awaitTermination()
    }
}

2.2direct方式说明

在receiver的方式中,框架会自动的将偏移量信息保存在zk中,在direct方式中并没有这样做,因为此时不需要zookeeper,是直接从kafka中拉取数据,此时的spark程序就是kafka的消费者,而且没有使用receiver,使用kafka底层的消费者api来实现的,自然就不会再这个目录下面创建相关的记录。

此时正常运行过程中的程序,是如果从正确的偏移量位置来拉取数据的?比如第一次拉取了10条数据,假如此时的偏移量为10,下一次拉取数据应该从11开始?

既然没有zk来管理offset,只有sparkstreaming来进行管理!偏移量信息都会保存在DStream中,此时DStream是一个有范围(offset)的RDD,把这个rdd用HasOffsetRange来表示。

2.2.1direct方式如何自己来管理offset

direct模式已经不用zk来管理offset,自己来管理,在选择从kafka什么位置读取数据的时候有两种方式:earliest、lastest。如果我们使用lastest的是没有办法读取到之前的数据;如果使用earliest,每一次都会从最开始的位置来进行消费,会有有会有重复的消费。所以直接使用这种方式,不管是lastest还是earliest都不行,还得需要我们人工来干预offset。

那么如何自己来管理offset呢?其实管理offset的方式非常多,比如,基于zk、checkpoint、hbase、redis、elasticsearch等等。

三、direct方式自己来管理offset的代码实现

1、zookeeper管理偏移量

1.1引入依赖

<dependencies>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming_2.11</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
            <version>2.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.8.0</version>
        </dependency>
    </dependencies>

1.2代码实现

import kafka.common.TopicAndPartition
import kafka.message.MessageAndMetadata
import kafka.serializer.StringDecoder
import org.apache.curator.framework.CuratorFrameworkFactory
import org.apache.curator.retry.ExponentialBackoffRetry
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka.{HasOffsetRanges, KafkaUtils, OffsetRange}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.zookeeper.data.Stat

import scala.collection.mutable

/**
  *  使用zk来管理direct模式下面的offset
  *  操作的流程
  *     和基于receiver一直,不同点在于offset是否能为我们掌控
  */
object StreamingWithDirectZKOps {

    def main(args: Array[String]): Unit = {
        val conf = new SparkConf()
            .setAppName("StreamingWithDirectZK")
            .setMaster("local[*]")
        val ssc = new StreamingContext(conf, Seconds(2))

        //kafkautils
        val kafkaParams = Map[String, String](
            "bootstrap.servers" -> "bigdata01:9092,bigdata02:9092,bigdata03:9092",
            "auto.offset.reset" -> "smallest",
            "group.id" -> "test-group-3"
        )

        val topics = "hadoop".split(",").toSet

        val messages = createMessage(ssc, kafkaParams, topics)
        //foreachRDD -->遍历dstream中的每一个rdd
        messages.foreachRDD((rdd, bTime) => {
            if(!rdd.isEmpty()) {
                println("-------------------------------------------")
                println(s"Time: $bTime")
                println("#####################rdd's count: " + rdd.count())
                println("-------------------------------------------")
                //存储偏移量
                storeOffsets(rdd.asInstanceOf[HasOffsetRanges].offsetRanges, kafkaParams("group.id"))
            }
        })
        ssc.start()
        ssc.awaitTermination()
    }

    /**
      * 更新offset
      *  /kafka/consumers/offsets/${topic}/${group}/${partition}
      */
    def storeOffsets(offsetRanges: Array[OffsetRange], group:String) = {
        for (offsetRange <- offsetRanges) {
            val topic = offsetRange.topic
            val partition = offsetRange.partition
            val offset = offsetRange.untilOffset
            val path = s"/offset/${topic}/${group}/${partition}"
            checkExist(path)
            curator.setData().forPath(path, (offset + "").getBytes())
        }
    }

    def createMessage(ssc: StreamingContext, kafkaParams: Map[String, String], topics:Set[String]): InputDStream[(String, String)] = {
        //step 1 读取偏移量
        val fromOffsets:Map[TopicAndPartition, Long] = getFromOffsets(topics, kafkaParams("group.id"))
        /*
            step 2 拉取kafka数据
                有offset
                    从相关偏移量的位置拉取数据
                无offset
                    从指定的offset.auto.reset对应的位置开始拉取数据
         */
        var messages:InputDStream[(String, String)] = null
        if(!fromOffsets.isEmpty) {
            //有偏移量
            val messageHandler = (mmd: MessageAndMetadata[String, String]) => (mmd.key, mmd.message)
            messages = KafkaUtils.createDirectStream[String, String,
                            StringDecoder, StringDecoder,
                            (String, String)](ssc,
                        kafkaParams, fromOffsets,
                        messageHandler)
        } else {
            //无偏移量
            messages = KafkaUtils.createDirectStream[String, String,
                        StringDecoder, StringDecoder](ssc, kafkaParams, topics)
        }
        messages
    }
    /*
        获取topic和partition对应的偏移量
        1、既然是基于zk的方式来管理offset,那么就需要在zk中的某个目录对应节点中存储offset信息,每次再更新
        2、所以我们可以模仿基于receiver的方式来保存的offset
        3、操作zk得需要zk的client
        约定数据存储的目录
        /kafka/consumers/bd-1901-group-1/offsets/spark/0 -->基于receiver的方式
        模仿
        /kafka/consumers/offsets/${topic}/${group}/${partition}
                                                    |data-offset
     */
    def getFromOffsets(topics:Set[String], group:String):Map[TopicAndPartition, Long] = {
        val fromOffset = mutable.Map[TopicAndPartition, Long]()
        import scala.collection.JavaConversions._
        for(topic <- topics) {
            val basePath = s"/offset/${topic}/${group}"
            //basepath可能存在,可能不存在
            checkExist(basePath)
            val partitions = curator.getChildren.forPath(basePath)//partitions是一个java的集合
            for(partition <- partitions) {
                val path = s"${basePath}/${partition}"
                val offset = new String(curator.getData.forPath(path)).toLong
                fromOffset.put(TopicAndPartition(topic, partition.toInt), offset)
            }
        }
        fromOffset.toMap
    }

    def checkExist(path:String): Unit = {
        val stat:Stat = curator.checkExists().forPath(path)
        if(stat == null) {
            //创建该目录
            curator.create()
                .creatingParentsIfNeeded()//如果目录中存在多级未创建的目录,需要指定递归创建目录的方式
                .forPath(path)
        }

    }

    val curator = {

        val client = CuratorFrameworkFactory.builder()
                    .connectString("bigdata01:2181,bigdata02:2181,bigdata03:2181")
                    .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                    .namespace("kafka/consumers")//需要协商确定存储的zk的路径
                    .build()
        client.start()
        client
    }
}

2、redis管理偏移量

一般在公司中都只搭建一个zookeeper集群,来管理hadoop、kafka、flink、zookeeper、hbase等等,如果我们还用zk的方式来管理offset,对zk的负载是很高的,那么如果zk挂了,后果就会很严重,所以我们可以用redis等等来代替zk在这里的功能。

2.1引入依赖

在pom里添加上redis依赖:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

2.2代码实现

/**
  *  使用redis来管理direct模式下面的offset
  *  操作的流程
  *     和基于receiver一直,不同点在于offset是否能为我们掌控
  */
object _02StreamingWithDirectRedisOps {

    def main(args: Array[String]): Unit = {
        val conf = new SparkConf()
            .setAppName("StreamingWithDirectRedis")
            .setMaster("local[*]")
        val ssc = new StreamingContext(conf, Seconds(2))

        //kafkautils
        val kafkaParams = Map[String, String](
            "bootstrap.servers" -> "bigdata01:9092,bigdata02:9092,bigdata03:9092",
            "auto.offset.reset" -> "smallest",
            "group.id" -> "test-group-3"
        )

        val topics = "hadoop".split(",").toSet

        val messages = createMessage(ssc, kafkaParams, topics)
        //foreachRDD -->遍历dstream中的每一个rdd
        messages.foreachRDD((rdd, bTime) => {
            if(!rdd.isEmpty()) {
                println("-------------------------------------------")
                println(s"Time: $bTime")
                println("#####################rdd's count: " + rdd.count())
                println("-------------------------------------------")
                //存储偏移量
                storeOffsets(rdd.asInstanceOf[HasOffsetRanges].offsetRanges, kafkaParams("group.id"))
            }
        })
        ssc.start()
        ssc.awaitTermination()
    }

    /**
      * 更新offset
      *  /kafka/consumers/offsets/${topic}/${group}/${partition}
      */
    def storeOffsets(offsetRanges: Array[OffsetRange], group:String) = {
        for (offsetRange <- offsetRanges) {
            val topic = offsetRange.topic
            val partition = offsetRange.partition
            val offset = offsetRange.untilOffset
            val field = s"${group}|${partition}"
            jedis.hset(topic, field, offset.toString)
        }
    }

    def createMessage(ssc: StreamingContext, kafkaParams: Map[String, String], topics:Set[String]): InputDStream[(String, String)] = {
        //step 1 读取偏移量
        val fromOffsets:Map[TopicAndPartition, Long] = getFromOffsets(topics, kafkaParams("group.id"))
        /*
            step 2 拉取kafka数据
                有offset
                    从相关偏移量的位置拉取数据
                无offset
                    从指定的offset.auto.reset对应的位置开始拉取数据
         */
        var messages:InputDStream[(String, String)] = null
        if(!fromOffsets.isEmpty) {
            //有偏移量
            val messageHandler = (mmd: MessageAndMetadata[String, String]) => (mmd.key, mmd.message)
            messages = KafkaUtils.createDirectStream[String, String,
                            StringDecoder, StringDecoder,
                            (String, String)](ssc,
                        kafkaParams, fromOffsets,
                        messageHandler)
        } else {
            //无偏移量
            messages = KafkaUtils.createDirectStream[String, String,
                        StringDecoder, StringDecoder](ssc, kafkaParams, topics)
        }
        messages
    }
    
    def getFromOffsets(topics:Set[String], group:String):Map[TopicAndPartition, Long] = {
        val fromOffset = mutable.Map[TopicAndPartition, Long]()
        import scala.collection.JavaConversions._
        for(topic <- topics) {
            val map = jedis.hgetAll(topic)
            for((field, value) <- map) {//field=group|partition
               val partition = field.substring(field.indexOf("|") + 1).toInt
               val offset = value.toLong
               fromOffset.put(TopicAndPartition(topic, partition), offset)
            }
        }
        fromOffset.toMap
    }

    /*
        redis的java的api是jedis
        jedis对象相当于redis的connection连接对象
     */
    val jedis = {
        val jedis = new Jedis("bigdata01", 6379)
        jedis
    }
}

对于上述的代码有可以优化的地方,就是使用类似数据库连接池的思想构建redis的连接池
封装redis的工具类

public class JedisUtil {
    private JedisUtil(){}
    private static JedisPool pool;
    static {
        Properties properties = new Properties();
        try {
            properties.load(JedisUtil.class.getClassLoader().getResourceAsStream("jedis.properties"));
            String host = properties.getProperty(Constants.JEDIS_HOST);
            int port = Integer.valueOf(properties.getProperty(Constants.JEDIS_PORT, "6379"));
            JedisPoolConfig config = new JedisPoolConfig();
            pool = new JedisPool(config, host, port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Jedis getJedis() {
        return pool.getResource();
    }

    public static void returnJedis(Jedis jedis) {
//        pool.returnResource(jedis);
        jedis.close();
    }
}
public interface Constants {
    String JEDIS_HOST = "jedis.host";
    String JEDIS_PORT = "jedis.port";
}

jedis.properties

jedis.host=hadoop01
jedis.port=6379

功能代码修正

import db.JedisUtil
import kafka.common.TopicAndPartition
import kafka.message.MessageAndMetadata
import kafka.serializer.StringDecoder
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka.{KafkaUtils, OffsetRange, HasOffsetRanges}
import org.apache.spark.streaming.{Seconds, StreamingContext}

import scala.collection.mutable


object _03StreamingWithDirectRedis {

  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
      .setAppName("StreamingWithDirectRedis")
      .setMaster("local[*]")
    val ssc = new StreamingContext(conf, Seconds(2))

    //kafkautils
    val kafkaParams = Map[String, String](
      "bootstrap.servers" -> "bigdata01:9092,bigdata02:9092,bigdata03:9092",
      "auto.offset.reset" -> "smallest",
      "group.id" -> "test-group-3"
    )

    val topics = "hadoop".split(",").toSet

    val messages = createMessage(ssc, kafkaParams, topics)
    //foreachRDD -->遍历dstream中的每一个rdd
    messages.foreachRDD((rdd, bTime) => {
      if(!rdd.isEmpty()) {
        println("-------------------------------------------")
        println(s"Time: $bTime")
        println("#####################rdd's count: " + rdd.count())
        println("-------------------------------------------")
        //存储偏移量
        storeOffsets(rdd.asInstanceOf[HasOffsetRanges].offsetRanges, kafkaParams("group.id"))
      }
    })
    ssc.start()
    ssc.awaitTermination()
  }

  /**
    * 更新offset
    *  /kafka/consumers/offsets/${topic}/${group}/${partition}
    */
  def storeOffsets(offsetRanges: Array[OffsetRange], group:String) = {
    val jedis = JedisUtil.getJedis
    for (offsetRange <- offsetRanges) {
      val topic = offsetRange.topic
      val partition = offsetRange.partition
      val offset = offsetRange.untilOffset
      val field = s"${group}|${partition}"
      jedis.hset(topic, field, offset.toString)
    }
    JedisUtil.returnJedis(jedis)
  }

  def createMessage(ssc: StreamingContext, kafkaParams: Map[String, String], topics:Set[String]): InputDStream[(String, String)] = {
    //读取偏移量
    val fromOffsets:Map[TopicAndPartition, Long] = getFromOffsets(topics, kafkaParams("group.id"))
    var messages:InputDStream[(String, String)] = null
    if(!fromOffsets.isEmpty) {
      //有偏移量
      val messageHandler = (mmd: MessageAndMetadata[String, String]) => (mmd.key, mmd.message)
      messages = KafkaUtils.createDirectStream[String, String,
        StringDecoder, StringDecoder,
        (String, String)](ssc,
        kafkaParams, fromOffsets,
        messageHandler)
    } else {
      //无偏移量
      messages = KafkaUtils.createDirectStream[String, String,
        StringDecoder, StringDecoder](ssc, kafkaParams, topics)
    }
    messages
  }
  def getFromOffsets(topics:Set[String], group:String):Map[TopicAndPartition, Long] = {
    val fromOffset = mutable.Map[TopicAndPartition, Long]()
    import scala.collection.JavaConversions._
    val jedis = JedisUtil.getJedis
    for(topic <- topics) {
      val map = jedis.hgetAll(topic)
      for((field, value) <- map) {//field=group|partition
      val partition = field.substring(field.indexOf("|") + 1).toInt
        val offset = value.toLong
        fromOffset.put(TopicAndPartition(topic, partition), offset)
      }
    }
    JedisUtil.returnJedis(jedis)
    fromOffset.toMap
  }
}

3、checkpoint

在设置ssc.checkpoint()即可完成offset的存储
但是这种方式一般情况下都不用,因为会生成大量的小文件存储hdfs中,同时读取文件的效率要远低于读取内存文件的效率。