SparkStreaming+kafka+redis+hbase从指定offes位置消费kafka的数据

最近在做实时流计算的一个项目,遇到N多问题,经过不断的调试,终于有点进展,记录一下,防止后人遇到同样的问题.

1,sparkstreaming消费kafka有两种方法,这里我就不介绍了,网上关于这方面的资料很多,我就简单说一下两者的区别吧,

(1)基于receiver的方式,是使用Kafka的高阶API来在ZooKeeper中保存消费过的offset的。这是消费Kafka数据的传统方式。这种方式配合着WAL机制可以保证数据零丢失的高可靠性,但是却无法保证数据被处理一次且仅一次,可能会处理两次。因为Spark和ZooKeeper之间可能是不同步的.

(2)基于direct的方式,使用kafka的简单api,Spark Streaming自己就负责追踪消费的offset,并保存在checkpoint中。Spark自己一定是同步的,因此可以保证数据是消费一次且仅消费一次,在实际生产环境中大都用Direct方式.

特别说明一下:kafka0.9之前,offest都是保存在zk中,有zk来维护的,0.9之后kafka的offest保存在kafka的 __consumer_offsets 这个topic中了.

2,手动维护kafka的offest.

(1),为了实现数据不丢失,不重复消费,我采用自己保存offest的方法,可以保存在zk,也可以保存在topic中,但是这两者都不太好,所以被我抛弃了,我选择把offest保存到redis中.创建Dstream之前,先判断是否消费过,如果没有消费就从头开始,如果已经消费过了,就从上次保存的offest处开始消费,废话不多说,直接上代码.(因代码有点多,就只贴了重要的部分)

(2),spark版本2.2.0,scala版本2.11.8,kafka版本0.10.1,hbase版本1.1.2.

代码如下:

object SparkStreamingKafka extends Serializable {
  var kafkaStreams: InputDStream[ConsumerRecord[String, String]] = null
  val dbIndex = 0; //redis默认的数据库;
  lazy val logger = Logger.getLogger(SparkStreamingKafka.getClass)
  val dateFormat = new SimpleDateFormat("yyyyMMddhh")

