kafka工作原理与使用

1. Kafka概述

1.1. 什么是Kafka

Apache Kafka是分布式发布-订阅消息系统(消息中间件)。它最初由LinkedIn公司开发,之后成为Apache项目的一部分。Kafka是一种快速、可扩展的、设计内在就是分布式的,分区的和可复制的提交日志服务。

传统消息中间件服务RabbitMQ、Apache ActiveMQ等,Apache Kafka与传统消息系统相比,有以下优点

  1. 它是分布式系统,易于向外扩展;
  2. 它同时为发布和订阅提供高吞吐量;
  3. 它支持多订阅者,当失败时能自动平衡消费者;
  4. 它将消息持久化到磁盘,因此可用于批量消费,例如ETL,以及实时应用程序。

1.2.kafka的几个重要概念

  • Broker:消息中间件处理结点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群;
  • Topic:一类消息,例如page view日志、click日志等都可以以topic的形式存在,Kafka集群能够同时负责多个topic的分发主题中的每条消息包括key-value和timestamp。可以定义多个topic,每个topic又可以划分为多个分区
  • Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队,通过key取哈希后把消息映射分发到一个指定的分区,每个分区都映射到broker上的一个目录。一般不同分区存储在不同broker上
  • PartitionId:一个整数,范围从0到分区数-1。
  • 分区名;其命名规则为<topic_name>-<partition_id>
  • Segment(环节; 部分)每个partition又由多个segment file组成;
  • offset:每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中。partition中的每个消息都有一个连续的序列号叫做offset,用于partition唯一标识一条消息;
  • message:这个算是kafka文件中最小的存储单位,即是 a commit log
  • kafka的message是以topic为基本单位,不同topic之间是相互独立的。每个topic又可分为几个不同的partition,每个partition存储一部的分message。topic与partition的关系如下:

注意1 :partition是以文件夹的形式存储在具体Broker本机上。

注意2副本一般不会和leader在同一个机器上,不同的leader的一般不再同一台机器上,这样同一台太忙,效率低

  • 日志段

一个日志又被划分为多个日志段(LogSegment),日志段是Kafka日志对象分片的最小单位。与日志对象一样,日志段也是一个逻辑概念,一个日志段对应磁盘上一个具体日志文件和两个索引文件。日志文件是以“.log”为文件名后缀的数据文件,用于保存消息实际数据。两个索引文件分别以“.index”和“.timeindex”作为文件名后缀,分别表示消息偏移量索引文件和消息时间戳索引文件。这样在查找指定 offset 的 Message 的时候,用二分查找就可以定位到该 Message 在哪个 segment 数据文件中。

1.3 segment中的文件

对于一个partition(在Broker中以文件夹的形式存在),里面又有很多大小相等的segment数据文件(这个文件的具体大小可以在config/server.properties中进行设置),这种特性可以方便old segment file的快速删除。

下面先介绍一下partition中的segment file的组成:

segment file 组成:

由2部分组成,分别为index file和data file,这两个文件是一一对应的,后缀”.index”和”.log”分别表示索引文件数据文件

其中.log用于存储真正的消息数据

segment file 命名规则:partition的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset,offset的数值最大为64位(long类型),20位数字字符长度,没有数字用0填充。如下图所示:

关于segment file中index与data file对应关系图,这里我们选用网上的一个图片,如下所示:

segment的索引文件中存储着大量的元数据,数据文件中存储着大量消息,索引文件中的元数据指向对应数据文件中的message的物理偏移地址。以索引文件中的3,497为例,在数据文件中表示第3个message(在全局partition表示第368772个message),以及该消息的物理偏移地址为497。

注意2:Partition中的每条message由offset来表示它在这个partition中的偏移量,这个offset并不是该Message在partition中实际存储位置,而是逻辑上的一个值(如上面的3),但它却唯一确定了partition中的一条Message(可以认为offset是partition中Message的id)

1.4 Kafka分区命名规则

partition是以文件的形式存储在文件系统中,比如,创建了一个名为page_visits的topic,其有5个partition,那么在Kafka的数据目录中(由配置文件中的log.dirs指定的)中就有这样5个目录: page_visits-0, page_visits-1,page_visits-2,page_visits-3,page_visits-4,其命名规则为<topic_name>-<partition_id>,里面存储的分别就是这5个partition的数据。

1.5 kafka数据顺序(重点)

同一个主题内同一个分区下 不会乱序,但是全部不能保证数据有序,所有如果想要数据有序,只能通过数据设置同一个key打到同一个分区里面。

1.6 Kafka集群结构图

 

