Kafka 原理初探

概览

kafka是一款优秀的流计算中间件系统。本篇文档只对其作为消息系统时做个介绍和分析。

kafka作为一款高性能,高吞吐量,高可用的消息中间件,受到了很多互联网公司以及开发者的喜爱,接下来就对其做个介绍和分析。

kafka 从最初到现在,中间经历个几个大版本的更新, 本篇文档不对其发展历程做过多的介绍,在接下来的文档介绍部分细节时会简单提到kafka版本之间的差别。

本篇文档主要是对kafka较深入的一个初步剖析,所以像topic, partition,consumer group等这些基础概念不做介绍,读者可前去官网或查找网上资源学习。

 

应用架构

kafka应用架构如下(本人理解)

 

左边一列: 是对中间部分的每一层做个简单的描述。

中间一列:是kafka broker的系统结构。

右边一列:为kafka依赖的外部系统。

 

kafka模块分析

 

Kafka 网络模型

Kafka做为一款高性能,高可用,高吞吐的优秀消息中间件系统。备受开发者和互联网公司的青睐。

消息中间件系统多为常量连接海量请求的模式,因此传统的BIO 是不适合的, 因此kafka 使用的NIO, 即大名鼎鼎的 Reactor模型。

Kafka的网络模型,涉及上图中API之上的组件:

 

Acceptor

监听Broker 端口, 接收新连接的建立,并将连接分发给 Processor。

Acceptor 把连接分发给Processor的算法其实很简单,使用的是轮询算法:

Acceptor轮询分发连接给 Processor , 当每一个 Processor都无法再接收新连接的时候(未超过连接总数限制的前提下), Acceptor会阻塞到轮询到最后一个Processor上。

RequestChannel

Processor之间所共享,其维护了Request队列,以及Processor的实例

Processor

每一个Processor都是一个单独的线程, Processor 用来处理网络IO,不涉及业务逻辑。

Acceptor根据轮训算法将新连接选择一个Processor,并交由其管理。

Processor 接收到网络请求,读取并解析为Request,放进RequestChannel中的Request请求队列中。

一个Processor可以处理多个连接,并且其维护了一个Response的队列, 所有Processor实例的网络IO均由同一个实例处理。

KafkaHandlerPool

业务处理线程池,从RequestChannel中的请求队列里获取请求并加以处理,处理之后的响应Response 会放进与Request所对应的Processor中的响应队列中

kafka 功能模块

下面对每一个上图中提到的模块做一个介绍,因每一个模块都是比较复杂的,且模块之间有的也存在着相互协调和依赖的关系,因此这里列出的并不是所有的,包括上图中以及下面的文档中,只是挑了一些我认为主要的东西来写的。

LogManager

日志管理, 负责处理 日志(消息) 的 创建, 检索,清理。 日志的读写操作被委托给了单个日志实例对象(可以简单的认为文件,kafka中有对应的对象)

ReplicaManager

副本管理,依赖 LogManager。

AdminManager

一些运维操作,比如创建主题,删除主题等

KafkaController

KafkaController组件,负责kafka集群的管理和协调。

GroupCoordinator

组协调器,负责处理消费者的join_group,syn_group, leave_group,会话,心跳,消费位移等请求。 其主要功能就是处理消费者组的,像消费者组成员变化,分区rebalance(分区重分配)等。

TransactionCoordinator

事务协调器,这个和kafka提供的事务消息有关(本篇不做详细介绍,未做过多的了解)

QuotaManagers

配额管理,诸如像 限流,网络带宽等

kafka 功能点分析

这里重点分析GroupCoordinator分析的深度和准确度可能不尽如意,内容仅为参考。

GroupCoordinator 组协调器

group coordinator 在每个kafka broker 中都有启动对应的组件,其主要和consumer 的消费处理有关。因此在展开介绍之前,先说一下consumer的消费机制。

读者应当了解 consumer group 的概念,一个consumer group 下可以有多个consumer 实例, 我们都知道 topic 是消息日志的逻辑容器,topic 下还有分区的概念,consumer 实例是按照分区进行消费的,之所以这样的设计,能够带来的更多的灵活性,且kafka的伸缩性(扩容/缩容)更好。

在早期consumer group 的消费位移存储在zookeeper中,但由于zookeeper的写性能不高,限制了consumer的TPS, 因此kafka 后来将消费位移的存储变更为 以kafka 内部主题 __consumer_offset的方式进行管理。

 

现在开始介绍group coordinator,consumer 实例如何确定那个broker 成为其所在组的coordinator呢? 这就和上面提到的 __consumer_offset 有关了, __consumer_offset 从表面上看和普通的topic 没什么主题, 其也有分区数,副本数 配置项。 __consumer_offset 的分区数默认是50个, 确定group coordinator 分两步:

