概览
名词 | 解释 |
---|---|
Broker | 一个 Kafka 节点就是一个 Broker,一个或者多个 Broker 可以组成一个 Kafka 集群 |
Topic | Kafka 根据 Topic 对消息进行归类,发布到 Kafka 集群的消息都需要指定Topic |
Producer | 向 Broker 发送消息的客户端 |
Consumer | 从 Broker 读取消息的客户端 |
ConsumerGroup | 由多个 Consumer 组成的消费者组,一条消息可以被多个不同的 Consumer Group 消费,但是一个 Consumer Group 中只能有一个 Consumer 能够消费该消息 |
Partition | 物理上的概念,一个 Topic 可以分为多个 Partition,在 Partition 内部消息是有序的,每个 Topic 的每个 Parition 下都有一个Leader |
Follower | Partition 的副本,在 Leader 掉线后在 Follower 中选出新的 Leader |
Replica | 副本分区,包含 Leader 和 Follower |
作用
- 消息系统(分布式消息队列):具备系统解耦、冗余存储、流量削峰、缓冲、异步通信、扩展性等功能。与此同时,Kafka 还提供了大多数消息系统难以实现的消息顺序性保障及回溯消费的功能。
- 存储系统: Kafka 把消息持久化到磁盘,有效地降低了数据丢失的风险。得益于 Kafka 的消息持久化功能和多副本机制,可以把 Kafka 作为长期的数据存储系统来使用。
- 流式处理平台: Kafka 不仅为每个流行的流式处理框架提供了可靠的数据来源,还提供了一个完整的流式处理类库,比如窗口、连接、变换和聚合等各类操作。
副本同步机制
《深入理解Kafka:核心设计与实践原理》
每一条消息被发送到broker之前,会根据分区规则选择存储到哪个具体的分区。如果分区规则设定得合理,所有的消息都可以均匀地分配到不同的分区中。如果一个主题只对应一个文件,那么这个文件所在的机器I/O将会成为这个主题的性能瓶颈,而分区解决了这个问题。在创建主题的时候可以通过指定的参数来设置分区的个数,当然也可以在主题创建完成之后去修改分区的数量,通过增加分区的数量可以实现水平扩展。
Kafka为分区引入了多副本(Replica)机制,通过增加副本数量可以提升容灾能力。同一分区的不同副本中保存的是相同的消息(在同一时刻,副本之间并非完全一样),副本之间是“一主多从”的关系,其中 leader副本负责处理读写请求,follower 副本只负责与leader 副本的消息同步。副本处于不同的 broker 中,当leader 副本出现故障时,从follower 副本中重新选举新的 leader 副本对外提供服务。Kafka通过多副本机制实现了故障的自动转移,当Kafka 集群中某个broker 失效时仍然能保证服务可用。
注意:Producer 和 Consumer 都只会和 Leader 建立联系进行消息的发送和接收,Follower 主动向 Leader 拉取最新的消息进行同步。
Kafka消费端也具备一定的容灾能力。Consumer使用 Pull 模式从服务端拉取消息,并且保存消费的具体位置,当消费者宕机后恢复上线时可以根据之前保存的消费位置重新拉取需要的消息进行消费,这样就不会造成消息丢失。
分区中的所有副本统称为 AR(Assigned Replicas)。所有与 leader副本保持一定程度同步的副本(包括leader 副本在内)组成 ISR(In-Sync Replicas),ISR 集合是AR集合中的一个子集。消息会先发送到leader 副本,然后follower 副本才能从leader 副本中拉取消息进行同步,同步期间内follower 副本相对于leader 副本而言会有一定程度的滞后。前面所说的“一定程度的同步”是指可忍受的滞后范围,这个范围可以通过参数进行配置。与leader 副本同步滞后过多的副本(不包括leader 副本)组成 OSR(Out-of-Sync Replicas),由此可见,AR=ISR+OSR.在正常情况下,所有的follower 副本都应该与leader 副本保持一定程度的同步,即AR=ISR,OSR 集合为空。
leader 副本负责维护和跟踪ISR集合中所有 follower 副本的滞后状态,当follower副本落后太多或失效时,leader 副本会把它从ISR 集合中剔除。如果 OSR 集合中有follower 副本“追上”了 leader副本,那么 leader 副本会把它从OSR集合转移至ISR集合。默认情况下,当leader 副本发生故障时,只有在ISR集合中的副本才有资格被选举为新的 leader,而在OSR集合中的副本则没有任何机会(不过这个原则也可以通过修改相应的参数配置来改变)。
ISR与 HW 和 LEO 也有紧密的关系。HW是 High Watermark的缩写,俗称高水位,它标识了一个特定的消息偏移量(offset),消费者只能拉取到这个offset之前的消息。如下图所示,它代表一个日志文件,这个日志文件中有 9 条消息,第一条消息的 offset(LogStartOffset)为 0,最后一条消息的 offset 为 8,为 9 的消息用虚线框表示,代表下一条待写入的消息。日志文件的 HW 为 6,表示消费者只能拉取到 offset 在 0 至 5 之间的消息,而 offset 为6 的消息对消费者而言是不可见的。
LEO 是 Log End Offset 的缩写,它标识当前日志文件中下一条待写入消息的offset,下图中offset为9的位置即为当前日志文件的LEO,LEO 的大小相当于当前日志分区中最后一条消息的 offset 值加1。分区 ISR 集合中的每个副本都会维护自身的LEO,而ISR集合中最小的 LEO即为分区的HW,对消费者而言只能消费HW之前的消息。
为了让读者更好地理解ISR集合,以及HW和LEO 之间的关系,下面通过一个简单的示例来进行相关的说明。假设某个分区的ISR 集合中有3个副本,即一个leader副本和2个follower 副本,此时分区的LEO 和HW都为3。消息3和消息4从生产者发出之后会被先存入 leader 副本。
在消息写入 leader 副本之后,follower 副本会发送拉取请求来拉取消息3和消息4以进行消息同步。
在同步过程中,不同的follower 副本的同步效率也不尽相同。如下图所示,在某一时刻follower 完全跟上了 leader 副本而 follower2只同步了消息3,如此 leader 副本的LEO 为5,follower1 的LEO为5,follower2的LEO为4,那么当前分区的HW取最小值4,此时消费者可以消费到offset为0至3之间的消息。
所有的副本都成功写入了消息3和消息4后,整个分区的HW和 LEO 都变为5,因此消费者可以消费到offset为4的消息了。
消费
消费者在消费的过程中需要记录自己消费了多少数据,即位移(offset)。很多消息引擎都把这部分信息保存在服务器端,这种方案实现简单,但有三个问题:
- broker 有状态,会影响伸缩性;
- 需要引入应答机制(acknowledgement)来确认消费成功;
- 由于要保存很多 consumer 的 offset 信息,必然引入复杂的数据结构,同时造成资源浪费。
而Kafka选择了不同的实现方式:每个 consumer group 保存自己的位移信息,那么只需要简单的一个整数表示位置就够了;同时可以引入 checkpoint 机制定期持久化,简化了应答机制的实现。
但 kafka 的 Consumer 也需要向 Kafka 上报自己的位移数据信息,我们将这个上报过程叫做提交位移(Committing Offsets),它是为了保证 Consumer 的消费进度正常,即 Consumer 发生故障重启(或 rebalance)后, 可以直接从之前提交的 Offset 位置开始进行消费而不用重头再来一遍(Kafka 认为小于提交的 Offset 的消息都已经成功消费了)。我们知道 Consumer 可以同时去消费多个分区的数据,所以位移提交是按照分区的粒度进行上报的,也就是说 Consumer 需要为分配给它的每个分区提交各自的位移数据。
位移管理
Kafka 默认是定期帮你自动提交位移的(enable.auto.commit = true)。另外 kafka 会定期( auto.commit.interval.ms = 5000)把 group 消费情况保存起来,做成一个 offset map。
在设置了 enable.auto.commit = true 时,Kafka 会保证在开始调用 Poll() 方法前,提交上一批消息的位移,再处理下一批消息, 因此它能保证不出现消费丢失的情况。但自动提交位移也有设计缺陷,那就是它可能会出现重复消费。就是在自动提交间隔之间发生 Rebalance 的时候,此时 Offset 还未提交,待 Rebalance 完成后, 所有 Consumer 需要将发生 Rebalance 前的消息进行重新消费一次。
下图中表明了 test-group 这个组当前的消费情况。
老版本的位移是提交到 zookeeper 中的,目录结构是:/consumers/<group.id>/offsets//,但zookeeper 其实并不适合进行大批量的读写操作,尤其是写操作。因此 kafka 提供了另一种解决方案:增加 __consumer_offsets topic,将 offset 信息写入这个 topic,摆脱对 zookeeper 的依赖(指保存offset这件事情)。 __consumer_offsets 中的消息保存了每个 consumer group 某一时刻提交的 offset 信息。以上图中的 consumer group为例,格式大概如下:
__consumers_offsets topic 配置了compact 策略,使得它总能保存最新的位移信息,既控制了该 topic 的日志总量,也能实现保存最新 offset 的目的。
特性
顺序性
kafka 会保证每个 partition 的内部的有序性,但一个 topic 有多个分区,那么 kafka 无法保证整个 topic 下的所有消息的有序性。
但即使消息处于同一个分区,如果消息 1 处理失败进行重试,就会出现在消息 2 之后,这个时候就可能出现顺序性失效的情况。解决的方案是设置 max.in.flight.requests.per.connection
参数。
该参数指定了生产者在收到服务器晌应之前可以发送多少个消息。它的值越高,就会占用越多的内存,不过也会提升吞吐量。把它设为 1 可以保证消息是按照发送的顺序写入服务器的,即使发生了重试。
可靠性
一致性
不丢数据
生产环节
生产者向 broker 发送的消息可能因网络错误而丢失,所以当没收到 broker 的确认(超时情况)或收到 broker 的失败确认时,生产者需要对消息进行发送重试。一般若不是 MQ 故障或到 MQ 的网络断开了,重试2~3次即可。但这种方案可能造成消息重复,从而在消费时重复消费同样的消息。
存储环节
Kafka 将消息存储本地磁盘,为减少消息存储时磁盘随机 I/O 对性能产生影响,一般会将消息先写到 OS 的 PageCache,然后再找合适时机刷盘。比如 Kafka 可以配置异步刷盘时机:
- 当达到某一时间间隔
- 累积一定消息数量
不过如果发生掉电或异常重启,Page Cache中还没有来得及刷盘的消息就会丢失了。那么怎么解决呢?你可能会
3. 把刷盘的间隔设置很短
4. 设置累积一条消息就刷盘
但频繁刷盘很影响性能,而且宕机或掉电几率也不高,所以不推荐这种做法。如果你的系统对消息丢失容忍度很低,可考虑集群部署 Kafka,通过部署多个副本备份数据,保证消息尽量不丢失。在 Leader 副本发生掉电或者宕机时,Kafka 会从 Follower 副本中消费消息,减少消息丢失的可能。由于默认 Follower 副本的是异步地从 Leader 复制的,所以一旦 Leader 宕机,那些还没有来得及复制到 Follower 的消息还是会丢失。为解决这个问题,Kafka 为生产者提供了 acks 参数。
- acks = 1:生产者需要接收到分区的 leader 副本响应后才能继续发送数据。
- acks = 0:生产者发送消息之后不需要等待任何服务端的响应。
- acks = -1 或 acks = all:生产者在消息发送之后,需要等待 ISR 中的所有副本都成功写入消息之后才能够收到来自服务端的成功响应。在其他配置环境相同的情况下,acks 设置为 -1 (all )可以达到最强的可靠性。但这并不意味着消息就一定可靠,因为 ISR 中可能只有 leader 副本,这样就退化成了 acks= 1 的情况。要获得更高的消息可靠性需要配合 min.insync.replicas (分区 ISR 集合中至少要有多少个副本,默认为 1)等参数的联动。
当 acks = all 时,生产者发送的每一条消息除了发给 Leader 副本外还会发给所有的 ISR 副本,并且必须得到 Leader 和所有 ISR 的确认后才被认为发送成功。这样只有 Leader 和所有的 ISR 都挂了消息才会丢失。
消费环节
一个消费者消费消息的进度是记录在消息队列集群中的,消费的过程可分为三步:
- 接收消息
- 处理消息
- 更新消费进度
这里面接收消息和处理消息的过程都可能会发生异常或者失败,
- 比如消息接收时网络发生抖动,导致消息并没有被正确的接收到
- 处理消息时可能发生一些业务的异常导致处理流程未执行完成
这时如果更新消费进度,这条失败的消息就永远不会被处理了,也可以认为消息丢失了。所以,在这里你需要注意的是,一定要等到消息接收和处理完成后才能更新消费进度,但是这也会造成消息重复的问题,所以也需要消费者做好写数据的幂等性。
存储结构
Kafka 把具体消息存储在.log文件中,即便使用了零拷贝,生产–>消费也需要经历两次磁盘IO,如果消息体过大势必会影响消费的速度,造成消息积压,因此在使用中要尽可能压缩消息的大小。
思考
基本概念
rebalance
Rebalance 本质上是一种协议,规定了一个 Consumer Group 下的所有 consumer 如何达成一致,来分配订阅的 Topic 下的分区。例如:某 Group 下有 20 个 consumer 实例,它订阅了一个具有 100 个 partition 的 Topic 。正常情况下,kafka 会为每个 Consumer 平均的分配 5 个分区,该分配过程就是 Rebalance。
Rebalance 的触发条件有3个。
组成员个数发生变化。例如有新的 consumer 实例加入该消费组或者离开组。
- 新成员加入组:
- 组成员“崩溃”(consumer 并不一定真的下线或不可用了。实例长时间 GC、网络延迟等使 coordinator 长时间无法收到该 consumer 的心跳,也会被 coordinator 认定为崩溃):组成员崩溃和组成员主动离开是两个不同的场景。因为在崩溃的成员并不会主动地告知coordinator此事,coordinator有可能需要一个完整的session.timeout周期(心跳周期)才能检测到这种崩溃,这必然会造成consumer的滞后。可以说离开组是主动地发起rebalance;而崩溃则是被动地发起rebalance。
- 组成员离开组:
订阅的 Topic 个数发生变化。
订阅 Topic 的分区数发生变化。
Rebalance 发生时,Group 下所有 consumer 实例回共同参与协调,kafka 会尽量保证分配时公平的。在 Rebalance 过程中 consumer group 下的所有消费者实例都会停止工作,直到 Rebalance 过程完成。
触发
- 有新的 Consumer 加入 Consumer Group
- 有 Consumer 宕机下线。Consumer 并不一定需要真正下线,例如遇到长时间的 GC、网络延迟导致消费者长时间未向 GroupCoordinator 发送 HeartbeatRequest 时,GroupCoordinator 会认为 Consumer 下线。
- 有 Consumer 主动退出 Consumer Group(发送 LeaveGroupRequest 请求)。比如客户端调用了 unsubscribe() 方法取消对某些主题的订阅。
- Consumer 消费超时,没有在指定时间(max.poll.interval.ms,默认为 5min)内提交 offset 偏移量。
- Consumer Group 所对应的 GroupCoordinator 节点发生了变更。
- Consumer Group 所订阅的任一主题或者主题的分区数量发生变化。
问题
- 消费暂停:Coordinator 协调者组件完成 topic 分区的分配的过程中,该消费组下所有消费实例都不能消费任何消息。
- 重复消费:Consumer 2 在消费分配给自己的 2 个队列时,必须接着从Consumer 1 之前已经消费到的offset继续开始消费。然而默认情况下,offset 是异步提交的,如 Consumer 1 当前消费到 offset 为 10,但是异步提交给 broker 的 offset 为 8;那么如果Consumer 2 从 8 的 offset 开始消费,那么就会有 2 条消息重复。也就是说,Consumer 2 并不会等待Consumer 1 提交完 offset 后,再进行 Rebalance,因此提交间隔越长,可能造成的重复消费就越多。
文章索引
Kafka 丢消息的情况
- 发送消息过程。kafka 使用 ACK 机制确保发送数据的可靠
- 0:代表 producer 往集群发送数据不需要等到集群的返回,不确保消息发送成功。安全性最低但是效率最高。
- 1:代表 producer 往集群发送数据只要分区 leader 应答就可以发送下一条,只确保分区 leader 发送成功。
- all:代表 producer 往集群发送数据需要所有的分区 follower 都完成从分区 leader 的同步才会发送下一条,确保分区 leader 发送成功和所有的副本都完成备份。安全性最高,但是效率最低。
所以这里可能是会丢消息的。
- 0:producer 发送的消息根本就没有到 broker 就发一下一条数据,没有重复发送该条数据,造成数据丢失。
- 1:保证分区 leader 不丢,但如果分区 leader 挂了,恰好选了一个没有 ACK 的 follower,那该条数据也丢了。
- all:保证 leader 和 follower 不丢,但是如果网络拥塞,没有收到 ACK,存在重复发的问题。
- 保存数据过程
kafka 将日志数据写入到文件的过程为:
在写入磁盘文件的时候,可以直接写入这个 OS Cache 里,也就是仅仅写入内存中,由操作系统自己决定什么时候把 OS Cache 里的数据真的刷入磁盘文件中。Kafka 提供了一个参数(producer.type)来控制是不是主动将 page cache 中的数据 flush 到磁盘。已经写入了OS cache 但还没来得及刷入磁盘的数据,此时遇到机器宕机或者 broker 重启,那这条数据就丢了。
- 消费数据阶段
消费消息的时候可以大致分为两个阶段:- 标示该消息已被消费(commit记录一下);
- 处理该条消息。
如果消息已经被 commit,但在处理消息时,消费该条消息的 consumer 发生异常,这条消息就丢了。
- kafka 默认使用自动提交的方式对消费位移进行提交,即在 kafka 拉取到数据之后就直接提交(假设 auto.commit.interval.ms = 0)。