1.6 Kafka细节说明

  1. 细节一:kafka的主体分区中,数据是从后向前追加的。
  2. 细节二: 每一个Consumer都有一个group id 一个group id 只能从主体中消费一次数据,仅仅一次。就是consumers是消息系统的流出接口,多个consumers逻辑上组成consumer Group。CG的目标是实现同一需求的消费吞吐量。同一个topic的message,只能被同一CG的一个Consumer消费;但可以被不同多个CG消费;
  3. 细节三:负载均衡,一般不同的leader不在同一台机器上。不然同一台机器压力太大。
  4. 细节四:副本一般不会和leader在一台机器上,方式宕机带来的数据丢失。
  5. 同一个groubId不能再不同的任务同时使用,测试时换成不同的groubId。
  6. Kafka不支持读写分离,具体参考:https://blog.csdn.net/qq_42046105/article/details/89368978

1.7 消息格式

消息格式,有三个版本V0,V1,V2 V0版本:主要指kafka0.10.0.0之前的版本,是kafka最早的消息版本

弊端: 1.删除过期日志只能根据日志段的最近修改时间 2.没有消息时间戳

4字节

1字节

1字节

4字节

k个字节

4个字节

v个字节

CRC

版本号

属性

key长度

key

value长度

value

V1版本:在kafka0.10.0.0后推出了V1版本 V1与V0有两个差别:

1.V1引入了8字节的时间戳字段

2.attribute字段增加了指定时间戳类型(create_time和log_append_time,前者是在producer手动指定时间戳,后者是broker自动生成时间戳),V0版本只指定了压缩类型

4字节

1字节

1字节

8字节

4字节

k个字节

4个字节

v个字节

CRC

版本号

属性

时间戳

key长度

key

value长度

value

1.8 消息集合

一个消息集合包含了若干个日志项,每个日志项都封装了实际的消息和一组元数据信息。每个消息集合中的日志由一条浅层消息和日志项头部组成

如果未启用压缩,那么message就是消息本身,否则kafka会将多条消息压缩到一起 统一封装进这条浅层消息里面,这条浅层消息就叫包装消息(wrapper),里面的消息 叫内部消息(inner),如果启用了压缩,此时offset是wrapper消息中最后一条 inner消息的offset,否则就是实际消息的offset

1.9 kafka发展历史

版本

功能变化

说明

0.7

提供了最基本的消息引擎服务

国内一般都是从这个版本开始接触kafka

0.8

增加了集群间的备份机制

正式加入备份机制,使得kafka成为完备的分布式消息引擎解决方案

0.8.2.x

使用java重写了producer

引入javaproducer替代scala版的producer

0.9.0.x

增加了kafka安全性设置

使用java重写了consumer

增加了 kafka connect组件

该版本正式加入kafka security,增加了kafka集群的安全度,引入了java版的consumer,引入kafka connect组件,实现了高扩展性,高可靠性的数据抽取服务,0.9之后offset使用了内部的broker来管理

0.10.0.x

增加了kafka streams组件

0.10.0版本开始,kafka正式转型成为分布式流式处理平台

0.11.0.x

增加了对事物以及幂等性的支持以及实现了精确一次处理语义

 

1.0.0

优化了kafka streams api

1.0.0版本开始,kafka正式进入1.0稳定版本

acks

  • 0:这意味着producer无需等待来自broker的确认而继续发送下一批消息。这种情况下数据传输效率最高,
  •     但是数据可靠性确是最低的。
  • 1(默认):这意味着producer在ISR中的leader已成功收到的数据并得到确认后发送下一条message。
  •       如果leader宕机了,则会丢失数据。
  • -1(或者是all):producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。
  •    但是这样也不能保证数据不丢失,比如当ISR中只有leader时(前面ISR那一节讲到,ISR中的成员由于某些情况会增加也会减少,最少就只剩一个leader),这样就变成了acks=1的情况。

leader&follower的理解

Kafka允许topic的分区拥有若干副本,这个数量是可以配置。Kafka会自动在每个个副本上备份数据,所以当一个节点down掉时数据依然是可用的。Kafka的副本功能不是必须的,你可以配置只有一个副本,这样其实就相当于只有一份数据。

