【MQ】Kafka笔记

笔记来源:尚硅谷视频笔记2.0版+2.1版 黑马视频:Kafka深入探秘者来了

  • kafka笔记地址:https://blog.csdn.net/hancoder/article/details/107446151
  • 笔记、代码下载地址:https://me.csdn.net/download/hancoder
  • 黑马kafka笔记下载地址:https://download.csdn.net/download/hancoder/12638762
  • 黑马视频地址(又名kafka探秘者):https://www.bilibili.com/video/BV1oK4y1x75C
  • 慕课网视频:Kafka多维度系统精讲,从入门到熟练掌握(可以取吾爱破解网找资源)
  • 慕课网的视频笔记地址:https://blog.csdn.net/hancoder/article/details/108553335

第1章 Kafka概述

1.1 定义

Kafka 是一个分布式的基于【发布/订阅模式】的消息队列(Message Queue),主要应用于大数据实时处理领域。

在流式计算中,Kafka一般用来缓存数据,Storm通过消费Kafka的数据进行计算。

1)Apache Kafka是一个开源消息系统,由Scala写成。是由Apache软件基金会开发的一个开源消息系统项目。(是基于scala开发的)

2)Kafka最初是由LinkedIn公司开发,并于2011年初开源。2012年10月从Apache Incubator毕业。该项目的目标是为处理实时数据提供一个统一、高通量、低等待的平台。

3)Kafka是一个分布式消息队列。Kafka对消息保存时根据Topic主题进行归类,发送消息者称为Producer,消息接受者称为Consumer,此外kafka集群有多个kafka实例组成,每个实例(server)称为broker。每台kafka服务器称为broker

消息队列保存在一个一个的Topic主题中。类似于一个水池子。

4)无论是kafka集群,还是consumer都依赖于zookeeper集群保存一些meta信息,来保证系统可用性。

1.2 消息队列

消息队列又称消息引擎,消息中间件

1.2.1 传统消息队列的应用场景

  • 异步处理
  • 解耦
  • 削峰
  • 日志处理

异步处理,应用解耦,流量削锋和消息通讯四个场景。

2.1异步处理
场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种 1.串行的方式;2.并行方式
a、串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。
【MQ】Kafka笔记_第1张图片

b、并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间
【MQ】Kafka笔记_第2张图片

假设三个业务节点每个使用50毫秒钟,不考虑网络等其他开销,则串行方式的时间是150毫秒,并行的时间可能是100毫秒。
因为CPU在单位时间内处理的请求数是一定的,假设CPU1秒内吞吐量是100次。则串行方式1秒内CPU可处理的请求量是7次(1000/150)。并行方式处理的请求量是10次(1000/100)
小结:如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。如何解决这个问题呢?

引入消息队列,将不是必须的业务逻辑,异步处理。改造后的架构如下:
【MQ】Kafka笔记_第3张图片
按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。

2.2应用解耦
场景说明:用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。如下图:
【MQ】Kafka笔记_第4张图片
传统模式的缺点:假如库存系统无法访问,则订单减库存将失败,从而导致订单失败,订单系统与库存系统耦合

如何解决以上问题呢?引入应用消息队列后的方案,如下图:
【MQ】Kafka笔记_第5张图片
订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功
库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作
假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦

2.3流量削锋
流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。
应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。
a、可以控制活动的人数
b、可以缓解短时间内高流量压垮应用
【MQ】Kafka笔记_第6张图片
用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。
秒杀业务根据消息队列中的请求信息,再做后续处理

2.4日志处理
日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。架构简化如下
【MQ】Kafka笔记_第7张图片
日志采集客户端,负责日志数据采集,定时写受写入Kafka队列
Kafka消息队列,负责日志数据的接收,存储和转发
日志处理应用:订阅并消费kafka队列中的日志数据

2.5消息通讯
消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等
点对点通讯:
【MQ】Kafka笔记_第8张图片
客户端A和客户端B使用同一队列,进行消息通讯。

聊天室通讯:
【MQ】Kafka笔记_第9张图片
客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果。

以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。

三、消息中间件示例
3.1电商系统
【MQ】Kafka笔记_第10张图片
消息队列采用高可用,可持久化的消息中间件。比如Active MQ,Rabbit MQ,Rocket Mq。
(1)应用将主干逻辑处理完成后,写入消息队列。消息发送是否成功可以开启消息的确认模式。(消息队列返回消息接收成功状态后,应用再返回,这样保障消息的完整性)
(2)扩展流程(发短信,配送处理)订阅队列消息。采用推或拉的方式获取消息并处理。
(3)消息将应用解耦的同时,带来了数据一致性问题,可以采用最终一致性方式解决。比如主数据写入数据库,扩展应用根据消息队列,并结合数据库方式实现基于消息队列的后续处理。

3.2日志收集系统
【MQ】Kafka笔记_第11张图片
分为Zookeeper注册中心,日志收集客户端,Kafka集群和Storm集群(OtherApp)四部分组成。
Zookeeper注册中心,提出负载均衡和地址查找服务
日志收集客户端,用于采集应用系统的日志,并将数据推送到kafka队列
Kafka集群:接收,路由,存储,转发等消息处理
Storm集群:与OtherApp处于同一级别,采用拉的方式消费队列中的数据

四、JMS消息服务
讲消息队列就不得不提JMS 。JMS(JAVA Message Service,java消息服务)API是一个消息服务的标准/规范,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。
在EJB架构中,有消息bean可以无缝的与JM消息服务集成。在J2EE架构模式中,有消息服务者模式,用于实现消息与应用直接的解耦。

4.1消息模型
在JMS标准中,有两种消息模型P2P(Point to Point),Publish/Subscribe(Pub/Sub)。

4.1.1 P2P模式
【MQ】Kafka笔记_第12张图片
P2P模式包含三个角色:消息队列(Queue),发送者(Sender),接收者(Receiver)。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。

P2P的特点
每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在消息队列中)
发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响到消息被发送到队列
接收者在成功接收消息之后需向队列应答成功
如果希望发送的每个消息都会被成功处理的话,那么需要P2P模式。

4.1.2 Pub/Sub模式
【MQ】Kafka笔记_第13张图片
包含三个角色主题(Topic),发布者(Publisher),订阅者(Subscriber) 多个发布者将消息发送到Topic,系统将这些消息传递给多个订阅者。

Pub/Sub的特点
每个消息可以有多个消费者
发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息
为了消费消息,订阅者必须保持运行的状态
为了缓和这样严格的时间相关性,JMS允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。
如果希望发送的消息可以不被做任何处理、或者只被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用Pub/Sub模型。

4.2消息消费
在JMS中,消息的产生和消费都是异步的。对于消费来说,JMS的消息者可以通过两种方式来消费消息。
(1)同步
订阅者或接收者通过receive方法来接收消息,receive方法在接收到消息之前(或超时之前)将一直阻塞;

(2)异步
订阅者或接收者可以注册为一个消息监听器。当消息到达之后,系统自动调用监听器的onMessage方法。

JNDI:Java命名和目录接口,是一种标准的Java命名系统接口。可以在网络上查找和访问服务。通过指定一个资源名称,该名称对应于数据库或命名服务中的一个记录,同时返回资源连接建立所必须的信息。
JNDI在JMS中起到查找和访问发送目标或消息来源的作用。

五、常用消息队列

一般商用的容器,比如WebLogic,JBoss,都支持JMS标准,开发上很方便。但免费的比如Tomcat,Jetty等则需要使用第三方的消息中间件。本部分内容介绍常用的消息中间件(Active MQ,Rabbit MQ,Zero MQ,Kafka)以及他们的特点。

5.1 ActiveMQ
ActiveMQ 是Apache出品,最流行的,能力强劲的开源消息总线。ActiveMQ 是一个完全支持JMS1.1和J2EE 1.4规范的 JMS Provider实现,尽管JMS规范出台已经是很久的事情了,但是JMS在当今的J2EE应用中间仍然扮演着特殊的地位。

ActiveMQ特性如下:
⒈ 多种语言和协议编写客户端。语言: Java,C,C++,C#,Ruby,Perl,Python,PHP。应用协议: OpenWire,Stomp REST,WS Notification,XMPP,AMQP
⒉ 完全支持JMS1.1和J2EE 1.4规范 (持久化,XA消息,事务)
⒊ 对Spring的支持,ActiveMQ可以很容易内嵌到使用Spring的系统里面去,而且也支持Spring2.0的特性
⒋ 通过了常见J2EE服务器(如 Geronimo,JBoss 4,GlassFish,WebLogic)的测试,其中通过JCA 1.5 resource adaptors的配置,可以让ActiveMQ可以自动的部署到任何兼容J2EE 1.4 商业服务器上
⒌ 支持多种传送协议:in-VM,TCP,SSL,NIO,UDP,JGroups,JXTA
⒍ 支持通过JDBC和journal提供高速的消息持久化
⒎ 从设计上保证了高性能的集群,客户端-服务器,点对点
⒏ 支持Ajax
⒐ 支持与Axis的整合
⒑ 可以很容易得调用内嵌JMS provider,进行测试

5.2 Kafka
Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。 对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka的目的是通过Hadoop的并行加载机制来统一线上和离线的消息处理,也是为了通过集群机来提供实时的消费。

Kafka是一种高吞吐量的分布式发布订阅消息系统,有如下特性:
通过O(1)的磁盘数据结构提供消息的持久化,这种结构对于即使数以TB的消息存储也能够保持长时间的稳定性能。(文件追加的方式写入数据,过期的数据定期删除)
高吞吐量:即使是非常普通的硬件Kafka也可以支持每秒数百万的消息
支持通过Kafka服务器和消费机集群来分区消息
支持Hadoop并行数据加载

一般应用在大数据日志处理或对实时性(少量延迟),可靠性(少量丢数据)要求稍低的场景使用。

消息队列的好处

1)解耦:

允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。

2)冗余:

消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。

3)扩展性:

因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。

4)灵活性 & 峰值处理能力:

在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。

5)可恢复性:

系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。

6)顺序保证:

在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。(Kafka保证一个Partition内的消息的有序性)

7)缓冲:

有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。

8)异步通信:

很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。

1.2.2 消息队列的两种模式

消息队列分为

  • 点对点模式
  • 发布/订阅模式

(1)点对点模式(一对一,消费者主动拉取数据,消息收到后消息清除)

点对点模型通常是一个基于拉取或者轮询的消息传送模型,这种模型从队列中请求信息,而不是将消息推送到客户端。这个模型的特点是发送到队列的消息被一个且只有一个接收者接收处理,即使有多个消息监听者也是如此。

  • 生产者生产消息发送到Queue中,然后消息消费者从Queue中取出并且消费消息。

  • 消息被消费后就从队列queue移除该消息,所以消费者不可能再消费到

  • 每条消息由一个生产者生产,且只被一个消费者消费(即使该队列有多个消费者)。

  • 生产者和消费者是一对一模式

【MQ】Kafka笔记_第14张图片

(2)发布/订阅模式(一对多,数据生产后,推送给所有订阅者,消费者消费数据之后不会清除消息)

发布订阅模型则是一个基于推送的消息传送模型。发布订阅模型可以有多种不同的订阅者,临时订阅者只在主动监听主题时才接收消息,而持久订阅者则监听主题的所有消息,即使当前订阅者不可用,处于离线状态。

  • 所有订阅了该主题的消费者都能收到同样的消息
  • 推送的速度成了发布订阅模模式的一个问题

【MQ】Kafka笔记_第15张图片

1.3 Kafka架构

Kafka整体架构图

img

1.4 kafka术语

producer和consumer都是客户端,broker才是服务端

1)Producer :消息生产者,就是向kafka broker发消息的客户端;

2)Consumer :消息消费者,向kafka broker取消息的客户端;

3)Topic :可以理解为一个队列, 生产者和消费者面向的都是一个 topic;

4) Consumer Group (CG)消费者组:这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个consumer)的手段。一个topic可以有多个CG。topic的消息会复制(不是真的复制,是概念上的)到所有的CG,但每个partion只会把消息发给该CG中的一个consumer。如果需要实现广播,只要每个consumer有一个独立的CG就可以了。要实现单播只要所有的consumer在同一个CG。用CG还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic;。 消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个 组内 消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即 消费者组是逻辑上的一个订阅者

5)Broker :一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以容纳多个topic;

6)Partition分区:为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列。partition中的每条消息都会被分配一个有序的id(offset)。kafka只保证按一个partition中的顺序将消息发给consumer,不保证一个topic的整体(多个partition间)的顺序;是针对主题的分区,而不是broker

ES也有分区的概念,记住所谓分区就是把数据分开放,每个分区都存储一定范围的key

7)Offset:kafka的存储文件都是按照offset.kafka来命名,用offset做名字的好处是方便查找。例如你想找位于2049的位置,只要找到2048.kafka的文件即可。当然the first offset就是0000000000 0.kafka。

8) Replica: 副本,为保证集群中的某个节点发生故障时,该节点上的 partition 数据不丢失,且 kafka 仍然能够继续工作,kafka 提供了副本机制,一个 topic 的每个分区都有若干个副本,一个 leader 和若干个 follower。kafka保证同一个partition的多个replication一定不会分配在同一台broker上.如果同一个partition的多个replication在同一个broker上,那么备份就没有意义了

ES中页一样,每个分区都有多个副本

既然有副本就设计主从了

9 )leader :每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 leader。服务和消费都只找leader,follow仅仅当备份作用

10 )follower :每个分区多个副本中的“从”,实时从 leader 中同步数据,保持和 leader 数据的同步。leader 发生故障时,某个 follower 会成为新的 follower。follower只用于同步数组,消费者不能从follower消费

如果用作秒杀的话,因为只能做单分区,所以kafka是没有优势的

每个分区数据不同

默认持久化,可配置临时,但是是以时间计数,不是消费过后删除

大数据流式计算,追加

选举很快

建议hash,不建议轮询

分区给消费者组是互斥的

有分区之后就不能保证绝对顺序,只能保证分区顺序

1.5 zk与kafka的关系

1.5.1、Broker注册

zk监听broker的上下线

Broker是分布式部署并且相互之间相互独立,但是需要有一个注册系统能够将整个集群中的Broker管理起来,此时就使用到了Zookeeper。在Zookeeper上会有一个专门用来进行Broker服务器列表记录的节点:

/brokers/ids

每个Broker在启动时,都会到Zookeeper上进行注册,即到/brokers/ids下创建属于自己的节点,如/brokers/ids/[0...N]

Kafka使用了全局唯一的数字来指代每个Broker服务器,不同的Broker必须使用不同的Broker ID进行注册,创建完节点后,每个Broker就会将自己的IP地址和端口信息记录到该节点中去。其中,Broker创建的节点类型是临时节点,一旦Broker宕机,则对应的临时节点也会被自动删除。

Kafka 集群中有一个 broker 会被选举为 Controller,负责管理集群 broker 的上下线,所有 topic 的分区副本分配和 leader 选举等工作。
Controller 的管理工作都是依赖于 Zookeeper 的。

以下为 partition 的 leader 选举过程:

1.5.2、Topic注册

在Kafka中,同一个Topic的消息会被分成多个分区并将其分布在多个Broker上,这些分区信息及与Broker的对应关系也都是由Zookeeper在维护,由专门的节点来记录,如:

/brokers/topics

Kafka中每个Topic都会以/brokers/topics/[topic]的形式被记录,如/brokers/topics/login和/brokers/topics/search等。Broker服务器启动后,会到对应Topic节点(/brokers/topics)上注册自己的Broker ID并写入针对该Topic的分区总数,如/brokers/topics/login/3->2,这个节点表示Broker ID为3的一个Broker服务器,对于"login"这个Topic的消息,提供了2个分区进行消息存储,同样,这个分区节点也是临时节点。

1.5.3、生产者负载均衡

由于同一个Topic消息会被分区并将其分布在多个Broker上,因此,生产者需要将消息合理地发送到这些分布式的Broker上,那么如何实现生产者的负载均衡,Kafka支持传统的四层负载均衡,也支持Zookeeper方式实现负载均衡。

  • (1) 四层负载均衡,根据生产者的IP地址和端口来为其确定一个相关联的Broker。通常,一个生产者只会对应单个Broker,然后该生产者产生的消息都发往该Broker。这种方式逻辑简单,每个生产者不需要同其他系统建立额外的TCP连接,只需要和Broker维护单个TCP连接即可。但是,其无法做到真正的负载均衡,因为实际系统中的每个生产者产生的消息量及每个Broker的消息存储量都是不一样的,如果有些生产者产生的消息远多于其他生产者的话,那么会导致不同的Broker接收到的消息总数差异巨大,同时,生产者也无法实时感知到Broker的新增和删除。
  • (2) 使用Zookeeper进行负载均衡,由于每个Broker启动时,都会完成Broker注册过程,生产者会通过该节点的变化来动态地感知到Broker服务器列表的变更,这样就可以实现动态的负载均衡机制。
1.5.4、消费者负载均衡

与生产者类似,Kafka中的消费者同样需要进行负载均衡来实现多个消费者合理地从对应的Broker服务器上接收消息,每个消费者组包含若干消费者,每条消息都只会发送给分组中的一个消费者,不同的消费者分组消费自己特定的Topic下面的消息,互不干扰。

1.5.5、分区 与 消费者 的关系

**消费组 (Consumer Group):**consumer group消费者组 下有多个 Consumer(消费者)。

对于每个消费者组 (Consumer Group),Kafka都会为其分配一个全局唯一的消费者组ID(Group ID),Group 内部的所有消费者共享该 ID。订阅的topic下的每个分区只能分配给某个 group 下的一个consumer(当然该分区还可以被分配给其他group)。

  • 分区一对多分组,但是分组内的消费者会争抢同一条消息

同时,Kafka为每个消费者分配一个Consumer ID,通常采用"Hostname:UUID"形式表示。

在Kafka中,规定了每个消息分区 只能被同组的一个消费者进行消费,因此,需要在 Zookeeper 上记录 消息分区 与 Consumer 之间的关系,每个消费者一旦确定了对一个消息分区的消费权力,需要将其Consumer ID 写入到 Zookeeper 对应消息分区的临时节点上,例如:

/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]

其中,[broker_id-partition_id]就是一个 消息分区 的标识,节点内容就是该 消息分区 上 消费者的Consumer ID。

1.5.6、消息 消费进度Offset 记录

在消费者对指定消息分区进行消息消费的过程中,需要定时地将分区消息的消费进度Offset记录到Zookeeper上(后面的版本改在kafka中记录),以便在该消费者进行重启或者其他消费者重新接管该消息分区的消息消费后,能够从之前的进度开始继续进行消息消费。Offset在Zookeeper中由一个专门节点进行记录,其节点路径为:

/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id]

节点内容就是Offset的值。

kafka0.9版本之前,zk会管理消费者消费进度offset

kafka0.9版本之后,消费者进度放到了kafka中

1.5.7、消费者注册

消费者服务器在初始化启动时加入消费者分组的步骤如下

注册到消费者分组。每个消费者服务器启动时,都会到Zookeeper的指定节点下创建一个属于自己的消费者节点,例如/consumers/[group_id]/ids/[consumer_id],完成节点创建后,消费者就会将自己订阅的Topic信息写入该临时节点。

对 消费者分组 中的 消费者 的变化注册监听。每个 消费者 都需要关注所属 消费者分组 中其他消费者服务器的变化情况,即对/consumers/[group_id]/ids节点注册子节点变化的Watcher监听,一旦发现消费者新增或减少,就触发消费者的负载均衡。

对Broker服务器变化注册监听。消费者需要对/broker/ids/[0-N]中的节点进行监听,如果发现Broker服务器列表发生变化,那么就根据具体情况来决定是否需要进行消费者负载均衡。

进行消费者负载均衡。为了让同一个Topic下不同分区的消息尽量均衡地被多个 消费者 消费而进行 消费者 与 消息 分区分配的过程,通常,对于一个消费者分组,如果组内的消费者服务器发生变更或Broker服务器发生变更,会发出消费者负载均衡。

zk会管理消费者、broker

1.5.8、kafka在zk中的架构图

以下是kafka在zookeeper中的详细存储结构图:

【MQ】Kafka笔记_第16张图片

【MQ】Kafka笔记_第17张图片

注意:producer不在zk中注册,消费者在zk中注册。

为什么把consumer的数据从zk转移到了kafka中?

早期版本的 kafka 用 zk 做 meta 信息存储,consumer 的消费状态,group 的管理以及 offset的值。

考虑到zk本身的一些因素以及整个架构较大概率存在单点问题,新版本中确实逐渐弱化了zookeeper的作用。新的consumer使用了kafka内部的group coordination协议,也减少了对zookeeper的依赖

1.7 消息格式

kafka的消息格式由很多字段组成。V1版本的消息完成格式为

  • CRC:4B
  • 版本号1B
  • 属性1B:低三位保存消息的压缩类型。0无压缩,1GZIP,2Snappy,3LZ4
  • 时间戳8B
  • key长度4B
  • key k个字节
  • value长度 4B
  • value v个字节

因为消息格式是确定的,每个字段都占用了固定的字节,如果我们发送一个非常小的消息的时候却花费了很多功夫在格式上。因此kafka使用紧凑二进制字节数组ByteBuffer而不是独立的对象,

第2章 Kafka集群部署与bash简单命令

2.1 集群规划

hadoop102 hadoop103 hadoop104

zk zk zk

kafka kafka kafka

2.2 Kafka集群安装部署

1)解压安装包

[atguigu@hadoop102 software]$ tar -zxvf kafka_2.11-0.11.0.0.tgz -C /opt/module/
# 可以改下文件夹名
[atguigu@hadoop102 module]$ mv kafka_2.11-0.11.0.0/ kafka

#在/opt/module/kafka目录下创建logs文件夹,存放kafka持久化文件
[atguigu@hadoop102 kafka]$ mkdir logs

2)分别在hadoop103和hadoop104上修改配置文件/opt/module/kafka/config/server.properties中的broker.id=1、broker.id=2

​ 注:broker.id不得重复

4)修改配置文件

[atguigu@hadoop102 kafka]$ cd config/
[atguigu@hadoop102 config]$ vi server.properties

server.properties输入以下内容:

#broker的全局唯一编号,不能重复
broker.id=0

#删除topic功能使能
delete.topic.enable=true

#处理网络请求的线程数量
num.network.threads=3

#用来处理磁盘IO的现成数量
num.io.threads=8

#发送套接字的缓冲区大小
socket.send.buffer.bytes=102400

#接收套接字的缓冲区大小
socket.receive.buffer.bytes=102400

#请求套接字的缓冲区大小
socket.request.max.bytes=104857600

#kkafka持久化消息的目录,可以用逗号分隔设置多个  #最好写成data。他里面的00000.log是存放的数据
log.dirs=/opt/module/kafka/logs

#topic在当前broker上的分区个数
num.partitions=1

#用来恢复和清理data下数据的线程数量
num.recovery.threads.per.data.dir=1

#segment文件保留的最长时间,超时将被删除
log.retention.hours=168

#配置连接Zookeeper集群地址,指定了zk的集群地址
zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181

5)配置环境变量

[atguigu@hadoop102 module]$ sudo vi /etc/profile

#KAFKA_HOME
export KAFKA_HOME=/opt/module/kafka
export PATH=$PATH:$KAFKA_HOME/bin

[atguigu@hadoop102 module]$ source /etc/profile

6)分发安装包

[atguigu@hadoop102 module]$ xsync kafka/

​ 注意:分发之后记得配置其他机器的环境变量

8)启动集群

依次在hadoop102、hadoop103、hadoop104节点上启动kafka

# 注意命令后面有个&符号。&符号代表后台启动
# 目录:/opt/module/kafka_2.11-0.11.0.0/
[atguigu@hadoop102 kafka]$ bin/kafka-server-start.sh config/server.properties &

[atguigu@hadoop103 kafka]$ bin/kafka-server-start.sh config/server.properties &

[atguigu@hadoop104 kafka]$ bin/kafka-server-start.sh config/server.properties &

9)关闭集群

[atguigu@hadoop102 kafka]$ bin/kafka-server-stop.sh stop

[atguigu@hadoop103 kafka]$ bin/kafka-server-stop.sh stop

[atguigu@hadoop104 kafka]$ bin/kafka-server-stop.sh stop

10)kafka 群起脚本

for i in hadoop102 hadoop103 hadoop104
do
echo "========== $i =========="
ssh  $i  '/opt/module/kafka/bin/kafka-server-start.sh  -daemon /opt/module/kafka/config/server.properties'
done

2.3 Kafka命令行操作

1)查看topic
# 查看当前服务器所有topic # 数据在zk集群里
bin/kafka-topics.sh --zookeeper hadoop102:2181 --list

# 查看刚才创建的topic
bin/kafka-topics.sh --zookeeper hadoop102:2181  --describe --topic first
2)创建topic
# 在zk集群里创建一个名为first的topic,该主题有2个分区,每个分区2个备份
bin/kafka-topics.sh --zookeeper hadoop102:2181 --create --replication-factor 2 --partitions 2 --topic first

# 这个用法和上面没有区别,只是多指定了zk机器,但是因为我们zk集群本来计算自动备份的,多指定几个只是怕集群里某个机器宕机了从而连不上全部集群。而如果没有宕机的话即使连的是zk的follower,他也会转发给zk的leader进行数据备份,备份好后通知zk的follower,该follower返回给kafka
bin/kafka-topics.sh --zookeeper hadoop102:2181,hadoop103:2181,hadoop104:2181 --create --replication-factor 2 --partitions 2 --topic first

#删除
bin/kafka-topics.sh --zookeeper hadoop102:2181,hadoop103:2181,hadoop104:2181 --delete --topic first

选项说明:

  • –topic 定义topic名
  • –replication-factor 定义副本数
  • –partitions 定义分区数

修改partition数目:

# 修改成2个分区 2个副本 只能增加分区,不能减少分区
bin/kafka-topics.sh --alter --zookeeper hadoop102:2181 --topic first --partitions 2

# 看一下修改成功没有
bin/kafka-topics.sh --zookeeper hadoop102:2181 --describe --topic first
3)删除topic
[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --zookeeper hadoop102:2181 --delete --topic first

前提:需要server.properties中设置delete.topic.enable=true否则只是标记删除或者直接重启。

  • 若delete.topic.enable=true:直接彻底删除该topic
  • 若delete.topic.enable=false(默认):
    • 若该topic没有被使用过,没有传输过信息,直接彻底删除
    • 若该topic被使用过,传输过信息,并没有真正删除topic,只是把该topic标记为删除(marked for deletion),重启kafka server后删除
4)发送消息

生产者不和zookeeper打交道,直接链接kafka

# 用生产者控制台 连接hadoop102这台服务器里的broker 获取其中的名为first主题 向这个主题中生产数据 
bin/kafka-console-producer.sh --broker-list hadoop102:9092 --topic first

#生产了2个数据hello world、atguigu
\>hello world
\>atguigu atguigu
5)消费消息

消费者跟zookeeper打交道,记录上一次消费到哪了需要给zookeeper备份

# 用消费者控制平台去 hadoop102这台zookeeper里 去消费 主题为first的主题
bin/kafka-console-consumer.sh --zookeeper hadoop102:2181 --from-beginning --topic first


#查看所有正在连接的Consumer信息
bin/kafka-consumer-groups.sh --zookeeper localhost:2181 --list

bin/kafka-consumer-groups.sh --new-consumer --bootstrap-server localhost:9092 --list

#查看单个Consumer信息
bin/kafka-consumer-groups.sh --zookeeper localhost:2181 --describe --group BrowseConsumer

bin/kafka-consumer-groups.sh --new-consumer --bootstrap-server localhost:9092 --describe --group BrowseConsumer

#重头开始消费某个Topic
bin/kafka-console-consumer.sh --zookeeper localhost:2181 --from-beginning --topic xxx

