Kafka偏移量(Offset)管理

标签: Spark Streaming和他的小伙伴们  Kafka  offset  spark streaming

1.定义

Kafka中的每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中。partition中的每个消息都有一个连续的序号,用于partition唯一标识一条消息。

Offset记录着下一条将要发送给Consumer的消息的序号。

流处理系统常见的三种语义:

最多一次 每个记录要么处理一次,要么根本不处理
至少一次 这比最多一次强,因为它确保不会丢失任何数据。但是可能有重复的
有且仅有一次 每条记录将被精确处理一次,没有数据会丢失,也没有数据会被多次处理

The semantics of streaming systems are often captured in terms of how many times each record can be processed by the system. There are three types of guarantees that a system can provide under all possible operating conditions (despite failures, etc.)

  1. At most once: Each record will be either processed once or not processed at all.
  2. At least once: Each record will be processed one or more times. This is stronger than at-most once as it ensure that no data will be lost. But there may be duplicates.
  3. Exactly once: Each record will be processed exactly once - no data will be lost and no data will be processed multiple times. This is obviously the strongest guarantee of the three.

 2.Kafka offset Management with Spark Streaming

Offset首先建议存放到Zookeeper中,Zookeeper相比于HBASE等来说更为轻量级,且是做HA(高可用性集群,High Available)的,offset更安全。

对于offset管理常见的两步操作:

  • 保存offsets
  • 获取offsets

3.环境准备

启动一个Kafka生产者,测试使用topic:tp_kafka:

./kafka-console-producer.sh --broker-list hadoop000:9092 --topic tp_kafka

启动一个Kafka消费者:

./kafka-console-consumer.sh --zookeeper hadoop000:2181 --topic tp_kafka

在IDEA中生产数据:

package com.taipark.spark;

import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;

import java.util.Properties;
import java.util.UUID;

public class KafkaApp {

    public static void main(String[] args) {
        String topic = "tp_kafka";

        Properties props = new Properties();
        props.put("serializer.class","kafka.serializer.StringEncoder");
        props.put("metadata.broker.list","hadoop000:9092");
        props.put("request.required.acks","1");
        props.put("partitioner.class","kafka.producer.DefaultPartitioner");
        Producer<String,String> producer = new Producer<>(new ProducerConfig(props));

        for(int index = 0;index <100; index++){
            KeyedMessage<String, String> message = new KeyedMessage<>(topic, index + "", "taipark" + UUID.randomUUID());
            producer.send(message);
        }
        System.out.println("数据生产完毕");

    }
}

4.第一种offset管理方式:smallest

Spark Streaming链接Kafka统计个数:

package com.taipark.spark.offset

import kafka.serializer.StringDecoder
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}

object Offset01App {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[2]").setAppName("Offset01App")
    val ssc = new StreamingContext(sparkConf,Seconds(10))

    val kafkaParams = Map[String, String](
      "metadata.broker.list" -> "hadoop000:9092",
      "auto.offset.reset" -> "smallest"
    )
    val topics = "tp_kafka".split(",").toSet
    val messages = KafkaUtils.createDirectStream[String,String,StringDecoder,StringDecoder](ssc,kafkaParams,topics)

    messages.foreachRDD(rdd=>{
      if(!rdd.isEmpty()){
        println("Taipark" + rdd.count())
      }
    })

    ssc.start()
    ssc.awaitTermination()
  }

}

再生产100条Kafka数据->Spark Streaming接受:

但这时如果Spark Streaming停止后重启:

会发现这里重头开始计数了,原因是代码里将auto.offset.reset的值设置为了smallest。(kafka-0.10.1.X版本之前)

5.第二种offset管理方式:checkpoint

在HDFS中创建一个/offset文件夹:

hadoop fs -mkdir /offset

使用Checkpoint:

package com.taipark.spark.offset

import kafka.serializer.StringDecoder
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Duration, Seconds, StreamingContext}

object Offset01App {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[2]").setAppName("Offset01App")