创建副本的单位是topic的分区,每个分区都有一个leader和零或多个followers.所有的读写操作都由leader处理,一般分区的数量都比broker的数量多的多,各分区的leader均匀的分布在brokers中。所有的followers都复制leader的日志,日志中的消息和顺序都和leader中的一致。flowers向普通的consumer那样从leader那里拉取消息并保存在自己的日志文件中。
许多分布式的消息系统自动的处理失败的请求,它们对一个节点是否
着(alive)”有着清晰的定义。Kafka判断一个节点是否活着有两个条件:
  1. 节点必须可以维护和ZooKeeper的连接,Zookeeper通过心跳机制检查每个节点的连接。
  2. 如果节点是个follower,他必须能及时的同步leader的写操作,延时不能太久。
符合以上条件的节点准确的说应该是“同步中的(in sync)”,而不是模糊的说是“活着的”或是“失败的”。Leader会追踪所有“同步中”的节点,一旦一个down掉了,或是卡住了,或是延时太久,leader就会把它移除。至于延时多久算是“太久”,是由参数replica.lag.max.messages决定的,怎样算是卡住了,怎是由参数replica.lag.time.max.ms决定的。 
只有当消息被所有的副本加入到日志中时,才算是“committed”,只有committed的消息才会发送给consumer,这样就不用担心一旦leader down掉了消息会丢失。Producer也可以选择是否等待消息被提交的通知,这个是由参数request.required.acks决定的。

Kafka保证只要有一个“同步中”的节点,“committed”的消息就不会丢失。

Lag原理与监控

         kafka consumer 消费会存在延迟情况,我们需要查看消息堆积情况,就是所谓的消息Lag。目前是市面上也有相应的监控工具KafkaOffsetMonitor

lag计算原理

        介绍lag如何计算之前必须先了解下面几个概念:

在 Kafka 中无论是 producer 往 topic 中写数据, 还是 consumer 从 topic 中读数据, 都避免不了和 offset 打交道, 关于 offset 主要有以下几个概念。

 

  • Last Committed Offset:consumer group 最新一次 commit 的 offset,表示这个 group 已经把 Last Committed Offset 之前的数据都消费成功了。
  • Current Position(ConsumerOffset):consumer group 当前消费数据的 offset,也就是说,Last Committed Offset 到 Current Position 之间的数据已经拉取成功,可能正在处理,但是还未 commit。
  • Log End Offset(LEO):记录底层日志 (log) 中的下一条消息的 offset。, 对 producer 来说,就是即将插入下一条消息的 offset。
  • High Watermark(HW):已经成功备份到其他 replicas 中的最新一条数据的 offset,也就是说 Log End Offset 与 High Watermark 之间的数据已经写入到该 partition 的 leader 中,consumer可见的的最大offset.但是还未完全备份到其他的 replicas 中,consumer 是无法消费这部分消息 (未提交消息)。

每个 Kafka 副本对象都有两个重要的属性:LEO 和 HW。注意是所有的副本,而不只是 leader 副本

知道以上信息后,可知Lag=HW-Current Position ,也就是最新可见的offset减去当前消费的offset,计算出来的值即为消费延迟情况。