bin/kafka-console-consumer.sh --new-consumer --bootstrap-server localhost:9092 --from-beginning --topic xxx
  • –from-beginning:会把first主题中以往所有的数据都读取出来。根据业务场景选择是否增加该配置。
  • 订阅了但不在线,等上线之后是能读到消息的

第3章 Kafka工作流程

每个broker是一台服务器

img

3.1 发送数据

producer就是生产者,是数据的入口。注意看图中的红色箭头,Producer在写入数据的时候永远的找leader,不会直接将数据写入follower!

那leader怎么找呢?写入的流程又是什么样的呢?我们看下图:

img

消息写入leader后,follower是主动的去leader进行同步的!producer采用push模式将数据发布到broker,每条消息追加到分区中,顺序写入磁盘,所以保证同一分区内的数据是有序的!写入示意图如下:

【MQ】Kafka笔记_第18张图片

分区的主要目的是:

1、 方便扩展。因为一个topic可以有多个partition,所以我们可以通过扩展机器去轻松的应对日益增长的数据量。
  2、 提高并发。以partition为读写单位,可以多个消费者同时消费数据,提高了消息的处理效率。

负载均衡:在kafka中,如果某个topic有多个partition,producer又怎么知道该将数据发往哪个partition呢?kafka中有几个原则:

1、 partition在写入的时候可以指定需要写入的partition,如果有指定,则写入对应的partition。
  2、 如果没有指定partition,但是设置了数据的key,则会根据key的值hash出一个partition。
  3、 如果既没指定partition,又没有设置key,则会轮询选出一个partition。

rabbitMQ是交换机和消息队列

保证消息不丢失是一个消息队列中间件的基本保证,那producer在向kafka写入消息的时候,怎么保证消息不丢失呢?其实上面的写入流程图中有描述出来,那就是通过ACK应答机制!在生产者向队列写入数据的时候可以设置参数来确定是否确认kafka接收到数据,这个参数可设置的值为01all

  • acks=0: producer 不等待 broker 的 ack,这一操作提供了一个最低的延迟, broker一接收到还没有写入磁盘就已经返回,当 broker 故障时有可能丢失数据;这种情况后面的producer.send的回调也会完成失去作用
  • acks=1: producer等待broker的ack,partition的==leader 落盘(写入磁盘)==成功后返回ack(只等待leader写完就发回ack),
    • 数据丢失:如果在 follower同步成功之前leader故障,那么将会丢失数据
  • acks=-1(all):producer 等待 broker 的 ack, partition的leader和follower(ISR里的follower) 全部落盘成功后才返回ack。
    • 数据重复:如果在follower同步完成后,broker发送ack之前,leader发生故障,那么会造成数据重复
    • 数据丢失:比如ISR中只有一个leader,leader写完了就发送ACK,但是还没同步就挂掉了,此时也会丢失数据。(生产者以为成功了,不会再发送了)

最后要注意的是,如果往不存在的topic写数据,能不能写入成功呢?kafka会自动创建topic,分区和副本的数量根据默认配置都是1。

3.2 文件存储机制

【MQ】Kafka笔记_第19张图片

Producer将数据写入kafka后,集群就需要对数据进行保存了!kafka将数据保存在磁盘,可能在我们的一般的认知里,写入磁盘是比较耗时的操作,不适合这种高并发的组件。Kafka初始会单独开辟一块磁盘空间,顺序写入数据(效率比随机写入高)。

Kafka 中消息是以 topic 进行分类的, 生产者生产消息,消费者消费消息,都是面向 topic的。

每个topic可以有多个分区,每个分区是一个集群,有1个leader和多个follower

topic 是逻辑上的概念,而 partition 是物理上的概念,每个 partition 对应于一个 log 文件,该 log 文件中存储的就是 producer 生产的数据。 Producer 生产的数据会被不断追加到该log 文件末端,且每条数据都有自己的 offset消费者组中的每个消费者, 都会实时记录自己消费到了哪个 offset(在kafka中记录),以便出错恢复时,从上次的位置继续消费

img

此外,由于生产者生产的消息会不断追加到log文件末尾,为防止log文件过大导致数据定位效率低下,kafka才去了分片和索引机制,将

  • 每个partition分为多个segments
    • 每个segment对应两个文件:
      • .index文件
      • .log文件
  • 这些文件位于一个文件夹partition文件夹下,partition文件夹的命名规则为:top名称+分区序号。

index和log文件以当前segment的第一条消息的offset命名,他俩除了后缀名都一样,是成对出现的。index是索引文件,他有序号i和对应的第i条信息的地址位置,index用二分查找法找到第i条消息的地址,然后用地址去log文件中定位。

[atguigu@hadoop102 logs]$ ll # 如下first这个topic有3个分区012
drwxrwxr-x. 2 atguigu atguigu 4096 8月  6 14:37 first-0
drwxrwxr-x. 2 atguigu atguigu 4096 8月  6 14:35 first-1
drwxrwxr-x. 2 atguigu atguigu 4096 8月  6 14:37 first-2

# 去0号分区里看看
[atguigu@hadoop102 logs]$ cd first-0 
[atguigu@hadoop102 first-0]$ ll
-rw-rw-r--. 1 atguigu atguigu 10485760 8月  6 14:33 00000000000000000000.index
-rw-rw-r--. 1 atguigu atguigu   219 8月  6 15:07 00000000000000000000.log
-rw-rw-r--. 1 atguigu atguigu 10485756 8月  6 14:33 00000000000000000000.timeindex
-rw-rw-r--. 1 atguigu atguigu    8 8月  6 14:37 leader-epoch-checkpoint

00000000000000000000.index
00000000000000000000.log
00000000000000170410.index
00000000000000170410.log
00000000000000239430.index
00000000000000239430.log

index 和 log 文件以当前 segment 的第一条消息的 offset 命名。下图为 index 文件和 log文件的结构示意图

“.index”文件存储大量的索引信息,“.log”文件存储大量的数据,索引文件中的元数据指向对应数据文件中 message 的物理偏移地址

3.1.3 副本(Replication)

一、什么是副本机制:

通常是指分布式系统在多台网络互联的机器上保存有相同的数据拷贝

二、副本机制的好处:

1、提供数据冗余

系统部分组件失效,系统依然能够继续运转,因而增加了整体可用性以及数据持久性

2、提供高伸缩性

支持横向扩展,能够通过增加机器的方式来提升读性能,进而提高读操作吞吐量

3、改善数据局部性

允许将数据放入与用户地理位置相近的地方,从而降低系统延时。

三、kafka的副本

1、本质就是一个只能追加写消息的日志文件

2、同一个分区下的所有副本保存有相同的消息序列

3、副本分散保存在不同的 Broker 上,从而能够对抗部分 Broker 宕机带来的数据不可用(Kafka 是有若干主题概,每个主题可进一步划分成若干个分区。每个分区配置有若干个副本)

如下:有 3 台 Broker 的 Kafka 集群上的副本分布情况

【MQ】Kafka笔记_第20张图片

四、kafka如何保证同一个分区下的所有副本保存有相同的消息序列:

基于领导者(Leader-based)的副本机制

工作原理如图:

【MQ】Kafka笔记_第21张图片

同一个partition可能会有多个replication(对应 server.properties 配置中的 default.replication.factor=N)。没有replication的情况下,一旦broker 宕机,其上所有 patition 的数据都不可被消费,同时producer也不能再将数据存于其上的patition。引入replication之后,同一个partition可能会有多个replication,而这时需要在这些replication之间选出一个leader,producer和consumer只与这个leader交互,其它replication作为follower从leader 中复制数据。

1、Kafka 中分成两类副本:领导者副本(Leader Replica)和追随者副本(Follower Replica)。每个分区在创建时都要选举一个领导者副本,其余的副本自动称为追随者副本

2、Kafka 中,追随者副本是不对外提供服务的。追随者副本不处理客户端请求,它唯一的任务就是从领导者副本,所有的读写请求都必须发往领导者副本所在的 Broker,由该 Broker 负责处理。(因此目前kafka只能享受到副本机制带来的第 1 个好处,也就是提供数据冗余实现高可用性和高持久性)

3、领导者副本所在的 Broker 宕机时,Kafka 依托于 ZooKeeper 提供的监控功能能够实时感知到,并立即开启新一轮的领导者选举,从追随者副本中选一个作为新的领导者。老 Leader 副本重启回来后,只能作为追随者副本加入到集群中。

五、kafka追随者副本到底在什么条件下才算与 Leader 同步

Kafka 引入了 In-sync Replicas,也就是所谓的 ISR 副本集合。ISR 中的副本都是与 Leader 同步的副本,相反,不在 ISR 中的追随者副本就被认为是与 Leader 不同步的

六、kafka In-sync Replicas(ISR)

1、ISR不只是追随者副本集合,它必然包括 Leader 副本。甚至在某些情况下,ISR 只有 Leader 这一个副本

2、通过Broker 端replica.lag.time.max.ms 参数(Follower 副本能够落后 Leader 副本的最长时间间隔)值来控制哪个追随者副本与 Leader 同步?只要一个 Follower 副本落后 Leader 副本的时间不连续超过 10 秒,那么 Kafka 就认为该 Follower 副本与 Leader 是同步的,即使此时 Follower 副本中保存的消息明显少于 Leader 副本中的消息。

3、ISR 是一个动态调整的集合,而非静态不变的。

某个追随者副本从领导者副本中拉取数据的过程持续慢于 Leader 副本的消息写入速度,那么在 replica.lag.time.max.ms 时间后,此 Follower 副本就会被认为是与 Leader 副本不同步的,因此不能再放入 ISR 中。此时,Kafka 会自动收缩 ISR 集合,将该副本“踢出”ISR。

倘若该副本后面慢慢地追上了 Leader 的进度,那么它是能够重新被加回 ISR 的。

4、ISR集合为空则leader副本也挂了,这个分区就不可用了,producer也无法向这个分区发送任何消息了。(反之leader副本挂了可以从ISR集合中选举leader副本)

七、kafka leader副本所在broker挂了,leader副本如何选举

1、ISR不为空,从ISR中选举

2、ISR为空,Kafka也可以从不在 ISR 中的存活副本中选举,这个过程称为Unclean 领导者选举,通过Broker 端参数 unclean.leader.election.enable 控制是否允许 Unclean 领导者选举。开启 Unclean 领导者选举可能会造成数据丢失,但好处是,它使得分区 Leader 副本一直存在,不至于停止对外提供服务,因此提升了高可用性。反之,禁止 Unclean 领导者选举的好处在于维护了数据的一致性,避免了消息丢失,但牺牲了高可用性。

一个分布式系统通常只能同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)中的两个。显然,在这个问题上,Kafka 赋予你选择 C 或 A 的权利。

强烈建议不要开启unclean leader election,毕竟我们还可以通过其他的方式来提升高可用性。如果为了这点儿高可用性的改善,牺牲了数据一致性,那就非常不值当了。

ps1:leader副本的选举也可以理解为分区leader的选举

ps2:broker的leader选举与分区leader的选举不同,

Kafka的Leader选举是通过在zookeeper上创建/controller临时节点来实现leader选举,并在该节点中写入当前broker的信息
{“version”:1,”brokerid”:1,”timestamp”:”1512018424988”}
利用Zookeeper的强一致性特性,一个节点只能被一个客户端创建成功,创建成功的broker即为leader,即先到先得原则,leader也就是集群中的controller,负责集群中所有大小事务。
当leader和zookeeper失去连接时,临时节点会删除,而其他broker会监听该节点的变化,当节点删除时,其他broker会收到事件通知,重新发起leader选举

八、如果允许 Follower 副本对外提供读服务,你觉得应该如何避免或缓解因 Follower 副本与 Leader 副本不同步而导致的数据不一致的情形?

删除策略

无论消息是否被消费,kafka都会保留所有消息。有两种策略可以删除旧数据:

  • 1)基于时间:log.retention.hours=168 (7天)
  • 2)基于大小:log.retention.bytes=1073741824

需要注意的是,因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高 Kafka 性能无关。

3.1.4 写入流程

producer写入消息流程如下:

  • 1)producer先从zookeeper的 "/brokers/…/state"节点找到该partition的leader(kafka不知道谁是kafka集群的leader,但zk知道谁是kafka集群的leader)
    • 来了之后先计算得到partition,然后获取该partition的leader,
  • 2)producer将消息发送给该leader
  • 3)leader将消息写入本地log
  • 4)followers从leader pull消息,写入本地log后向leader发送ACK
  • 5)leader收到所有ISR中的replication的ACK后,增加HW(high watermark,最后commit 的offset)并向producer发送ACK

2pc:两阶段提交

消费数据

消息存储在log文件后,消费者就可以进行消费了。与生产消息相同的是,消费者在拉取消息的时候也是找leader去拉取。

push模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。push模式的目标是尽可能以最快速度传递消息,但是这样很容易造成消费者来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。

而pull模式则可以根据consumer的消费能力以适当的速率消费消息。

  • 另外它有助于消费者合理的批处理消息。不同的消费者消费速率,外部硬件环境都不一样,交由消费者自己决定以何种频率拉取消息更合适。
  • 基于pull模式不足之处在于,如果broker没有数据,消费者会轮询,忙等待数据直到数据到达,为了避免这种情况,我们允许消费者在pull请求时候使用“long poll”进行阻塞,直到数据到达 。

多个消费者可以组成一个消费者组(consumer group),每个消费者组都有一个组id!同一个消费组者的消费者可以消费同一topic下不同分区的数据,但是不会组内多个消费者消费同一分区的数据!!!

img

图示是消费者组内的消费者小于partition数量的情况,所以会出现某个消费者消费多个partition数据的情况,消费的速度也就不及只处理一个partition的消费者的处理速度!如果是消费者组的消费者多于partition的数量,那会不会出现多个消费者消费同一个partition的数据呢?上面已经提到过不会出现这种情况!多出来的消费者不消费任何partition的数据。所以在实际的应用中,建议消费者组的consumer的数量与partition的数量一致

在保存数据的小节里面,我们聊到了partition划分为多组segment,每个segment又包含.log、.index、.timeindex文件,存放的每条message包含offset、消息大小、消息体……我们多次提到segment和offset,查找消息的时候是怎么利用segment+offset配合查找的呢?假如现在需要查找一个offset为368801的message是什么样的过程呢?我们先看看下面的图:

img

  • 0、已经是一个指定的partition了
  • 1、 先找到offset的368801message所在的segment文件(利用二分法查找),这里找到的就是在第二个segment文件。
  • 2、 打开找到的segment中的.index文件(也就是368796.index文件,该文件起始偏移量为368796+1,我们要查找的offset为368801的message在该index内的偏移量为368796+5=368801,所以这里要查找的相对offset为5)。由于该文件采用的是稀疏索引的方式存储着相对offset及对应message物理偏移量的关系,所以直接找相对offset为5的索引找不到,这里同样利用二分法查找相对offset小于或者等于指定的相对offset的索引条目中最大的那个相对offset,所以找到的是相对offset为4的这个索引。
  • 3、 根据找到的相对offset为4的索引确定message存储的物理偏移位置为256。打开数据文件,从位置为256的那个地方开始顺序扫描直到找到offset为368801的那条Message。

这套机制是建立在offset为有序的基础上,利用segment+有序offset+稀疏索引+二分查找+顺序查找等多种手段来高效的查找数据!至此,消费者就能拿到需要处理的数据进行处理了。那每个消费者又是怎么记录自己消费的位置呢?在早期的版本中,消费者将消费到的offset维护zookeeper中,consumer每间隔一段时间上报一次,这里容易导致重复消费,且性能不好!在新的版本中消费者消费到的offset已经直接维护在kafk集群的__consumer_offsets这个topic中!

3.4 Kafka高效读写数据

1)顺序写磁盘

Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端,为顺序写。 官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。

2)零复制技术

零拷贝

零宝贝就是直接从File–Page Cache–NIC

3.5 kafka事务

Kafka 从 0.11 版本开始引入了事务支持。事务可以保证 Kafka 在 Exactly Once 语义的基础上,生产和消费可以跨分区和会话,要么全部成功,要么全部失败

3.5.1 Producer 事务

为了实现跨分区跨会话的事务,需要引入一个全局唯一的 Transaction ID,并将 Producer获得的PID 和Transaction ID 绑定。这样当Producer 重启后就可以通过正在进行的 Transaction ID 获得原来的 PID。

为了管理 Transaction, Kafka 引入了一个新的组件 Transaction Coordinator。 Producer 就是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态。 Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行

3.5.2 Consumer 事务

上述事务机制主要是从 Producer 方面考虑,对于 Consumer 而言,事务的保证就会相对较弱,尤其时无法保证 Commit 的信息被精确消费。这是由于 Consumer 可以通过 offset 访问任意信息,而且不同的 Segment File 生命周期不同,同一事务的消息可能会出现重启后被删除的情况

第4章 AdminClient API

<dependency>
    <groupId>org.apache.kafkagroupId>
    <artifactId>kafka-clientsartifactId>
    <version>2.0.0version>
dependency>

kafka所有的操作都要创建一个connect

API 作用
AdminClient 客户端对象
NewTopic 创建主题
CreateTopicsResult 创建主题的返回结果
ListTopicsOptions 查询主题列表
ListTopicsOptions 查询主题列表及选项
DescribeOptionsResult 查询主题
DescribeConfigsResult 查询主题配置项

在Kafka官网中这么描述AdminClient:The AdminClient API supports managing and inspecting topics, brokers, acls, and other Kafka objects. 具体的KafkaAdminClient包含了一下几种功能(以Kafka1.0.0版本为准):

  • 创建Topic:createTopics(Collection newTopics)
  • 删除Topic:deleteTopics(Collection topics)
  • 罗列所有Topic:listTopics()
  • 查询Topic:describeTopics(Collection topicNames)
  • 查询集群信息:describeCluster()
  • 查询ACL信息:describeAcls(AclBindingFilter filter)
  • 创建ACL信息:createAcls(Collection acls)
  • 删除ACL信息:deleteAcls(Collection filters)
  • 查询配置信息:describeConfigs(Collection resources)
  • 修改配置信息:alterConfigs(Map configs)
  • 修改副本的日志目录:alterReplicaLogDirs(Map replicaAssignment)
  • 查询节点的日志目录信息:describeLogDirs(Collection brokers)
  • 查询副本的日志目录信息:describeReplicaLogDirs(Collection replicas)
  • 增加分区:createPartitions(Map newPartitions)
import org.apache.kafka.clients.admin.*;
import org.apache.kafka.common.KafkaFuture;
import org.apache.kafka.common.config.ConfigResource;
import org.apache.kafka.common.internals.Topic;
import org.apache.kafka.server.quota.ClientQuotaEntity;

import java.util.*;
import java.util.concurrent.ExecutionException;

public class AdminSample {

    public final static String TOPIC_NAME="jiangzh-topic";

    public static void main(String[] args) throws Exception {
//        AdminClient adminClient = AdminSample.adminClient();
//        System.out.println("adminClient : "+ adminClient);
        // 创建Topic实例
       createTopic();
        // 删除Topic实例
//        delTopics();
        // 获取Topic列表
//        topicLists();
        // 描述Topic
        describeTopics();
        // 修改Config
//        alterConfig();
        // 查询Config
//        describeConfig();
        // 增加partition数量
//        incrPartitions(2);
    }

    /*
        增加partition数量 // 分区只能增加,不能减少
     */
    public static void incrPartitions(int partitions) throws Exception{
        AdminClient adminClient = adminClient();
        Map<String, NewPartitions> partitionsMap = new HashMap<>();
        NewPartitions newPartitions = NewPartitions.increaseTo(partitions);
        partitionsMap.put(TOPIC_NAME, newPartitions);


        CreatePartitionsResult createPartitionsResult = adminClient.createPartitions(partitionsMap);
        createPartitionsResult.all().get();
    }

    /*
        修改Config信息
     */
    public static void alterConfig() throws Exception{
        AdminClient adminClient = adminClient();
//        Map configMaps = new HashMap<>();
//
//        // 组织两个参数
//        ConfigResource configResource = new ConfigResource(ConfigResource.Type.TOPIC, TOPIC_NAME);
//        Config config = new Config(Arrays.asList(new ConfigEntry("preallocate","true")));
//        configMaps.put(configResource,config);
//        AlterConfigsResult alterConfigsResult = adminClient.alterConfigs(configMaps);

        /*
            从 2.3以上的版本新修改的API
         */
        Map<ConfigResource,Collection<AlterConfigOp>> configMaps = new HashMap<>();
        // 组织两个参数
        ConfigResource configResource = new ConfigResource(ConfigResource.Type.TOPIC, TOPIC_NAME);
        AlterConfigOp alterConfigOp =
                new AlterConfigOp(new ConfigEntry("preallocate","false"),AlterConfigOp.OpType.SET);
        configMaps.put(configResource,Arrays.asList(alterConfigOp));

        AlterConfigsResult alterConfigsResult = adminClient.incrementalAlterConfigs(configMaps);
        alterConfigsResult.all().get();
    }

    /**
        查看配置信息
        ConfigResource(type=TOPIC, name='jiangzh-topic') ,
        Config(
            entries=[
             ConfigEntry(
               name=compression.type,
               value=producer,
               source=DEFAULT_CONFIG,
               isSensitive=false,
               isReadOnly=false,
               synonyms=[]),
             ConfigEntry(
                name=leader.replication.throttled.replicas,
                value=,
                source=DEFAULT_CONFIG,
                isSensitive=false,
                isReadOnly=false,
                synonyms=[]), ConfigEntry(name=message.downconversion.enable, value=true, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=min.insync.replicas, value=1, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=segment.jitter.ms, value=0, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=cleanup.policy, value=delete, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=flush.ms, value=9223372036854775807, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=follower.replication.throttled.replicas, value=, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=segment.bytes, value=1073741824, source=STATIC_BROKER_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=retention.ms, value=604800000, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=flush.messages, value=9223372036854775807, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=message.format.version, value=2.4-IV1, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=file.delete.delay.ms, value=60000, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=max.compaction.lag.ms, value=9223372036854775807, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=max.message.bytes, value=1000012, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=min.compaction.lag.ms, value=0, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=message.timestamp.type, value=CreateTime, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]),
             ConfigEntry(name=preallocate, value=false, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=min.cleanable.dirty.ratio, value=0.5, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=index.interval.bytes, value=4096, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=unclean.leader.election.enable, value=false, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=retention.bytes, value=-1, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=delete.retention.ms, value=86400000, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=segment.ms, value=604800000, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=message.timestamp.difference.max.ms, value=9223372036854775807, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[]), ConfigEntry(name=segment.index.bytes, value=10485760, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[])])
     */
    public static void describeConfig() throws Exception{
        AdminClient adminClient = adminClient();
        // TODO 这里做一个预留,集群时会讲到
//        ConfigResource configResource = new ConfigResource(ConfigResource.Type.BROKER, TOPIC_NAME);

        ConfigResource configResource = new ConfigResource(ConfigResource.Type.TOPIC, TOPIC_NAME);
        DescribeConfigsResult describeConfigsResult = adminClient.describeConfigs(Arrays.asList(configResource));
        Map<ConfigResource, Config> configResourceConfigMap = describeConfigsResult.all().get();
        configResourceConfigMap.entrySet().stream().forEach((entry)->{
            System.out.println("configResource : "+entry.getKey()+" , Config : "+entry.getValue());
        });
    }

    /**
        描述Topic
        name :jiangzh-topic ,
        desc: (name=jiangzh-topic,
            internal=false,
            partitions=
                (partition=0,
                 leader=192.168.220.128:9092
                 (id: 0 rack: null),
                 replicas=192.168.220.128:9092
                 (id: 0 rack: null),
                 isr=192.168.220.128:9092
                 (id: 0 rack: null)),
                 authorizedOperations=[])
     */
    public static void describeTopics() throws Exception {
        AdminClient adminClient = adminClient();
        DescribeTopicsResult describeTopicsResult = adminClient.describeTopics(Arrays.asList(TOPIC_NAME));
        Map<String, TopicDescription> stringTopicDescriptionMap = describeTopicsResult.all().get();
        Set<Map.Entry<String, TopicDescription>> entries = stringTopicDescriptionMap.entrySet();
        entries.stream().forEach((entry)->{
            System.out.println("name :"+entry.getKey()+" , desc: "+ entry.getValue());
        });
    }

    /*
        删除Topic
     */
    public static void delTopics() throws Exception {
        AdminClient adminClient = adminClient();
        DeleteTopicsResult deleteTopicsResult = adminClient.deleteTopics(Arrays.asList(TOPIC_NAME));
        deleteTopicsResult.all().get();
    }

    /*
        获取Topic列表
     */
    public static void topicLists() throws Exception {
        AdminClient adminClient = adminClient();
        // 是否查看internal选项
        ListTopicsOptions options = new ListTopicsOptions();
        options.listInternal(true);
//        ListTopicsResult listTopicsResult = adminClient.listTopics();
        ListTopicsResult listTopicsResult = adminClient.listTopics(options);
        Set<String> names = listTopicsResult.names().get();
        Collection<TopicListing> topicListings = listTopicsResult.listings().get();
        KafkaFuture<Map<String, TopicListing>> mapKafkaFuture = listTopicsResult.namesToListings();
        // 打印names
        names.stream().forEach(System.out::println);
        // 打印topicListings
        topicListings.stream().forEach((topicList)->{
            System.out.println(topicList);
        });
    }

    /*
        创建Topic实例
     */
    public static void createTopic() {
        AdminClient adminClient = adminClient();
        // 副本因子
        Short rs = 1;
        NewTopic newTopic = new NewTopic(TOPIC_NAME, 1 , rs);
        CreateTopicsResult topics = adminClient.createTopics(Arrays.asList(newTopic));
        System.out.println("CreateTopicsResult : "+ topics);
    }

    /*
        设置AdminClient
     */
    public static AdminClient adminClient(){
        Properties properties = new Properties();
        properties.setProperty(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,"kafka1:9092");

        AdminClient adminClient = AdminClient.create(properties);
        return adminClient;
    }

}
@Component
public class KafkaConfig{

    // 配置Kafka
    public Properties getProps(){
        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092");
        /*    props.put("retries", 2); // 重试次数
        props.put("batch.size", 16384); // 批量发送大小
        props.put("buffer.memory", 33554432); // 缓存大小,根据本机内存大小配置
        props.put("linger.ms", 1000); // 发送频率,满足任务一个条件发送*/
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        return props;
    }

}
@RestController
public class KafkaTopicManager {

    @Autowired
    private KafkaConfig kafkaConfig;

    @GetMapping("createTopic")
    public void createTopic(){
        AdminClient adminClient = KafkaAdminClient.create(kafkaConfig.getProps());//AdminClient.create

        NewTopic newTopic = new NewTopic("test1",4, (short) 1);
        Collection<NewTopic> newTopicList = new ArrayList<>();
        newTopicList.add(newTopic);
        adminClient.createTopics(newTopicList);

        adminClient.close();
    }
    @GetMapping("deleteTopic")
    public void deleteTopic(){
        AdminClient adminClient = KafkaAdminClient.create(kafkaConfig.getProps());
        adminClient.deleteTopics(Arrays.asList("test1"));
        adminClient.close();
    }
    @GetMapping("listAllTopic")
    public void listAllTopic(){
        AdminClient adminClient = KafkaAdminClient.create(kafkaConfig.getProps());
        ListTopicsResult result = adminClient.listTopics();
        KafkaFuture<Set<String>> names = result.names();
        try {
            names.get().forEach((k)->{
                System.out.println(k);
            });
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        adminClient.close();
    }
    @GetMapping("getTopic")
    public void getTopic(){
        AdminClient adminClient = KafkaAdminClient.create(kafkaConfig.getProps());

        DescribeTopicsResult describeTopics = adminClient.describeTopics(Arrays.asList("syn-test"));

        Collection<KafkaFuture<TopicDescription>> values = describeTopics.values().values();

        if(values.isEmpty()){
            System.out.println("找不到描述信息");
        }else{
            for (KafkaFuture<TopicDescription> value : values) {
                System.out.println(value);
            }
        }
        adminClient.close();
    }
}
package com.rbs.kafka.config;
 
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.KafkaAdmin;
 
import java.util.HashMap;
import java.util.Map;
 
/**
 * @ClassName KafkaInitialConfiguration
 * @Author ywj
 * @Describe
 * @Date 2019/5/31 0031 9:53
 */
@Configuration
public class KafkaInitialConfiguration {
    @Value("${spring.kafka.bootstrap-servers}")
    private String bootstrapservers;
 