  def main(args: Array[String]): Unit = {
    //屏蔽不需要的日志;
    Logger.getLogger("org.apache.spark").setLevel(Level.INFO)
    Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.INFO)
    Logger.getLogger("org.apache.kafka.clients.consumer").setLevel(Level.ERROR)
    val conf = new SparkConf().setAppName("spark-kafka-jason")
    conf.setMaster("local[4]") //本地运行模式;
    ///conf.setMaster("spark://master:7077") //运行与集群模式;   //spark on yarn 模式不需要指定master;
    conf.set("spark.streaming.kafka.maxRatePerPartition", "10000") // 每秒钟从topic的每个partition最多消费的消息条数
    ////conf.set("spark.rdd.compress", "true")  //rdd进行压缩处理;
    ////conf.set("spark.streaming.stopGracefullyOnShutdown","true")     //停止sparkstreaming程序时用到;
    val scc = new StreamingContext(conf, Seconds(5)) //设置5秒的间隔;
    ///scc.checkpoint("/jason/checkpoint/")   //设置hdfs的缓存路径;
    val topic = PropertiesScalaUtils.loadProperties("topic")
    val topicSet: Set[String] = Set(topic) //设置kafkatopic;
    val kafkaParams = Map[String, Object](
      /*"auto.offset.reset" -> "latest"*/
      "value.deserializer" -> classOf[StringDeserializer] //key,value的反序列化;
      , "key.deserializer" -> classOf[StringDeserializer]
      /*  , "security.protocol" -> "SASL_PLAINTEXT"
        , "sasl.mechanism" -> "PLAIN"*/
      //      , "sasl.jaas.config"->"org.apache.kafka.common.security.plain.PlainLoginModule required username=\"ucrealmarket\" password=\"B6!techsi\";"
      , "bootstrap.servers" -> PropertiesScalaUtils.loadProperties("broker")
      , "group.id" -> PropertiesScalaUtils.loadProperties("groupId")
      , "enable.auto.commit" -> (false: java.lang.Boolean)
    )
    val maxTotal = 10000
    val maxIdle = 100
    val minIdle = 10
    val testOnBorrow = false
    val testOnReturn = false
    val maxWaitMillis = 500
    //默认db,用户存放Offsetpv数据
    //Redis获取上一次存的Offset
    RedisPool.makePool(PropertiesScalaUtils.loadProperties("redisHost"), PropertiesScalaUtils.loadProperties("redisPort").toInt, maxTotal, maxIdle, minIdle, testOnBorrow, testOnReturn, maxWaitMillis)
    val jedis = RedisPool.getPool.getResource
    jedis.select(dbIndex)
    val keys: util.Set[String] = jedis.keys(topic + "*") //redis,获取offest的位置;
    if (keys.size() == 0) { //如果redis里没有保存过offest,就视为第一次启动;
      println("第一次启动,从最开始的位置读取-------------------------------")
      kafkaStreams = KafkaUtils.createDirectStream[String, String](
        scc,
        LocationStrategies.PreferConsistent,
        ConsumerStrategies.Subscribe[String, String](topicSet, kafkaParams))
    } else { //redis里有offest,说明不是第一次启动了,从保存的offest的位置开始读;
      println("不是第一次启动,从上次保存放的位置开始读取---------------------------")
      // val offsetList = RedisKeysListUtils.getKeysList(redisHost,redisPort,topic)  //redis里取出所有的topic,patition,构建一个list;
      val fromOffsets: Map[TopicPartition, Long] = RedisKeysListUtils.getKeysList(PropertiesScalaUtils.loadProperties("redisHost"), PropertiesScalaUtils.loadProperties("redisPort").toInt, topic) //构建每个分区开始的fromoffest,即这样的格式Map[TopicAndPartition, Long];
      //val messageHandler = (mam: MessageAndMetadata[String, String]) => (mam.topic, mam.message()) //构建MessageAndMetadata,这个是旧版本的方法;
      //val fromOffsets = Map(new TopicPartition("test", 1) -> 1100449855L,new TopicPartition("test", 0) -> 1100449855L,new TopicPartition("test", 0) -> 1100449855L)
      kafkaStreams = KafkaUtils.createDirectStream[String, String](
        scc,
        LocationStrategies.PreferConsistent,
        ConsumerStrategies.Subscribe[String, String](topicSet, kafkaParams, fromOffsets)) //从指定位置读取,这个是kafka-0.10.x 之后的新写法;
      //Subscribe:允许你订阅一个固定的topic的集合,Assign:允许你指定固定分区的集合;
    }
    RedisPool.getPool.returnResource(jedis) //释放redis的连接;
    var connection: Connection = null
    var myTable: HTable = null
    //打印从kafka消费的数据;
    kafkaStreams.map(_.value()).print()
    kafkaStreams.foreachRDD(rdd => {
      val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges //获得偏移量对象数组
      rdd.foreachPartition(partitionOfRecords => {
        //每个分区创建一个hbase的连接,所以需要序列化;
        try {
          val conf = HBaseConfiguration.create()
          conf.set("hbase.zookeeper.quorum", PropertiesScalaUtils.loadProperties("zk")) //zk的地址;
          conf.set("hbase.zookeeper.property.clientPort", PropertiesScalaUtils.loadProperties("zk_port"))
          conf.set("hbase.master", PropertiesScalaUtils.loadProperties("hbase_master"))
          conf.set("hbase.defaults.for.version.skip", "true")
          conf.set("hhbase.rootdir", PropertiesScalaUtils.loadProperties("hhbase.rootdir"))
          conf.set("zookeeper.znode.parent", PropertiesScalaUtils.loadProperties("zookeeper.znode.parent"))
          connection = ConnectionFactory.createConnection(conf)
          myTable = new HTable(conf, TableName.valueOf("test"))
          myTable.setAutoFlush(false, false) //关闭自动提交
          myTable.setWriteBufferSize(2 * 1024 * 1024) //设置hbase的缓存;
          partitionOfRecords.foreach(pair => {
            //自己的处理逻辑;
          })
          myTable.flushCommits() // 批量插入hbase;
          //Try(table.put(list)).getOrElse(table.close()) //把数据写入hbase,如果出错,关闭table连接;
          myTable.close() //分区数据写入hbase后关闭连接;
          println("myTable关闭了")
          //更新Offset
          offsetRanges.foreach { offsetRange =>
            println("partition : " + offsetRange.partition + " fromOffset:  " + offsetRange.fromOffset + " untilOffset: " + offsetRange.untilOffset)
            val topic_partition_key_new = offsetRange.topic + "_" + offsetRange.partition
            p1.set(topic_partition_key_new, offsetRange.untilOffset + "")
          }
          p1.exec(); //提交事务
          p1.sync(); //关闭pipeline
        } catch {
          case e: Exception => logger.info("报错了:" + e.getMessage)
        } finally {
          myTable.close()
          connection.close()
          RedisPool.getPool.returnResource(jedis_new)
          RedisPool.getPool.returnResource(jedis_jason)
          println("所有的连接都关闭了------------------------------------------------------------")
        }
      })
    })
    scc.start()
    scc.awaitTermination()
  }
}


代码已经测试过了,没有问题,本人小白一枚,如果有写的不对的地方,欢迎各位前辈大神指正,代码上面基本都有注释,如果有不理解的地方或者需要完整代码的,联系我,可以加QQ群340297350,谢谢



猜你喜欢

转载自blog.csdn.net/xianpanjia4616/article/details/80738961