举个例子:麦当劳点餐时,当我们选择外带的时候,餐厅制作好餐之后会放在一个取餐台,而且取餐台是按照不同的取餐码尾号分开放置的,按照餐品的产出顺序进行放置的。这时候我们可以在我们空闲的时候去取餐,而餐厅也不用等待我们拿完餐再生产。而这个取餐台就像一个消息队列。我们现在想想如果没有这个取餐台会发生什么?餐厅不断需要顾客及时取走,不然会严重影响到餐厅的出餐和订单处理。那消费者也要关注取餐的信息,自己的时间安排自由度就下降了。so,取餐台这个中间缓冲的对象就可以接触消费者和餐厅的强绑定关系,让餐厅和消费者都可以自由化做各自的事情。
Kafka所扮演的角色就是类似当前取餐台的功能,当然起作用不仅仅是缓冲,在接下来的内容中一起揭开kafka的面纱,一窥其工作机理和设计思想。
我们先来看看Kafka是怎么定义的?
kafka是一个分布式的基于发布/订阅模式的消息队列。
那啥是消息队列啊。如我们开篇讲到的那个取餐台就是一个消息队列。就是在消息传输过程中保存消息的容器。其本质就是:
消息发送者(我们称之为生产者,多形象)——>MQ(message queue消息队列,消息保存的容器)——>消息接受者(消息的消费者)
消息队列就是可以接受生产者发送的消息并保存起来,队列Queue,按照消息接受的顺序存储,然后等待消费者进行消费消息。消息队列的作用就是保存消息并转发消息。
1)解耦
允许我们独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
2)可恢复性
系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
3)缓冲
有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。
4)灵活性 & 峰值处理能力 (削峰)
在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。比如:618,双十一等活动,00:00 大量的手速点击访问,如果没有消息队列作为缓冲,所有请求都打到Redis,mysql等服务器,他们也扛不住啊。
5)异步通信
很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
(1)点对点模式(一对一,消费者主动拉取数据,消息收到后消息清除)
消息生产者生产消息发送到Queue中,然后消息消费者从Queue中取出并且消费消息。
消息被消费以后,queue中不再有存储,所以消息消费者不可能消费到已经被消费的消息。Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
2)发布/订阅模式 (一对多,消费者消费数据之后不会清除消息)
消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费。
发布-订阅(Publish/Subscribe)模式(又被称为观察者模式,属于行为型模式的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。
我们看看传统的MQ有什么问题?
假设现在我们的应用程序需要往别处发送监控信息,可以直接在应用程序和另一个可以在仪表盘上显示度量指标的应用程序之间建立连接 然后通过这个连接推送度量指标,
我们可以这样做:
这是刚接触监控系统时简单问题的应对方案。过了不久,你需要分析更长时间片段的度量指标,而此时的仪表盘程序满足不了需求,于是,你启动了一个新的服务来接收度盘指标。该服务把度量指标保存起来,然后进行分析。与此同时,你修改了原来的应用程序, 把度量指标同时发送到两个仪表盘系统上。
现在,你又多了3个可以生成度量指标 应用 程序,它们都与这两个服务直接相连。而你的同事认为最好可以对这些服务进行轮询以便 获得告警功能,于是你为每一个应用程序增加了一个服务器,用于提供度量指标。再过一阵子,有更多的应用程序出于各自的目的,都从这些服务器获取度主指标。这时的架构看起来就像下图所示一样,节点间的连接一团糟。
我们创建一个基于发布订阅的消息队列, 用于接收来自其他应用程序的度量指标,井为其他系统提供了一个查询服务器。
这时候一切都看起来这么清爽和简单,但是当我们和度量指标进行了一轮“艰苦奋战”之后,其他同事也要和各自的任务进行battle。另一个同事也正在跟日志消息奋战。还有另一个同事正在跟踪网站用户的行为,为负责机器学习开发的同事提供信息 ,同时为管理团队生成报告。你和同事们使用相同的方式创建这些系统,解辑信息的发布者和订阅者。然后发现世界好像又不美好了?看图:
由于不同的业务任务,我们产生了多个消息队列进行各自业务的处理,但是这里有很多重复的地方。而且由于不同的业务模块,开发人员需要为各自的业务指标任务维护一套内容,而且之后或许还有其他的业务需要构建新的消息队列进行处理,资源浪费且每天维护这些内容,出现BUG的排查等等都会带来极大的不便性。但是又因为传统消息队列中的一个消息只能被消费一次,这时候我们就想,如果消息队列可以对于不同的业务的消费者看做不同的消费者,他们都可以消费消息队列中的消息就可以共用这些消息系统了。岂不是美滋滋,即便是后边需要增加业务,也不用独立的使用新的消息队列,世界又变得美好万分。
Kafka的数据按照一定的顺序持久化保存,可以按需读取,通过对于不同的群组的消费者重新数据的消费状态实现多消费者共同消费消息等。具体的数据一致性保证以及生产者消费者写入和读取数据是怎么进行的,在后边的内容会与展开讲述。
Kafka 的数据单元被称为消息。消息由字节数组组成,所以对于Kafka来说,消息里的数据没有特别的格式或含义。消息可以有一个可选的元数据,也就是键。键也是一个字节数组,与消息一样,对于Kafka来说也没有特殊的含义。消息以一种可控的方式写入不同的分区时,会用到键。最简单的例子就是为键生成一个一致性散列值,然后使用散列值对主题分区数进行取模,为消息选取分区。
p a t i t i o n n u m = m o d ( h a s h ( k e y ) , p a r t i t i o n s ) patitionnum = mod(hash(key) , partitions) patitionnum=mod(hash(key),partitions)
这样可以保证具有相同键的消息总是被写到相同的分区上。为了提高效率,消息被分批次写入 Kafka 。批次就是一组消息,这些消息属于同一个主题和分区。如果每 个消息都单独在网络传输,会导致大量的网络开销,把消息分成批次传输可以减少网络开销。不过,这要在时间延迟和吞吐量之间作出权衡:批次越大,单位时间内处理的消息就越多,单个消息的传输时间就越长。批次数据会被压缩,这样可以提升数据的传输和存储能力,但要做更多的计算处理。
对于Kafka而言,消息的底层是一组字节数组,是我们难以辨识的内容。为了更好的理解这些消息,就有开发者提出用额外的一种结构来定义消息内容。比如常见的JSON和XML。这些结构不仅易用,且可读性好。Kafka一般采用Avro。Avro提供了一种紧凑的序列化格式,其模式和消息体是分开的。另外Avro也是由Doug Cutting创建的哦。因为Avro的一些特性,很适合Kafka这样的消息队列。消除了消息读写操作之间的耦合性。
如果读写操作紧密地耦合在一起,消息订阅者需要升级应用程序才能同时处理新旧两种数据格式。在消息订阅者升级了之后,消息发布者才能跟着升级,以便使用新的数据格式。新的 应用程序如果需要使用数据,就要与消息发布者发生耦合,导致开发者需要做很多繁杂操作。
在对于当前架构进行拆分看待前,我们先来关注一些Kafka独到的机制和单元:
图片引用来自知乎老刘
1)Producer :消息生产者,就是向kafka broker发消息的客户端
生产者创建消息。一般情况下,一个消息会被发布到一个特定的主题上。生产者在默认情况下把消息均衡地分布到主题的所有分区上,而并不关心特定消息会被写到哪个分区。不过,在某些情况下,生产 者会把消息直接写到指定的分区。这通常是通过消息键和分区器来实现的,分区器为键生 成一个散列值,并将其映射到指定的分区上。这样可以保证包含同一个键的消息会被写到 同一个分区上。生产者也可以使用自定义的分区器,根据不同的业务规则将消息映射到分区。
生产者具体的一些消息写入细节会在下一个文章进行讲述。毕竟一个篇幅如果太长,也不能做到详尽,也怕小伙伴没有时间看完。
2)Consumer:消息消费者,向kafka broker取消息的客户端
消费者读取消息。消费者订阅一个或多个主题,并按照消息生成的顺序读取它们。消费者通过检查消息的偏移量来区 分已经读取过的消息。 偏移量是另一种元数据,它是一个不断递增的整数值,在创建消息时, Kafka 会把它添加到消息里。在给定的分区里,每个悄息的偏移量都是唯一的。消费者把每个分区最后读取的悄息偏移量保存在 Zookeeper或Kafka 上,如果悄费者关闭或重启,它的读取状态不会丢失。
消费者是消费者群组的一部分,也就是说,会有一个或多个消费者共同读取一个主题。群组保证每个分区只能被一个消费者使用 。
有3消费者同时读取1个主题。其中的两个消费者各自读取一个分区,另外一个消费者读取其他两个分区。消费者与分区之间的映射通常被称为消费者对分区的所有权关系。
通过这种方式,消费者可以消费包含大量消息的主题。而且,如果一个消费者失效,群组里的其他消费者可以接管失效消费者的工作。
3)Consumer Group (CG):消费者组,由多个consumer组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
消费者组,组中有多个消费者。
组中消费者的个数最好 = topic分区数
如果消费者组中消费者个数>topic分区数,此时有个别消费者没有分区可以消费
如果消费者组中消费者个数
**4)Broker **:一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以容纳多个topic。
broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。 broker 为消费者提供服务,对读取分区的请求作出响应,返 回已经提交到磁盘上的消息。根据特定的硬件及其性能特征,单个 broker 可以轻松处理数千个分区以及每秒百万级的消息量。
broker 是集群的组成部分。每个集群都有一个 broker 同时充当了集群控制器的角色Collector(自动从集群的活跃成员中选举出来)。控制器负责管理工作,包括将分区分配给 broker 和监控 broker。在集群中, 一个分区从属于一个 broker, 该broker 被称为分区的首领。一个分区可以分配给多个 broke ,这个时候会发生分区复制。这种复制机制为分区提供 了消息冗余,如果有一个 broker 失效,其他 broker 可以接管领导权。不过,相关的消费者和生产者都要重新连接到新的首领。
5)Topic: 可以理解为一个队列,生产者和消费者面向的都是一个topic
6)Partition:为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列;
7)Replica:副本,为保证集群中的某个节点发生故障时,该节点上的partition数据不丢失,且kafka仍然能够继续工作,kafka提供了副本机制,一个topic的每个分区都有若干个副本,一个leader和若干个follower。
8)leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是leader。
9)follower:每个分区多个副本中的“从”,实时从leader中同步数据,保持和leader数据的同步。leader发生故障时,某个follower会成为新的leader。
Kafka 可以无缝地支持多个生产者,不管客户端在使用单个主题还是多个主题。所以它很适合用来从多个前端系统收集数据,并以统 的格式对外提供数据。
Kafka 也支持多个消费者从一个单独的消息流上读取数据,而且消费者之间互不影响。这与其他队列系统不同,其他队列系统的消息一旦被一个客户端读 取,其他客户端就无法再读取它。另外,多个消费者可以组成一个群组,它们共享一个消息流,并保证整个群组对每个给定的消息只处理一次。
Kafka 的数据 保留特性。消息被提交到磁盘,根据设置的保留规则进行保存。每个主题可以设置单独的保留规则,以便满足不同消费者的需求,各个主题可以保留不同数量的消息。消费者可能会因为处理速度慢或突发的流量高峰导致无陆及时读取消息,而持久化数据可以保证数据 不会丢失。消费者可以在进行应用程序维护时离线一小段时间,而无需担心消息丢失或堵塞在生产者端。消费者可以被关闭,但消息会继续保留在 Kafka 里。消费者可以从上次中断的地方继续处理消息。
为了能够轻松处理大量数据, Kafka 一开始就被设计成一个具有灵活伸缩性的系统。用户在开发阶段可以先使用单个 broker ,再扩展到包含3个 broker 的小型开发集群,然后随着数据量不断增长,部署到生产环境的集群可能包含上百个 broker 。对在线集群进行扩展丝毫不影响整体系统的可用性。也就是说,一个包含多个 broker 的集群,即使个别Broker失效,仍然可以持续地为客户提供服务。
前面提到的所有特性,让 Kafka 成为了一个高性能的发布与订阅消息系统。通过横向扩展生产者、消费者和 broker, Kafka 可以轻松处理巨大的消息流。在处理大量数据的同时, 它还能保证亚秒级的消息延迟。
Kafka中消息是以topic进行分类的,生产者生产消息,消费者消费消息,都是面向topic的。
topic是逻辑上的概念,而partition是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是producer生产的数据。Producer生产的数据会被不断追加到该log文件末端,且每条数据都有自己的offset。消费者组中的每个消费者,都会实时记录自己消费到了哪个offset,以便出错恢复时,从上次的位置继续消费。
为什么分区?
1)方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了;
2)可以提高并发,因为可以以Partition为单位读写了。
Kafka 使用 Zookeeper 来维护集群成员的信息。每个 broker (每一个节点就是一个broker)都有一个唯一标识符,这个标识符可以自动生成,也可以在配置文件里指定(我们一般也这样做,常见的做法是通过kafka安装目录下conf/server.properties 文件进行配置) 。配置如下:
# see kafka.server.KafkaConfig for additional details and defaults
############################# Server Basics #############################
# The id of the broker. This must be set to a unique integer for each broker.
# 这个id值 集群全局唯一
broker.id=2
在 broker 启动的时候,它通过创建临时节点把自己的 ID 注册到 Zookeeper, Kafka 组件订阅 Zookeeper 的/brokers/ids 路径 (broker在Zookeeper 上的注册路径),当有 broker 加入集群或退出集群时,这些组件就可以获得通知。
在broker 停机、出现网络分区或长时间垃圾回收停顿时,broker 会从 Zookeeper 上断开连 接,此时 broker 在启动时创建的临时节点会自动从 Zookeeper 上移除。监听 broker 列表的 Kafka 组件会被告知该 broker 已移除。
当集群启动之后,Kafka集群开始工作了,如上图所示:
首先,集群启动后,集群中的broker会通过选举机制选出一个控制器Controller,具体的选举细节在后边在进行细说。控制器除了具有一般的broker的功能之处,还负责分区leader的选举。我们到现在已经知道:
kafka使用主题Topic来进行组织数据,
每个主题被分成若干个分区(分区一般在我们创建topic的时候指定,默认是1个分区);
每个分区有多个副本(副本数量一般同样是我们创建的时候指定,默认为1 ,但是其值不能超过节点的个数,因为副本是均衡分布的)。
生产者会创建一个ProducerRecord对象通过指定的主题向集群发送消息,ProducerRecord对象需要将消息的键值序列化才能在网络中传输。数据被发送到集群中的某个broker的时候,这个时候会先经过分区器确认数据要写入在那个分区。这时候分区器对于数据的键key进行检测,会有如下三种情况:
确定好分区信息后,生产者就知道该往那个主题和那个分区发送该条记录了。但是这个消息不会立即发送,而是将这条记录添加到一个记录批次中,这个批次的所有消息都是发送同一主题和分区的。批次发送有两个参数:设定的时间和批次的容量,只要满足其一就发送。发送是由一个独立的线程负责处理。服务器也就是broker收到消息返回是否写入成功,
当对应主题和分区的broker接收到一批数据写入请求时,broker先进行一些验证:
我们知道kafka是分布式消息队列,如何保证数据的可靠性和不丢失是重中之重。生产者写入过程(Kafka 还从broker内部也就是分区的方面进行了可靠性的保障机制,稍后展开)如何进行可靠性的保证,Kafka采用了Acks应答机制。topic的每个partition收到producer发送的数据后,都需要向producer发送ack(acknowledgement确认收到),如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。
Acks机制提供了三种可靠性级别:
这时候,broker开始写入producer发送的一批数据。Kafka是顺序写磁盘的方式持久化数据,在我们的认知中,是不是觉得写磁盘很慢,但有大量数据请求写入,怕是写的黄花菜都凉了哦。别急,kafka 能够如此火热自然有其特殊之处,正所谓:没有金刚钻,不揽瓷器活嘛。Kafka是对于数据进行追加的方式顺序写入,这样就减少了大量的磁头寻址的时间。官网数据表明:同样的磁盘,顺序写能到600M/s,而随机写只有100K/s。
由于生产者生产的消息会不断追加到log文件末尾,为防止log文件过大导致数据定位效率低下,Kafka采取了分片和索引机制,将每个partition分为多个segment。每个segment对应三个文件——“.index”文件、“.timeindex”文件和“.log”文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic名称+分区序号。
segment的命名规则:
1、每个分区第一个segment的文件名= 0000000000000000000
2、后续第N个segment文件名 = 第N-1个segment中最后一个offset+1
segment给log文件建索引的时候是每个一段范围**[4k]**建一个索引,是为稀疏索引。
“.index”文件存储大量的索引信息,“.log”文件存储大量的数据,索引文件中的元数据指向对应数据文件中message的物理偏移地址。如果我们现在查询第三条数据即offset=2的数据:则其索引为000000000000000002,通过在index文件中确认其索引,找到对应的数据记录的log中的地址,然后再找到对应的数据位置,读取出来。
producer向leader写入数据之后,follower需要向leader同步消息信息也就是需要复制多少份数据,但是我们应该配置多少个副本呢?又因为副本的均衡分布,就是一个broker只会有同主题同分区的一个副本。那配置副本就是配置broker,也就是需要多少个节点可以满足我们数据的可靠性保证呢?在Kafka中,每个分区的默认副本数=3。就是说最小集群的配置数,HDFS的默认副本也是3,所以一般3副本就足以保证数据不会丢失,当然也要考虑机架配置的哦。如果复制系数为N ,那么在N-1个broker 失效的情况下,仍然能够从主题读取数据或向主题写入数据。所以,更高的复制系数会带来更高的可用性、可靠性和更少的故障。另一方面,复制系数N需要至少N个broker ,而且会有N个数据副本。我们可以根据自身需求来确认,比如:银行为了保证数据更高的可靠性,就可以将复制系数设置为5。如果我们可以接受主题偶尔的不可用,也可以配置为2,当一台broker崩溃,另一台broker作为新的controller继续进行后续的工作。
在前边我们了解Ack应答机制有三种应答级别。最为可靠的设置为all。在当前应答级别下,假设leader收到数据,所有follower都开始同步数据,但有一个follower,因为某种故障,迟迟不能与leader进行同步,那leader就要一直等下去,直到它完成同步,才能发送ack。这个问题怎么解决呢?
Leader维护了一个动态的in-sync replica set (ISR),意为和leader保持同步的follower集合。当ISR中的follower完成数据的同步之后,leader就会给producer发送ack。如果follower长时间未向leader同步数据,则该follower将被踢出ISR,该时间阈值由replica.lag.time.max.ms参数设定。Leader发生故障之后,就会从ISR中选举新的leader。ISR队列中follower的选择标准:
我们知道消费者也是只和leader分区进行通讯进行消费数据,如下:
1)follower故障
follower发生故障后会被临时踢出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等该follower的LEO大于等于该Partition的HW,即follower追上leader之后,就可以重新加入ISR了。
2)leader故障
leader发生故障之后,会从ISR中选出一个新的leader,之后,为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader同步数据。
注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
Kafka消费者从属于消费者群组。一个群组里的消费者订阅的是同一个主题,每个消费者接收主题一部分分区的消息。假设主题T1有4个分区,我们创建了消费者C1,他是群组G1中唯一的消费者,我们用它订阅主题T1,消费者C1将收到主题T1全部4个分区的消息:
如果群组G1新增一个消费者C2,那么每个消费者将分别从两个分区接受消息。我们假设消费者C1消费分区0和分区2的消息,消费者C2接收消费分区1和分区3的消息,如图:
如果群组G1有4个消费者,那么每个消费者都分配到一个分区:
如果我们继续往群组里添加更多消费者,超过主题的分区数量,那么有一部分消费者就会被闲置,不会接收到任何的消息:
往群组里增加消费者是横向伸缩消费能力的主要方式。 Kafka 消费者经常会做一些高延迟 的操作,比如把数据写到数据库或 HDFS ,或者使用数据进行比较耗时的计算。在这些情况下,单个消费者无法跟上数据生成的速度,所以可以增加更多的消费者,让它们分担负载,每个消费者只处理部分分区的消息,这就是横向伸缩的主要手段。我们有必要为主题创建大量的分区,在负载增长时可以加入更多的消费者。不过要注意,不要让消费者的数量超过主题分区的数量,多余的消费者只会被闲置。
除了通过增加消费者来横向伸缩单个应用程序外,还经常出现多个应用程序从同 主题 读取数据的情况。实际上, Kafka 设计的主要目标之 ,就是要让 Kafka 主题里的数据能 够满足企业各种应用场景的需求。在这些场景里,每个应用程序可以获取到所有的消息, 而不只是其中的部分。只要保证每个应用程序有自己的消费者群组,就可以让它们获取到主题所有的消息。不同于传统的消息系统,横向伸缩 Kafka 消费者和消费者群组并不 对性能造成负面影响。
在上面的例子里,如果新增 个只包含 个消费者的群组 G2 ,那么这个消费者将从主题 Tl 上接收所有的消息,与群组 Gl 之间互不影响。群组 G2 可以增加更多的消费者,每个 消费者可以悄费若干个分区,就像群组 Gl 那样,如图所示。总的来说,群组 G2 还是 会接收到所有消息,不管有没有其他群组存在。
我们通过上边的例子知道,群组里的消费者共同读取主题的分区。一个新的悄费者加 入群组时,它读取的是原本由其他消费者读取的消息。当一个消费者被关闭或发生崩愤时,它就离开群组,原本由它读取的分区将由群组里的其他消费者来读取。在主题发生变化时 比如管理员添加了新的分区,会发生分区重分配。 分区的所有权从 个消费者转移到另 个消费者,这样的行为被称为再均衡。再均衡非常 重要, 它为消费者群组带来了高可用性和伸缩性(我们可以放心地添加或移除梢费者), 不过在正常情况下,我们并不希望发生这样的行为。在再均衡期间,消费者无法读取消息,造成整个群组一小段时间的不可用。另外,当分区被重新分配给另一个消费者时,消费者当前的读取状态会丢失,它有可能还需要去刷新缓存,在它重新恢复状态之前会拖慢应用程序。如何进行安全的再均衡,以及如何避免不必要的再均衡。
消费者通过向被指派为群组协调器的 broker (不同的群组可以有不同的协调器)发送心跳来维持它们和群组的从属关系以及它们对分区的所有权关系。只要消费者以正常的时间间隔发送心跳,就被认为是活跃的,说明它还在读取分区里的消息。消费者会在轮询消息 (为了获取消息)或提交偏移量时发送心跳。如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,就会触发一次再均衡。 如果一个消费者发生崩溃,井停止读取消息,群组协调器会等待几秒钟,确认它死亡了才 触发再均衡。在这几秒钟时间里,死掉的消费者不会读取分区里的消息。在清理消费者时,消费者会通知协调器它将要离开群组,协调器会立即触发一次再均衡,尽量降低处理停顿。
当消费者要加入群组时,它会向群组协调器发送 Join Group 请求。第1个加入群组的消费者将成为“群主”。群主从协调器那里获得群组的成员列表(列表中包含了所有最近发送过心跳的消费者,它们被认为是活跃的),并负责给每一个消费者分配分区。它使用 个实现了 PartitionAssignor接口的类来决定哪些分区应该被分配给哪个消费者。分配完毕之后,群主把分配情况列表发送给群组协调器,协调器再把这些信息发 送给所有消费者。每个消费者只能看到自己的分配信息,只有群主知道群组里所有消费者的分配信息。这个过程会在每次再均衡时重复发生。
上边我们知道,分区会被分配个群组里的消费者。PartitionAssignor 根据给定的消费者和主题,决定哪些分区应该被分配给哪个消费者。Kafka 有两个默认的分配策:Range 和RoundRobin。
Range:
该策略会把主题的若干个连续的分区分配给消费者。假设消费者C1和消费者 C2 同时 订阅了主题 T1 和主题 T2 ,井且每个主题有3个分区。那么消费者 C1有可能分配到这两个主题的分区1和分区3,而消费者 C2 分配到这两个主题的分区2 。因为每个主题拥有奇数个分区,而分配是在主题内独立完成的,第一个消费者最后分配到比第二个消费者更多的分区。只要使用了Range策略,而且分区数量无法被消费者数量整除,就会出现这种情况。
如图中,第一次先分配Topic1的三个分区,对于消费者,就是C1先分第一块,然后C2分得第二块,C1接着分配到第三块。同样对于第二个Topic,同样的顺序进行分配分区,C1分得第一块分区,C2分得第二块分区,C1接着分得第三块。最后就是C1分配到4个分区进行消费,而C2只分得两个分区。
RoundRobin
该策略把主题的所有分区逐个分配给消费者。如果使用 RoundRobin 策略来给消费者 C1和消费者 C2 分配分区,那么消费者C1将分到主题 T1的分区1和分区3以及主题 T2 的分区2 ,消费者 C2 将分配到主题 T1分区2 以及主题T2的分区1和分区3。一般 来说 ,如果所有消费者都订阅相同的主题(这种情况很常见), RoundRobin 策略会给所有消费者分配相同数量的分区(或最多就差1个分区)。
如图:RoundRobin策略相当于把当前消费者组订阅的主题中所有分区看做统一的整体,然后对消费者群组中的每一个活跃者进行轮流分配。
在此,我们有必要明白Consumer是如何消费的。调用了那些方法,做了那些行为来完成一次消费;
对于轮询阶段我们进行详细分析说明:
轮询不只是获取数据那么简单。在第一次调用新消费者的 poll()方法时,它会负责查找 GroupCoordinator 然后加入群组,接受分配的分区。如果发生了再均衡,整个过程也 在轮询期间进行 。当然,心跳也是从轮询里发送出去的。所以,我们要确保在轮询期间,所做的任何处理工作都应该尽快完成。
每次调用 poll ()方法,它总是返回由生产者写入 Kafka 但还没有被消费者读取过的记录 我们因此可以追踪到哪些记录是被群组里的哪个消费者读取的。这是 Kafka 个独特之处。消费者可以使用 Kafka 来追踪消息在分区里的位置(偏移量)。
我们把更新分区当前位置的操作叫作提交。消费者消费消息是按照批次进行的。
那么消费者是如何提交偏移量的呢?消费者往一个叫作 __consumer_offset 特殊主题发送消息,消息里包含每个分区的偏移量。如果消费者一直处于运行状态,那么偏移量就没有什么用处。不过,如果悄费者发生崩溃或者有新的消费者加入群组,就会触发再均衡,完成再均衡之后,每个消费者可能分配到新的分区,而不是之前处理的那个。为了能够继续之前的工作,消费者需要读取每个分区最后一次提交的偏移量,然后从偏移量指定的地方继续处理。说人话也就是说消费者群组发生变化的时候或者消费者组重启之后,要能从上一次消费的地方接着消费数据。消费者组会知道并记录每一次消费的时候消费者消费主题分区的最后一个消息的offset,这样,下一次当前消费者组再开始消费的时候,就能从具体分区的最后一次消费的地方接着消费。
如果提交的偏移量小于客户端处理的最后一个消息的偏移量 ,那么处于两个偏移量之间的消息就会被重复处理,如图:
如果提交的偏移量大于客户端处理的最后 个消息的偏移量,那么处于两个偏移量之间的 消息将会丢失:
所以,处理偏移量的方式对客户端会有很大的影响。
最简单的提交方式是让悄费者自动提交偏移量。如果 enable .auto.commit 被设为 true ,那 么每过5s,消费者会自动把从 poll()方法接收到的最大偏移量提交上去。提交时间间隔 auto.commit.interval.ms 控制,默认值是 5s 。与梢费者里的其他东西一样,自动提交也是在轮询里进行的。消费者每次在进行轮询时会检查是否该提交偏移量了,如果是,那么就会提交从上一次轮询返回的偏移量。
不过当前策略有什么缺陷呢?可以想想。
假设我们仍然使用默认的 5s 提交时间间隔,在最近一次提交之后的 3s 发生了再均衡,再均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。这个时候偏移量已经落后 3s ,所以在这 3s 内到达的消息会被重复处理。可以通过修改提交时间间隔来更频繁地提交偏移量,减小可能出现重复悄息的时间窗,不过这种情况是无法完全避免的。
在使用自动提交 ,每次调用轮询方法上一次调用返回的偏移量提交上去,它并不知道具体哪些消息已经被处理了,所以在再次调用之前最好确保所有当前调用返回的消息都已经处理完毕(在调用 close()方位之前也 行自动提交)。
我们可以通过控制提交偏移量的时间尽可能消除丢失消息的可能性和再均衡时重复消费数据的数量。此外消费者API 提供了另一种提交偏移量的方式 ,让我们可以基于处理消息的时候需要提交的去提交当前偏移盘,而不是基于时间间隔。
首先我们需要在消费者的配置中关闭自动提交参数:auto.commit.offset 设为false,让应用程序决定何时提交偏移量。
使用 commitSync() 提交偏移量最简单最可靠,因为这个方法是同步方法。这个方法会提交由 poll()方法返回的最新偏移量,提交成功后马上返回,如果提交失败就抛出异常。
要记住, commitSync() 将会提交由 poll ()返回的最新偏移量,所以在处理完所有记录后要确保调用了 commitSync() ,否则还是会有丢失消息的风险。如果发生了再均衡,从最近一批消息到发生再均衡之间的所有消息都将被重复处理。
同步提交有一个不足之处, 在broker对提交请求作出回应之前,应用程序会一直阻塞,这样会限制应用程序的吞吐量。我们可以通过降低提交频率来提升吞吐量,但如果发生了再均衡, 会增加重复消息的数量。这时候我们可以使用异步提交方式进行提交:commitASync()。
这时候我们只发送提交请求,不用等待broker的响应.
我们通过麦当劳的例子开始进入消息队列,并从点对点业务到发布订阅的消息队列以及他们存在的弊端引入Kafka。我们知道了Kafka是一个流平台,将数据看做是持续变化和不断增长的流,可以通过发布和订阅数据流,并把他们保存起来进行处理的数据系统。我们称之为数据系统是因为kafka有别于消息系统的分布式部署方式,可以自由伸缩、处理企业内所有的应用程序。kafka不仅仅是传递消息,其还可以数据的可复制、持久化,其保留时长由我们来进行设置。
也了解了Kafka的基础架构,Kafka的组成以及自个的作用和简单做了什么事情。对于Kafka大致有一个比较简单的了解,接着对于Kafka内部到底是做了什么。以及生产者消费者如何写入和消费数据的,如何在写入和消费的时候保证的数据一致性。解决节点失效以及内部的选举机制等等。
怕什么真理无穷,进一寸有一寸的欢喜。我是清风,希望这篇文章对你有帮助。如有不准确之处,还请评论区留言讨论。