    @Bean
    public KafkaAdmin kafkaAdmin() {
        Map<String, Object> props = new HashMap<>();
        //配置Kafka实例的连接地址
        props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,bootstrapservers);
        KafkaAdmin admin = new KafkaAdmin(props);
        return admin;
    }
 
    @Bean
    public AdminClient adminClient() {
        return AdminClient.create(kafkaAdmin().getConfig());
    }
}
package com.rbs.kafka.util;
 
 
import org.apache.kafka.clients.admin.*;
import org.apache.kafka.common.KafkaFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
 
 
/**
 * @Describe 主题操作控制类
 */
@Service
public class KafkaConsole {
    @Autowired
    private AdminClient adminClient;
 
    /**
     * 返回主题的信息
     * @param topicName 主题名称
     * @return
     */
    public KafkaFuture<Map<String, TopicDescription>> SelectTopicInfo(String topicName) {
        DescribeTopicsResult result = adminClient.describeTopics(Arrays.asList(topicName));
        KafkaFuture<Map<String, TopicDescription>> all = result.all();
        return all;
    }
 
 
    /**
     * 增加某个主题的分区(注意分区只能增加不能减少)
     * @param topicName  主题名称
     * @param number  修改数量
     */
    public void edit(String topicName,Integer number){
        Map<String, NewPartitions> newPartitions=new HashMap<String, NewPartitions>();
        //创建新的分区的结果
        newPartitions.put(topicName,NewPartitions.increaseTo(number));
        adminClient.createPartitions(newPartitions);
    }
 
}

创建主题

public void create(String topic, int partitions, int replication, Map<String, String> configs) throws Exception {
    // 为了兼容性增加一层副本系数和节点数量的判断
    if (replication > getBrokerNums())
        throw new RuntimeException("副本系数不能大于broker节点数量");

    short replication_short = (short) replication;
    NewTopic newTopic = new NewTopic(topic, partitions, replication_short);
    // 创建主题的相关配置
    if (null != configs && configs.size() > 0)
        newTopic.configs(configs);
    CreateTopicsResult result = adminClient.createTopics(Arrays.asList(newTopic));
    result.all().get(timeout, TimeUnit.SECONDS);
}
12345678910111213

修改主题

public void update(String topic, List<AlterConfigOp> alterConfigOps) throws Exception {
    ConfigResource resource = new ConfigResource(ConfigResource.Type.TOPIC, topic);
    Map<ConfigResource, Collection<AlterConfigOp>> configs = new HashMap<>();
    configs.put(resource, alterConfigOps);
    adminClient.incrementalAlterConfigs(configs).all().get(timeout, TimeUnit.SECONDS);
}

// 新增、更新的参数
// ConfigEntry configEntry = new ConfigEntry(property.getKey(), property.getValue());
// AlterConfigOp alterConfigOp = new AlterConfigOp(configEntry, AlterConfigOp.OpType.SET);

// 删除的参数
// ConfigEntry configEntry = new ConfigEntry(deleteProperty, null);
// AlterConfigOp alterConfigOp = new AlterConfigOp(configEntry, AlterConfigOp.OpType.DELETE);
1234567891011121314

删除主题

public void delete(String topic) throws Exception {
    // 服务端server.properties需要设置delete.topic.enable=true,才可以使用同步删除,否则只是将主题标记为删除
    adminClient.deleteTopics(Arrays.asList(topic));
}
1234

列出主题

public Set<String> list() throws Exception {
    ListTopicsResult listTopicsResult = adminClient.listTopics();
    // Set topics = listTopicsResult.names().get();
    Set<String> topics = listTopicsResult.names().get(timeout, TimeUnit.SECONDS);
    
    return topics;
}
12345

描述主题

public TopicDescription describe(String topic) throws Exception {
    TopicDescription description = adminClient.describeTopics(Arrays.asList(topic)).all()
        .get(timeout, TimeUnit.SECONDS).get(topic);
    return description;
}
12345

分区管理

列出分区

public List<Integer> partitions(String topic) throws Exception {
    List<TopicPartitionInfo> partitionInfos = describe(topic).partitions();
    List<Integer> result = new ArrayList<>();
    for (TopicPartitionInfo partitionInfo : partitionInfos) {
        result.add(partitionInfo.partition());
    }
    return result;
}

public List<TopicPartition> topicPartitions(String topic) throws Exception {
    List<TopicPartitionInfo> partitionInfos = describe(topic).partitions();
    List<TopicPartition> result = new ArrayList<>();
    for (TopicPartitionInfo partitionInfo : partitionInfos) {
        result.add(new TopicPartition(topic, partitionInfo.partition()));
    }
    return result;
}
1234567891011121314151617

新增分区


第5章 Producer API

4.1 消息发送流程

  • 生产者要发送消息的属性封装到Properties中,将Properties传到KafkaProducer构造器里,创建一个生产者

  • 发送的消息封装成ProducerRecord对象,包含topic、分区、key、value。分区和key可不指定,由kafka自行确定目标分区

  • KafkaProducer调用KafkaProducer的send()方法发送到zookeeper,发送中还会把经过序列化器和分区器

    • 序列化器会把消息的key和value序列化成字节数组
    • 如果没有指定分区就会用key来生成一个分区
  • 消费者将要订阅的主题封装在Properties对象中,传入KafkaConsumer构造器中,创建一个消费者

  • KafkaConsumer调用poll()从zookeeper取消费消息

  • 发送时将数据序列化,消费时将数据反序列化

producer使用一个线程(用户主线程,也就是用户启动producer的线程)将待发送的消息封装成一个ProducerRecord实例,然后将其序列化后发送给partition,再由partition确定目标分区后一同发送到位于producer程序中的一块内存缓冲区中。而producer的另一个线程(I/O发送线程,也称Sender线程)则负责实时地将缓冲区中提取出准备就绪的消息封装进一个批次(batch),统一发送给对应的broker。

消息发送的过程中,涉及到两个线程协同工作,

  • 主线程首先将业务数据封装成ProducerRecord对象,之后调用send()方法将消息放入RecordAccumulator(消息收集器,也可以理解为主线程与Sender线程直接的缓冲区)中暂存,
  • Sender线程负责将消息信息构成请求,并最终执行网络I/O的线程,它从RecordAccumulator中取出消息并批量发送出去,需要注意的是,KafkaProducer是线程安全的,多个线程间可以共享使用同一个KafkaProducer对象

Kafka 的 Producer 发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程,以及一个线程共享变量——RecordAccumulator。

main 线程将消息发送给 RecordAccumulator, Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka broker

相关参数:

  • batch.size: 只有数据积累到 batch.size 之后, sender 才会发送数据。
  • linger.ms: 如果数据迟迟未达到 batch.size, sender 等待 linger.time 之后就会发送数据。
//调用send()后;
public Future<RecordMetadata> send(ProducerRecord<K, V> record) {
    return this.send(record, (Callback)null);
}
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
    // 获取拦截器并拦截
    ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
    //开始发送
    return this.doSend(interceptedRecord, callback);
}

private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
    TopicPartition tp = null;

    try {
        this.throwIfProducerClosed();

        KafkaProducer.ClusterAndWaitTime clusterAndWaitTime;
        try {
            clusterAndWaitTime = this.waitOnMetadata(record.topic(), record.partition(), this.maxBlockTimeMs);
        } catch (KafkaException var20) {
            if (this.metadata.isClosed()) {
                throw new KafkaException("Producer closed while send in progress", var20);
            }

            throw var20;
        }

        long remainingWaitMs = Math.max(0L, this.maxBlockTimeMs - clusterAndWaitTime.waitedOnMetadataMs);
        Cluster cluster = clusterAndWaitTime.cluster;

        // key的序列化器
        byte[] serializedKey;
        try {
            serializedKey = this.keySerializer.serialize(record.topic(), record.headers(), record.key());
        } catch (ClassCastException var19) {
            throw new SerializationException("Can't convert key of class " + record.key().getClass().getName() + " to class " + this.producerConfig.getClass("key.serializer").getName() + " specified in key.serializer", var19);
        }

        // value的序列化器
        byte[] serializedValue;
        try {
            serializedValue = this.valueSerializer.serialize(record.topic(), record.headers(), record.value());
        } catch (ClassCastException var18) {
            throw new SerializationException("Can't convert value of class " + record.value().getClass().getName() + " to class " + this.producerConfig.getClass("value.serializer").getName() + " specified in value.serializer", var18);
        }

        // 分区器计算分区
        int partition = this.partition(record, serializedKey, serializedValue, cluster);
        tp = new TopicPartition(record.topic(), partition);
        this.setReadOnly(record.headers());
        Header[] headers = record.headers().toArray();
        int serializedSize = AbstractRecords.estimateSizeInBytesUpperBound(this.apiVersions.maxUsableProduceMagic(), this.compressionType, serializedKey, serializedValue, headers);
        this.ensureValidRecordSize(serializedSize);
        long timestamp = record.timestamp() == null ? this.time.milliseconds() : record.timestamp();
        if (this.log.isTraceEnabled()) {
            this.log.trace("Attempting to append record {} with callback {} to topic {} partition {}", new Object[]{record, callback, record.topic(), partition});
        }

        Callback interceptCallback = new KafkaProducer.InterceptorCallback(callback, this.interceptors, tp);
        if (this.transactionManager != null && this.transactionManager.isTransactional()) {
            this.transactionManager.failIfNotReadyForSend();
        }

        // 添加最终消息到线程队列中
        RecordAppendResult result = this.accumulator.append(tp, timestamp, serializedKey, serializedValue, headers, interceptCallback, remainingWaitMs, true);
        if (result.abortForNewBatch) {
            int prevPartition = partition;
            this.partitioner.onNewBatch(record.topic(), cluster, partition);
            partition = this.partition(record, serializedKey, serializedValue, cluster);
            tp = new TopicPartition(record.topic(), partition);
            if (this.log.isTraceEnabled()) {
                this.log.trace("Retrying append due to new batch creation for topic {} partition {}. The old partition was {}", new Object[]{record.topic(), partition, prevPartition});
            }

            interceptCallback = new KafkaProducer.InterceptorCallback(callback, this.interceptors, tp);
            result = this.accumulator.append(tp, timestamp, serializedKey, serializedValue, headers, interceptCallback, remainingWaitMs, false);
        }

        if (this.transactionManager != null && this.transactionManager.isTransactional()) {
            this.transactionManager.maybeAddPartitionToTransaction(tp);
        }

        if (result.batchIsFull || result.newBatchCreated) {
            this.log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
            this.sender.wakeup();
        }

        return result.future;
    } catch (ApiException var21) {
        ...;
    }
}

4.2 producer hello-world

1)pom依赖

<dependency>
    <groupId>org.apache.kafkagroupId>
    <artifactId>kafka-clientsartifactId>
    <version>0.11.0.0version>
dependency> 
// 不带回调函数的发送者
package com.atguigu.kafka;
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;

public class CustomProducer {
    public static void main(String[] args) throws ExecutionException,InterruptedException {
        Properties props = new Properties();
        //kafka 集群, broker-list // 必须指定 // 如果kafka集群中机器数很多,那么只需指定部分broker即可,不需要列出所有的机器。因为不管指定几台机器,producer都会通过该参数找到并发现集群中所有broker。为该参数指定多态机器只是为了故障转移使用。这样即使某一台broker挂掉了,producer重启后依然可以通过该参数指定的其他broker连入kafka集群
        props.put("bootstrap.servers", "hadoop102:9092");
        props.put("acks", "all");
        //重试次数
        props.put("retries", 1);
        //批次大小
        props.put("batch.size", 16384);
        //等待时间
        props.put("linger.ms", 1);
        //RecordAccumulator 缓冲区大小
        props.put("buffer.memory", 33554432);
        // 必须指定
        props.put("key.serializer",
                  "org.apache.kafka.common.serialization.StringSerializer");
        // 必须指定
        props.put("value.serializer",
                  "org.apache.kafka.common.serialization.StringSerializer");
        Producer<String, String> producer = new
            KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
            producer.send(
                new ProducerRecord<String, String>(
                    "first",
                    Integer.toString(i), 
                    Integer.toString(i)));
        }
        producer.close();// 所有的通道打开都需要关闭
    }
}

①Properties对象

http://kafka.apache.org/documentation/#producerconfigs

必须指定

  • bootstrap.servers:指定了ip:port对,用于创建向kafka broker服务器的链接。如k1:9092,k2:9092。如果kafka集群中机器数很多,那么只需指定部分broker即可,不需要列出所有的机器。因为不管指定几台机器,producer都会通过该参数找到并发现集群中所有broker。为该参数指定多态机器只是为了故障转移使用。这样即使某一台broker挂掉了,producer重启后依然可以通过该参数指定的其他broker连入kafka集群

  • key.serializer被发送到broker端的任何消息的格式都必须是字节数组,因此消息的各个组件必须首先做序列化,然后才能发送到broker。该参数就是为消息的key做序列化用的。这个参数指定的是实现了org.apache.kafka.common.serialization.StringSerializer接口的类的全限定名称。kafka为大部分的初始类型默认提供了现成的序列化器。用户可以自定义序列化器,只要实现Serializer接口即可。即使没有指定key,key.serializer也必须设置。

  • value.serializer:与上面类似,不过是用来对消息体部分做序列化,将消息value部分转换成字节数组。这两个参数都得使用全限定类名,不能只写类名。这两个参数可以卸载properties中,也可以卸载下面构造函数的后面。

    // kafka提供了默认的字符串序列化器
    import org.apache.kafka.common.serialization.StringSerializer;
    // 还有整型和字节数组序列化器,这些序列化器都实现了接口org.apache.kafka.common.serialization.Serializer,基本上能够满足大部分场景的需求。
    
    // key序列化器
    properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
    // 等价于
    //props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
    
    // value序列化器
    properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
    // 等价于
    //props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
    

可以选择参数:

  • acks:producer给broker生产消息,broker返回“已提交”信息给producer。详见之前的ack 应答机制章节。properties.put("acks","1")注意在java中不写引号会报错

    • acks=0:不等broker的返回
    • acks=-1或者all:等待ISR中所有副本都成功落盘后发送回来“已提交”信息
    • acks=1:只等leader的落盘成功就应答
  • buffer.memory:producer段缓存消息的缓冲区大小,字节为单位,默认值32MB。由于采用了异步发送消息的设计架构,java版本的producer启动时会首先创建一块内存缓冲区用于保存待发送的消息,然后由另一个专属线程负责从缓冲区读取消息执行真正的发送。这部分内存空间的大小即是由buffer.memory参数指定的。若producer向缓冲区写消息的速度超过了专属I/O线程发送消息的速度,那么必然造成缓冲区的不断增大。此时producer会停止手头的工作等待I/O线程追上来,若一段时间之后I/O线程还是无法追上producer的进度,那么producer就会抛出异常并期望用户介入进行处理。若producer要给很多分区发送消息,那么就需要注意别让这个参数降低了producer整体的吞吐量。properties.put("buffer.memory",33554432)properties.put(Producer.Config.BUFFER_CONFIG,33554432)

  • compression.type:producer发送时是否压缩数据。默认none。还有GZIP、Snappy、LZ4。LZ4性能最好。还有Zstandard

  • reties:broker写入请求时可能有瞬时故障(比如leader选举)导致发送失败,这种失败可自行恢复,可以封装进回调函数中重新发送,但我们并不需要使用回调函数,直接设置该参数即可实现重试。默认为0不重试。

    • 重试可能导致消息重复:比如瞬时的网络抖动使得broker段已成功写入但没有发送响应给producer,因此producer认为失败而重发。为了应对这一风险,kafka要求用户在consumer段必须执行去重操作。0.11.0.0版本开始支持“精准一次”处理语义,从设计上避免了类似的问题。
    • 重试可能造成消息的乱码—当前producer会将多个消息发送请求(默认5个)缓存在内存中没如果由于某种原因发送了消息发送的重试,就可能造成小溪流的乱序。为了避免乱序发送,java版本的producer提供了max.in.flight.requets.per.connection参数。一旦用户设置为1,producer将确保某一时刻只能发送一个请求
    • 两次重试之间会停顿一段时间,防止频繁重试对系统带来冲击。try.backoff.ms设置,默认100ms,推荐用户通过测试来计算平均leader选举时间来设定retries和retry.backoff.ms
    • properties.put(“reties”,100)或properties.put(ProducerConfig.RETRIES_CONFIG,100)
  • batchsize:重要。通常一个小的batch中包含的消息数很少,因而一次发生请求能够写入的消息数也很少,所以producer吞吐量会很低。但若batch非常大就会给内存使用带来压力,因为不管是否能够填满,producer都会为该batch分配固定的大小。

    • 默认16KB,一般都增加。
  • linger.ms:即使batchsize没满,超过该设置时间后也会发送。默认为0表示消息立即发送,无需关心batch是否填满。但这会降低吞吐量,producer将更多的时间花费在了发送上。

  • max.request.size:能发送的最大消息大小,但包含一些消息头。默认1048576太小了

  • request.timeout.ms:超过默认的30s后仍没收到返回结果就会发生异常

②KafkaProducer对象

Producer<String, String> producer = new KafkaProducer<>(props);

③ ProducerRecord对象

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

<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不起作用,代码逻辑决定)

④发送消息

kafka producer发送消息的主方法是send()方法。通过Java提供的Future同时实现了同步发送异步发送+回调两种发送方式

异步方式

异步发送即调用.send()即可

send返回一个java的Future对象供用户稍后获取发送结果,也就是所谓的回调机制。

for (int i = 0; i < 100; i++) {
    producer.send(
        new ProducerRecord<String, String>(
            "first",
            Integer.toString(i), 
            Integer.toString(i)),
        	new Callback() {
                //回调函数, 该方法会在 Producer 收到 ack 时调用,为异步调用
                @Override
                public void onCompletion(RecordMetadata metadata,//两个参数不会同时非空,即至少一个为null。若消息发送失败,metadata为null
                                         Exception exception) {//当消息发送成功时e为null。
                    if (exception == null) { // 发送成功
                        System.out.println("success->" +
                                           metadata.offset());
                    } else { 
                        exception.printStackTrace();
                    }
            }
        });
}
同步发送

同步发送调用send().get()即可,get方法会一直等待下去直至broker将发送结果返回给producer程序。

同步发送和异步发送是通过java的Future来区分的,调用Future.get()无限等待结果返回,即实现同步发送的效果

for (int i = 0; i < 100; i++) {
    producerRecord<String,String> record = new producerRecord<>("first",Integer.toString(i));
    // get方法会一直等待下去直至broker将发送结果返回给producer程序。
    // 返回的时候要不返回发送结果,要么抛出异常由producer自行处理。
    // 如果成功,get将返回对应的RecordMetadata实例(包含了已发送消息的所有元数据消息),包含了topic、分区、位移
    
    // send的返回值类型是是Future // 调用get方法即可获取器结果
    producer.send(record).get();
}

⑤异常

不管是同步发送还是异步发送,发送都有可能失败,导致返回异常错误。当前kafka的错误类型包含了两类:可重试异常和不可重复异常。

常见的可重试异常:

  • LeaderNotAvailableException:分区的leader副本不可用,这通常出现在leader换届选择期间,因此通常是瞬时的异常,重试之后可以自行恢复
  • NotControllerException:controller当前不可用。这通常表明controller在经历着新一轮的选举,这也是可以通过重试机制自动恢复的
  • NetworkException:网络瞬时故障导致的异常,可重试

对于可重试异常,如果在producer程序中配置了重试次数,那么只要在规定的重试次数内自行恢复了,便不会出现在onCompletion的exception中。不过若超过了重试次数仍没成功免责仍然会封装进exception中。此时就需要producer程序自行处理这种异常

所有可重试异常都继承自org.apache.kafka.common.errors.RetriableException抽象类。理论上讲所有未继承RetriableException类的其他异常都属于不可重试异常,这类异常都表明了一些严重或kafka无法处理的问题,与producer相关的如下:

  • RecordToolLargeException:发送的消息尺寸太大,超过了规定的大小上限
  • SerializationException:序列化失败异常
  • KafkaException:其他类型的异常

这些不可重试异常一旦被捕获都会被封装进Future的计算结果并返回给producer程序,用户需要自行处理这些异常。由于不可重试异常和可重试异常在producer程序段可能有不同的处理逻辑,因此可以使用下面的代码进行区分:

producer.send(record,new Callback(){
    @Override
    public void onCompletion(RecordMetaData metadata,Exception exception){
        if (exception == null) { // 发送成功
            System.out.println("success->" +
                               metadata.offset());
        } else { 
            if(exception instanceof RetriableException){
                //处理可重试异常
            }else{// 不可重试异常
                exception.printStackTrace();
            }
            
        }
    }
})

⑥关闭producer

producer程序结束一定要关闭producer。因为producer程序运行时占用了系统资源(比如创建了额外的线程,申请了很多内存以及创建了多个Socket链接等),因此必须要显示地调用KafkaProducer.close()。不管发送成功还是失败,只要producer程序完成了既定的工作,就应该被关闭。

producer.close(参数)

  • 无参:处理完发送的请求后再关闭
  • 有参:等待timeout完成锁处理的请求后强制关闭

4.3 生产者代码示例

1.不带回调函数的 生产者
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
    public static void main(String[] args) throws ExecutionException,InterruptedException {
        Properties props = new Properties();
        //kafka 集群, broker-list // 必须指定 // 如果kafka集群中机器数很多,那么只需指定部分broker即可,不需要列出所有的机器。因为不管指定几台机器,producer都会通过该参数找到并发现集群中所有broker。为该参数指定多态机器只是为了故障转移使用。这样即使某一台broker挂掉了,producer重启后依然可以通过该参数指定的其他broker连入kafka集群
        props.put("bootstrap.servers", "hadoop102:9092");
        props.put("acks", "all");
        //重试次数
        props.put("retries", 1);
        //批次大小
        props.put("batch.size", 16384);
        //等待时间
        props.put("linger.ms", 1);
        //RecordAccumulator 缓冲区大小
        props.put("buffer.memory", 33554432);
        // 必须指定
        props.put("key.serializer",
                  "org.apache.kafka.common.serialization.StringSerializer");
        // 必须指定
        props.put("value.serializer",
                  "org.apache.kafka.common.serialization.StringSerializer");
        Producer<String, String> producer = new
            KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
            producer.send(
                new ProducerRecord<String, String>(
                    "first",
                    Integer.toString(i), 
                    Integer.toString(i)));
        }
        producer.close();
    }
}
2 带回调函数的 生产者

回调函数会在 producer 收到 ack 时调用,为异步调用, 该方法有两个参数,分别是RecordMetadata 和 Exception,如果 Exception 为 null,说明消息发送成功,如果Exception 不为 null,说明消息发送失败。
注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。

import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
    public static void main(String[] args) throws ExecutionException,
    InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop102:9092");//kafka 集群, broker-list
        props.put("acks", "all"); // 确认机制
        props.put("retries", 1);//重试次数
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待时间
        props.put("buffer.memory", 33554432);//RecordAccumulator 缓冲区大小
        props.put("key.serializer",
                  "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer",
                  "org.apache.kafka.common.serialization.StringSerializer");
        Producer<String, String> producer = new
            KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
            producer.send(new ProducerRecord<String, String>("first",Integer.toString(i), Integer.toString(i)), 
                          new Callback() {
                //回调函数, 该方法会在 Producer 收到 ack 时调用,为异步调用
                @Override
                public void onCompletion(RecordMetadata metadata,
                                         Exception exception) {
                    if (exception == null) {
                        System.out.println("success->" +
                                           metadata.offset());
                    } else {
                        exception.printStackTrace();
                    }
                }
            });
        }
        producer.close();
    }
}

3 同步发送 生产者

同步发送的意思就是,一条消息发送之后,会阻塞当前线程, 直至返回 ack。
由于 send 方法返回的是一个 Future 对象,根据 Futrue 对象的特点,我们也可以实现同步发送的效果,只需在调用 Future 对象的 get()方发即可。

package com.atguigu.kafka;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
    public static void main(String[] args) throws ExecutionException,
    InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop102:9092");//kafka 集群, broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重试次数
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待时间
        props.put("buffer.memory", 33554432);//RecordAccumulator 缓冲区大小
        props.put("key.serializer",
                  "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer",
                  "org.apache.kafka.common.serialization.StringSerializer");
        Producer<String, String> producer = new
            KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
            // 用Future.get()实现同步调用
            producer.send(new ProducerRecord<String, String>("first",
                                                             Integer.toString(i), Integer.toString(i))).get();
        }
        producer.close();
    }
}

4.4 生产过程细节

4.4.1 写入方式

producer采用推(push)模式将消息发布到broker,每条消息都被追加(append)到分区(patition)中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障kafka吞吐率)。

4.4.2 分区器

topic由partition构成,partition由多个log文件构造,log文件由多个消息构成

消息发送时都被发送到一个topic,其本质就是一个目录,而topic是由一些Partition Logs(分区日志)组成,其组织结构如下图所示:

【MQ】Kafka笔记_第22张图片

我们可以看到,每个Partition中的消息都是有序的,生产的消息被不断追加到Partition log上,其中的每一个消息都被赋予了一个唯一的offset值。

本身kafka有自己的分区策略的,如果未指定,就会使用默认的分区策略:Kafka根据传递消息的key来进行分区的分配,即hash(key) % numPartitions。如果Key相同的话,那么就会分配到统一分区。

1)分区的原因

  • (1)方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了;
  • (2)可以提高并发,因为可以以Partition为单位读写了。

只有leader用于交互,follower只用作备份

2)分区的原则

我们需要将producer发送的数据封装成一个ProducerRecord对象。

ProducerRecord类有如下的构造函数

ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers) ;
ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) ;
ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers) ;
ProducerRecord(String topic, Integer partition, K key, V value);
ProducerRecord(String topic, K key, V value) ;
ProducerRecord(String topic, V value) ;

DefaultPartitioner类源,我们也可以模仿他实现Partition接口实现我们自己的分区器