Leader的选择(ISR

Kafka的核心是日志文件,日志文件在集群中的同步是分布式数据系统最基础的要素。  

如果leaders永远不会down的话我们就不需要followers了!一旦leader down掉了,需要在followers中选择一个新的leader.但是followers本身有可能延时太久或者crash,所以必须选择高质量的follower作为leader.必须保证,一旦一个消息被提交了,但是leader down掉了,新选出的leader必须可以提供这条消息。大部分的分布式系统采用了多数投票法则选择新的leader,对于多数投票法则,就是根据所有副本节点的状况动态的选择最适合的作为leader.Kafka并不是使用这种方法。

Kafaka动态维护了一个同步状态的副本的集合(a set of in-sync replicas),简称ISR,在这个集合中的节点都是和leader保持高度一致的,任何一条消息必须被这个集合中的每个节点读取并追加到日志中了,才回通知外部这个消息已经被提交了。因此这个集合中的任何一个节点随时都可以被选为leader.ISR在ZooKeeper中维护。ISR中有f+1个节点,就可以允许在f个节点down掉的情况下不会丢失消息并正常提供服。ISR的成员是动态的,如果一个节点被淘汰了,当它重新达到“同步中”的状态时,他可以重新加入ISR.这种leader的选择方式是非常快速的,适合kafka的应用场景。

一个邪恶的想法:如果所有节点都down掉了怎么办?Kafka对于数据不会丢失的保证,是基于至少一个节点是存活的,一旦所有节点都down了,这个就不能保证了。
实际应用中,当所有的副本都down掉时,必须及时作出反应。可以有以下两种选择:
  1. 等待ISR中的任何一个节点恢复并担任leader。
  2. 选择所有节点中(不只是ISR)第一个恢复的节点作为leader.
这是一个在可用性和连续性之间的权衡。如果等待ISR中的节点恢复,一旦ISR中的节点起不起来或者数据都是了,那集群就永远恢复不了了。如果等待ISR意外的节点恢复,这个节点的数据就会被作为线上数据,有可能和真实的数据有所出入,因为有些数据它可能还没同步到。Kafka目前选择了第二种策略,在未来的版本中将使这个策略的选择可配置,可以根据场景灵活的选择。

这种窘境不只Kafka会遇到,几乎所有的分布式数据系统都会遇到。

副本管理

以上仅仅以一个topic一个分区为例子进行了讨论,但实际上一个Kafka将会管理成千上万的topic分区.Kafka尽量的使所有分区均匀的分布到集群所有的节点上而不是集中在某些节点上,另外主从关系也尽量均衡这样每个几点都会担任一定比例的分区的leader.

优化leader的选择过程也是很重要的,它决定了系统发生故障时的空窗期有多久。Kafka选择一个节点作为“controller”,当发现有节点down掉的时候它负责在游泳分区的所有节点中选择新的leader,这使得Kafka可以批量的高效的管理所有分区节点的主从关系。如果controller down掉了,活着的节点中的一个会备切换为新的controller.

kafka的幂等性 

             在之前的旧版本中,Kafka只能支持两种语义:At most once和At least once。At most once保证消息不会朝服,但是可能会丢失。在实践中,很有有业务会选择这种方式。At least once保证消息不会丢失,但是可能会重复,业务在处理消息需要进行去重。Kafka在0.11.0.0版本支持增加了对幂等的支持。幂等是针对生产者角度的特性。幂等可以保证上生产者发送的消息,不会丢失,而且不会重复

如何实现幂等性

              HTTP/1.1中对幂等性的定义是:一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同

Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.

实现幂等的关键点就是服务端可以区分请求是否重复,过滤掉重复的请求。要区分请求是否重复的有两点:

  • 唯一标识:要想区分请求是否重复,请求中就得有唯一标识。例如支付请求中,订单号就是唯一标识
  • 记录下已处理过的请求标识:光有唯一标识还不够,还需要记录下那些请求是已经处理过的,这样当收到新的请求时,用新请求中的标识和处理记录进行比较,如果处理记录中有相同的标识,说明是重复交易,拒绝掉

Kafka幂等性实现原理

为了实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number(序列号)。

  • PID:每个新的Producer在初始化的时候会被分配一个唯一的PID,这个PID对用户是不可见的。
  • Sequence Numbler:(对于每个PID,该Producer发送数据的每个<Topic, Partition>都对应一个从0开始单调递增的Sequence Number

Kafka可能存在多个生产者,会同时产生消息,但对Kafka来说,只需要保证每个生产者内部的消息幂等就可以了,所有引入了PID来标识不同的生产者。

对于Kafka来说,要解决的是生产者发送消息的幂等问题。也即需要区分每条消息是否重复。Kafka通过为每条消息增加一个Sequence Numbler,通过Sequence Numbler来区分每条消息。每条消息对应一个分区,不同的分区产生的消息不可能重复。所有Sequence Numbler对应每个分区

如何避免消息重复提交

             Broker端在缓存中保存了这seq number,对于接收的每条消息,如果其序号比Broker缓存中序号大于1(s_new=s_old+1),如果s_new>s_old+1则说明中间有数据没有写入,此时生产者会抛出异常。这样就可以实现了消息重复提交了。但是,只能保证单个Producer对于同一个<Topic, Partition>的Exactly Once语义。不能保证同一个Producer一个topic不同的partion幂等,并且不会对消息的内容进行幂等性。

kafka幂等性的注意事项

  1.    当幂等性开启的时候acks即为all。如果显性的将acks设置为0,1,那么将会报错Must set acks to all in order to use the idempotent producer. Otherwise we cannot guarantee idempotence.
  2. MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION 小于等于5

为什么要求 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION 小于等于5

           其实这里,要求 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION 小于等于 5 的主要原因是:Server 端的 ProducerStateManager 实例会缓存每个 PID 在每个 Topic-Partition 上发送的最近 5 个batch 数据(这个 5 是写死的,至于为什么是 5,可能跟经验有关,当不设置幂等性时,当这个设置为 5 时,性能相对来说较高,社区是有一个相关测试文档),如果超过 5,ProducerStateManager 就会将最旧的 batch 数据清除。假设应用将 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION 设置为 6,假设发送的请求顺序是 1、2、3、4、5、6,这时候 server 端只能缓存 2、3、4、5、6 请求对应的 batch 数据,这时候假设请求 1 发送失败,需要重试,当重试的请求发送过来后,首先先检查是否为重复的 batch,这时候检查的结果是否,之后会开始 check 其 sequence number 值,这时候只会返回一个 OutOfOrderSequenceException 异常,client 在收到这个异常后,会再次进行重试,直到超过最大重试次数或者超时,这样不但会影响 Producer 性能,还可能给 Server 带来压力(相当于client 狂发错误请求)。

幂等性示例

生产者要使用幂等性很简单,只需要增加以下配置即可:

props.put(enable.idempotence,"true");
或者
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
Properties props = new Properties();
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
props.put("acks", "all"); // 当 enable.idempotence 为 true,这里默认为 all
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

KafkaProducer producer = new KafkaProducer(props);
producer.send(new ProducerRecord(topic, "test");

Prodcuer 幂等性对外保留的接口非常简单,其底层的实现对上层应用做了很好的封装,应用层并不需要去关心具体的实现细节,对用户非常友好

注意;Kafka的幂等性可以保证生产只对一个分区实现Exactl once语义,需要多个分区也实现这个语义,还需要引入消息事务机制保证原子性。

kafk事务机制

             幂等设计只能保证单个Producer对于同一个<Topic, Partition>Exactly Once语义。另外,它并不能保证写操作的原子性——即多个写操作,要么全部被Commit要么全部不被Commit。更不能保证多个读写操作的的原子性。尤其对于Kafka Stream应用而言,典型的操作即是从某个Topic消费数据,经过一系列转换后写回另一个Topic,保证从源Topic的读取与向目标Topic的写入的原子性有助于从故障中恢复。事务保证可使得应用程序将生产数据和消费数据当作一个原子单元来处理,要么全部成功,要么全部失败,即使该生产或消费跨多个<Topic, Partition>

kafka事务api注意事项

  1. 需要消费者的自动模式设置为false,并且不能子再手动的进行执行consumer#commitSync或者consumer#commitAsyc
  2. 生产者配置transaction.id属性,props.setProperty("transcational.id", "tid");
  3. 生产者不需要再配置enable.idempotence,因为如果配置了transaction.id,则此时enable.idempotence会被设置为true
  4. 消费者需要配置Isolation.level。在consume-trnasform-produce模式下使用事务时,必须设置为READ_COMMITTED。

事务使用方式

Producer <String,String> producer = new KafkaProducer <>(props,new StringSerializer(),new StringSerializer()); 
     producer.initTransactions(); 
     try { 
         producer.beginTransaction(); 
         for(int i = 0; i <100; i ++)
             producer.send(new ProducerRecord <>(“my-topic”,Integer.toString(i),Integer.toString(i))); 
         producer.commitTransaction(); 
     } catch(ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e){
         //我们无法从这些异常中恢复,因此我们唯一的选择是关闭生产者并退出。
         producer.close(); 
     } catch(KafkaException e){ 
         //对于所有其他异常,只需中止事务并再试一次。
         producer.abortTransaction(); 
     } 
     producer.close();

2.kafka调优

2.1 producer端参数

batch.size: producer发送消息的批次大小,batch越大就可以将 更多消息封装到一个请求,提升这个参数可以提升吞吐量 linger.ms: 提升这个参数可以让producer等待更长时间发送数据, 每次就会发送更多的batch,这个值默认是0

compression.type: kafka支持GZIP、Snappy、LZ4,目前kafka 和LZ4组合的性能是最好的,如果CPU资源充足,可以启用 producer端压缩 acks: 0:不用等待broker响应 1:只要leader返回应答算写入成功 all:ISR中的副本都要写入才算成功

2.2 kafka的jvm调优

JVM设置 因为kafka并未大量使用堆上内存,而是使用对外内存,所以不需要太大的堆空间,一般设置6G就够了,LinkedIn 1500+台kafka集群,其JVM也是6G。用G1收集器

 2.3  吞吐量

理论上分区越多并发量就越大,但并不是分区越多越好,producer会为每个分区缓存数据,达到一定条件就会发送,如果分区过多,占用的内存就越大,而且分区越多,日志段文件就越多,kafka一旦打开文件便不会显式在关闭文件,文件句柄就不会释放,文件句柄会越来越多,如果某个broker挂掉,恢复的时间久越长

2.Kafka功能概述

2.1 首先有几个概念:

  1. · Kafka作为一个集群运行在一台或多台可以跨越多个数据中心的服务器上。
  2. · kafka集群在称为主题的类别中存储记录流。
  3. · 每个记录由一个键,一个值和一个时间戳组成。

2.2 kafka有四个核心API:

  1. · 生产者API允许应用程序发布的记录流至一个或多个kafka的话题。
  2. · 消费者API允许应用程序订阅一个或多个主题,并处理所产生的对他们记录的数据流。
  3. · 流API允许应用程序充当流处理器,从一个或多个主题消耗的输入流,并产生一个输出流至一个或多个输出的主题,有效地变换所述输入流,以输出流。
  4. · 连接器API允许构建和运行卡夫卡主题连接到现有的应用程序或数据系统中重用生产者或消费者。例如,连接到关系数据库的连接器可能会捕获对表的每个更改。

2.3 生产者API

Producer API允许应用程序将数据流发送到Kafka集群中的主题。

Producer发布消息

  • producer 采用 push 模式将消息发布到 broker,每条消息都被 append 到 partition 中,属于顺序写磁盘(顺序读写极大的提高了写数据的速度)
  • producer 发送消息到 broker 时,会根据分区算法选择将其存储到哪一个partition

1. 指定了 partition,则直接使用

2. 未指定 partition 但指定 key,通过对 key 的 value 进行hash 选出一个 partition

3. partition 和 key 都未指定,使用轮询选出一个 partition 。

写数据流程


1. producer 先从 zookeeper 的 "/brokers/.../state" 节点找到该 partition 的 leader
2. producer 将消息发送给该 leader
3. leader 将消息写入本地 log
4. followers 从 leader pull 消息,写入本地 log 后 leader 发送 ACK
5. leader 收到所有 ISR 中的 replica 的 ACK 后,增加 HW(high watermark,最后 commit 的 offset) 并向 producer 发送 ACK

Producer内部数据流程图

流程:用户构建producerRcord,然后调用kafkaProducer,然后kafkaProducer接受数据进行序列化,然后结合本地缓存的元数据一起发送给partitioner去确定目标分区,最后追加写入内存的消息缓冲池。

要使用生产者,你可以使用下面的maven依赖关系:

<dependency>  
  <groupId>org.apache.kafka</groupId>   
 <artifactId>kafka-clients</artifactId>
  <version>1.1.0</version>
</dependency>     

ProducerRecord()所有的 构造方法

  1. ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value)
  2. ProducerRecord(String topic, Integer partition, K key, V value)
  3. ProducerRecord(String topic, K key, V value)
  4. ProducerRecord(String topic, V value)

Producer配置项

acks参数:
在考虑请求完成之前,生产者要求leader收到的确认数量,这将控制发送的记录的持久性。

  1. acks=0如果设置为零,则生产者不会等待来自服务器的任何确认。该记录将被立即添加到套接字缓冲区并被视为已发送。在这种情况下,retries不能保证服务器已经收到记录,并且配置不会生效(因为客户端通常不会知道任何故障)。为每个记录返回的偏移量将始终设置为-1。
  2. acks=1这意味着领导者会将记录写入其本地日志中,但会在未等待所有追随者完全确认的情况下作出响应。在这种情况下,如果领导者在承认记录后但在追随者复制之前立即失败,那么记录将会丢失。
  3. acks=all这意味着领导者将等待全套的同步副本确认记录。这保证只要至少有一个同步副本保持活动状态,记录就不会丢失。这是最强有力的保证。这相当于acks = -1设置。

batch.size
只要有多个记录被发送到同一个分区,生产者就会尝试将记录一起分成更少的请求。这有助于客户端和服务器的性能。该配置以字节为单位控制默认的批量大小。不会尝试批量大于此大小的记录。发送给brokers的请求将包含多个批次,每个分区有一个可用于发送数据的分区。小批量大小将使批次不太常见,并可能降低吞吐量(批量大小为零将完全禁用批次)。一个非常大的批量大小可能会更浪费一点使用内存,因为我们将始终为预期的额外记录分配指定批量大小的缓冲区。

buffer.memory
生产者可用于缓冲等待发送到服务器的记录的总内存字节数。如果记录的发送速度比发送到服务器的速度快,那么生产者将会阻止max.block.ms它,然后它会抛出异常。

这个设置应该大致对应于生产者将使用的总内存,但不是硬性限制,因为不是所有生产者使用的内存都用于缓冲。一些额外的内存将用于压缩(如果启用了压缩功能)以及维护正在进行的请求。

compression.type
指定给定主题的最终压缩类型。该配置接受标准压缩编解码器('gzip','snappy','lz4')。它另外接受'未压缩',这相当于没有压缩,这意味着保留制片人设置的原始压缩编解码器,也可以修改源码,自定义压缩类型。

connections.max.idle.ms
在此配置指定的毫秒数后关闭空闲连接。

linger.ms
生产者将在请求传输之间到达的任何记录归入单个批处理请求。通常情况下,这只会在记录到达速度快于发送时才发生。但是,在某些情况下,即使在中等负载下,客户端也可能希望减少请求的数量。此设置通过添加少量人工延迟来实现此目的 - 即不是立即发送记录,而是生产者将等待达到给定延迟以允许发送其他记录,以便发送可以一起批量发送。这可以被认为与TCP中的Nagle算法类似。这个设置给出了批量延迟的上限:一旦我们得到batch.size值得记录的分区,它将被立即发送而不管这个设置如何,但是如果我们为这个分区累积的字节数少于这个数字,我们将在指定的时间内“等待”,等待更多的记录出现。该设置默认为0(即无延迟)。linger.ms=5例如,设置可以减少发送请求的数量,但会对在无效负载中发送的记录添加高达5毫秒的延迟。

retries
生产者发送失败后的重试次数,默认0

其余参数请查询官网

2.4. 消费者API

Consumer API允许应用程序从Kafka集群中的主题读取数据流。要使用消费者,您可以使用以下maven依赖项:

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>1.1.0</version>
</dependency>

poll(timeout):poll方法是consumer的核心方法之一,返回的是订阅的分区上的一组消息,如果某些分区没有准备好,那么poll返回空的消息集合,poll()返回需要满足以下任意一个条件即可返回:

1 获取了足够多的数据

2 等待时间超过了指定的超时设置

注意3:这里与sparkstreaming拉取kafka数据有些不同,spark拉取kafka中的数据默认时间是512毫秒,一般需要改成更大的时间

注意4:只要不更改group.id,每次重新消费kafka,都是从上次消费结束的地方继续开始,不论"auto.offset.reset”属性设置的是什么

2.5   AdminClient API

AdminClient API支持管理和检查主题,代理,acl和其他Kafka对象。

要使用AdminClient API,请添加以下Maven依赖项:

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>1.1.0</version>
</dependency>

生产者与消费

学会利用官网学习

http://spark.apachecn.org/docs/cn/2.2.0/streaming-programming-guide.html#input-dstreams-%E5%92%8C-receivers%E6%8E%A5%E6%94%B6%E5%99%A8

相关参数说明:

Spark streaming的参数配置

每秒钟每个分区kafka拉取消息的速率

 conf.set("spark.streaming.kafka.maxRatePerPartition", "5")

程序优雅的关闭    

conf.set("spark.streaming.stopGracefullyOnShutdown", "true")

spark拉取kafka数据等待时间,默认512毫秒可以设置大一点,10秒,一分钟

spark.streaming.kafka.consumer.poll.ms=10000 (默认512)

Kafka的参数配置 

val kafkaParams = Map[String, Object](
            "bootstrap.servers" -> "kk-01:9092,kk-02:9092,kk-03:9092",
            "key.deserializer" -> classOf[StringDeserializer], // 类.class.getName
            "value.deserializer" -> classOf[StringDeserializer],
            "group.id" -> groupId,
            "auto.offset.reset" -> "earliest",
 // 不记录消费的偏移量信息  true 记录
            "enable.auto.commit" -> (false: java.lang.Boolean)        )

kafka offset 维护方式

zookeeper维护

  1. 使用zookeeper来维护offset
    kafka 0.9 以前的版本是将offset 存储在zookeeper上的,kafka在传输数据时,数据消费成功就会修改偏移量,这样就可以保证数据不会丢失而导致传输出错;但是这也存在一个问题:那就是每次消费数据时都要将数据的offset写入一次,当读写速度非常快的时候,需要频繁的读取zk,效率比较低,而且zookeeper与kafka的offset变化确认也需要走网络IO,这样就会给offset的维护带来不稳定性和低效。

kafka自己维护offset

  1. 使用broker来维护offset
    kafka 0.9 以后,offset的使用了内部的broker来管理,这样仅仅只需要broker,而不要zookeeper来维护,都是将topic提交给__consumer_offsets函数来执行。

high level 和low level

  1. 将zookeeper维护offset 的方式称为 low level API
  2. 将kafka broker 维护offset的方式称为high level API
  1. 使用high level API 更新offset具体设置
  1. 自动提交:设置enable.auto.commit=true,(默认值是true),更新的频率根据参数【auto.commit.interval.ms】(范围:[0,Integer.MAX],默认值是 5000 (5 s))来定。这种方式也被称为【at most once】,fetch到消息后就可以更新offset,无论是否消费成功。
  2. 手动提交:设置enable.auto.commit=false,这种方式称为【at least once】。fetch到消息后,等消费完成再调用方法【consumer.commitSync()】,手动更新offset;如果消费失败,则offset也不会更新,此条消息会被重复消费一次。

"auto.offset.reset"参数设置

Kafka单独写consumer时

可选参数:

earliest:自动将偏移重置为最早的偏移量

latest:自动将偏移量重置为最新的偏移量(默认)

none:如果consumer group没有发现先前的偏移量,则向consumer抛出异常。

其他的参数:向consumer抛出异常(无效参数)

参考: 
http://kafka.apache.org/documentation/

SparkStreaming整合时:

注意:和SparkStreaming整合时,上面的可选参数是无效的,只有两个可选参数:

smallest:简单理解为从头开始消费,其实等价于上面的 earliest

largest:简单理解为从最新的开始消费,其实等价于上面的 latest

 屏蔽日志

Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)

 Kafka源码中的Producer Record定义

1. ProducerRecord<K, V> 含义: 

发送给Kafka Broker的key/value 值对

原码:

public final class ProducerRecord<K, V> {
private final String topic;
private final Integer partition;
private final K key;
private final V value; }

2.内部数据结构:

-- Topic (名字)
-- PartitionID ( 可选)
-- Key[( 可选 )
-- Value

3.生产者记录(简称PR)的发送逻辑:

<1> 若指定Partition ID,则PR被发送至指定Partition

<2> 若未指定Partition ID,但指定了Key, PR会按照hasy(key)发送至对应Partition

<3> 若既未指定Partition ID也没指定Key,PR会按照round-robin模式发送到每个Partition

<4> 若同时指定了Partition ID和Key, PR只会发送到指定的Partition (Key不起作用,代码逻辑决定)

4.生产者记录(PR)的实现:

 针对3,提供三种构造函数形参:

-- ProducerRecord(topic, partition, key, value)

-- ProducerRecord(topic, key, value)

-- ProducerRecord(topic, value)

生产者与消费者实例

生产者

代码核心:

第一步:使用一个map去封装一个kafka生产者 的信息,然后创建一个生产者

val producer = new KafkaProducer[String, String](props)

第二步:根据自己的需求创建一个消息对象,然后由生产者发送到kafka中去

 val message = new ProducerRecord[String, String](topic, null, str)
 producer.send(message)

生产者案例


package spark_kafka
import java.util
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord}

object KafkaProduce {
  def main(args: Array[String]): Unit = {
    val topic = "test"
    val brokers = "wangzhihua1:9092,wangzhihua2:9092,wangzhua3:9092"
    val messagesPerSec=1 //每秒发送几条信息
    val wordsPerMessage =4 //一条信息包括多少个单词
    val props = new util.HashMap[String,Object]()
    props.put("bootstrap.servers", brokers)
props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer")
props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer")
 val producer = new KafkaProducer[String, String](props)
    while(true) {
      (1 to messagesPerSec.toInt).foreach { messageNum =>
        val str = (1 to wordsPerMessage.toInt).map(x => scala.util.Random.nextInt(10).toString)
          .mkString(" ")
        val message = new ProducerRecord[String, String](topic, null, str)
        producer.send(message)
        println(message)
      }
      Thread.sleep(1000)
    }
  }

消费者


import org.apache.spark.streaming._
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.KafkaUtils
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe

object  Kafka_consumer {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("kafka_wordcount").setMaster("local[*]")
    val ssc = new  StreamingContext(conf,Seconds(2))
//设置kafka连接参数
    val kafkaParams = Map[String, Object](
    "bootstrap.servers" -> "wangzhihua1:9092,wangzhihua2:9092,wangzhua3:9092",
    "key.deserializer" -> classOf[StringDeserializer],
    "value.deserializer" -> classOf[StringDeserializer],
    "group.id" -> "use_a_separate_group_id_for_each_stream",
    "auto.offset.reset" -> "latest",
    "enable.auto.commit" -> (false: java.lang.Boolean)
  )
    val messages: InputDStream[ConsumerRecord[String, String]] =      KafkaUtils.createDirectStream[String, String](
      ssc,
      PreferConsistent,
      Subscribe[String, String](Array("test"), kafkaParams)
    )
   messages.map(t=>t.value()).print()
    ssc.start()
    ssc.awaitTermination()
  }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿华田512

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值