    val kafkaParams = Map[String, String](
      "metadata.broker.list" -> "hadoop000:9092",
      "auto.offset.reset" -> "smallest"
    )
    val topics = "tp_kafka".split(",").toSet
    val checkpointDirectory = "hdfs://hadoop000:8020/offset/"
    def functionToCreateContext():StreamingContext = {
      val ssc = new StreamingContext(sparkConf,Seconds(10))
      val messages = KafkaUtils.createDirectStream[String,String,StringDecoder,StringDecoder](ssc,kafkaParams,topics)
      //设置checkpoint
      ssc.checkpoint(checkpointDirectory)
      messages.checkpoint(Duration(10*1000))

      messages.foreachRDD(rdd=>{
        if(!rdd.isEmpty()){
          println("Taipark" + rdd.count())
        }
      })

      ssc
    }
    val ssc = StreamingContext.getOrCreate(checkpointDirectory,functionToCreateContext _)




    ssc.start()
    ssc.awaitTermination()
  }

}

注:IDEA修改HDFS用户,在设置里的VM options中:

-DHADOOP_USER_NAME=hadoop

先启动:

发现消费了之前的100条。这是停止之后,生产100条,再启动:

发现这里只读取了上次结束到这次启动之间的100条,而不是像smallest一样读取之前所有条数。

但是checkpiont存在问题,如果采用这种方式管理offset,只要业务逻辑发生了变化,则checkpoint就没有作用了。因为其调用的是getOrCreate()。

6.第三种offset管理方式:手动管理偏移量

思路:

  1. 创建StreamingContext
  2. 从Kafka获取数据  <== 拿到offset
  3. 根据业务逻辑进行处理
  4. 将处理结果写入外部存储 ==>保存offset
  5. 启动程序等待线程终止
package com.taipark.spark.offset

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.{Seconds, StreamingContext}

object Offset01App {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[2]").setAppName("Offset01App")
    val ssc = new StreamingContext(sparkConf,Seconds(10))


    val kafkaParams = Map[String, String](
      "metadata.broker.list" -> "hadoop000:9092",
      "auto.offset.reset" -> "smallest"
    )
    val topics = "tp_kafka".split(",").toSet
    //从某地获取偏移量
    val fromOffsets = Map[TopicAndPartition,Long]()

    val messages = if(fromOffsets.size == 0){  //从头消费
      KafkaUtils.createDirectStream[String,String,StringDecoder,StringDecoder](ssc,kafkaParams,topics)
    }else{  //从指定偏移量消费

      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=>{
      if(!rdd.isEmpty()){
        //业务逻辑
        println("Taipark" + rdd.count())

        //将offset提交保存到某地
        val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
        offsetRanges.foreach(x =>{
          //提交如下信息提交到外部存储
          println(s"${x.topic} ${x.partition} ${x.fromOffset} ${x.untilOffset}")
        })
      }
    })

    ssc.start()
    ssc.awaitTermination()
  }

}
  • 先保存offset后保存数据可能导致数据丢失
  • 先保存数据后保存offset可能导致数据重复执行

解决方式1:实现幂等(idempotent)

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。

解决方式2:事务 (transaction)

1.数据库事务可以包含一个或多个数据库操作,但这些操作构成一个逻辑上的整体。

2.构成逻辑整体的这些数据库操作,要么全部执行成功,要么全部不执行。

3.构成事务的所有操作,要么全都对数据库产生影响,要么全都不产生影响,即不管事务是否执行成功,数据库总能保持一致性状态。

4.以上即使在数据库出现故障以及并发事务存在的情况下依然成立。

将业务逻辑与offset保存放在一个事务里,仅执行一次。

7.Kafka-0.10.1.X版本之后的auto.kafka.reset:

earliest 当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
latest 当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
none topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常

 

版权声明:本文为qq_36329973原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_36329973/article/details/104825902

智能推荐

Java8 Stream流操作

前言 我们常常需要将一个容器转化成另一个容器,或是对这个容器中的数据进行批量处理,这时使用Stream流可以大大减少我们的工作量。 1 Stream概述 Java 8 是一个非常成功的版本,这个版本新增的Stream,配合同版本出现的 Lambda ,给我们操作集合(Collection)提供了极大的便利。 那么什么是Stream? Stream将要处理的元素集合看作一种流,在流的过程中,借助St...