/**
 * The default partitioning strategy:
 默认的分区策略:
 如果给定了分区,使用他
 如果没有分区但是有个key,就是就根据key的hash值取分区
 如果分区和key值都没有,就采样轮询
 * 
    *
  • If a partition is specified in the record, use it *
  • If no partition is specified but a key is present choose a partition based on a hash of the key *
  • If no partition or key is present choose a partition in a round-robin fashion */ public class DefaultPartitioner implements Partitioner { private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>(); // 必要资源的初始化工作 public void configure(Map<String, ?> configs) {} // 返回要放入的paritition public int partition(String topic, // 主题 Object key, // 给定的key byte[] keyBytes, // key序列化后的值 Object value, // 要放入的值 byte[] valueBytes, // 序列化后的值 Cluster cluster) { // 当前集群 List<PartitionInfo> partitions = cluster.partitionsForTopic(topic); // 对应主题的分区数 int numPartitions = partitions.size(); // 如果key为null if (keyBytes == null) { // 获取主题轮询的下一个partition值,但还没取模 int nextValue = nextValue(topic); List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic); if (availablePartitions.size() > 0) { // 把上面的partition值取模得到真正的分区值 int part = Utils.toPositive(nextValue) % availablePartitions.size(); // 得到对应的分区 return availablePartitions.get(part).partition(); } else { // 没有分区 // no partitions are available, give a non-available partition return Utils.toPositive(nextValue) % numPartitions; } } else { // 输入了key值,直接对key的hash值取模就可以得到分区号了 return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions; } } private int nextValue(String topic) { AtomicInteger counter = topicCounterMap.get(topic); if (null == counter) { counter = new AtomicInteger(ThreadLocalRandom.current().nextInt()); AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter); if (currentCounter != null) { counter = currentCounter; } } // 自增 return counter.getAndIncrement(); } // 关闭partitioner// 主要是关闭那些partitioner时初始化的系统资源等 public void close() {} }
/* 自定义分区器*/
public class DefinePartitioner implements Partitioner {
    private final AtomicInteger counter = new AtomicInteger(0);
    @Override
    public int partition(String topic,  // 主题
                         Object key,  // 给定的key
                         byte[] keyBytes,  // key序列化后的值
                         Object value,  // 要放入的值
                         byte[] valueBytes, // 序列化后的值
                         Cluster cluster) { // 当前集群

        //然后在生产者中假如一行代码即可
        // 自定义分区 // 在生产者中指定
        props.put("partitioner.class", "com.atguigu.kafka.CustomPartitioner");
        int numPartitions = partitions.size();
        if (null == keyBytes) {
            return counter.getAndIncrement() % numPartitions;
        } else
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
    }
    @Override
    public void close() {
    }
    @Override
    public void configure(Map<String, ?> configs) {
    }
}


//然后在生产者中假如一行代码即可
// 自定义分区 // 在生产者中指定
props.put("partitioner.class", "com.atguigu.kafka.CustomPartitioner");

4.4.2 生产数据确认机制

3) ack 应答机制

对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等 ISR 中的 follower 全部接收成功,(分为只要leader收到、或leader写入磁盘就行、ISR全部写入才能确认,即数目为0、1、all)。(本来ISR就不是kafka集群的全部机器了,ISR居然也能不是全部)

所以 Kafka 为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,在生产者段选择以下的配置参数。

acks 参数配置:

  • acks=0: producer 不等待 broker 的 ack,这一操作提供了一个最低的延迟, broker一接收到还没有写入磁盘就已经返回,当 broker 故障时有可能丢失数据;这种情况后面的producer.send的回调也会完成失去作用
  • acks=1: producer等待broker的ack,partition的==leader 落盘(写入磁盘)==成功后返回ack(只等待leader写完就发回ack),
    • 数据丢失:如果在 follower同步成功之前leader故障,那么将会丢失数据
  • acks=-1(all):producer 等待 broker 的 ack, partition的leader和follower(ISR里的follower) 全部落盘成功后才返回ack。
    • 数据重复:如果在follower同步完成后,broker发送ack之前,leader发生故障,那么会造成数据重复
    • 数据丢失:比如ISR中只有一个leader,leader写完了就发送ACK,但是还没同步就挂掉了,此时也会丢失数据。(生产者以为成功了,不会再发送了)

设置方法:

//JAVA API中的
properties.put(ProducerConfig.ASKS_CONFIG,"0");//无需得到回复

为保证 producer 发送的数据,能可靠的发送到指定的 topic, topic 的每个 partition 收到producer 发送的数据后(kafka集群同步基本完成), 都需要向 producer 发送ack(acknowledgement 确认收到) ,如果producer 收到 ack, 就会进行下一轮的发送,否则重新发送数据。但这其中还有些细节:

  • 1)producer先从zookeeper的 "/brokers/…/state"节点找到该partition的leader(kafka不知道谁是kafka集群的leader,但zk知道谁是kafka集群的leader)
  • 2)producer将消息发送给该leader
  • 3)leader将消息写入本地log
  • 4)followers从leader pull消息,写入本地log后向leader发送ACK
  • 5)leader收到所有ISR中的replication的ACK后,增加HW(high watermark,最后commit 的offset)并向producer发送leader的ACK
1) 副本数据同步策略
方案 优点 缺点
半数以上完成同步, 就发送 ack 延迟低 选举新的 leader 时, 容忍 n 台 节点的故障,需要 2n+1 个副本
全部完成同步,才发送 ack 选举新的 leader 时,容忍 n 台节点的故障,需要 n+1 个副本 延迟高
只要ISR集合中同步完成即可发送ack

Kafka 选择了第二种方案,原因如下:

  • 1.同样为了容忍 n 台节点的故障,第一种方案需要 2n+1 个副本,而第二种方案只需要 n+1个副本,而 Kafka 的每个分区都有大量的数据, 第一种方案会造成大量数据的冗余。
  • 2.虽然第二种方案的网络延迟会比较高,但网络延迟对 Kafka 的影响较小
2) ISR

采用第二种方案之后,设想以下情景: leader 收到数据,所有 follower 都开始同步数据,但有一个 follower,因为某种故障,迟迟不能与 leader 进行同步,那 leader 就要一直等下去,直到它完成同步,才能发送 ack。这个问题怎么解决呢?

Leader 维护了一个动态的 in-sync replica set (ISR),意为和 leader 保持同步的 follower 集合,即每个partition动态维护一个replication集合。当 ISR 中的 follower 完成数据的同步之后,follower 就会给 leader 发送 ack。如果 follower长 时 间 未 向 leader 同 步 数 据 , 则 该 follower 将 被 踢 出 ISR , 该 时 间 阈 值 由 replica.lag.time.max.ms 参数设定。(最终目的为在 Leader 发生故障之后,就会从 ISR 中选举新的 leader,不影响使用。这句话没能理解)

  • 对于一个partition,集合中每个replication都同步完后,kafka才会将该消息标记为“已提交”状态,认为该条消息发送成功
  • 只要这个集合中至少存在一个replication或者,已提交的信息就不会丢失
  • 当一小部分replication开始落后于leader replication的速度速度时,就踢出ISR
  • 被踢出去的replication还在同步,只是不算在ISR里。被踢出去的同步追上leader后,又重新计入ISR
bin/kafka-topics.sh --describe --topic first --zookeeper hadoop102:2181
# 输出
Topic:first     PartitionCount:1        ReplicationFactor:3     Configs:
        Topic: first    Partition: 0    Leader: 3       Replicas: 3,4,2 Isr: 3
# 看最后的ISR

老版本中两个条件: leader与follower消息差距条数、距离上次同步的时间

leader和follower发消息差距大于10条就踢出ISR,如果小于10条再加进来。为什么踢出ISR还会又加进来呢?因为ISR只是决定了什么时候返回ACK,而无论在不在ISR里,都仍在继续同步数据。我们不能因为他慢了点就直接不用他备份。

生产者以batch发送数据,比如这个batch12条,如果batch大于大于了设定的10条阻塞限制,那么所有的follower都被踢出ISR。频繁发送batch,就频繁加入ISR,踢出ISR,频繁操作ZK

5 序列化器

消息要到网络上进行传输,必须进行序列化,用字节在网络中传输,而序列化器的作用就是如此。

Kafka 提供了默认的字符串序列化器(org.apache.kafka.common.serialization.StringSerializer),还有整型(IntegerSerializer)和字节数组(BytesSerializer)序列化器,这些序列化器都实现了接口(org.apache.kafka.common.serialization.Serializer)基本上能够满足大部分场景的需求。

序列化器负责在producer发送前将该消息转换成字节数组;而与之相反,解序列化器则用于将consumer接收到的字节数组转换成相应的对象

自定义序列化器:实现接口org.apache.kafka.common.serialization.Serializer,然后在发送的properties中指定类全类名即可

/**
* 自定义序列化器
北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090
使用自定义的序列化器
见代码库:com.heima.kafka.chapter2.ProducerDefineSerializer
*/
public class CompanySerializer implements Serializer<Company> {
    @Override
    public void configure(Map configs, boolean isKey) {
    }
    @Override
    public byte[] serialize(String topic, Company data) {//传入Company实例,转成byte[]
        if (data == null) {
            return null;
        }
        byte[] name, address;
        try {
            if (data.getName() != null) {
                name = data.getName().getBytes("UTF-8");
            } else {
                name = new byte[0];
            }
            if (data.getAddress() != null) {
                address = data.getAddress().getBytes("UTF-8");
            } else {
                address = new byte[0];
            }
            ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + name.length + address.length);
            buffer.putInt(name.length);
            buffer.put(name);
            buffer.putInt(address.length);
            buffer.put(address);
            return buffer.array();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return new byte[0];
    }
    @Override
    public void close() {
    }
}
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
       CompanySerializer.class.getName());

...;//

KafkaProducer<String, Company> producer =  new KafkaProducer<>(properties);
Company company = Company.builder().name("kafka").address("北京").build();
//    Company company = Company.builder().name("hiddenkafka")
//        .address("China").telphone("13000000000").build();
ProducerRecord<String, Company> record =
    new ProducerRecord<>(topic, company);
producer.send(record).get();

jackson-mapper-asl包的ObjectMapper可以把对象转换成字节数组

objectMapper.writeValueAsString(data).getBytes("utf-8");

6 拦截器

Producer拦截器是个相当新的功能,他和consumer端interceptor是在kafka 0.10版本被引入的,主要用于实现clients端的定制化控制逻辑

生产者拦截器可以用在消息发送前做一些准备工作,producer也支持指定多个interceptor按序作用域同一条消息从而形成一个拦截器链。实现接口org.apache.kafka.clients.producer.ProducerInterceptor

使用场景:

  • 按照某个规则过滤掉不符合要求的消息
  • 修改消息的内容
  • 统计类需求
// 拦截器接口
public interface ProducerInterceptor<K, V> extends Configurable {
    
    // 获取配置信息和初始化数据时使用
    configure(config);

    // 该方法被封装进KafkaProducer.send()方法中,即它允许在用户主线程中。producer确保在消息被序列化计算分区前调用该方法。可以操作消息,但最好不要修改topic和分区,否则会影响目标分区的集散
    public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);

    // 消息被应答之前或消息发送失败时调用。运行在producer的IO线程中因此不要在该方法中放入很“重"的逻辑,否则会拖慢producer的发送效率
    // 可以用e==null时判断消息发送成功计数
    public void onAcknowledgement(RecordMetadata metadata, Exception exception);

    // 拦截器关闭时调用
    public void close();
}

如前所述,interceptor可能被运行在多个线程中,因此在具体实现时用户需要自行确保线程安全。另外倘若指定了多个interceptor,则producer将按照指定顺序调用它们,并仅仅是捕获每个interceptor可能抛出的异常记录到错误日志中而非在向上传递。这在使用过程中要特别留意。

6.1 拦截器案例

http://www.cnblogs.com/huxi2b/p/7072447.html

1)需求:

实现一个简单的双interceptor组成的拦截链。第一个interceptor会在消息发送前将时间戳信息加到消息value的最前部;第二个interceptor会在消息发送后更新成功发送消息数或失败发送消息数。

2)案例实操

(1)增加时间戳拦截器

package com.atguigu.kafka.interceptor;
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

public class TimeInterceptor implements ProducerInterceptor<String, String> {

	@Override
	public void configure(Map<String, ?> configs) {}

	@Override
	public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
		// 创建一个新的record,把时间戳写入消息体的最前部
		return new ProducerRecord(record.topic(), record.partition(), record.timestamp(), record.key(),
				System.currentTimeMillis() + "," + record.value().toString());
	}

	@Override
	public void onAcknowledgement(RecordMetadata metadata, Exception exception) {}
	@Override
	public void close() {}
}

(2)统计发送消息成功和发送失败消息数,并在producer关闭时打印这两个计数器

package com.atguigu.kafka.interceptor;
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

public class CounterInterceptor implements ProducerInterceptor<String, String>{
    private int errorCounter = 0;
    private int successCounter = 0;

	@Override
	public void configure(Map<String, ?> configs) {}

	@Override
	public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
		 return record;
	}

	@Override
	public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
		// 统计成功和失败的次数
        if (exception == null) {
            successCounter++;
        } else {
            errorCounter++;
        }
	}

	@Override
	public void close() {
        // 保存结果
        System.out.println("Successful sent: " + successCounter);
        System.out.println("Failed sent: " + errorCounter);
	}
}

(3)producer主程序//配置拦截器

package com.atguigu.kafka.interceptor;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;

public class InterceptorProducer {

	public static void main(String[] args) throws Exception {
		// 1 设置配置信息
		Properties props = new Properties();
		props.put("bootstrap.servers", "hadoop102:9092");
		props.put("acks", "all");
		props.put("retries", 0);
		props.put("batch.size", 16384);
		props.put("linger.ms", 1);
		props.put("buffer.memory", 33554432);
		props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
		props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
		
		// 2 构建拦截链
		List<String> interceptors = new ArrayList<>();
		interceptors.add("com.atguigu.kafka.interceptor.TimeInterceptor"); 	interceptors.add("com.atguigu.kafka.interceptor.CounterInterceptor"); 
		props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
		 
		String topic = "first";
		Producer<String, String> producer = new KafkaProducer<>(props);
		
		// 3 发送消息
		for (int i = 0; i < 10; i++) {
			
		    ProducerRecord<String, String> record = new ProducerRecord<>(topic, "message" + i);
		    producer.send(record);
		}
		 
		// 4 一定要关闭producer,这样才会调用interceptor的close方法
		producer.close();
	}
}

3)测试

(1)在kafka上启动消费者,然后运行客户端java程序。

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh --zookeeper hadoop102:2181 --from-beginning --topic first

1501904047034,message0
1501904047225,message1
1501904047230,message2
1501904047234,message3
1501904047236,message4
1501904047240,message5
1501904047243,message6
1501904047246,message7
1501904047249,message8
1501904047252,message9

(2)观察java平台控制台输出数据如下:

Successful sent: 10
Failed sent: 0

7 消息压缩

producer段蕊,broker段保持,consumer段解压缩

默认不压缩。

8 多线程处理

只有一个用户主线程通常无法满足所需的吞吐量目标,因此需要构造多个线程同时给Kafka集群发送消息。有如下两种基本用法:

  • 多线程单KafkaProducer实例:全局只有一个生产者,然后多个线程共享使用。由于KafkaProducer是线程安全的,所以这种使用方法也是线程安全的
  • 多线程多KafkaProducer实例:在每个producer主线程中都构造一个KafkaProducer实例,并且此实例在线程中封装

第6章 broker存储结构

  • 每一个 partition(文件夹)相当于一个巨型文件被平均分配到多个大小相等segment(段)数据文件里。
    但每一个段segment file消息数量不一定相等,这样的特性方便old segment file高速被删除。(默认情况下每一个文件大小为1G)
  • 每一个 partiton仅仅须要支持顺序读写即可了。segment文件生命周期由服务端配置參数决定。

partiton中segment文件存储结构:

segment file组成:由2大部分组成,此2个文件一一相应,成对出现。

  • 索引文件index file:后缀.index
  • 数据文件data file:后缀.log

segment文件命名规则:partion全局的第一个segment从0开始,兴许每一个segment文件名称为上一个segment文件最后一条消息的offset值。

数值最大为64位long大小。19位数字字符长度,没有数字用0填充。

itcast@Server-node:/mnt/d/kafka_2.12-2.2.1$ ll /tmp/kafka/log/heima-0/ # 0号分区
total 20480
-rw-r--r-- 1 itcast sudo  10485760 Aug 29 09:38 00000000000000000000.index
-rw-r--r-- 1 itcast sudo     0 Aug 29 09:38 00000000000000000000.log
-rw-r--r-- 1 itcast sudo  10485756 Aug 29 09:38 00000000000000000000.timeindex
-rw-r--r-- 1 itcast sudo     8 Aug 29 09:38 leader-epoch-checkpoint

2 日志索引

2.1 数据文件的分段

kafka姐姐查询效率的手段之一就是将数据文件分段,比如有100条消息,他们的offset是从0到99。面,数据文件以该段中最小的offset命名。这样在查找指定offset的Message的时候,用二分查找就可以定位到该Message在哪个段中

2.2 偏移量索引

数据文件分段使得可以在一个较小的数据文件中查找对应offset的Message了,但是这依然需要顺序扫描才能找到对应offset的Message。为了进一步提高查找的效率,Kafka为每个分段后的数据文件建立了索引文件,文件名与数据文件的名字是一样的,只是文件扩展名为.index。

比如:要查找绝对 offset为7的Message:

首先是用二分查找确定它是在哪个LogSegment中,自然是在第一个Segment中。 打开这个Segment的index文件,也是用二分查找找到offset小于或者等于指定offset的索引条目中最大的那个offset。自然offset为6的那个索引是我们要找的,通过索引文件我们知道offset为6的Message在数据文件中的位置
为9807。

打开数据文件,从位置为9807的那个地方开始顺序扫描直到找到offset为7的那条Message。

这套机制是建立在offset是有序的。索引文件被映射到内存中,所以查找的速度还是很快的。

一句话,Kafka的Message存储采用了分区(partition),分段(LogSegment)和稀疏索引这几个手段来达到了高效性。

3 日志清理

3.1 日志删除

Kafka日志管理器允许定制删除策略。目前的策略是删除修改时间在N天之前的日志(按时间删除),也可以使用另外一个策略:保留最后的N GB数据的策略(按大小删除)。为了避免在删除时阻塞读操作,采用了copy-on-write形式的实现,删除操作进行时,读取操作的二分查找功能实际是在一个静态的快
照副本上进行的,这类似于Java的CopyOnWriteArrayList。 Kafka消费日志删除思想:Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用

conf/server.properties

log.cleanup.policy=delete # 启动删除策略
直接删除,删除后的消息不可恢复,可配置以下两个策略:
清理超过指定之间清理
log.retention.hours=16
超过指定大小后,删除旧的信息
log.retention.bytes=1073741824

3.2 日志压缩

将数据压缩,只保留每个key最后一个版本的数据。

首先在broker的配置中设置log.cleaner.enable=true启用cleaner,这个默认是关闭的。

在Topic的配置中设置log.cleanup.policy=compact启用压缩策略。

【MQ】Kafka笔记_第23张图片

压缩后的offset可能是不连续的,比如上图中没有5和7,因为这些offset的消息被merge了,当从这些offset消费消息时,将会拿到比这个offset大的offset对应的消息,比如,当试图获取offset为5的消息时,实际上会拿到offset为6的消息,并从这个位置开始消费。

这种策略只适合特殊场景,比如消息的key是用户ID,消息体是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。

压缩策略支持删除,当某个Key的最新版本的消息没有内容时,这个Key将被删除,这也符合以上逻辑。

除了消息顺序追加,页缓存等技术, Kafka还使用了零拷贝技术来进一步提升性能。“零拷贝技术”只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作。如果有10个消费者,传统方式下,数据复制次数为4*10=40次,而使用“零拷贝技术”只需要1+10=11次,一次为从磁盘复制到页面缓存,10次表示10个消费者各自读取一次页面缓存。

4 磁盘存储优势

Kafka在设计的时候,采用了文件追加的方式来写入消息,即只能在日志文件的尾部追加新的消息,并且不允许修改已经写入的消息,这种方式属于典型的顺序写入此判断的操作,所以就算是Kafka使用磁盘作为存储介质,所能实现的额吞吐量也非常可观。

Kafka中大量使用页缓存,这页是Kafka实现高吞吐的重要因素之一。

第7章 Consumer API

Consumer 消费数据时的可靠性是很容易保证的,因为数据在 Kafka 中是持久化的,故不用担心数据丢失问题。

由于 consumer 在消费过程中可能会出现断电宕机等故障, consumer 恢复后,需要从故障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费。

所以 offset 的维护是 Consumer 消费数据是必须考虑的问题。

  • 0.9.0.0版本之前是Scala语言写的,之后是java语写的
  • 新版本使用的是org.apache.kafka.client.consumer.KafkaConsumer
  • 旧版本使用的是kafka.consumer.ZookeeperConsumerConnector/SimpleConsumer

7.1 消费者流程

consumer 采用 pull(拉) 模式从 broker 中读取数据。(生产者是push)

push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成 consumer 来不及处理消息, 典型的表现就是拒绝服务以及网络拥塞。而 pull 模式则可以根据 consumer 的消费能力以适当的速率消费消息。

pull 模式不足之处是,如果 kafka 没有数据,消费者可能会陷入循环中, 一直返回空数据。 针对这一点, Kafka 的消费者在消费数据时会传入一个时长参数 timeout,如果当前没有数据可供消费, consumer 会等待一段时间之后再返回,这段时长即为 timeout

对于Kafka而言,pull模式更合适,它可简化broker的设计,consumer可自主控制消费消息的速率,同时consumer可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。

7.2 消费者创建

  • 回忆控制台的消费者创建:
# 启动zk和kafka集群,在kafka集群中打开一个消费者
[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh \
--zookeeper hadoop102:2181 --topic first
# --from-beginning是指从头消费,与后面javaAPI中的auto.offset.reset=earilest一样
  • java消费创建

pom.xml

<dependencies>
    
    <dependency>
        <groupId>org.apache.kafkagroupId>
        <artifactId>kafka-clientsartifactId>
        <version>0.11.0.0version>
    dependency>
    
    <dependency>
        <groupId>org.apache.kafkagroupId>
        <artifactId>kafka_2.12artifactId>
        <version>0.11.0.0version>
    dependency>
dependencies>

Kafka消费者Java API

  • 构造一个java.util.Properties对象,至少指定bootstrap.servers、key.deserializer、value.deserializer和group.id的值
  • 使用Properties实例构造KafkaConsumer对象
  • 调用KafkaConsumer.subscribe()订阅一个topic列表
  • 循环调用KafkaConsumer.poll()获取封装在ConsumerRecord的topic信息
  • 处理获取到的ConsumerRecord对象
  • 关闭KafkaConsumer
import java.util.Arrays;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

public class CustomNewConsumer {

	public static void main(String[] args) {

		Properties props = new Properties();
		// 定义kakfa 服务的地址,不需要将所有broker指定上 
		props.put("bootstrap.servers", "hadoop102:9092");
		// 制定consumer group 
		props.put("group.id", "test");
		// 是否自动确认offset 
		props.put("enable.auto.commit", "true"); 
		// 自动确认offset的时间间隔 
		props.put("auto.commit.interval.ms", "1000");
		// key的序列化类
		props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
		// value的序列化类 
		props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
		// 定义consumer 
		KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
		
		// 消费者订阅的topic, 可同时订阅多个 
		consumer.subscribe(Arrays.asList("first", "second","third"));

		while (true) {
			// 读取数据,读取超时时间为100ms 
			ConsumerRecords<String, String> records = consumer.poll(100);
			
			for (ConsumerRecord<String, String> record : records)
				System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
		}
	}
}

①Properties对象

http://kafka.apache.org/documentation/#consumerconfigs

必选的参数:

  • bootstrap.servers:与生产者类似,一个ip:port对,可以用逗号分隔多组。同样如果是集群的话无需都指定,指定几个防止指定的宕机即可,zookeeper会自动订阅集群内所有的指定topic。如果broker段没有使用ip配置advertised.listeners的话,就不要把bootstrap.servers写成ip,而应该是主机名,因为kafka诶不使用的全称域名FQDN。倘若不统一,会出现无法获取元数据的异常。
  • group.id
  • key.deserializer:从字节流转成如utf-8,。可以自己实现覆盖,需要实现接口Deserializer,常用StringDeserializer,但注意要传入全限定类名
  • value.deserializer:同理,得是全限定类名

可选参数

  • session.timeout.ms:检测组内成员发送崩溃的时间。如果设置为5分钟,如果某个消费者组内成员崩溃了,那么可能需要5分钟才会发现这个崩溃。同时还代表consumer消息处理逻辑的最大时间----倘若consumer两次poll()时间间隔超过这个参数,就会检测为coordinator就会认为这个consumer已经跟不上消费者组内其他成员的消费进度了,因此就把该消费者提出消费者组,原来他消费的partition再分配给其他consumer。但0.10.1.0版本之后该参数仅代表coordinator检测失败的时间。
  • max.poll.interval.ms:即上面consumer处理逻辑最大时间。
  • auto.offset.reset:指定了消费者要消费的信息的唯一不在当前消息日志合理区间范围时kafka的应对策略。如果指定了从头消费,消费了一些后重启消费者组也会接着消费,因为kafka保存 了该组位移信息,因此再消费会无视auto.offset.reset该设置
    • earliest:从最早的位置开始消费。注意这里的最早的位移不一定是0
    • latest:从最新处位移开始消费
    • none:为发现位移信息或唯一越界,则抛出异常。不常用。
  • enable.auto.commit:自动提交。对精准处理一次语义需求的用户来说,设置为false
  • fetch.max.bytes:单次获取数据的最大字节数
  • max.poll.records:单次poll返回的最大消息数。
  • heartbeat.interbal.ms:组内其他成员感知rebalance的时长,必须小于session.timeout.ms,即如果consumer在timeout时长内都不发送心跳,coordinator就会认为它已经dead
  • connections.max.idle.ms:周期性观测到请求平均处理时间在飙升,也可能是因为kafka会定期第关闭空闲socket连接导致下次consumer处理请求时需要重新创建broker的socket连接。默认为9分钟。设置为-1后就不会关闭空闲连接

②构造KafkaConsumer对象

// 定义consumer 
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
//或
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props,new StringDeserializer(),new StringDeserializer());

订阅topic列表

// 消费者订阅的topic, 可同时订阅多个 
consumer.subscribe(Arrays.asList("first", "second","third"));

// 通过正则表达式匹配多个主题。并且订阅之后如果又有新的匹配的新主题,那么这个消费者组会立即对齐进行消费。非常有用
consumer.subscribe(Pattern.compile("kafka.*"),
                   new NoOpConsumerRebalanceListener());//实现了ConsumerRebalanceListener接口,但这里说明都不做

// 如果是使用独立consumer,可以手动订阅指定分区
TopicPartition tp1 = new TopicPartition("topic-name",0);
TopicPartition tp2 = new TopicPartition("topic-name",1);
consumer.assign(Arrays.asList(tp1,tp2));//用的是assign

④获取消息

while (true) {//需要在其他线程中调用consumer.wakeup()触发consumer的关闭。虽然consumer是线程不安全的,但其他用户调用这个函数是安全的
    // 读取数据,读取超时时间为100ms 
    ConsumerRecords<String, String> records = consumer.poll(100);

    for (ConsumerRecord<String, String> record : records)
        System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}

⑤处理ConsumerRecord对象

如④

⑥关闭Consumer

清楚consumer创结点socket资源,还会通知消费者组coordinator主动离组从而更快地开启新一轮rebalance。

try{
    while (true) {
        // 读取数据,读取超时时间为100ms 
        ConsumerRecords<String, String> records = consumer.poll(100);

        for (ConsumerRecord<String, String> record : records)
            System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
    }
    
}finally{
    KafkaConsumer.close();
    KafkaConsumer.close(timeout);//关闭消费者并最多当代timeout秒
}

poll

java consumer是一个多线程或者说是一个双线程的java进程

  • 用户主线程:创建ConsumerKafkaConsumer的线程。(poll在这里运行)
  • 后台心跳线程:consumer后台创建一个心跳线程

消费者组执行rebalance、消息获取、coordinator管理、异步任务结果的处理甚至位移提交等操作都是运行在用户主线程中的。

常见用法

while (true) {
    // 读取数据,读取超时时间为100ms ,即每个100ms拉取一次
    ConsumerRecords<String, String> records = consumer.poll(100);//但是拉取到的数据可能处理失败,所以这里容易出问题,重新启动时因为有offset我们就不能重新处理数据了,所以我们后面改用手动提交

    for (ConsumerRecord<String, String> record : records)
        System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}