1.确定consumer goup 的位移数据存放在 __consumer_offset的哪个分区

2.确定__consumer_offset 的分区后,其分区leader副本所在的broker 便是group coordinator

 

确定分区的算法是:

Math.abs(groupId.hashCode) % partitions

(partitions = __consumer_offset的分区数) 

 

下图为consuemr 实例和 kafka broker或group coordinator的交互:

 

 

上图 看起来比较直观

【1】确定group coordinator 为哪个broker

【2】加入分组(group leader 负责执行分区分配方案)

【3】同步分区方案

【4】获取消费位移

【5】拉取消息

(提交位移未体现,这里不做介绍)

 

consumer group 状态

 

consumer group 有一下几种状态:

Empty

组内没有任何成员,但消费者组可能存在已提交的位移数据,而且这些位移尚未过期

PreparingRebalance

消费者组准备开启rebalance,此时所有成员都要重新请求加入消费者组

CompletingRebalance

消费者组所有成员已加入,各个成员正在等待分配方案。

Stable

消费者组稳定状态,组内成员可以正常消费数据

Dead

组内没有任何成员,组元数据信息被coordinator删除

状态流转如下图所示:

 

 

rebalance

 

下面介绍一下 rebalance, 什么是 rebalance? 简单的说就是 分区分配,就是consumer group 下的consumer 实例 各自负责哪些分区

什么时候会进行rebalance?

在一下场景会触发rebalance:

【1】group成员变更

【2】group订阅topic数变更

【3】group订阅topic分区数变更

 

kafka rebalance存在什么样的问题呢?

当rebalance 触发时,consumer group 内的所有成员都会参与, 也就是说 consumer group内的consumer 实例会停止消费,和JVM中GC stop the world 类似。当consumer group 下成员很多的话,这个rebalance过程可能会持续很久,网上看到一个国外公司的列子是 group 下有上百个成员,一次rebalance持续了几个小时。

目前kafka似乎也没有什么很好的方案避免rebalance的问题,因此我们实际使用中应当尽量避免发生rebalance, 那就要在触发rebalance触发的条件的上找突破了

上面提到的触发rebalance 2,3一般是计划内的rebalance,我们所能做的避免rebalance只能在成员变更上,成员变更有一下两种,其中第二种是我们所关注的:

1.consumer 实例增加, 这种是正常的的rebalance

2.consumer 实例离开group或group coordinator 将consumer实例踢出组

 

consumer 实例主动离开group:

consuemr端有个配置参数 max.poll.interval.ms 表示两次调用poll方法的间隔,如果超过配置的值,那么consumer 实例将向group coordinator 发起 leave group 的请求,会触发 rebalance。

因此,max.poll.interval.ms 要配置成合理的值,这和consumer的处理能力是有关系。

 

consumer 实例被 group coordinator 踢出group:

group coordinator 如何判定consumer 实例应当被踢出呢? consumer 实例定期会向 group coordinator发送心跳(频率由 heartbeat.interval.ms 确定), consumer 端还有一个配置 session.timeout.ms 会话超时时间,如果在 session.timeout.ms 内没有收到consumer端的心跳,那么coordinator 便认为 consumer 实例下线, 会触发rebalance。因此在配置 session.timeout.ms,heartbeat.interval.ms时应当保证 在 session.timeout.ms 内至少要能够发送多次心跳, 比如 session.timeout.ms = 6000, heartbeat.interval.ms=2000。

group.instance.id

在介绍一下group.instance.id,这是个consuemr 端配置参数,表示 consumer是实例的id, 在consumer group  内具有唯一性, 配置此值,group coordinator 便会把此consumer 实例当成静态成员。 那有什么好处呢?

上面介绍rebalance 时,成员变更会触发 rebalance, 其实还有一种情况,就是consumer实例重启,短暂的与group coordinator 断开,此时也会触发 rebalance, 但实际上这种情况可以不用触发 rebalance的,那为什么group.instance.id可以避免这种情况的 rebalance呢? 下面就介绍一下。

consumer 实例和 group coordinator 交互, coordiator 会给每一个consumer 实例分配一个memberId, 生成memberId的算法:

client.id + "-" + UUID           (1)

group.instance.id + "-" + UUID      (2)

当配置group.instance.id 时memberId 按照(2)的方式生成(client.id 为consumer 发起join_group 请求的参数)。