带你玩转Visual Studio——带你跳出坑爹的Runtime Library坑

在Windows下进行C++的开发,不可避免的要与Windows的底层库进行交互,然而VS下的一项设置MT、MTd、MD和MDd却经常让人搞迷糊,相信不少人都被他坑过,特别是你工程使用了很多第三库的时候,及容易出现各种链接问题。看一下下面这个错误提示:  LIBCMT.lib(_file.obj) : error LNK2005: ___initstdio already defined...

git 设置 gitignore 忽略 __pycache__

清除git缓存中的pycache 直接删掉硬盘上的文件 如果我想保留硬盘上的这个文件,而只删除版本管理中的文件,就需要加入--cached参数。 切换分支出现问题 尽管我已经删除了__pycache__,硬盘也没有了,但是切换分支的时候依然是会提示本地重写的情况。 提示:需要我在切换分支之前,提交一次更新。 提交了更新之后,再来尝试切换分支,如下: 成功了,说明当做了任何变更之后,切换分支前需要执...

人生苦短,我用Python(二)— 爬取会议网站 EasyChair Smart CFP

寒假留校帮学长写了个爬虫,抓取会议网站上一些CFP信息。想着把一些知识点、坑点记下来,一来做个小总结给工作收收尾,二是以后再遇到好从容应对。 这是我写的第二个比较完善的爬虫了,比第一个要简单许多,完全过程化的代码,而且easychair这个网站页面布局比较友好,适合python新手、前端小白入门练习。但这个网站反爬比较厉害,写爬虫的过程中就被ban了好几次…… 制定抓取...

(三)Single Threaded Execution模式

一、定义 一个线程执行。有时也称为Critical Section(临界区)。   二、模式案例 案例: 假设有三个人,频繁地通过一扇门,规定每次只能通过一个人,当通过一个人时,程序会将通过的总人次加1,同时记录该次通过人的姓名和出生地。   门的定义:   人的定义     测试类     多次执行后发现可能出现以下结果: &...

猜你喜欢

Linux服务管理之系统管理员需要掌握的命令

为什么80%的码农都做不了架构师?>>>    systemd的主要命令行工具是systemctl。大多数Linux系统管理员应该都已经非常熟悉系统服务和init系统管理,比如service,chkconfig和telinit命令的使用。systemd也完成同样的管理任务,只是命令工具systemctl的语法有所不同而已。 1. sysvinit和system...

StringBuilder和StringBuffer的区别(检查多线程环境下会触发的问题和异常)

StringBuilder和StringBuffer的区别在哪里?                当接触这个问题的时候,我们可能第一反应就是,StringBuilder是线程不安全的,StringBuffer是线程安全的 为什么StringBuilder是线程不安全,StringBuffer是线程安全?   ...

机器学习实战之决策树基础笔记

决策树的优缺点 优点 计算复杂度不高 输出结果容易理解 对中间值的缺失不敏感 可以处理不相关特征数据 缺点 可能会产生过度匹配问题 决策树原理 《机器学习实战》书中讲了二十个问题的游戏的一个例子:就是参与游戏的一方脑子里想着某个事物。其他参与者可以向他提29个问题,但是答案只能用对错来回答。比如最简单的猜数游戏。我心里想一个数是7.然后A说你心里想的数比100小。然后我说正确。然后B说你心里想的数...

前端模型--纯js实现手风琴accordian

本文主要介绍纯js实现手风琴accordian。 手风琴模型图如下: 前端代码.html+.js Tips:本文讲两者放在一起,如需引用请自行将js放在外部 样式重置reset.css tips:由于前端html代码元素都自带基础样式,所以需要重置 手风琴样式accordian.css...

minSdkVersion、compileSdkVersion、targetSdkVersion

android:minSdkVersion 如果 compileSdkVersion 设置为可用的最新 API,那么 minSdkVersion 则是应用可以运行的最低要求,限制用户设备的最低版本号。minSdkVersion 是 Google Play 商店用来判断用户设备是否可以安装某个应用的标志之一。 在开发时 minSdkVersion 也起到一个重要角色:lint 默认会在项目中运行,它...