poll()方法根据当前consumer消费的唯一返回消息集合。当poll首先被调用用,新的消费者组会被创建并根据对应的唯一重设策略(auto.offset.reset)来设定消费者组的位移。一旦consumer开始提交位移,每个后续的rebalance完成后都会将位置设置为上次已提交的位移。

传递给poll()方法的超时设定参数用于控制consumer等待消息的最大阻塞时间。由于某些原因,broker端有时候无法立即满足consumer端的获取请求(比如consumer要求至少一次获取1MB的数据,但broker端无法立即全部给出),那么此时consumer端就会阻塞以等待数据不断累积被满足consumer需求。如果用户不想让consumer一直处于阻塞状态,则需要给定一个超时时间。因此poll方法返回满足一下任一条件时即可返回

  • 获取了足够多的可用数据
  • 等待时间超过指定的超时设置

consumer是单线程的设计理念,因此consumer运行在它专属的线程中。新版本的java consumer不是线程安全的,如果没有显示地同步锁保护机制,kafka会排除KafkaConsumer is not safe for multi-threaded access异常,这代表同一个kafkaconsumer实例用在了多个线程中,这是不允许的。

上面的是while(isRunning)来判断是否退出消费循环结束consumer应用。具体的做法是让isRunning边控几位volatile型

7.3 消费者组

消费者使用一个消费者组名group.id来表示自己,topic的每条消息都只会发送到每个订阅他的消费者组的一个消费者实例上。

  • 一个消费者组包含若各个消费者
  • 每个partition消息只能被发送到消费者组中一个消费者实例上(即一个partition只能对应一个consumer,不能对应2个consumer,所以消费者组里的consumer不要比partition数多,多了没有意义)
    • 要求:patitions>consumers
  • partition消息可以发送到多个消费者组中
  • consumer从partition中消费消息是顺序消费,默认从头开始消费

因此,基于消费者组可以实现:

  • 基于队列的模型:所有消费者都在同一消费者组里,每条消息只会被一个消费者处理
  • 基于发布订阅的模型:消费者属于不同的消费者组。极端情况下每个消费者都有自己的消费者组,这样kafka消息就能广播到所有消费者实例上。

在下图中,有一个由三个消费者组成的group,有一个消费者读取主题中的两个分区,另外两个分别读取一个分区。某个消费者读取某个分区,也可以叫做某个消费者是某个分区的拥有者。

【MQ】Kafka笔记_第24张图片

在这种情况下,消费者可以通过水平扩展的方式同时读取大量的消息。另外,如果一个消费者失败了,那么其他的group成员会自动负载均衡读取之前失败的消费者读取的分区。

消费者组案例

1)需求:测试同一个消费者组中的消费者,同一时刻只能有一个消费者消费。

2)案例实操

​ (1)在hadoop102、hadoop103上修改/opt/module/kafka/config/consumer.properties配置文件中的group.id属性为固定组名。不指定的话,虽然那几个成员还是属于一个组的,但是组名是变化的(也可以体会出默认是基于消息队列模式的)

[atguigu@hadoop102 config]$ vim consumer.properties
group.id=atguigu

[atguigu@hadoop103 config]$ vim consumer.properties
group.id=atguigu

​ (2)在hadoop102、hadoop103上分别启动消费者

# consumer.properties配置文件里指定了组
[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh --zookeeper hadoop102:2181 --topic first --consumer.config config/consumer.properties

[atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh --zookeeper hadoop102:2181 --topic first --consumer.config config/consumer.properties

# 此时可以在zk集群上查看消费者组
bin/zkCli.sh

ls /consumers
console-consumer-9       console-consumer-84342   test-consumer-group      atguigu console-consumer-67579

ls /consumers/atguigu

​ (3)在hadoop104上启动生产者

[atguigu@hadoop104 kafka]$ bin/kafka-console-producer.sh  --broker-list hadoop102:9092 --topic first
# 输入
1
2
3
# 发现轮询第输出在了2个消费者上,而不是全输出到一个消费者上
# 增加了消费者后就是一人一个分区了,原来是一人消费2个分区

消费者组的意义

消费者组里某个消费者挂了组内其他消费能接管partition,这叫重平衡。

  • 高伸缩性
  • 高容错性

消费者组再平衡

再均衡是指,对于一个消费者组,分区的所属从一个消费者转移到另一个消费者的行为,分区的消费者改变了。他为消费者组剧本里高可用性和伸缩性提供了保障,使得我们既可以方便又安全地删除组内的消费者或者往消费者组里添加消费者。不过再均衡发生期间,消费者是无法拉取信息的。

  • (1)指定了patition情况下,则直接使用;
  • (2)未指定patition但指定key情况下,将key的hash值与topic的partition数进行取余得到partition值;
  • (3)patition和key都未指定情况下,第一次调用时随便生成一个整数(后面每次调用在这个整数上自增),将这个值与topic可用的partition总数取余得到partition值。即使用round-robin算法轮询选出一个patition。

监听器,分区消费者一旦发生变化时,可以处理,再均衡期间,消费者无法拉取消息。

消费者组再平衡:比如有20个消费者组,订阅了100个partition,正常情况下消费者组会为每个消费者分配5个partition,每个消费者负责读取5个分区的数据。

一个 consumer group 中有多个 consumer,一个 topic 有多个 partition,所以必然会涉及到 partition 的分配问题,即确定那个 partition 由哪个 consumer 来消费。

再平衡触发条件:

  1. 当一个 consumer 加入组时,读取的是原本由其他 consumer 读取的分区。
  2. 当一个 consumer 离开组时(被关闭或发生崩溃),原本由它读取的分区将由组里的其他 consumer 来读取。
  3. 当 Topic 发生变化时,比如添加了新的分区,会发生分区重分配。

producter是线程安全的,consumer不是线程安全的。有两种典型的处理模式:一是每个线程里创建consumer。二是只用一个consumer然后在里面分线程

// 出现再均衡时,马上再提交一回
public class CommitSynclnRebalance {
    public static void main(String[] args) {
        Properties properties = initNewConfig();
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);

        HashMap<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();
        consumer.subscribe(Arrays.asList("first"), new ConsumerRebalanceListener() {
            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
                // 尽量避免重复消费
                consumer.commitAsync(currentOffsets);
            }

            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
                // do nothing
            }
        });

        try {
            while (isRunning.get()){
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
                for (ConsumerRecord<String, String> record : records) {
                    
                    System.out.println(record.offset()+":"+record.value());
                    currentOffsets.put(new TopicPartition(record.topic(),record.partition()),new OffsetAndMetadata(record.offset()+1))
                }
                // // 也可以通过records.partitions()分partition处理
                /*
                for(TopicParition partition:records.partitions()){
                	List> recordOfPartition = records.records(partition);//取得指定分区的record
                	long lastOffset = pRecord.get(pRecord.size()-1).offset();
                	Map offset = new HashMap();
                	offset.put(partition,new OffsetAndMetadata(lastOffset+1));//从下一个位置开始查
                	consumer.commitAsync(offset);//针对每一次partition单独提交offset
                }
                */
    
            }
        } finally {
            consumer.commitAsync(currentOffsets,null);
        }
    }
}

public static void main(String[] args) {
    Properties props = initConfig();
    KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
    Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();
    // 订阅主题时指定监听器
    consumer.subscribe(Arrays.asList(topic), new ConsumerRebalanceListener()
                       {
                           @Override
                           public void onPartitionsRevoked(Collection<TopicPartition>
                                                           partitions) {
                               // 劲量避免重复消费
                               consumer.commitSync(currentOffsets);
                           }
                           @Override
                           public void onPartitionsAssigned(Collection<TopicPartition>
                                                            partitions) {
                               //do nothing.
                           }
                       });
    try {
        while (isRunning.get()) {
            ConsumerRecords<String, String> records =
                consumer.poll(Duration.ofMillis(1000));
            for (ConsumerRecord<String, String> record : records) {
                System.out.println(record.offset() + ":" + record.value());
                // 异步提交消费位移,在发生再均衡动作之前可以通过再均衡监听器的onPartitionsRevoked回调执行commitSync方法同步提交位移。
                    currentOffsets.put(new TopicPartition(record.topic(),
                                                          record.partition()),
                                       new OffsetAndMetadata(record.offset() + 1));
            }
            consumer.commitAsync(currentOffsets, null);
        }
    } finally {
        consumer.close();
    }
}

再平衡分配策略

Kafka 有两种分配策略,一是 RoundRobin,一是 Range 。触发时机:消费者组里个数发生变化时。

1) RoundRobin

1) RoundRobin :把所有的 partition 和所有的 consumer 都列出来,然后按照 hashcode 进行排序,最后通过轮询算法来分配 partition 给到各个消费者。

轮询关注的是组

假如有3个Topic :T0(三个分区P0-0,P0-1,P0-2),T1(两个分区P1-0,P1-1),T2(四个分区P2-0,P2-1,P2-2,P2-3)

有三个消费者:C0(订阅了T0,T1),C1(订阅了T1,T2),C2(订阅了T0,T2)

那么分区过程如下图所示

【MQ】Kafka笔记_第25张图片

分区将会按照一定的顺序排列起来,消费者将会组成一个环状的结构,然后开始轮询。

C0: P0-0,P0-2,P1-1
C1:P1-0,P2-0,P2-2
C2:P0-1,P2-1,P2-3

2)Range

2)Range:范围分区策略是对每个 topic 而言的。首先对同一个 topic 里面的分区按照序号进行排序,并对消费者(不是消费者组)按照字母顺序进行排序。通过 partitions数/consumer数 来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费 1 个分区。

range跟组没什么关系,只给订阅了的消费者发,而不是给订阅了的消费者组发

7.4 offset

这里的offset是consumer端的offset。

offset:每个consumer实例需要为他消费的partition维护一个记录自己消费到哪里的偏移offset。

kafka把offset保存在消费端的消费者组里。kafka引入了检查点机制定期对offset进行持久化。kafka consumer在内部使用一个map保存其订阅topic所属分区的offset。如记录topicA-0:8;topicA-1:6…

offset可以避免 consumer 在消费过程中可能会出现断电宕机等故障, consumer 恢复后,需要从故障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费。

位移提交

poll()之后从partition拉取了一些数组,然后可以调用commit()函数告诉 partition这一批数据消费成功,返回值这一批数据最高的偏移量提交给partition。

位移提交offset commit:consumer客户端需要定期向kafka集群汇报自己消费数据的进度。当我们调用poll()时就会根据该信息消费。

消费者的offset的保存位置:

  • 旧版本:旧版本的记录在zookeeper的/consumers/[group.id]/offsets/[topic]/[partitionID]下。缺点是zk是一个协调服务组件,不适合用作位移信息的存储组件,频繁高并发读写不是zk擅长的事情。

  • 新版本:0.9.0.0版本之后,位移提交放到kafka-Broker内部一个名为__concumer_offsets的topic里。

  • 提交间隔时长:当我们将enable.auto.commit设置为true,那么消费者会在poll方法调用后每隔5秒(由auto.commit.interval.ms指定)提交一次位移。和很多其他操作一样,自动提交也是由poll()方法来驱动的。在调用poll()时,消费者判断是否到达提交时间,如果是则提交上一次poll返回的最大位移

  • 默认是自动提交offset的,

    • 自动提交:隔段时间就自动提交offset,告诉partition已经消费好了。(broker并不跟踪消息是否被消费到,而是消费者自己提交消费好的位移,但默认是自动提交的,即刚拿到就确认了)
      • 自动提交 offset 的相关参数:

        • enable.auto.commit: 是否开启自动提交 offset 功能
        • auto.commit.interval.ms: 自动提交 offset 的时间间隔
    • 手动提交:适合有较强的精确一次处理语义时,可以确保只要消息被处理完后再提交位移。
  • 注意:返回给broker的offset是下一条要消费的位移

[zk: localhost:2181(CONNECTED) 1] ls /
[cluster, controller_epoch, brokers, zookeeper, admin, isr_change_notification, consumers, latest_producer_id_block, config]

[zk: localhost:2181(CONNECTED) 4] ls /brokers/topics
[first]


[zk: localhost:2181(CONNECTED) 6] ls /consumers
[console-consumer-9, console-consumer-67579] # 不同机器里的结果是一样的。数字是消费者组

# 67579号消费者组里offset信息里的 主题first 0号partition 的offset
[zk: localhost:2181(CONNECTED) 10] get /consumers/console-consumer-67579/offsets/first/0
4
cZxid = 0x100000060
ctime = Thu Jul 16 15:59:43 CST 2020
mZxid = 0x100000060
mtime = Thu Jul 16 15:59:43 CST 2020
pZxid = 0x100000060
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 1
numChildren = 0

【MQ】Kafka笔记_第26张图片

__concumer_offsets

【MQ】Kafka笔记_第27张图片

consumer会在kafka集群的所有broker中选择一个broker作为consumer group的coordinator,用于实现组成员管理、消费分配方案制定以及提交位移等。和普通的kafka topic相同,该topic有多个分区,每个分区有多个副本,他存在的唯一目的就是保存consumer提交的位移。

当消费者组首次启动时,由于没有初始的位移信息,coordinator不需要为其确定初始位移值,这就是consumer参数auto.offset.reset的作用。通常情况下,consumer要么从最早的位移开始读取,要么从最新的位移开始读取。

当consumer运行了一段时间之后,它必须要提交自己的位移值。如果consumer崩溃或者被关闭,它负责的分区就会被分配给其他consumer,因此要在其他consumer读取这些分区前就做好位移提交工作,否则会出现消息的重复消费。

cosnsumer提交位移的主要机制是通过向所属的coordinator发送位移提交请求来实现的。每个位移提交请求都会往__consumer_offsets对应分区上追加写入一条消息。消息的key是group.id+topic+partition的元组,而value就是位移值。如果consumer为同一个group的同一个topic分区提交了多次位移那么__consumer_offsets对应的分区上就会有若干条key相同但value不同的消息,但显然我们只关心最新一次提交的那条消息。从某种程序来说,只有最新提交的位移值是有效的,其他消息包含的位移值其实都已经过期了。kafka通过compact策略来处理这种消息使用模式。

考虑到一个kafka生产环境可能有很多consumer或consumer group,如果这些consumer同时提交位移,则必将加重__concumer_offsets的写入组咋,因此默认为该topic创建了50个分区,并且对每个group.id哈希后取模运算,分散到__concumer_offsets上。也就是说,每个消费者组保存的offset都有极大的概率出现在该topic的不同分区上

两种offset

Offset从语义上来看拥有两种:Current Offset和Committed Offset。

Current Offset

Current Offset保存在Consumer客户端中,它表示Consumer希望收到的下一条消息的序号。它仅仅在poll()方法中使用。例如,Consumer第一次调用poll()方法后收到了20条消息,那么Current Offset就被设置为20。这样Consumer下一次调用poll()方法时,Kafka就知道应该从序号为21的消息开始读取。这样就能够保证每次Consumer poll消息时,都能够收到不重复的消息。

Committed Offset

Committed Offset保存在Broker上,它表示Consumer已经确认消费过的消息的序号。主要通过commitSynccommitAsync
API来操作。举个例子,Consumer通过poll() 方法收到20条消息后,此时Current Offset就是20,经过一系列的逻辑处理后,并没有调用consumer.commitAsync()consumer.commitSync()来提交Committed Offset,那么此时Committed Offset依旧是0。

Committed Offset主要用于Consumer Rebalance。在Consumer Rebalance的过程中,一个partition被分配给了一个Consumer,那么这个Consumer该从什么位置开始消费消息呢?答案就是Committed Offset。另外,如果一个Consumer消费了5条消息(poll并且成功commitSync)之后宕机了,重新启动之后它仍然能够从第6条消息开始消费,因为Committed Offset已经被Kafka记录为5。

总结一下,Current Offset是针对Consumer的poll过程的,它可以保证每次poll都返回不重复的消息;而Committed Offset是用于Consumer Rebalance过程的,它能够保证新的Consumer能够从正确的位置开始消费一个partition,从而避免重复消费。

Offset查询

前面我们已经描述过offset的存储模型,它是按照groupid-topic-partition -> offset的方式存储的。然而Kafka只提供了根据offset读取消息的模型,并不支持根据key读取消息的方式。那么Kafka是如何支持Offset的查询呢?

答案就是Offsets Cache!!

【MQ】Kafka笔记_第28张图片

如图所示,Consumer提交offset时,Kafka Offset Manager会首先追加一条条新的commit消息到__consumers_offsets topic中,然后更新对应的缓存。读取offset时从缓存中读取,而不是直接读取__consumers_offsets这个topic。

指定消费位置

消息的拉取是 根据poll()方法的逻辑来处理的,但是这个方法对普通开发人员来说是个黑盒子,无法精确账务其消费的起始位置。

seek()方法正好提供了这个功能,让我们得以追踪以前的消费或者回溯消费

consumer.seek(topicPartition,3);//第一个参数为分区,第二个参数为分区中的位置
public class SeekDemo {
    static String topic="first";


    public static void main(String[] args) {
        Properties properties = initNewConfig();
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
        consumer.subscribe(Arrays.asList(topic));
        consumer.poll(Duration.ofMillis(2000));
        Set<TopicPartition> assignment = consumer.assignment();
        System.out.println(assignment);
        Map<TopicPartition, Long> offsets = consumer.endOffsets(assignment);//获取分区的offset
        for (TopicPartition topicPartition : assignment) {
            // 指定从分区的那个offset开始消费
            consumer.seek(topicPartition,3);//第一个参数为分区,第二个参数为分区中的位置
            // 如果想要从分区末尾开始消费
            //            consumer.seek(topicPartition,offsets.get(topicPartition);
        }
        while (true){
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
            for (ConsumerRecord<String, String> record : records) {
                System.out.println(record.offset()+":"+record.value());
            }
        }
    }
}

// 指定从分区末尾开始消费
Map<TopicPartition, Long> offsets = consumer.endOffsets(assignment);
for (TopicPartition tp : assignment) {
    consumer.seek(tp, offsets.get(tp));
}
//演示位移越界操作
for (TopicPartition tp : assignment) {
    //consumer.seek(tp, offsets.get(tp));
    consumer.seek(tp, offsets.get(tp) + 1);
}

1)修改配置文件 /kafka/conf/consumer.properties

# 为了看数据
exclude.internal.topics=false

2)读取 offset

重新启动消费者控制台

0.11.0.0 之前版本:

bin/kafka-console-consumer.sh --topic __consumer_offsets --zookeeper hadoop102:2181 --formatter "kafka.coordinator.GroupMetadataManager\$OffsetsMessageFormatter" --consumer.config config/consumer.properties --from-beginning

0.11.0.0 之后版本(含):

bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --from-beginning --topic first

大概1s更新一次

# 解读
# [组,主题,分区] # 以这个组合来记录消费者组消费到哪里了
# 后面16代表offset
[console-consumer-16121,first,0]::[OffsetMetadata[16,NO_METADATA],CommitTime 1595055672594,ExpirationTime 1595142072594]
[console-consumer-16121,first,0]::[OffsetMetadata[16,NO_METADATA],CommitTime 1595055677590,ExpirationTime 1595142077590]
[console-consumer-16121,first,0]::[OffsetMetadata[16,NO_METADATA],CommitTime 1595055682592,ExpirationTime 1595142082592]
[console-consumer-16121,first,0]::[OffsetMetadata[16,NO_METADATA],CommitTime 1595055687595,ExpirationTime 1595142087595]
[console-consumer-16121,first,0]::[OffsetMetadata[16,NO_METADATA],CommitTime 1595055692596,ExpirationTime 1595142092596]
[console-consumer-16121,first,0]::[OffsetMetadata[16,NO_METADATA],CommitTime 1595055697597,ExpirationTime 1595142097597]
...
# 然后让生成者又生成了2条,发现从16变到17/18
[console-consumer-16121,first,0]::[OffsetMetadata[17,NO_METADATA],CommitTime 1595056012701,ExpirationTime 1595142412701]
[console-consumer-16121,first,0]::[OffsetMetadata[17,NO_METADATA],CommitTime 1595056017704,ExpirationTime 1595142417704]
[console-consumer-16121,first,0]::[OffsetMetadata[17,NO_METADATA],CommitTime 1595056022708,ExpirationTime 1595142422708]
[console-consumer-16121,first,0]::[OffsetMetadata[18,NO_METADATA],CommitTime 1595056027708,ExpirationTime 1595142427708]
[console-consumer-16121,first,0]::[OffsetMetadata[18,NO_METADATA],CommitTime 1595056032713,ExpirationTime 1595142432713]
...

消费手动提交offset

虽然自动提交 offset 十分简介便利,但由于其是基于时间提交的, 开发人员难以把握offset 提交的时机。因此 Kafka 还提供了手动提交 offset 的 API。

手动提交 offset 的方法有两种:

  • commitSync(同步提交):阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败)。
  • commitAsync(异步提交):没有失败重试机制,故有可能提交失败。
  • 可以传入一个Map显示地告诉kafka为那些分区提交位移。

1) 同步提交 offset

由于同步提交 offset 有失败重试机制,故更加可靠,以下为同步提交 offset 的示例

package com.atguigu.kafka.consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays;
import java.util.Properties;
public class CustomComsumer {
    public static void main(String[] args) {
        Properties props = new Properties();
        //Kafka 集群
        props.put("bootstrap.servers", "hadoop102:9092");
        //消费者组,只要 group.id 相同,就属于同一个消费者组
        props.put("group.id", "test");
        props.put("enable.auto.commit", "false"); // 关闭自动提交offset
        props.put("key.deserializer",
                  "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer",
                  "org.apache.kafka.common.serialization.StringDeserializer");
        KafkaConsumer<String, String> consumer = new
            KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList("first"));//消费者订阅主题
        while (true) {
            //消费者拉取数据
            ConsumerRecords<String, String> records =
                consumer.poll(100);
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
            //同步提交,当前线程会阻塞直到 offset 提交成功
            consumer.commitSync();
        }
    }
}

手动提交同步案例2

List<ConsumerRecord<String,String>> buffer = new ArrayList<>();
while(true){
    ConsumerRecord<String,String> records = consumer.poll(1000);
    for(ConsumerRecord<String,String> record:records){
        // 先添加到缓冲区中
        buffer.add(record);
    }
    if(buffer.size>=minBatchSize){
        // 缓冲区中累积到一定大小后才刷新到数据库
        insertIntoDb(buffer);
        // 同步提交
        consumer.commitSync();
        // 清空缓冲区
        buffer.clear();
    }
}

2)异步提交 offset

虽然同步提交 offset 更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因此吞吐量会收到很大的影响。因此更多的情况下,会选用异步提交 offset 的方式。

consumer在后续调用时轮询该位移提交的结果。特别注意的是,这里的异步提交位移不是指consumer使用单独的线程进行位移提交。实际上consumer依然会在用户主线程的poll方法中不断轮询这次异步提交的结果。只是该提交发起时此方法是不会阻塞的,因而被称为异步提交。

package com.atguigu.kafka.consumer;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
public class CustomConsumer {
    public static void main(String[] args) {
        Properties props = new Properties();
        //Kafka 集群
        props.put("bootstrap.servers", "hadoop102:9092");
        //消费者组,只要 group.id 相同,就属于同一个消费者组
        props.put("group.id", "test");
        //关闭自动提交 offset
        props.put("enable.auto.commit", "false");
        props.put("key.deserializer",
                  "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer",
                  "org.apache.kafka.common.serialization.StringDeserializer");
        KafkaConsumer<String, String> consumer = new
            KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList("first"));//消费者订阅主题
        while (true) {
            ConsumerRecords<String, String> records =
                consumer.poll(100);//消费者拉取数据
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
            //异步提交
            consumer.commitAsync(new OffsetCommitCallback() {
                @Override
                public void onComplete(Map<TopicPartition,
                                       OffsetAndMetadata> offsets,
                                       Exception exception) {
                    if (exception != null) {
                        System.err.println("Commit failed for" + offsets);
                    }
                }
            });
        }
    }
}
while(running){
    ConsumerRecords<String,String> records = consumer.poll(1000);
    // 按照分区进行位移提交
    for(TopicPartition partition,records.partition()){
        // 获取当前分区的消息
        List<ConsumerRecord<String,String>>partitionRecords = records.records(partition);
        // 处理当前分区的消息
        for(ConsumerRecord<String,String> record:partitionRecords){
            sout()
        }
        // 获得当前分区的最大位移
        long lastOffset = partitionRecords.get(partitionRecords.size()-1).offset();
        // 提交的位移是吓一跳待读取消息的位移
        consumer.commitSync(Collections.singletonMap(partition,new OffsetAndMetadata(lastOffset+1)));
    }
}

3) 数据漏消费和重复消费分析

无论是同步提交还是异步提交 offset,都有可能会造成数据的漏消费或者重复消费。

先提交 offset 后消费,有可能造成数据的漏消费;

而先消费后提交 offset,有可能会造成数据的重复消费。

位移管理

consumer端需要为每个它要读取的分区保存消费进度,即分区中当前最新消费消息的位置。该位置就被称为位移(ofet)。 consumer需要定期地向Kaka提交自己的位置信息,实际上,这里的位移值通常是下一条待消费的消息的位置。假设 consumer已经读取了某个分区中的第N条消息,那么它应该提交位移值为N,因为位移是从0开始的,位移为N的消息是第N+1条消息。这样下次 consumer重启时会从第N+1条消息开始消费。总而言之, offset就是consumer端维护的位置信息。

offset对于 consumer非常重要,因为它是实现消息交付语义保证( message delivery semantic)的基石。常见的3种消息交付语义保证如下。

  • 最多一次( at most once)处理语义:消息可能丢失,但不会被重复处理。
  • 最少一次( at least once)处理语义(默认):消息不会丢失,但可能被处理多次
  • 精确一次( exactly once)处理语义:消息一定会被处理且只会被处理一次。

显然,若 consumer在消息消费之前就提交位移,那么便可以实现 at most once—因为若consumer在提交位移与消息消费之间崩溃,则 consumer重启后会从新的 onset位置开始消费,前面的那条消息就丢失了。

相反地,若提交位移在消息消费之后,则可实现 at least once语义。由于Kaka没有办法保证这两步操作可以在同一个事务中完成,因此Kafka默认提供的就是at least once的处理语义。好消息是 Kafka社区已于0.11.0.0版本正式支持事务以及精确一次处理语义。

既然offset本质上就是一个位置信息,那么就需要和其他一些位置信息区别开来。

  • 当前提交位移last committed offset:consumer最近已经已经提交的offset
  • 当前位置:consumer已读取但尚未提交时的位置
  • 日志最新位移LEO(log end offset):指的是每个副本最大的 offset;与生产者的ISR有关
  • 水位/高水位(HW high water):指的是消费者能见到的最大的 offset, ISR 队列中最小的 LEO。即公共部分,消费者只能看到HW的部分。与生产者的ISR有关

3.2.3 Exactly Once 语义

精准一次性Exactly Once:

将服务器的 ACK 级别设置为-1,可以保证 Producer 到 Server 之间不会丢失数据,即 At Least Once 语义。可以保证数据不丢失,但是不能保证数据不重复;

相对的,将服务器 ACK 级别设置为 0,可以保证生产者每条消息只会被发送一次,即 At Most Once 语义。可以保证数据不重复,但是不能保证数据不丢失。

但是,对于一些非常重要的信息,比如说交易数据,下游数据消费者要求数据既不重复也不丢失,即 Exactly Once 语义。 在 0.11 版本以前的 Kafka,对此是无能为力的,只能保证数据不丢失,再在下游消费者对数据做全局去重。对于多个下游应用的情况,每个都需要单独做全局去重,这就对性能造成了很大影响。