group coordinator 并不持久化 memberId, 当consumer端重启时,发起加入组请求时,请求参数中 member.id = UNKNOWN_MEMBER_ID, coordinator 在处理时,如果未配置 group.instance.id, 那么会把此请求当成是一个新成员的加入,如果配置了, 则会当成是静态成员,为其生成新的 member.id, 将group.instance.id对应的旧的member.id 替换为新的。 当然这一切的前提是 consumer端配置的 session.timeou.ms 要比较大,消费者重启往往是需要一两分钟的时间。

group coordinator 就讲到这里,未尽事宜,请读者自行了解。

 

Log Cleaner

由于消费者的消费位移是存储在kafka __consumer_offset 内部主题中,其存储的数据格式我们可以抽象为是KV,其中key = <group.id,topic,partition>, v= 消费位移, 那么就需要处理 过期的位移,过期位移的清理是由一个单独的线程 LogCleaner线程进行的,如果在生产环境出现,消费位移占用的磁盘空间高居不下,那么就可以查看 LogCleaner 线程是否挂掉。 除此之外kafka 对 __consumer_offset 还提供了整理的功能,称为 compact,举个例子: 比如 

 

key1

key2

key1

key2

v1

v3

v5

v6

 

经compact 后:

 

key1

key2

v5

v6

 

 

分区和副本(Replica)

下面的文字描述中,有些是直接来自kafka官方的设计文档中的翻译文本。

概述

分布式系统存储的高可用,通常使用的手段就是数据冗余,在kafka里也是如此,称其为副本, ReplicaManager 就是来做这件事的。不过遗憾的是,kafka中的副本只是作为数据冗余而已,未能提高读的性能, 不过kafka中分区机制已经达到topic级别的负载均衡。

根据kafka副本机制的定义,同一个分区下的所有副本保存有相同的消息序列,这些副本分散保存在不同的broker上,从而能够对抗部分broker故障带来的数据不可用。

在生产环境中,没台broker 都可能保存有各个主题下不同分区的不同副本。

副本角色

既然分区能够提供配置多个副本,而且这些副本还要内容一致,那么很自然的一个问题就是,如何确保副本中所有的数据都是一致的呢? 我们可能想到的是master-slave或leader-follower机制, kafka 就是采用这样的机制。基于leader的副本机制。

 

 

【1】在kafka中,副本角色分成两类: leader副本和follower副本。每个分区在创建时都要选举一个副本作为leader,其余副本自动成为follower。follower副本就像普通的kafka consumer一样,消费leader副本的消息。

【2】在kafka中 follower副本是不对外提供服务的,也就是说任何一个follower副本都不能响应消费者或生产者的读写请求,所有的请求都要由leader副本来处理,活着说所有的读写请求都要发往leader副本所在的broker,由该broker处理。follower副本唯一的任务就是从leader副本异步拉取消息,并写入自己的提交日志中,从而实现与leader的同步。

【3】当leader副本挂掉或leader副本所在broker宕机时,kafka依托于 zookeeper提供的监控机制能够实时感知到,并立即开启新一轮的leader选举,从follower副本中选一个作为leader,老的leader 副本恢复后,只能作为新leader的follower加入到集群中。

 

第【2】点就说明了,为什么kafka的副本不能扩展读的性能,这样设计的好处就是,被提交的消息日志能够被消费者立即读取到,如果副本可对外提供服务,那么由于副本同步是异步拉取的,总是会出现延迟的可能的,那么就可能生产者提交的一个消息,在一个时间内,消费者读取不到。

 

ISR (In Sync Replicas)

 

上面提到,follower副本和leader副本之间的数据同步是follwer节点异步拉取的,那么就存在着follower不可能和leader 保持实时同步的风险,所以应当先定义,follower和leader 同步的含义,即同步的语义是什么,或者说follwer副本在什么条件下才算与leader同步

 

基于这个想法,kafka引用了 In-Sync-Replicas, 也就是 ISR 同步副本集合。 ISR中的副本都是与leader保持同步的副本,相反,不在ISR中的副本就被认为是与leader不同步,那么什么副本能够进入ISR呢?

 

首先要明确,leader副本天然的就在ISR中,也就是说,ISR不只是follower副本的集合, 它必然包括leader副本。甚至在某些情况下,ISR中只有leader 这一个副本。

 

与大多数分布式系统一样,自动处理故障需要对节点“处于活动状态”的含义进行精确定义。 对于Kafka节点,活跃度有两个条件:

【1】节点必须能够与ZooKeeper保持会话(通过ZooKeeper的心跳机制)

【2】如果是追随者,则必须复制在领导者上发生的写入,并且不要落后太远

我们将满足这两个条件的节点称为“同步”,以避免模糊“有效”或“失败”。 领导者跟踪“同步”节点的集合。 如果追随者死亡,卡住或掉队,则领导者会将其从同步副本列表中删除。 卡住和滞后的副本的确定是由replica.lag.time.max.ms 配置控制的。

 