0.11 版本的 Kafka,引入了一项重大特性:幂等性。所谓的幂等性就是指 Producer 不论向 Server 发送多少次重复数据, Server 端都只会持久化一条。幂等性结合 At Least Once 语义,就构成了 Kafka 的 Exactly Once 语义。即:

At Least Once + 幂等性 = Exactly Once

要启用幂等性,只需要将 Producer 的参数中 enable.idompotence 设置为 true 即可。 Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的 Producer 在初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number。而Broker 端会对做缓存,当具有相同主键的消息提交时, Broker 只会持久化一条。

但是 PID 重启就会变化,同时不同的 Partition 也具有不同主键,所以幂等性无法保证跨分区跨会话的 Exactly Once

4.2.4 自定义分区生产者

4)测试

​ (1)在hadoop102上监控/opt/module/kafka/logs/目录下first主题3个分区的log日志动态变化情况

[atguigu@hadoop102 first-0]$ tail -f 00000000000000000000.log
[atguigu@hadoop102 first-1]$ tail -f 00000000000000000000.log
[atguigu@hadoop102 first-2]$ tail -f 00000000000000000000.log

消费者拦截器

在消费到消息或者在消费提交消费位移时进行的一些定制化操作

场景:

对消费消息设置一个有效期的属性,如果某条消息在既定的时间窗口内无法到达,那就视为无效,不需要再被处理。

public ConsumerRecords<String, String> onConsume(ConsumerRecords<String, String>
                                                 records) {
    System.out.println("before:" + records);
    long now = System.currentTimeMillis();
    Map<TopicPartition, List<ConsumerRecord<String, String>>> newRecords
        = new HashMap<>();
    for (TopicPartition tp : records.partitions()) {
        List<ConsumerRecord<String, String>> tpRecords =
            records.records(tp);
        List<ConsumerRecord<String, String>> newTpRecords = new ArrayList<>
            ();
        for (ConsumerRecord<String, String> record : tpRecords) {
            if (now - record.timestamp() < EXPIRE_INTERVAL) {
                newTpRecords.add(record);
            }
        }
        if (!newTpRecords.isEmpty()) {
            newRecords.put(tp, newTpRecords);
        }
    }
    return new ConsumerRecords<>(newRecords);
}

实现自定义拦截器之后,需要在 KafkaConsumer中配置指定这个拦截器,如下

// 指定消费者拦截器
props.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG,ConsumerInterceptorTTL.class.getName());

发送端同时发送两条消息,其中一条修改timestamp的值来使其变得超时,如下:

ProducerRecord<String, String> record = new ProducerRecord<>(topic, "Kafka-demo-001", "hello, Kafka!");
ProducerRecord<String, String> record2 = new ProducerRecord<>(topic, 0,System.currentTimeMillis() - 10 * 1000, "Kafka-demo-001", "hello, Kafka!->超时");

消息结构

public class Message implements Serializable{
    private CRC32 crc;
    private short magic;
    private boolean codecEnabled;
    private short codecClassOrdinal;
    private String key;
    private String value;
}

java对象比消息开销太大,JMM通常会对用户自定义的类进行字段重排,以试图减少内存占用。

为什么重排会减少内存占用?因为JMM要求java对象必须按照9字节对齐,未对齐的部分会填充空白字节进行补齐,该操作被称为padding。若用户随意指定对象字段的顺序,那么由于每个字段类型占用字节各异,可能会造成不会要的补齐,所以JMM会尝试对各个字段重排以期望降低整体的对象开销

// 重排后相当于
public class Message implements Serializable{
    // 16B头部
    
    private short magic;
    private short codecClassOrdinal;
    // 上面8个字节
    private boolean codecEnabled;
    private CRC32 crc;
    // 上面8个字节
    private String key;
    private String value;
    // 上面8个字节
}
// 16B头部+2Bmafic+2Bcodec+1字节codecEnabled+4字节CRC+4字节String+4字节String+7字节补齐=40字节

40字节仍然很占内存,kafka的实现方式本质上是使用java NIO的ByteBuffer来保存消息,同时依赖文件系统提供的页缓存机制,而非依靠java缓存。毕竟在大部分情况下,我们在堆上保存的对象在写入文件系统后很有可能在操作系统的页缓存中仍保留着,从而造成资源的浪费。

稳定性

kafka的消息传输保障机制非常直观。当producer发送消息时,一旦这条消息被commit,由于副本机制replication的存在,他就不会丢失。但是如果producer发送数据给broker后,遇到的网络问题而造成通信中断,那producer就无法判断该条消息是否已经提交(commit)。虽然Kafka无法确定网络故障期间发生了什么,但是producer可以retry多次,确保消息已经正确传输到broker中,所以目前Kafka实现的是at least once至少一次。

7.1 幂等性

所谓幂等性,就是对接口的多次调用所产生的结果和调用一次是一致的。生产者在进行重试的时候有可能会重复写入消息,而使用Kafka的幂等性功能就可以避免这种情况。

幂等性是有条件的:

  • 只能保证 Producer 在单个会话内不丢不重,如果 Producer 出现意外挂掉再重启是无法保证的(幂等性情况下,是无法获取之前的状态信息,因此是无法做到跨会话级别的不丢不重);
  • 幂等性不能跨多个 Topic-Partition,只能保证单个 partition 内的幂等性,当涉及多个 Topic-Partition 时,这中间的状态并没有同步。

Producer 使用幂等性的示例非常简单,与正常情况下 Producer 使用相比变化不大,只需要把Producer 的配置 enable.idempotence 设置为 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");

2 事务

幂等性并不能跨多个分区运作,而事务可以弥补这个缺憾,事务可以保证对多个分区写入操作的原子性。操作的原子性是指多个操作要么全部成功,要么全部失败,不存在部分成功部分失败的可能。

为了实现事务,应用程序必须提供唯一的transactionalId,这个参数通过客户端程序来进行设定。

见代码库:com.heima.kafka.chapter7.ProducerTransactionSend

properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, transactionId);

前期准备
事务要求生产者开启幂等性特性,因此通过将transactional.id参数设置为非空从而开启事务特性的同时需要将ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG设置为true(默认值为true),如果显示设置为false,则会抛出异常。
KafkaProducer提供了5个与事务相关的方法,详细如下:

//初始化事务,前提是配置了transactionalId
public void initTransactions()
//开启事务
public void beginTransaction()
//为消费者提供事务内的位移提交操作
public void sendOffsetsToTransaction(Map<TopicPartition,OffsetAndMetadata> offsets, String consumerGroupId)
//提交事务
public void commitTransaction()
//终止事务,类似于回滚
public void abortTransaction(

第一种方式
当输入参数为“error”值时,进行了回滚操作。


# 事务支持
spring.kafka.producer.transaction-id-prefix=kafka_tx.
    
// 事务操作
   template.executeInTransaction(t -> {
     t.send(topic, input);
     if ("error".equals(input)) {
       throw new RuntimeException("input is error");
     }
     t.send(topic, input + " anthor");
     return true;
   });
   return "send success";


@GetMapping("/sendt/{input}")
 @Transactional(rollbackFor = RuntimeException.class)
 public String sendToKafka2(@PathVariable String input) throws
ExecutionException, InterruptedException {
   template.send(topic, input);
   if ("error".equals(input)) {
     throw new RuntimeException("input is error");
   }
   template.send(topic, input + " anthor");
   return "send success";
 }
/* * Kafka Producer事务的使用   */
public class ProducerTransactionSend {
    public static final String topic = "topic-transaction";
    public static final String brokerList = "localhost:9092";
    public static final String transactionId = "transactionId";
    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
                       StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                       StringSerializer.class.getName());
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, transactionId);
        KafkaProducer<String, String> producer = new KafkaProducer<>
            (properties);

        producer.initTransactions();
        producer.beginTransaction();
        try {
            //处理业务逻辑并创建ProducerRecord
            ProducerRecord<String, String> record1 = new ProducerRecord<>(topic,
                                                                          "msg1");
            producer.send(record1);
            ProducerRecord<String, String> record2 = new ProducerRecord<>(topic,
                                                                          "msg2");
            producer.send(record2);
            ProducerRecord<String, String> record3 = new ProducerRecord<>(topic,
                                                                          "msg3");
            producer.send(record3);
            //处理一些其它逻辑
            producer.commitTransaction();
        } catch (ProducerFencedException e) {
            producer.abortTransaction();
        }

模拟事务回滚案例

try {
    //处理业务逻辑并创建ProducerRecord
    ProducerRecord<String, String> record1 = new ProducerRecord<>(topic,
                                                                  "msg1");
    producer.send(record1);
    //模拟事务回滚案例
    System.out.println(1/0);
    ProducerRecord<String, String> record2 = new ProducerRecord<>(topic,
                                                                  "msg2");
    producer.send(record2);
    ProducerRecord<String, String> record3 = new ProducerRecord<>(topic,
                                                                  "msg3");
    producer.send(record3);
    //处理一些其它逻辑
    producer.commitTransaction();
} catch (ProducerFencedException e) {
    producer.abortTransaction();
}

从上面案例中, msg1发送成功之后,出现了异常事务进行了回滚,则msg1消费端也收不到消息。

3 控制器

在Kafka集群中会有一个或者多个broker,其中有一个broker会被选举为控制器(Kafka Controller),它负责管理整个集群中所有分区和副本的状态。当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本。当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息。当使用kafka-topics.sh脚本为某个topic增加分区数量时,同样还是由控制器负责分区的重新分配。

Kafka中的控制器选举的工作依赖于Zookeeper,成功竞选为控制器的broker会在Zookeeper中创建/controller这个临时(EPHEMERAL)节点,此临时节点的内容参考如下:

ZooInspector管理
使用 zookeeper图形化的客户端工具(ZooInspector)提供的jar来进行管理,启动如下:
1、定位到jar所在目录
2、运行jar文件 java -jar zookeeper-dev-ZooInspector.jar
3、连接Zookeeper

7.4 可靠性保证
\1. 可靠性保证:确保系统在各种不同的环境下能够发生一致的行为
\2. Kafka的保证
保证分区消息的顺序
如果使用同一个生产者往同一个分区写入消息,而且消息B在消息A之后写入
那么Kafka可以保证消息B的偏移量比消息A的偏移量大,而且消费者会先读取消息A再
读取消息B
只有当消息被写入分区的所有同步副本时(文件系统缓存),它才被认为是已提交
生产者可以选择接收不同类型的确认,控制参数 acks
只要还有一个副本是活跃的,那么已提交的消息就不会丢失
消费者只能读取已经提交的消息
失效副本
怎么样判定一个分区是否有副本是处于同步失效状态的呢?从Kafka 0.9.x版本开始通过唯一的一个参数replica.lag.time.max.ms(默认大小为10,000)来控制,当ISR中的一个follower副本滞后leader副本
的时间超过参数replica.lag.time.max.ms指定的值时即判定为副本失效,需要将此follower副本剔出除
ISR之外。具体实现原理很简单,当follower副本将leader副本的LEO(Log End Offset,每个分区最后
一条消息的位置)之前的日志全部同步时,则认为该follower副本已经追赶上leader副本,此时更新该
副本的lastCaughtUpTimeMs标识。Kafka的副本管理器(ReplicaManager)启动时会启动一个副本过
期检测的定时任务,而这个定时任务会定时检查当前时间与副本的lastCaughtUpTimeMs差值是否大于
参数replica.lag.time.max.ms指定的值。千万不要错误的认为follower副本只要拉取leader副本的数据
就会更新lastCaughtUpTimeMs,试想当leader副本的消息流入速度大于follower副本的拉取速度时,
follower副本一直不断的拉取leader副本的消息也不能与leader副本同步,如果还将此follower副本置
于ISR中,那么当leader副本失效,而选取此follower副本为新的leader副本,那么就会有严重的消息丢
失。

7.4 可靠性保证
\1. 可靠性保证:确保系统在各种不同的环境下能够发生一致的行为
\2. Kafka的保证
保证分区消息的顺序
如果使用同一个生产者往同一个分区写入消息,而且消息B在消息A之后写入
那么Kafka可以保证消息B的偏移量比消息A的偏移量大,而且消费者会先读取消息A再
读取消息B
只有当消息被写入分区的所有同步副本时(文件系统缓存),它才被认为是已提交
生产者可以选择接收不同类型的确认,控制参数 acks
只要还有一个副本是活跃的,那么已提交的消息就不会丢失
消费者只能读取已经提交的消息
失效副本
怎么样判定一个分区是否有副本是处于同步失效状态的呢?从Kafka 0.9.x版本开始通过唯一的一个参数
replica.lag.time.max.ms(默认大小为10,000)来控制,当ISR中的一个follower副本滞后leader副本
的时间超过参数replica.lag.time.max.ms指定的值时即判定为副本失效,需要将此follower副本剔出除
ISR之外。具体实现原理很简单,当follower副本将leader副本的LEO(Log End Offset,每个分区最后
一条消息的位置)之前的日志全部同步时,则认为该follower副本已经追赶上leader副本,此时更新该
副本的lastCaughtUpTimeMs标识。Kafka的副本管理器(ReplicaManager)启动时会启动一个副本过
期检测的定时任务,而这个定时任务会定时检查当前时间与副本的lastCaughtUpTimeMs差值是否大于
参数replica.lag.time.max.ms指定的值。千万不要错误的认为follower副本只要拉取leader副本的数据
就会更新lastCaughtUpTimeMs,试想当leader副本的消息流入速度大于follower副本的拉取速度时,
follower副本一直不断的拉取leader副本的消息也不能与leader副本同步,如果还将此follower副本置
于ISR中,那么当leader副本失效,而选取此follower副本为新的leader副本,那么就会有严重的消息丢
失。

在服务端现在只有一个参数需要配置replica.lag.time.max.ms。这个参数解释replicas响应partition
leader的最长等待时间。检测卡住或失败副本的探测——如果一个replica失败导致发送拉取请求时间间
隔超过replica.lag.time.max.ms。Kafka会认为此replica已经死亡会从同步副本列表从移除。检测慢副
本机制发生了变化——如果一个replica开始落后leader超过replica.lag.time.max.ms。Kafka会认为太
缓慢并且会从同步副本列表中移除。除非replica请求leader时间间隔大于replica.lag.time.max.ms,因
此即使leader使流量激增和大批量写消息。Kafka也不会从同步副本列表从移除该副本。

7.5 一致性保证
在leader宕机后,只能从ISR列表中选取新的leader,无论ISR中哪个副本被选为新的leader,它都
知道HW之前的数据,可以保证在切换了leader后,消费者可以继续看到HW之前已经提交的数
据。
HW的截断机制:选出了新的leader,而新的leader并不能保证已经完全同步了之前leader的所有
数据,只能保证HW之前的数据是同步过的,此时所有的follower都要将数据截断到HW的位置,
再和新的leader同步数据,来保证数据一致。 当宕机的leader恢复,发现新的leader中的数据和
自己持有的数据不一致,此时宕机的leader会将自己的数据截断到宕机之前的hw位置,然后同步
新leader的数据。宕机的leader活过来也像follower一样同步数据,来保证数据的一致性。
Leader Epoch引用
数据丢失场景

Kafka 0.11.0.0.版本解决方案
造成上述两个问题的根本原因在于HW值被用于衡量副本备份的成功与否以及在出现failture时作为日志
截断的依据,但HW值的更新是异步延迟的,特别是需要额外的FETCH请求处理流程才能更新,故这中
间发生的任何崩溃都可能导致HW值的过期。鉴于这些原因,Kafka 0.11引入了leader epoch来取代HW
值。Leader端多开辟一段内存区域专门保存leader的epoch信息,这样即使出现上面的两个场景也能很
好地规避这些问题。
所谓leader epoch实际上是一对值:(epoch,offset)。epoch表示leader的版本号,从0开始,当
leader变更过1次时epoch就会+1,而offset则对应于该epoch版本的leader写入第一条消息的位移。因
此假设有两对值:
(0, 0)
(1, 120)
则表示第一个leader从位移0开始写入消息;共写了120条[0, 119];而第二个leader版本号是1,从位移
120处开始写入消息。
leader broker中会保存这样的一个缓存,并定期地写入到一个checkpoint文件中。
避免数据丢失:

7.6 消息重复的场景及解决方案
7.6.1 生产者端重复
生产发送的消息没有收到正确的broke响应,导致producer重试。
producer发出一条消息,broke落盘以后因为网络等种种原因发送端得到一个发送失败的响应或者网络
中断,然后producer收到一个可恢复的Exception重试消息导致消息重复。
解决方案:
1、启动kafka的幂等性
要启动kafka的幂等性,无需修改代码,默认为关闭,需要修改配置文
件:enable.idempotence=true 同时要求 ack=all 且 retries>1。
2、ack=0,不重试

能会丢消息,适用于吞吐量指标重要性高于数据丢失,例如:日志收集。
7.6.2 消费者端重复
1、根本原因
数据消费完没有及时提交offset到broker。
解决方案
1、取消自动自动提交
每次消费完或者程序退出时手动提交。这可能也没法保证一条重复。
2、下游做幂等
一般的解决方案是让下游做幂等或者尽量每消费一条消息都记录offset,对于少数严格的场景可能需要
把offset或唯一ID,例如订单ID和下游状态更新放在同一个数据库里面做事务来保证精确的一次更新或者
在下游数据表里面同时记录消费offset,然后更新下游数据的时候用消费位点做乐观锁拒绝掉旧位点的
数据更新

Kafka监控

5.1 Kafka Eagle
1.修改 kafka 启动命令
修改 kafka-server-start.sh 命令中

if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
fi

if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -
XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"
export JMX_PORT="9999"
#export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
fi

注意:修改之后在启动 Kafka 之前要分发之其他节点
2.上传压缩包 kafka-eagle-bin-1.3.7.tar.gz 到集群/opt/software 目录
3.解压到本地

[atguigu@hadoop102 software]$ tar -zxvf kafka-eagle-bin-
1.3.7.tar.gz

4.进入刚才解压的目录

[atguigu@hadoop102 kafka-eagle-bin-1.3.7]$ ll
总用量 82932
-rw-rw-r--. 1 atguigu atguigu 84920710 8 月 13 23:00 kafka-eagleweb-1.3.7-bin.tar.gz

5.将 kafka-eagle-web-1.3.7-bin.tar.gz 解压至/opt/module

[atguigu@hadoop102 kafka-eagle-bin-1.3.7]$ tar -zxvf kafka-eagleweb-1.3.7-bin.tar.gz -C /opt/module/

6.修改名称

[atguigu@hadoop102 module]$ mv kafka-eagle-web-1.3.7/ eagle

7.给启动文件执行权限

[atguigu@hadoop102 eagle]$ cd bin/
[atguigu@hadoop102 bin]$ ll
总用量 12
-rw-r--r--. 1 atguigu atguigu 1848 8 月 22 2017 ke.bat
-rw-r--r--. 1 atguigu atguigu 7190 7 月 30 20:12 ke.sh
[atguigu@hadoop102 bin]$ chmod 777 ke.sh

8.修改配置文件

######################################
# multi zookeeper&kafka cluster list
######################################
kafka.eagle.zk.cluster.alias=cluster1
cluster1.zk.list=hadoop102:2181,hadoop103:2181,hadoop104:2181
######################################
# kafka offset storage
######################################
cluster1.kafka.eagle.offset.storage=kafka
######################################
# enable kafka metrics
######################################
kafka.eagle.metrics.charts=true
kafka.eagle.sql.fix.error=false
######################################
# kafka jdbc driver address
######################################
kafka.eagle.driver=com.mysql.jdbc.Driver
kafka.eagle.url=jdbc:mysql://hadoop102:3306/ke?useUnicode=true&ch
aracterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
kafka.eagle.username=root
kafka.eagle.password=000000

9.添加环境变量
export KE_HOME=/opt/module/eagle
export PATH= P A T H : PATH: PATH:KE_HOME/bin
注意: source /etc/profile
10.启动

[atguigu@hadoop102 eagle]$ bin/ke.sh start
... ...
... ...
*****************************************************************
**
* Kafka Eagle Service has started success.
* Welcome, Now you can visit 'http://192.168.9.102:8048/ke'
* Account:admin ,Password:123456
*****************************************************************
**
*  ke.sh [start|status|stop|restart|stats] 
*  https://www.kafka-eagle.org/ 
****************************

注意:启动之前需要先启动 ZK 以及 KAFKA

11.登录页面查看监控数据
http://192.168.9.102:8048/ke

第6章 Kafka Streams

6.1 概述

6.1.1 Kafka Streams

Kafka Streams。Apache Kafka开源项目的一个组成部分。是一个功能强大,易于使用的库。用于在Kafka上构建高可分布式、拓展性,容错的应用程序。

6.1.2 Kafka Streams特点

1)功能强大

高扩展性,弹性,容错

2)轻量级

无需专门的集群

一个库,而不是框架

3)完全集成

100%的Kafka 0.10.0版本兼容

易于集成到现有的应用程序

4)实时性

毫秒级延迟

并非微批处理

窗口允许乱序数据

允许迟到数据

6.1.3 为什么要有Kafka Stream

当前已经有非常多的流式处理系统,最知名且应用最多的开源流式处理系统有Spark Streaming和Apache Storm。Apache Storm发展多年,应用广泛,提供记录级别的处理能力,当前也支持SQL on Stream。而Spark Streaming基于Apache Spark,可以非常方便与图计算,SQL处理等集成,功能强大,对于熟悉其它Spark应用开发的用户而言使用门槛低。另外,目前主流的Hadoop发行版,如Cloudera和Hortonworks,都集成了Apache Storm和Apache Spark,使得部署更容易。

既然Apache Spark与Apache Storm拥用如此多的优势,那为何还需要Kafka Stream呢?主要有如下原因。

第一,Spark和Storm都是流式处理框架,而Kafka Stream提供的是一个基于Kafka的流式处理类库。框架要求开发者按照特定的方式去开发逻辑部分,供框架调用。开发者很难了解框架的具体运行方式,从而使得调试成本高,并且使用受限。而Kafka Stream作为流式处理类库,直接提供具体的类给开发者调用,整个应用的运行方式主要由开发者控制,方便使用和调试。

第二,虽然Cloudera与Hortonworks方便了Storm和Spark的部署,但是这些框架的部署仍然相对复杂。而Kafka Stream作为类库,可以非常方便的嵌入应用程序中,它对应用的打包和部署基本没有任何要求。

第三,就流式处理系统而言,基本都支持Kafka作为数据源。例如Storm具有专门的kafka-spout,而Spark也提供专门的spark-streaming-kafka模块。事实上,Kafka基本上是主流的流式处理系统的标准数据源。换言之,大部分流式系统中都已部署了Kafka,此时使用Kafka Stream的成本非常低。

第四,使用Storm或Spark Streaming时,需要为框架本身的进程预留资源,如Storm的supervisor和Spark on YARN的node manager。即使对于应用实例而言,框架本身也会占用部分资源,如Spark Streaming需要为shuffle和storage预留内存。但是Kafka作为类库不占用系统资源。

第五,由于Kafka本身提供数据持久化,因此Kafka Stream提供滚动部署和滚动升级以及重新计算的能力。

第六,由于Kafka Consumer Rebalance机制,Kafka Stream可以在线动态调整并行度。

6.2 Kafka Stream数据清洗案例

0)需求:

​ 实时处理单词带有”>>>”前缀的内容。例如输入”atguigu>>>ximenqing”,最终处理成“ximenqing”

1)需求分析:

【MQ】Kafka笔记_第29张图片

2)案例实操

(1)创建一个工程,并添加jar包

(2)创建主类

package com.atguigu.kafka.stream;
import java.util.Properties;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.processor.Processor;
import org.apache.kafka.streams.processor.ProcessorSupplier;
import org.apache.kafka.streams.processor.TopologyBuilder;

public class Application {

	public static void main(String[] args) {

		// 定义输入的topic
        String from = "first";
        // 定义输出的topic
        String to = "second";

        // 设置参数
        Properties settings = new Properties();
        settings.put(StreamsConfig.APPLICATION_ID_CONFIG, "logFilter");
        settings.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");

        StreamsConfig config = new StreamsConfig(settings);

        // 构建拓扑
        TopologyBuilder builder = new TopologyBuilder();

        builder.addSource("SOURCE", from)
               .addProcessor("PROCESS", new ProcessorSupplier<byte[], byte[]>() {

					@Override
					public Processor<byte[], byte[]> get() {
						// 具体分析处理
						return new LogProcessor();
					}
				}, "SOURCE")
                .addSink("SINK", to, "PROCESS");

        // 创建kafka stream
        KafkaStreams streams = new KafkaStreams(builder, config);
        streams.start();
	}
}

(3)具体业务处理

package com.atguigu.kafka.stream;
import org.apache.kafka.streams.processor.Processor;
import org.apache.kafka.streams.processor.ProcessorContext;

public class LogProcessor implements Processor<byte[], byte[]> {
	
	private ProcessorContext context;
	
	@Override
	public void init(ProcessorContext context) {
		this.context = context;
	}

	@Override
	public void process(byte[] key, byte[] value) {
		String input = new String(value);
		
		// 如果包含“>>>”则只保留该标记后面的内容
		if (input.contains(">>>")) {
			input = input.split(">>>")[1].trim();
			// 输出到下一个topic
			context.forward("logProcessor".getBytes(), input.getBytes());
		}else{
			context.forward("logProcessor".getBytes(), input.getBytes());
		}
	}

	@Override
	public void punctuate(long timestamp) {
		
	}

	@Override
	public void close() {
		
	}
}

(4)运行程序

(5)在hadoop104上启动生产者

[atguigu@hadoop104 kafka]$ bin/kafka-console-producer.sh --broker-list hadoop102:9092 --topic first

>hello>>>world
>h>>>atguigu
>hahaha

(6)在hadoop103上启动消费者

[atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh --zookeeper hadoop102:2181 --from-beginning --topic second

world
atguigu
hahaha

分区

Kafka可以将主题划分为多个分区(Partition),会根据分区规则选择把消息存储到哪个分区中,只要如果分区规则设置的合理,那么所有的消息将会被均匀的分布到不同的分区中,这样就实现了负载均衡和水平扩展。另外,多个订阅者可以从一个或者多个分区中同时消费数据,以支撑海量数据处理能力。

顺便说一句,由于消息是以追加到分区中的,多个分区顺序写磁盘的总效率要比随机写内存还要高(引用Apache Kafka – A High Throughput Distributed Messaging System的观点),是Kafka高吞吐率的重要保证之一

5.1 副本机制
由于Producer和Consumer都只会与Leader角色的分区副本相连,所以kafka需要以集群的组织形式提供主题下的消息高可用。kafka支持主备复制,所以消息具备高可用和持久性。

一个分区可以有多个副本,这些副本保存在不同的broker上。每个分区的副本中都会有一个作为Leader。当一个broker失败时,Leader在这台broker上的分区都会变得不可用,kafka会自动移除Leader,再其他副本中选一个作为新的Leader。

在通常情况下,增加分区可以提供kafka集群的吞吐量。然而,也应该意识到集群的总分区数或是单台服务器上的分区数过多,会增加不可用及延迟的风险。

5.2 分区Leader选举
可以预见的是,如果某个分区的Leader挂了,那么其它跟随者将会进行选举产生一个新的leader,之后所有的读写就会转移到这个新的Leader上,在kafka中,其不是采用常见的多数选举的方式进行副本的Leader选举,而是会在Zookeeper上针对每个Topic维护一个称为ISR(in-sync replica,已同步的副本)的集合,显然还有一些副本没有来得及同步。只有这个ISR列表里面的才有资格成为leader(先使用ISR里面的第一个,如果不行依次类推,因为ISR里面的是同步副本,消息是最完整且各个节点都是一样的)。 通过ISR,kafka需要的冗余度较低,可以容忍的失败数比较高。假设某个topic有f+1个副本,kafka可以容忍f
个不可用,当然,如果全部ISR里面的副本都不可用,也可以选择其他可用的副本,只是存在数据的不一致。

5.3 分区重新分配

我们往已经部署好的Kafka集群里面添加机器是最正常不过的需求,而且添加起来非常地方便,我们需要做的事是从已经部署好的Kafka节点中复制相应的配置文件,然后把里面的broker id修改成全局唯一的,最后启动这个节点即可将它加入到现有Kafka集群中。
但是问题来了,新添加的Kafka节点并不会自动地分配数据,所以无法分担集群的负载,除非我们新建一个topic。但是现在我们想手动将部分分区移到新添加的Kafka节点上,Kafka内部提供了相关的工具来重新分布某个topic的分区。

具体步骤
第一步:我们创建一个有三个节点的集群,详情可查看第九章集群的搭建

5.5 分区分配策略
按照Kafka默认的消费逻辑设定,一个分区只能被同一个消费组(ConsumerGroup)内的一个消费者
消费。假设目前某消费组内只有一个消费者C0,订阅了一个topic,这个topic包含7个分区,也就是说
这个消费者C0订阅了7个分区,参考下图

Connect

8.2 数据管道Connect
8.2.1 概述
Kafka是一个使用越来越广的消息系统,尤其是在大数据开发中(实时数据处理和分析)。为何集成其
他系统和解耦应用,经常使用Producer来发送消息到Broker,并使用Consumer来消费Broker中的消
息。Kafka Connect是到0.9版本才提供的并极大的简化了其他系统与Kafka的集成。Kafka Connect运
用用户快速定义并实现各种Connector(File,Jdbc,Hdfs等),这些功能让大批量数据导入/导出Kafka很方
便。

【MQ】Kafka笔记_第30张图片

在 Kafka Connect中还有两个重要的概念:Task 和 Worker。
Connect中一些概念
连接器:实现了Connect API,决定需要运行多少个任务,按照任务来进行数据复制,从work进程获取
任务配置并将其传递下去
任务:负责将数据移入或移出Kafka
work进程:相当与connector和任务的容器,用于负责管理连接器的配置、启动连接器和连接器任务,
提供REST API
转换器:kafka connect和其他存储系统直接发送或者接受数据之间转换数据
8.2.2 独立模式–文件系统
场景
以下示例使用到了两个Connector,将文件source.txt 中的内容通过Source连接器写入Kafka主题中,
然后将内容写入srouce.sink.txt中。
FileStreamSource :从source.txt中读取并发布到Broker中
FileStreamSink :从Broker中读取数据并写入到source.sink.txt文件中
步骤详情
首先我们来看下Worker进程用到的配置文件${KAFKA_HOME}/config/connect-standalone.properties

// Kafka集群连接的地址
bootstrap.servers=localhost:9092
// 格式转化类
key.converter=org.apache.kafka.connect.json.JsonConverter
value.converter=org.apache.kafka.connect.json.JsonConverter
// json消息中是否包含schema
key.converter.schemas.enable=true
value.converter.schemas.enable=true
// 保存偏移量的文件路径
offset.storage.file.filename=/tmp/connect.offsets
// 设定提交偏移量的频率
offset.flush.interval.ms=10000

其中的 Source使用到的配置文件是${KAFKA_HOME}/config/connect-file-source.properties

// 配置连接器的名称
name=local-file-source
// 连接器的全限定名称,设置类名称也是可以的
connector.class=FileStreamSource
// task数量
tasks.max=1
// 数据源的文件路径
file=/tmp/source.txt
// 主题名称
topic=topic0703

其中的 Sink使用到的配置文件是${KAFKA_HOME}/config/connect-file-sink.properties

name=local-file-sink
connector.class=FileStreamSink
tasks.max=1
file=/tmp/source.sink.txt
topics=topic0703

启动 source连接器

itcast@Server-node:/mnt/d/kafka_2.12-2.2.1$ bin/connect-standalone.sh
config/connect-standalone.properties config/connect-file-source.properties

启动 slink连接器

itcast@Server-node:/mnt/d/kafka_2.12-2.2.1$ bin/connect-standalone.sh
config/connect-standalone.properties config/connect-file-sink.properties

source 写入文本信息

itcast@Server-node:/mnt/d/kafka_2.12-2.2.1$ echo "Hello kafka,I
coming;">>/tmp/source.tx

查看 slink文件

itcast@Server-node:/mnt/d/kafka_2.12-2.2.1$ cat /tmp/source.sink.txt
hello,kafka
I to do some
ello kafka,I coming;
Hello kafka,I coming;

8.2.3 信息流–ElasticSearch
概述
Kafka connect workers有两种工作模式,单机模式和分布式模式。在开发和适合使用单机模式的场景
下,可以使用standalone模式, 在实际生产环境下由于单个worker的数据压力会比较大,distributed模
式对负载均和和扩展性方面会有很大帮助。(本测试使用standalone模式)
关于Kafka Connect的详细情况可以参考[ Kafka Connect ]
Kafka Connect 安装
[ Kafka Connec 下载地址]

本文下载的为开源版本 confluent-community-5.3.0-2.12.tar.gz,下载后解压即可。
Worker配置
本测试使用standalone模式,因此修改…/etc/schema-registry/connect-avro-standalone.properties

bootstrap.servers=localhost:9092

Elasticsearch Connector 配置
修改…/etc/kafka-connect-elasticsearch/quickstart-elasticsearch.properties

name=elasticsearch-sink
connector.class=io.confluent.connect.elasticsearch.ElasticsearchSinkConnector
tasks.max=1
//其中topics不仅对应Kafka的topic名称,同时也是Elasticsearch的索引名,
//当然也可以通过topic.index.map来设置从topic名到Elasticsearch索引名的映射
topics=topic0703
key.ignore=true
connection.url=http://localhost:9200
type.name=kafka-connect

启动
Elasticsearch

itcast@Server-node:~$ curl 'http://localhost:9200/?pretty'
{
"name" : "MY-20190430BUDR",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "ha3pnLkhRuGEIgXQstYnbQ",
"version" : {
 "number" : "7.2.0",
 "build_flavor" : "default",
 "build_type" : "tar",
 "build_hash" : "508c38a",
 "build_date" : "2019-06-20T15:54:18.811730Z",
 "build_snapshot" : false,
 "lucene_version" : "8.0.0",
 "minimum_wire_compatibility_version" : "6.8.0",
 "minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}

启动 schema Registry

itcast@Server-node:/mnt/d/confluent-5.3.0$ bin/schema-registry-start etc/schema-
registry/schema-registry.properties
[2019-07-22 06:01:00,059] INFO SchemaRegistryConfig values:
   access.control.allow.headers =
   access.control.allow.methods =
   access.control.allow.origin =
   authentication.method = NONE
   authentication.realm =
   authentication.roles = [*]

查看服务是否正常

itcast@Server-node:~$ jps -l
1139 kafka.Kafka
2403 io.confluent.kafka.schemaregistry.rest.SchemaRegistryMain
2474 jdk.jcmd/sun.tools.jps.Jps
2172 org.elasticsearch.bootstrap.Elasticsearch
28 org.apache.zookeeper.server.quorum.QuorumPeerMain

启动 Connector

itcast@Server-node:/mnt/d/confluent-5.3.0$ ./bin/connect-standalone etc/schema-
registry/connect-avro-standalone.properties etc/kafka-connect-
elasticsearch/quickstart-elasticsearch.properties

8.4 流式处理Spark
Spark最初诞生于美国加州大学伯克利分校(UC Berkeley)的AMP实验室,是一个可应用于大规模数
据处理的快速、通用引擎。2013年,Spark加入Apache孵化器项目后,开始获得迅猛的发展,如今已
成为Apache软件基金会最重要的三大分布式计算系统开源项目之一(即Hadoop、Spark、Storm)。
Spark最初的设计目标是使数据分析更快——不仅运行速度快,也要能快速、容易地编写程序。为了使
程序运行更快,Spark提供了内存计算,减少了迭代计算时的IO开销;而为了使编写程序更为容易,
Spark使用简练、优雅的Scala语言编写,基于Scala提供了交互式的编程体验。虽然,Hadoop已成为大
数据的事实标准,但其MapReduce分布式计算模型仍存在诸多缺陷,而Spark不仅具备Hadoop
MapReduce所具有的优点,且解决了Hadoop MapReduce的缺陷。Spark正以其结构一体化、功能多
元化的优势逐渐成为当今大数据领域最热门的大数据计算平台。

8.4.1 Spark 安装与应用
官网
http://spark.apache.org/downloads.html
下载安装包解压即可
启动

itcast@Server-node:/mnt/d/spark-2.4.3-bin-hadoop2.7$ sbin/start-all.sh
starting org.apache.spark.deploy.master.Master, logging to /mnt/d/spark-2.4.3-
bin-hadoop2.7/logs/spark-dayuan-org.apache.spark.deploy.master.Master-1-MY-
20190430BUDR.out
itcast@localhost's password:
localhost: starting org.apache.spark.deploy.worker.Worker, logging to
/mnt/d/spark-2.4.3-bin-hadoop2.7/logs/spark-dayuan-
org.apache.spark.deploy.worker.Worker-1-MY-20190430BUDR.out

验证

itcast@Server-node:/mnt/d/spark-2.4.3-bin-hadoop2.7$ jps -l
2819 kafka.Kafka
3972 jdk.jcmd/sun.tools.jps.Jps
3894 org.apache.spark.deploy.worker.Worker
28 org.apache.zookeeper.server.quorum.QuorumPeerMain
3726 org.apache.spark.deploy.master.Master
dayuan@MY-20190430BUDR:/mnt/d/spark-2.4.3-bin-hadoop2.7$

浏览器输入: http://127.0.0.1:8080 验证

8.4.3 Spark和Kafka整合
见代码:com.spark.SparkStreamingFromkafka
演示
发送消息

itcast@Server-node:/mnt/d/kafka_2.12-2.2.1$ bin/kafka-console-producer.sh --
broker-list localhost:9092 --topic heima
>hello
>hello
>I coming;
>
>
>
>I coming;
>

authentication.skip.paths = []
avro.compatibility.level = backward
compression.enable = true
debug = false
host.name = MY-20190430BUDR.localdomain
idle.timeout.ms = 30000
inter.instance.headers.whitelist = []
inter.instance.protocol = http
kafkastore.bootstrap.servers = []

第7章 扩展

7.1 Kafka与Flume比较

在企业中必须要清楚流式数据采集框架flume和kafka的定位是什么:

flume:cloudera公司研发:

​ 适合多个生产者;

适合下游数据消费者不多的情况;

适合数据安全性要求不高的操作;

适合与Hadoop生态圈对接的操作。

kafka:linkedin公司研发:

适合数据下游消费众多的情况;

适合数据安全性要求较高的操作,支持replication。

因此我们常用的一种模型是:

线上数据 --> flume --> kafka --> flume(根据情景增删该流程) --> HDFS

7.2 Flume与kafka集成

1)配置flume(flume-kafka.conf)

# define
a1.sources = r1
a1.sinks = k1
a1.channels = c1

# source
a1.sources.r1.type = exec
a1.sources.r1.command = tail -F -c +0 /opt/module/datas/flume.log
a1.sources.r1.shell = /bin/bash -c

# sink
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
a1.sinks.k1.kafka.topic = first
a1.sinks.k1.kafka.flumeBatchSize = 20
a1.sinks.k1.kafka.producer.acks = 1
a1.sinks.k1.kafka.producer.linger.ms = 1

# channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100

# bind
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1

2) 启动kafkaIDEA消费者

3) 进入flume根目录下,启动flume

$ bin/flume-ng agent -c conf/ -n a1 -f jobs/flume-kafka.conf

4) 向 /opt/module/datas/flume.log里追加数据,查看kafka消费者消费情况

$ echo hello > /opt/module/datas/flume.log

7.3 Kafka配置信息

7.3.1 Broker配置信息

属性 默认值 描述
broker.id 必填参数,broker的唯一标识
log.dirs /tmp/kafka-logs Kafka数据存放的目录。可以指定多个目录,中间用逗号分隔,当新partition被创建的时会被存放到当前存放partition最少的目录。
port 9092 BrokerServer接受客户端连接的端口号
zookeeper.connect null Zookeeper的连接串,格式为:hostname1:port1,hostname2:port2,hostname3:port3。可以填一个或多个,为了提高可靠性,建议都填上。注意,此配置允许我们指定一个zookeeper路径来存放此kafka集群的所有数据,为了与其他应用集群区分开,建议在此配置中指定本集群存放目录,格式为:hostname1:port1,hostname2:port2,hostname3:port3/chroot/path 。需要注意的是,消费者的参数要和此参数一致。
message.max.bytes 1000000 服务器可以接收到的最大的消息大小。注意此参数要和consumer的maximum.message.size大小一致,否则会因为生产者生产的消息太大导致消费者无法消费。
num.io.threads 8 服务器用来执行读写请求的IO线程数,此参数的数量至少要等于服务器上磁盘的数量。
queued.max.requests 500 I/O线程可以处理请求的队列大小,若实际请求数超过此大小,网络线程将停止接收新的请求。
socket.send.buffer.bytes 100 * 1024 The SO_SNDBUFF buffer the server prefers for socket connections.
socket.receive.buffer.bytes 100 * 1024 The SO_RCVBUFF buffer the server prefers for socket connections.
socket.request.max.bytes 100 * 1024 * 1024 服务器允许请求的最大值, 用来防止内存溢出,其值应该小于 Java heap size.
num.partitions 1 默认partition数量,如果topic在创建时没有指定partition数量,默认使用此值,建议改为5
log.segment.bytes 1024 * 1024 * 1024 Segment文件的大小,超过此值将会自动新建一个segment,此值可以被topic级别的参数覆盖。
log.roll.{ms,hours} 24 * 7 hours 新建segment文件的时间,此值可以被topic级别的参数覆盖。
log.retention.{ms,minutes,hours} 7 days Kafka segment log的保存周期,保存周期超过此时间日志就会被删除。此参数可以被topic级别参数覆盖。数据量大时,建议减小此值。
log.retention.bytes -1 每个partition的最大容量,若数据量超过此值,partition数据将会被删除。注意这个参数控制的是每个partition而不是topic。此参数可以被log级别参数覆盖。
log.retention.check.interval.ms 5 minutes 删除策略的检查周期
auto.create.topics.enable true 自动创建topic参数,建议此值设置为false,严格控制topic管理,防止生产者错写topic。
default.replication.factor 1 默认副本数量,建议改为2。
replica.lag.time.max.ms 10000 在此窗口时间内没有收到follower的fetch请求,leader会将其从ISR(in-sync replicas)中移除。
replica.lag.max.messages 4000 如果replica节点落后leader节点此值大小的消息数量,leader节点就会将其从ISR中移除。
replica.socket.timeout.ms 30 * 1000 replica向leader发送请求的超时时间。
replica.socket.receive.buffer.bytes 64 * 1024 The socket receive buffer for network requests to the leader for replicating data.
replica.fetch.max.bytes 1024 * 1024 The number of byes of messages to attempt to fetch for each partition in the fetch requests the replicas send to the leader.
replica.fetch.wait.max.ms 500 The maximum amount of time to wait time for data to arrive on the leader in the fetch requests sent by the replicas to the leader.
num.replica.fetchers 1 Number of threads used to replicate messages from leaders. Increasing this value can increase the degree of I/O parallelism in the follower broker.
fetch.purgatory.purge.interval.requests 1000 The purge interval (in number of requests) of the fetch request purgatory.
zookeeper.session.timeout.ms 6000 ZooKeeper session 超时时间。如果在此时间内server没有向zookeeper发送心跳,zookeeper就会认为此节点已挂掉。 此值太低导致节点容易被标记死亡;若太高,.会导致太迟发现节点死亡。
zookeeper.connection.timeout.ms 6000 客户端连接zookeeper的超时时间。
zookeeper.sync.time.ms 2000 H ZK follower落后 ZK leader的时间。
controlled.shutdown.enable true 允许broker shutdown。如果启用,broker在关闭自己之前会把它上面的所有leaders转移到其它brokers上,建议启用,增加集群稳定性。
auto.leader.rebalance.enable true If this is enabled the controller will automatically try to balance leadership for partitions among the brokers by periodically returning leadership to the “preferred” replica for each partition if it is available.
leader.imbalance.per.broker.percentage 10 The percentage of leader imbalance allowed per broker. The controller will rebalance leadership if this ratio goes above the configured value per broker.
leader.imbalance.check.interval.seconds 300 The frequency with which to check for leader imbalance.
offset.metadata.max.bytes 4096 The maximum amount of metadata to allow clients to save with their offsets.
connections.max.idle.ms 600000 Idle connections timeout: the server socket processor threads close the connections that idle more than this.
num.recovery.threads.per.data.dir 1 The number of threads per data directory to be used for log recovery at startup and flushing at shutdown.
unclean.leader.election.enable true Indicates whether to enable replicas not in the ISR set to be elected as leader as a last resort, even though doing so may result in data loss.
delete.topic.enable false 启用deletetopic参数,建议设置为true。
offsets.topic.num.partitions 50 The number of partitions for the offset commit topic. Since changing this after deployment is currently unsupported, we recommend using a higher setting for production (e.g., 100-200).
offsets.topic.retention.minutes 1440 Offsets that are older than this age will be marked for deletion. The actual purge will occur when the log cleaner compacts the offsets topic.
offsets.retention.check.interval.ms 600000 The frequency at which the offset manager checks for stale offsets.
offsets.topic.replication.factor 3 The replication factor for the offset commit topic. A higher setting (e.g., three or four) is recommended in order to ensure higher availability. If the offsets topic is created when fewer brokers than the replication factor then the offsets topic will be created with fewer replicas.
offsets.topic.segment.bytes 104857600 Segment size for the offsets topic. Since it uses a compacted topic, this should be kept relatively low in order to facilitate faster log compaction and loads.
offsets.load.buffer.size 5242880 An offset load occurs when a broker becomes the offset manager for a set of consumer groups (i.e., when it becomes a leader for an offsets topic partition). This setting corresponds to the batch size (in bytes) to use when reading from the offsets segments when loading offsets into the offset manager’s cache.
offsets.commit.required.acks -1 The number of acknowledgements that are required before the offset commit can be accepted. This is similar to the producer’s acknowledgement setting. In general, the default should not be overridden.
offsets.commit.timeout.ms 5000 The offset commit will be delayed until this timeout or the required number of replicas have received the offset commit. This is similar to the producer request timeout.

7.3.2 Producer配置信息

属性 默认值 描述
metadata.broker.list 启动时producer查询brokers的列表,可以是集群中所有brokers的一个子集。注意,这个参数只是用来获取topic的元信息用,producer会从元信息中挑选合适的broker并与之建立socket连接。格式是:host1:port1,host2:port2。
request.required.acks 0 参见3.2节介绍
request.timeout.ms 10000 Broker等待ack的超时时间,若等待时间超过此值,会返回客户端错误信息。
producer.type sync 同步异步模式。async表示异步,sync表示同步。如果设置成异步模式,可以允许生产者以batch的形式push数据,这样会极大的提高broker性能,推荐设置为异步。
serializer.class kafka.serializer.DefaultEncoder 序列号类,.默认序列化成 byte[] 。
key.serializer.class Key的序列化类,默认同上。
partitioner.class kafka.producer.DefaultPartitioner Partition类,默认对key进行hash。
compression.codec none 指定producer消息的压缩格式,可选参数为: “none”, “gzip” and “snappy”。关于压缩参见4.1节
compressed.topics null 启用压缩的topic名称。若上面参数选择了一个压缩格式,那么压缩仅对本参数指定的topic有效,若本参数为空,则对所有topic有效。
message.send.max.retries 3 Producer发送失败时重试次数。若网络出现问题,可能会导致不断重试。
retry.backoff.ms 100 Before each retry, the producer refreshes the metadata of relevant topics to see if a new leader has been elected. Since leader election takes a bit of time, this property specifies the amount of time that the producer waits before refreshing the metadata.
topic.metadata.refresh.interval.ms 600 * 1000 The producer generally refreshes the topic metadata from brokers when there is a failure (partition missing, leader not available…). It will also poll regularly (default: every 10min so 600000ms). If you set this to a negative value, metadata will only get refreshed on failure. If you set this to zero, the metadata will get refreshed after each message sent (not recommended). Important note: the refresh happen only AFTER the message is sent, so if the producer never sends a message the metadata is never refreshed
queue.buffering.max.ms 5000 启用异步模式时,producer缓存消息的时间。比如我们设置成1000时,它会缓存1秒的数据再一次发送出去,这样可以极大的增加broker吞吐量,但也会造成时效性的降低。
queue.buffering.max.messages 10000 采用异步模式时producer buffer 队列里最大缓存的消息数量,如果超过这个数值,producer就会阻塞或者丢掉消息。
queue.enqueue.timeout.ms -1 当达到上面参数值时producer阻塞等待的时间。如果值设置为0,buffer队列满时producer不会阻塞,消息直接被丢掉。若值设置为-1,producer会被阻塞,不会丢消息。
batch.num.messages 200 采用异步模式时,一个batch缓存的消息数量。达到这个数量值时producer才会发送消息。
send.buffer.bytes 100 * 1024 Socket write buffer size
client.id “” The client id is a user-specified string sent in each request to help trace calls. It should logically identify the application making the request.

7.3.3 Consumer配置信息

属性 默认值 描述
group.id Consumer的组ID,相同goup.id的consumer属于同一个组。
zookeeper.connect Consumer的zookeeper连接串,要和broker的配置一致。
consumer.id null 如果不设置会自动生成。
socket.timeout.ms 30 * 1000 网络请求的socket超时时间。实际超时时间由max.fetch.wait + socket.timeout.ms 确定。
socket.receive.buffer.bytes 64 * 1024 The socket receive buffer for network requests.
fetch.message.max.bytes 1024 * 1024 查询topic-partition时允许的最大消息大小。consumer会为每个partition缓存此大小的消息到内存,因此,这个参数可以控制consumer的内存使用量。这个值应该至少比server允许的最大消息大小大,以免producer发送的消息大于consumer允许的消息。
num.consumer.fetchers 1 The number fetcher threads used to fetch data.
auto.commit.enable true 如果此值设置为true,consumer会周期性的把当前消费的offset值保存到zookeeper。当consumer失败重启之后将会使用此值作为新开始消费的值。
auto.commit.interval.ms 60 * 1000 Consumer提交offset值到zookeeper的周期。
queued.max.message.chunks 2 用来被consumer消费的message chunks 数量, 每个chunk可以缓存fetch.message.max.bytes大小的数据量。
auto.commit.interval.ms 60 * 1000 Consumer提交offset值到zookeeper的周期。
queued.max.message.chunks 2 用来被consumer消费的message chunks 数量, 每个chunk可以缓存fetch.message.max.bytes大小的数据量。
fetch.min.bytes 1 The minimum amount of data the server should return for a fetch request. If insufficient data is available the request will wait for that much data to accumulate before answering the request.
fetch.wait.max.ms 100 The maximum amount of time the server will block before answering the fetch request if there isn’t sufficient data to immediately satisfy fetch.min.bytes.
rebalance.backoff.ms 2000 Backoff time between retries during rebalance.
refresh.leader.backoff.ms 200 Backoff time to wait before trying to determine the leader of a partition that has just lost its leader.
auto.offset.reset largest What to do when there is no initial offset in ZooKeeper or if an offset is out of range ;smallest : automatically reset the offset to the smallest offset; largest : automatically reset the offset to the largest offset;anything else: throw exception to the consumer
consumer.timeout.ms -1 若在指定时间内没有消息消费,consumer将会抛出异常。
exclude.internal.topics true Whether messages from internal topics (such as offsets) should be exposed to the consumer.
zookeeper.session.timeout.ms 6000 ZooKeeper session timeout. If the consumer fails to heartbeat to ZooKeeper for this period of time it is considered dead and a rebalance will occur.
zookeeper.connection.timeout.ms 6000 The max time that the client waits while establishing a connection to zookeeper.
zookeeper.sync.time.ms 2000 How far a ZK follower can be behind a ZK leader

kafka面试题

7.1 面试问题
1.Kafka 中的 ISR(InSyncRepli)、 OSR(OutSyncRepli)、 AR(AllRepli)代表什么?
2.Kafka 中的 HW、 LEO 等分别代表什么?
3.Kafka 中是怎么体现消息顺序性的?
4.Kafka 中的分区器、序列化器、拦截器是否了解?它们之间的处理顺序是什么?
5.Kafka 生产者客户端的整体结构是什么样子的?使用了几个线程来处理?分别是什么?
6.“消费组中的消费者个数如果超过 topic 的分区,那么就会有消费者消费不到数据”这句话是否正确?
7.消费者提交消费位移时提交的是当前消费到的最新消息的 offset 还是 offset+1?
8.有哪些情形会造成重复消费?
9.那些情景会造成消息漏消费?

10.当你使用 kafka-topics.sh 创建(删除)了一个 topic 之后, Kafka 背后会执行什么逻辑?
1)会在 zookeeper 中的/brokers/topics 节点下创建一个新的 topic 节点,如:
/brokers/topics/first
2)触发 Controller 的监听程序
3) kafka Controller 负责 topic 的创建工作,并更新 metadata cache
11.topic 的分区数可不可以增加?如果可以怎么增加?如果不可以,那又是为什么?
12.topic 的分区数可不可以减少?如果可以怎么减少?如果不可以,那又是为什么?
13.Kafka 有内部的 topic 吗?如果有是什么?有什么所用?
14.Kafka 分区分配的概念?
15.简述 Kafka 的日志目录结构?
16.如果我指定了一个 offset, Kafka Controller 怎么查找到对应的消息?
17.聊一聊 Kafka Controller 的作用?
18.Kafka 中有那些地方需要选举?这些地方的选举策略又有哪些?
19.失效副本是指什么?有那些应对措施?
20.Kafka 的哪些设计让它有如此高的性能?
7.2 参考答案

微信小程序

kafka配置