ISR 是动态改变的,也就是说当某个follower 落后于leader的时间超过 replica.lag.time.max.ms 参数值,则该follower副本会被踢出 ISR列表, 如果后面该follower副本慢慢追上来,则会被重新加入到ISR中。

 

kafka通过维护动态的ISR集合, 当leader副本挂掉后,直接可以从ISR选择一个follower来作为新的leader,因此使用ISR模型(f+1)个副本,kakfa主题可以容忍 f 个副本故障而不会丢失已提交的消息。

 

Unclean leader election

请注意,Kafka关于数据丢失的保证是基于至少一个保持同步的副本。如果所有复制分区的节点都死了,则此保证不再成立。但是,当所有副本都故障时,实际系统需要做一些合理的事情。如果您很不幸发生这种情况,请务必考虑将要发生的事情。可以实现两种行为:

【1】等待ISR中的副本复活,然后选择该副本作为领导者(希望它仍然拥有所有数据)。

【2】选择第一个作为leader复活的副本(不一定在ISR中)。

 

这是可用性和一致性之间的简单权衡。如果我们在ISR中等待副本,则只要这些副本已关闭,我们将保持不可用状态。如果此类副本被破坏或它们的数据丢失,那么我们将永远处于瘫痪状态。另一方面,如果非同步副本复活并且我们允许它成为领导者,那么即使不能保证它包含所有已提交的消息,其日志也将成为事实来源。从0.11.0.0版本开始,默认情况下,Kafka选择第一个策略,并倾向于等待一致的副本。可以使用配置属性unclean.leader.election.enable更改此行为,以支持正常运行时间比一致性更好的场景。

 

这种困境并非仅针对卡夫卡。它存在于任何基于仲裁的方案中。例如,在多数表决方案中,如果大多数服务器遭受永久性故障,那么您必须选择丢失100%的数据,或者通过将现有服务器上剩余的数据作为新的事实来源来破坏一致性。

 

可用性和持久性

当写入Kafka时,生产者可以选择是等待消息被0,1还是全部(-1)副本确认。请注意,“所有副本的确认”不能保证已分配的副本的全部集合都已收到该消息。默认情况下,当acks = all时,所有当前的同步副本都收到消息后立即进行确认。例如,如果一个主题仅配置了两个副本,而一个失败(即仅保留一个同步副本),则指定acks = all的写入将成功。但是,如果其余副本也失败,则这些写操作可能会丢失。尽管这确保了分区的最大可用性,但是对于某些倾向于持久性而不是可用性的用户而言,此行为可能是不希望的。因此,我们提供了两个主题级别的配置,可用于优先考虑消息的持久性而不是可用性:

 

禁用unclean.leader.election.enable, 如果所有副本都变得不可用,则该分区将保持不可用,直到最新的领导者再次变得可用为止。与消息丢失的风险相比,这实际上更倾向于不可用性。

 

指定最小ISR大小-如果ISR的大小大于某个最小值,分区将仅接受写入,以防止丢失仅写入单个副本的消息,该消息随后将变得不可用。仅当生产者使用acks = all并保证至少有这么多同步副本确认该消息时,此设置才生效。此设置在一致性和可用性之间进行权衡。最小ISR大小的较高设置可确保更好的一致性,因为可以确保将消息写入更多副本,从而降低了丢失消息的可能性。但是,这会降低可用性,因为如果同步副本的数量下降到最小阈值以下,则分区将无法进行写操作

 

分区Leader选举

 

上面关于副本的讨论实际上仅涵盖了一个副本,即一个主题分区。但是,Kafka群集将管理成百上千个这样的分区。我们尝试以循环方式平衡群集中的分区,以避免在少数节点上将所有分区聚集在一起,以应对大量主题。同样,我们尝试平衡leader,以使每个节点都是按比例分配其分区的领导者。

 

优化leader选举过程也很重要,因为这是不可用的关键窗口。最简单的领导者选举实现将最终在该节点发生故障时为该节点托管的所有分区的每个分区运行一个选举。相反,我们选择一个Broker作为“controller”。该控制器在broker级别检测到故障,并负责更改故障broker中所有受影响分区的领导者。结果是,我们能够将许多必需的leader变更通知汇总在一起,从而使大量分区的选举过程便宜又快捷。如果controller发生故障,幸存的broker中的之一将成为新的controller。 这就是上面提到的KafkaController组件。

 

写在最后

 

本篇文档只介绍了kafka的架构,网络模型,模块组成,并对组协调器,副本做了一翻简单的剖析。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值