Property Default Description
broker.id 每个broker都可以用一个唯一的非负整数id进行标识;这个id可以作为broker的“名字”,并且它的存在使得broker无须混淆consumers就可以迁移到不同的host/port上。你可以选择任意你喜欢的数字作为id,只要id是唯一的即可。
log.dirs /tmp/kafka-logs kafka存放数据的路径。这个路径并不是唯一的,可以是多个,路径之间只需要使用逗号分隔即可;每当创建新partition时,都会选择在包含最少partitions的路径下进行。
port 6667 server接受客户端连接的端口
zookeeper.connect null ZooKeeper连接字符串的格式为:hostname:port,此处hostname和port分别是ZooKeeper集群中某个节点的host和port;为了当某个host宕掉之后你能通过其他ZooKeeper节点进行连接,你可以按照一下方式制定多个hosts: hostname1:port1, hostname2:port2, hostname3:port3. ZooKeeper 允许你增加一个“chroot”路径,将集群中所有kafka数据存放在特定的路径下。当多个Kafka集群或者其他应用使用相同ZooKeeper集群时,可以使用这个方式设置数据存放路径。这种方式的实现可以通过这样设置连接字符串格式,如下所示: hostname1:port1,hostname2:port2,hostname3:port3/chroot/path 这样设置就将所有kafka集群数据存放在/chroot/path路径下。注意,在你启动broker之前,你必须创建这个路径,并且consumers必须使用相同的连接格式。
message.max.bytes 1000000 server可以接收的消息最大尺寸。重要的是,consumer和producer有关这个属性的设置必须同步,否则producer发布的消息对consumer来说太大。
num.network.threads 3 server用来处理网络请求的网络线程数目;一般你不需要更改这个属性。
num.io.threads 8 server用来处理请求的I/O线程的数目;这个线程数目至少要等于硬盘的个数。
background.threads 4 用于后台处理的线程数目,例如文件删除;你不需要更改这个属性。
queued.max.requests 500 在网络线程停止读取新请求之前,可以排队等待I/O线程处理的最大请求个数。
host.name null broker的hostname;如果hostname已经设置的话,broker将只会绑定到这个地址上;如果没有设置,它将绑定到所有接口,并发布一份到ZK
advertised.host.name null 如果设置,则就作为broker 的hostname发往producer、consumers以及其他brokers
advertised.port null 此端口将给与producers、consumers、以及其他brokers,它会在建立连接时用到; 它仅在实际端口和server需要绑定的端口不一样时才需要设置。
socket.send.buffer.bytes 100 * 1024 SO_SNDBUFF 缓存大小,server进行socket 连接所用
socket.receive.buffer.bytes 100 * 1024 SO_RCVBUFF缓存大小,server进行socket连接时所用
socket.request.max.bytes 100 * 1024 * 1024 server允许的最大请求尺寸; 这将避免server溢出,它应该小于Java heap size
num.partitions 1 如果创建topic时没有给出划分partitions个数,这个数字将是topic下partitions数目的默认数值。
log.segment.bytes 101410241024 topic partition的日志存放在某个目录下诸多文件中,这些文件将partition的日志切分成一段一段的;这个属性就是每个文件的最大尺寸;当尺寸达到这个数值时,就会创建新文件。此设置可以由每个topic基础设置时进行覆盖。 查看 the per-topic configuration section
log.roll.hours 24 * 7 即使文件没有到达log.segment.bytes,只要文件创建时间到达此属性,就会创建新文件。这个设置也可以有topic层面的设置进行覆盖; 查看the per-topic configuration section
log.cleanup.policy delete
log.retention.minutes和log.retention.hours 7 days 每个日志文件删除之前保存的时间。默认数据保存时间对所有topic都一样。 log.retention.minutes 和 log.retention.bytes 都是用来设置删除日志文件的,无论哪个属性已经溢出。 这个属性设置可以在topic基本设置时进行覆盖。 查看the per-topic configuration section
log.retention.bytes -1 每个topic下每个partition保存数据的总量;注意,这是每个partitions的上限,因此这个数值乘以partitions的个数就是每个topic保存的数据总量。同时注意:如果log.retention.hours和log.retention.bytes都设置了,则超过了任何一个限制都会造成删除一个段文件。 注意,这项设置可以由每个topic设置时进行覆盖。 查看the per-topic configuration section
log.retention.check.interval.ms 5 minutes 检查日志分段文件的间隔时间,以确定是否文件属性是否到达删除要求。
log.cleaner.enable false 当这个属性设置为false时,一旦日志的保存时间或者大小达到上限时,就会被删除;如果设置为true,则当保存属性达到上限时,就会进行log compaction。
log.cleaner.threads 1 进行日志压缩的线程数
log.cleaner.io.max.bytes.per.second None 进行log compaction时,log cleaner可以拥有的最大I/O数目。这项设置限制了cleaner,以避免干扰活动的请求服务。
log.cleaner.io.buffer.size 50010241024 log cleaner清除过程中针对日志进行索引化以及精简化所用到的缓存大小。最好设置大点,以提供充足的内存。
log.cleaner.io.buffer.load.factor 512*1024 进行log cleaning时所需要的I/O chunk尺寸。你不需要更改这项设置。
log.cleaner.io.buffer.load.factor 0.9 log cleaning中所使用的hash表的负载因子;你不需要更改这个选项。
log.cleaner.backoff.ms 15000 进行日志是否清理检查的时间间隔
log.cleaner.min.cleanable.ratio 0.5 这项配置控制log compactor试图清理日志的频率(假定log compaction是打开的)。默认避免清理压缩超过50%的日志。这个比率绑定了备份日志所消耗的最大空间(50%的日志备份时压缩率为50%)。更高的比率则意味着浪费消耗更少,也就可以更有效的清理更多的空间。这项设置在每个topic设置中可以覆盖。 查看the per-topic configuration section。
log.cleaner.delete.retention.ms 1day 保存时间;保存压缩日志的最长时间;也是客户端消费消息的最长时间,荣log.retention.minutes的区别在于一个控制未压缩数据,一个控制压缩后的数据;会被topic创建时的指定时间覆盖。
log.index.size.max.bytes 1010241024 每个log segment的最大尺寸。注意,如果log尺寸达到这个数值,即使尺寸没有超过log.segment.bytes限制,也需要产生新的log segment。
log.index.interval.bytes 4096 当执行一次fetch后,需要一定的空间扫描最近的offset,设置的越大越好,一般使用默认值就可以
log.flush.interval.messages Long.MaxValue log文件“sync”到磁盘之前累积的消息条数。因为磁盘IO操作是一个慢操作,但又是一个“数据可靠性”的必要手段,所以检查是否需要固化到硬盘的时间间隔。需要在“数据可靠性”与“性能”之间做必要的权衡,如果此值过大,将会导致每次“发sync”的时间过长(IO阻塞),如果此值过小,将会导致“fsync”的时间较长(IO阻塞),如果此值过小,将会导致”发sync“的次数较多,这也就意味着整体的client请求有一定的延迟,物理server故障,将会导致没有fsync的消息丢失。
log.flush.scheduler.interval.ms Long.MaxValue 检查是否需要fsync的时间间隔
log.flush.interval.ms Long.MaxValue 仅仅通过interval来控制消息的磁盘写入时机,是不足的,这个数用来控制”fsync“的时间间隔,如果消息量始终没有达到固化到磁盘的消息数,但是离上次磁盘同步的时间间隔达到阈值,也将触发磁盘同步。
log.delete.delay.ms 60000 文件在索引中清除后的保留时间,一般不需要修改
auto.create.topics.enable true 是否允许自动创建topic。如果是真的,则produce或者fetch 不存在的topic时,会自动创建这个topic。否则需要使用命令行创建topic
controller.socket.timeout.ms 30000 partition管理控制器进行备份时,socket的超时时间。
controller.message.queue.size Int.MaxValue controller-to-broker-channles的buffer 尺寸
default.replication.factor 1 默认备份份数,仅指自动创建的topics
replica.lag.time.max.ms 10000 如果一个follower在这个时间内没有发送fetch请求,leader将从ISR重移除这个follower,并认为这个follower已经挂了
replica.lag.max.messages 4000 如果一个replica没有备份的条数超过这个数值,则leader将移除这个follower,并认为这个follower已经挂了
replica.socket.timeout.ms 30*1000 leader 备份数据时的socket网络请求的超时时间
replica.socket.receive.buffer.bytes 64*1024 备份时向leader发送网络请求时的socket receive buffer
replica.fetch.max.bytes 1024*1024 备份时每次fetch的最大值
replica.fetch.min.bytes 500 leader发出备份请求时,数据到达leader的最长等待时间
replica.fetch.min.bytes 1 备份时每次fetch之后回应的最小尺寸
num.replica.fetchers 1 从leader备份数据的线程数
replica.high.watermark.checkpoint.interval.ms 5000 每个replica检查是否将最高水位进行固化的频率
fetch.purgatory.purge.interval.requests 1000 fetch 请求清除时的清除间隔
producer.purgatory.purge.interval.requests 1000 producer请求清除时的清除间隔
zookeeper.session.timeout.ms 6000 zookeeper会话超时时间。
zookeeper.connection.timeout.ms 6000 客户端等待和zookeeper建立连接的最大时间
zookeeper.sync.time.ms 2000 zk follower落后于zk leader的最长时间
controlled.shutdown.enable true 是否能够控制broker的关闭。如果能够,broker将可以移动所有leaders到其他的broker上,在关闭之前。这减少了不可用性在关机过程中。
controlled.shutdown.max.retries 3 在执行不彻底的关机之前,可以成功执行关机的命令数。
controlled.shutdown.retry.backoff.ms 5000 在关机之间的backoff时间
auto.leader.rebalance.enable true 如果这是true,控制者将会自动平衡brokers对于partitions的leadership
leader.imbalance.per.broker.percentage 10 每个broker所允许的leader最大不平衡比率
leader.imbalance.check.interval.seconds 300 检查leader不平衡的频率
offset.metadata.max.bytes 4096 允许客户端保存他们offsets的最大个数
max.connections.per.ip Int.MaxValue 每个ip地址上每个broker可以被连接的最大数目
max.connections.per.ip.overrides 每个ip或者hostname默认的连接的最大覆盖
connections.max.idle.ms 600000 空连接的超时限制
log.roll.jitter.{ms,hours} 0 从logRollTimeMillis抽离的jitter最大数目
num.recovery.threads.per.data.dir 1 每个数据目录用来日志恢复的线程数目
unclean.leader.election.enable true 指明了是否能够使不在ISR中replicas设置用来作为leader
delete.topic.enable false 能够删除topic
offsets.topic.num.partitions 50 The number of partitions for the offset commit topic. Since changing this after deployment is currently unsupported, we recommend using a higher setting for production (e.g., 100-200).
offsets.topic.retention.minutes 1440 存在时间超过这个时间限制的offsets都将被标记为待删除
offsets.retention.check.interval.ms 600000 offset管理器检查陈旧offsets的频率
offsets.topic.replication.factor 3 topic的offset的备份份数。建议设置更高的数字保证更高的可用性
offset.topic.segment.bytes 104857600 offsets topic的segment尺寸。
offsets.load.buffer.size 5242880 这项设置与批量尺寸相关,当从offsets segment中读取时使用。
offsets.commit.required.acks -1 在offset commit可以接受之前,需要设置确认的数目,一般不需要更改
Property Default Server Default Property Description
cleanup.policy delete log.cleanup.policy 要么是”delete“要么是”compact“; 这个字符串指明了针对旧日志部分的利用方式;默认方式(“delete”)将会丢弃旧的部分当他们的回收时间或者尺寸限制到达时。”compact“将会进行日志压缩
delete.retention.ms 86400000 (24 hours) log.cleaner.delete.retention.ms 对于压缩日志保留的最长时间,也是客户端消费消息的最长时间,通log.retention.minutes的区别在于一个控制未压缩数据,一个控制压缩后的数据。此项配置可以在topic创建时的置顶参数覆盖
flush.messages None log.flush.interval.messages 此项配置指定时间间隔:强制进行fsync日志。例如,如果这个选项设置为1,那么每条消息之后都需要进行fsync,如果设置为5,则每5条消息就需要进行一次fsync。一般来说,建议你不要设置这个值。此参数的设置,需要在"数据可靠性"与"性能"之间做必要的权衡.如果此值过大,将会导致每次"fsync"的时间较长(IO阻塞),如果此值过小,将会导致"fsync"的次数较多,这也意味着整体的client请求有一定的延迟.物理server故障,将会导致没有fsync的消息丢失.
flush.ms None log.flush.interval.ms 此项配置用来置顶强制进行fsync日志到磁盘的时间间隔;例如,如果设置为1000,那么每1000ms就需要进行一次fsync。一般不建议使用这个选项
index.interval.bytes 4096 log.index.interval.bytes 默认设置保证了我们每4096个字节就对消息添加一个索引,更多的索引使得阅读的消息更加靠近,但是索引规模却会由此增大;一般不需要改变这个选项
max.message.bytes 1000000 max.message.bytes kafka追加消息的最大尺寸。注意如果你增大这个尺寸,你也必须增大你consumer的fetch 尺寸,这样consumer才能fetch到这些最大尺寸的消息。
min.cleanable.dirty.ratio 0.5 min.cleanable.dirty.ratio 此项配置控制log压缩器试图进行清除日志的频率。默认情况下,将避免清除压缩率超过50%的日志。这个比率避免了最大的空间浪费
min.insync.replicas 1 min.insync.replicas 当producer设置request.required.acks为-1时,min.insync.replicas指定replicas的最小数目(必须确认每一个repica的写数据都是成功的),如果这个数目没有达到,producer会产生异常。
retention.bytes None log.retention.bytes 如果使用“delete”的retention 策略,这项配置就是指在删除日志之前,日志所能达到的最大尺寸。默认情况下,没有尺寸限制而只有时间限制
retention.ms 7 days log.retention.minutes 如果使用“delete”的retention策略,这项配置就是指删除日志前日志保存的时间。
segment.bytes 1GB log.segment.bytes kafka中log日志是分成一块块存储的,此配置是指log日志划分成块的大小
segment.index.bytes 10MB log.index.size.max.bytes 此配置是有关offsets和文件位置之间映射的索引文件的大小;一般不需要修改这个配置
segment.ms 7 days log.roll.hours 即使log的分块文件没有达到需要删除、压缩的大小,一旦log 的时间达到这个上限,就会强制新建一个log分块文件
segment.jitter.ms 0 log.roll.jitter.{ms,hours} The maximum jitter to subtract from logRollTimeMillis.

消费者控制

Property Default Description
group.id 用来唯一标识consumer进程所在组的字符串,如果设置同样的group id,表示这些processes都是属于同一个consumer group
zookeeper.connect 指定zookeeper的连接的字符串,格式是hostname:port,此处host和port都是zookeeper server的host和port,为避免某个zookeeper 机器宕机之后失联,你可以指定多个hostname:port,使用逗号作为分隔: hostname1:port1,hostname2:port2,hostname3:port3 可以在zookeeper连接字符串中加入zookeeper的chroot路径,此路径用于存放他自己的数据,方式: hostname1:port1,hostname2:port2,hostname3:port3/chroot/path
consumer.id null 不需要设置,一般自动产生
socket.timeout.ms 30*100 网络请求的超时限制。真实的超时限制是 max.fetch.wait+socket.timeout.ms
socket.receive.buffer.bytes 64*1024 socket用于接收网络请求的缓存大小
fetch.message.max.bytes 1024*1024 每次fetch请求中,针对每次fetch消息的最大字节数。这些字节将会督导用于每个partition的内存中,因此,此设置将会控制consumer所使用的memory大小。这个fetch请求尺寸必须至少和server允许的最大消息尺寸相等,否则,producer可能发送的消息尺寸大于consumer所能消耗的尺寸。
num.consumer.fetchers 1 用于fetch数据的fetcher线程数
auto.commit.enable true 如果为真,consumer所fetch的消息的offset将会自动的同步到zookeeper。这项提交的offset将在进程挂掉时,由新的consumer使用
auto.commit.interval.ms 60*1000 consumer向zookeeper提交offset的频率,单位是秒
queued.max.message.chunks 2 用于缓存消息的最大数目,以供consumption。每个chunk必须和fetch.message.max.bytes相同
rebalance.max.retries 4 当新的consumer加入到consumer group时,consumers集合试图重新平衡分配到每个consumer的partitions数目。如果consumers集合改变了,当分配正在执行时,这个重新平衡会失败并重入
fetch.min.bytes 1 每次fetch请求时,server应该返回的最小字节数。如果没有足够的数据返回,请求会等待,直到足够的数据才会返回。
fetch.wait.max.ms 100 如果没有足够的数据能够满足fetch.min.bytes,则此项配置是指在应答fetch请求之前,server会阻塞的最大时间。
rebalance.backoff.ms 2000 在重试reblance之前backoff时间
refresh.leader.backoff.ms 200 在试图确定某个partition的leader是否失去他的leader地位之前,需要等待的backoff时间
auto.offset.reset largest zookeeper中没有初始化的offset时,如果offset是以下值的回应: smallest:自动复位offset为smallest的offset largest:自动复位offset为largest的offset anything else:向consumer抛出异常
consumer.timeout.ms -1 如果没有消息可用,即使等待特定的时间之后也没有,则抛出超时异常
exclude.internal.topics true 是否将内部topics的消息暴露给consumer
paritition.assignment.strategy range 选择向consumer 流分配partitions的策略,可选值:range,roundrobin
client.id group id value 是用户特定的字符串,用来在每次请求中帮助跟踪调用。它应该可以逻辑上确认产生这个请求的应用
zookeeper.session.timeout.ms 6000 zookeeper 会话的超时限制。如果consumer在这段时间内没有向zookeeper发送心跳信息,则它会被认为挂掉了,并且reblance将会产生
zookeeper.connection.timeout.ms 6000 客户端在建立通zookeeper连接中的最大等待时间
zookeeper.sync.time.ms 2000 ZK follower可以落后ZK leader的最大时间
offsets.storage zookeeper 用于存放offsets的地点: zookeeper或者kafka
offset.channel.backoff.ms 1000 重新连接offsets channel或者是重试失败的offset的fetch/commit请求的backoff时间
offsets.channel.socket.timeout.ms 10000 当读取offset的fetch/commit请求回应的socket 超时限制。此超时限制是被consumerMetadata请求用来请求offset管理
offsets.commit.max.retries 5 重试offset commit的次数。这个重试只应用于offset commits在shut-down之间。他
dual.commit.enabled true 如果使用“kafka”作为offsets.storage,你可以二次提交offset到zookeeper(还有一次是提交到kafka)。在zookeeper-based的offset storage到kafka-based的offset storage迁移时,这是必须的。对任意给定的consumer group来说,比较安全的建议是当完成迁移之后就关闭这个选项
partition.assignment.strategy range 在“range”和“roundrobin”策略之间选择一种作为分配partitions给consumer 数据流的策略; 循环的partition分配器分配所有可用的partitions以及所有可用consumer 线程。它会将partition循环的分配到consumer线程上。如果所有consumer实例的订阅都是确定的,则partitions的划分是确定的分布。循环分配策略只有在以下条件满足时才可以:(1)每个topic在每个consumer实力上都有同样数量的数据流。(2)订阅的topic的集合对于consumer group中每个consumer实例来说都是确定的。

生产者

Property Default Description
metadata.broker.list 服务于bootstrapping。producer仅用来获取metadata(topics,partitions,replicas)。发送实际数据的socket连接将基于返回的metadata数据信息而建立。格式是: host1:port1,host2:port2 这个列表可以是brokers的子列表或者是一个指向brokers的VIP
request.required.acks 0 此配置是表明当一次produce请求被认为完成时的确认值。特别是,多少个其他brokers必须已经提交了数据到他们的log并且向他们的leader确认了这些信息。典型的值包括: 0: 表示producer从来不等待来自broker的确认信息(和0.7一样的行为)。这个选择提供了最小的时延但同时风险最大(因为当server宕机时,数据将会丢失)。 1:表示获得leader replica已经接收了数据的确认信息。这个选择时延较小同时确保了server确认接收成功。 -1:producer会获得所有同步replicas都收到数据的确认。同时时延最大,然而,这种方式并没有完全消除丢失消息的风险,因为同步replicas的数量可能是1.如果你想确保某些replicas接收到数据,那么你应该在topic-level设置中选项min.insync.replicas设置一下。请阅读一下设计文档,可以获得更深入的讨论。
request.timeout.ms 10000 broker尽力实现request.required.acks需求时的等待时间,否则会发送错误到客户端
producer.type sync 此选项置顶了消息是否在后台线程中异步发送。正确的值: (1) async: 异步发送 (2) sync: 同步发送 通过将producer设置为异步,我们可以批量处理请求(有利于提高吞吐率)但是这也就造成了客户端机器丢掉未发送数据的可能性
serializer.class kafka.serializer.DefaultEncoder 消息的序列化类别。默认编码器输入一个字节byte[],然后返回相同的字节byte[]
key.serializer.class 关键字的序列化类。如果没给与这项,默认情况是和消息一致
partitioner.class kafka.producer.DefaultPartitioner partitioner 类,用于在subtopics之间划分消息。默认partitioner基于key的hash表
compression.codec none 此项参数可以设置压缩数据的codec,可选codec为:“none”, “gzip”, “snappy”
compressed.topics null 此项参数可以设置某些特定的topics是否进行压缩。如果压缩codec是NoCompressCodec之外的codec,则对指定的topics数据应用这些codec。如果压缩topics列表是空,则将特定的压缩codec应用于所有topics。如果压缩的codec是NoCompressionCodec,压缩对所有topics军不可用。
message.send.max.retries 3 此项参数将使producer自动重试失败的发送请求。此项参数将置顶重试的次数。注意:设定非0值将导致重复某些网络错误:引起一条发送并引起确认丢失
retry.backoff.ms 100 在每次重试之前,producer会更新相关topic的metadata,以此进行查看新的leader是否分配好了。因为leader的选择需要一点时间,此选项指定更新metadata之前producer需要等待的时间。
topic.metadata.refresh.interval.ms 600*1000 producer一般会在某些失败的情况下(partition missing,leader不可用等)更新topic的metadata。他将会规律的循环。如果你设置为负值,metadata只有在失败的情况下才更新。如果设置为0,metadata会在每次消息发送后就会更新(不建议这种选择,系统消耗太大)。重要提示: 更新是有在消息发送后才会发生,因此,如果producer从来不发送消息,则metadata从来也不会更新。
queue.buffering.max.ms 5000 当应用async模式时,用户缓存数据的最大时间间隔。例如,设置为100时,将会批量处理100ms之内消息。这将改善吞吐率,但是会增加由于缓存产生的延迟。
queue.buffering.max.messages 10000 当使用async模式时,在在producer必须被阻塞或者数据必须丢失之前,可以缓存到队列中的未发送的最大消息条数
batch.num.messages 200 使用async模式时,可以批量处理消息的最大条数。或者消息数目已到达这个上线或者是queue.buffer.max.ms到达,producer才会处理
send.buffer.bytes 100*1024 socket 写缓存尺寸
client.id “” 这个client id是用户特定的字符串,在每次请求中包含用来追踪调用,他应该逻辑上可以确认是那个应用发出了这个请求
Name Type Default Importance Description
boostrap.servers list high 用于建立与kafka集群连接的host/port组。数据将会在所有servers上均衡加载,不管哪些server是指定用于bootstrapping。这个列表仅仅影响初始化的hosts(用于发现全部的servers)。这个列表格式: host1:port1,host2:port2,… 因为这些server仅仅是用于初始化的连接,以发现集群所有成员关系(可能会动态的变化),这个列表不需要包含所有的servers(你可能想要不止一个server,尽管这样,可能某个server宕机了)。如果没有server在这个列表出现,则发送数据会一直失败,直到列表可用。
acks string 1 high producer需要server接收到数据之后发出的确认接收的信号,此项配置就是指procuder需要多少个这样的确认信号。此配置实际上代表了数据备份的可用性。以下设置为常用选项: (1)acks=0: 设置为0表示producer不需要等待任何确认收到的信息。副本将立即加到socket buffer并认为已经发送。没有任何保障可以保证此种情况下server已经成功接收数据,同时重试配置不会发生作用(因为客户端不知道是否失败)回馈的offset会总是设置为-1; (2)acks=1: 这意味着至少要等待leader已经成功将数据写入本地log,但是并没有等待所有follower是否成功写入。这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失。 (3)acks=all: 这意味着leader需要等待所有备份都成功写入日志,这种策略会保证只要有一个备份存活就不会丢失数据。这是最强的保证。 (4)其他的设置,例如acks=2也是可以的,这将需要给定的acks数量,但是这种策略一般很少用。
buffer.memory long 33554432 high producer可以用来缓存数据的内存大小。如果数据产生速度大于向broker发送的速度,producer会阻塞或者抛出异常,以“block.on.buffer.full”来表明。 这项设置将和producer能够使用的总内存相关,但并不是一个硬性的限制,因为不是producer使用的所有内存都是用于缓存。一些额外的内存会用于压缩(如果引入压缩机制),同样还有一些用于维护请求。
compression.type string none high producer用于压缩数据的压缩类型。默认是无压缩。正确的选项值是none、gzip、snappy。 压缩最好用于批量处理,批量处理消息越多,压缩性能越好。
retries int 0 high 设置大于0的值将使客户端重新发送任何数据,一旦这些数据发送失败。注意,这些重试与客户端接收到发送错误时的重试没有什么不同。允许重试将潜在的改变数据的顺序,如果这两个消息记录都是发送到同一个partition,则第一个消息失败第二个发送成功,则第二条消息会比第一条消息出现要早。
batch.size int 16384 medium producer将试图批处理消息记录,以减少请求次数。这将改善client与server之间的性能。这项配置控制默认的批量处理消息字节数。 不会试图处理大于这个字节数的消息字节数。 发送到brokers的请求将包含多个批量处理,其中会包含对每个partition的一个请求。 较小的批量处理数值比较少用,并且可能降低吞吐量(0则会仅用批量处理)。较大的批量处理数值将会浪费更多内存空间,这样就需要分配特定批量处理数值的内存大小。
client.id string medium 当向server发出请求时,这个字符串会发送给server。目的是能够追踪请求源头,以此来允许ip/port许可列表之外的一些应用可以发送信息。这项应用可以设置任意字符串,因为没有任何功能性的目的,除了记录和跟踪
linger.ms long 0 medium producer组将会汇总任何在请求与发送之间到达的消息记录一个单独批量的请求。通常来说,这只有在记录产生速度大于发送速度的时候才能发生。然而,在某些条件下,客户端将希望降低请求的数量,甚至降低到中等负载一下。这项设置将通过增加小的延迟来完成–即,不是立即发送一条记录,producer将会等待给定的延迟时间以允许其他消息记录发送,这些消息记录可以批量处理。这可以认为是TCP种Nagle的算法类似。这项设置设定了批量处理的更高的延迟边界:一旦我们获得某个partition的batch.size,他将会立即发送而不顾这项设置,然而如果我们获得消息字节数比这项设置要小的多,我们需要“linger”特定的时间以获取更多的消息。 这个设置默认为0,即没有延迟。设定linger.ms=5,例如,将会减少请求数目,但是同时会增加5ms的延迟。
max.request.size int 1028576 medium 请求的最大字节数。这也是对最大记录尺寸的有效覆盖。注意:server具有自己对消息记录尺寸的覆盖,这些尺寸和这个设置不同。此项设置将会限制producer每次批量发送请求的数目,以防发出巨量的请求。
receive.buffer.bytes int 32768 medium TCP receive缓存大小,当阅读数据时使用
send.buffer.bytes int 131072 medium TCP send缓存大小,当发送数据时使用
timeout.ms int 30000 medium 此配置选项控制server等待来自followers的确认的最大时间。如果确认的请求数目在此时间内没有实现,则会返回一个错误。这个超时限制是以server端度量的,没有包含请求的网络延迟
block.on.buffer.full boolean true low 当我们内存缓存用尽时,必须停止接收新消息记录或者抛出错误。默认情况下,这个设置为真,然而某些阻塞可能不值得期待,因此立即抛出错误更好。设置为false则会这样:producer会抛出一个异常错误:BufferExhaustedException, 如果记录已经发送同时缓存已满
metadata.fetch.timeout.ms long 60000 low 是指我们所获取的一些元素据的第一个时间数据。元素据包含:topic,host,partitions。此项配置是指当等待元素据fetch成功完成所需要的时间,否则会跑出异常给客户端。
metadata.max.age.ms long 300000 low 以微秒为单位的时间,是在我们强制更新metadata的时间间隔。即使我们没有看到任何partition leadership改变。
metric.reporters list [] low 类的列表,用于衡量指标。实现MetricReporter接口,将允许增加一些类,这些类在新的衡量指标产生时就会改变。JmxReporter总会包含用于注册JMX统计
metrics.num.samples int 2 low 用于维护metrics的样本数
metrics.sample.window.ms long 30000 low metrics系统维护可配置的样本数量,在一个可修正的window size。这项配置配置了窗口大小,例如。我们可能在30s的期间维护两个样本。当一个窗口推出后,我们会擦除并重写最老的窗口
recoonect.backoff.ms long 10 low 连接失败时,当我们重新连接时的等待时间。这避免了客户端反复重连
retry.backoff.ms long 100 low 在试图重试失败的produce请求之前的等待时间。避免陷入发送-失败的死循环中。

你可能感兴趣的:(大数据框架,java框架,kafka,黑马,尚硅谷)