Kafka-核心概念

消息

消息是Kafka中最基本的数据单元。

消息由一串字节构成,其中主要由key和value构成,key和value也都是byte数组。

key的主要作用是根据一定的策略,将此消息路由到指定的分区中,这样就可以保证包含同一key的消息全部写入同一分区中,key可以是null。

消息的真正有效负载是value部分的数据。

为了提高网络和存储的利用率,生产者会批量发送消息到Kafka,并在发送之前对消息进行压缩。

Topic&分区&Log

Topic是用于存储消息的逻辑概念,可以看作一个消息集合。

每个Topic可以有多个生产者向其中推送(push)消息,也可以有任意多个消费者消费其中的消息。

每个Topic可以划分成多个分区(每个Topic都至少有一个分区),同一Topic下的不同分区包含的消息是不同的。

每个消息在被添加到分区时,都会被分配一个offset,它是消息在此分区中的唯一编号,Kafka通过offset保证消息在分区内的顺序,offset的顺序性不跨分区,即Kafka只保证在同一个分区内的消息是有序的;

同一Topic的多个分区内的消息,Kafka并不保证其顺序性。

同一Topic的不同分区会分配在不同的Broker(Broker的概念见下文)上。

分区是Kafka水平扩展性的基础,我们可以通过增加服务器并在其上分配Partition的方式来增加Kafka的并行处理能力。

分区在逻辑上对应着一个Log,当生产者将消息写入分区时,实际上是写入到了分区对应的Log中。

Log是一个逻辑概念,可以对应到磁盘上的一个文件夹。

Log由多个Segment组成,每个Segment对应一个日志文件和索引文件。

在面对海量数据时,为避免出现超大文件,每个日志文件的大小是有限制的,当超出限制后则会创建新的Segment,继续对外提供服务。

这里要注意,因为Kafka采用顺序VO,所以只向最新的Segment追加数据。

为了权衡文件大小、索引速度、占用内存大小等多方面因素,索引文件采用稀疏索引的方式,大小并不会很大,在运行时会将其内容映射到内存,提高索引速度。

保留策略(Retention Policy)&日志压缩(Log Compaction)

无论消费者是否已经消费了消息,Kafka都会一直保存这些消息,但并不会像数据库那样长期保存。

为了避免磁盘被占满,Kafka会配置相应的"保留策略"(retentionpolicy),以实现周期性地删除陈旧的消息。

Kafka中有两种"保留策略":

一种是根据消息保留的时间,当消息在Kafka中保存的时间超过了指定时间,就可以被删除;

另一种是根据Topic存储的数据大小,当Topic所占的日志文件大小大于一个阈值,则可以开始删除最旧的消息。

Kafka会启动一个后台线程,定期检查是否存在可以删除的消息。

"保留策略"的配置是非常灵活的,可以有全局的配置,也可以针对Topic进行配置覆盖全局配置。

除此之外,Kafka还会进行"日志压缩"(Log Compaction)。

在很多场景中,消息的key与value的值之间的对应关系是不断变化的,就像数据库中的数据会不断被修改一样,消费者只关心key对应的最新value值。

此时,可以开启Kafka的日志压缩功能,Kafka会在后台启动一个线程,定期将相同key的消息进行合并,只保留最新的value值。

日志压缩的工作原理如图所示,图展示了一次日志压缩过程的简化版本,为了图片清晰,只展示了key3的压缩过程。

Broker

一个单独的Kafka server就是一个Broker。

Broker的主要工作就是接收生产者发过来的消息,分配offset,之后保存到磁盘中;

同时,接收消费者、其他Broker的请求,根据请求类型进行相应处理并返回响应。

在一般的生产环境中,一个Broker独占一台物理服务器。

副本

Kafka对消息进行了冗余备份,每个Partition可以有多个副本,每个副本中包含的消息是一样的(在同一时刻,副本之间其实并不是完全一样的,本书后面在介绍副本机制的时候会再进行说明)。

每个分区至少有一个副本,当分区中只有一个副本时,就只有Leader副本,没有Follower副本。

每个分区的副本集合中,都会选举出一个副本作为Leader副本,Kafka在不同的场景下会采用不同的选举策略。

所有的读写请求都由选举出的Leader副本处理,其他都作为Follower副本,Follower副本仅仅是从Leader副本处把数据拉取到本地之后,同步更新到自己的Log中。下图展示了一个拥有三个Replica的Partition。

一般情况下,同一分区的多个分区会被分配到不同的Broker上,这样,当Leader所在的Broker宕机之后,可以重新选举新的Leader,继续对外提供服务。

ISR集合

ISR(In-Sync Replica)集合表示的是目前"可用"(alive)且消息量与Leader相差不多的副本集合,这是整个副本集合的一个子集。"可用"和"相差不多"都是很模糊的描述,其实际含义是ISR集合中的副本必须满足下面两个条件:

  1. 副本所在节点必须维持着与ZooKeeper的连接。
  2. 副本最后一条消息的offset与Leader副本的最后一条消息的offset之间的差值不能超出指定的阈值。

每个分区中的Leader副本都会维护此分区的ISR集合。

写请求首先由Leader副本处理,之后Follower副本会从Leader上拉取写入的消息,这个过程会有一定的延迟,导致Follower副本中保存的消息略少于Leader副本,只要未超出阈值都是可以容忍的。

如果一个Follower副本出现异常,比如:宕机,发生长时间GC而导致Kafka僵死或是网络断开连接导致长时间没有拉取消息进行同步,就会违反上面的两个条件,从而被Leader副本踢出ISR集合。

当Follower副本从异常中恢复之后,会继续与Leader副本进行同步,当Follower副本"追上"(即最后一条消息的offset的差值小于指定阈值)Leader副本的时候,此Follower副本会被Leader副本重新加入到ISR中。

HW&LEO

HW(HighWatermark)和LEO与上面的ISR集合紧密相关。HW标记了一个特殊的offset,当消费者处理消息的时候,只能拉取到HW之前的消息,HW之后的消息对消费者来说是不可见的。

与ISR集合类似,HW也是由Leader副本管理的。

当ISR集合中全部的Follower副本都拉取HW指定消息进行同步后,Leader副本会递增HW的值。

Kafka官方网站将HW之前的消息的状态称为"commit",其含义是这些消息在多个副本中同时存在,即使此时Leader副本损坏,也不会出现数据丢失。

下面通过一个示例进行分析,图中展示了针对offset为11的消息,ISR集合、HW与LEO是如何协调工作的:

  1. Producer向此Partition推送消息。
  2. Leader副本将消息追加到Log中,并递增其LEO。
  3. Follower副本从Leader副本拉取消息进行同步。
  4. Follower副本将拉取到的消息更新到本地Log中,并递增其LEO。
  5. 当ISR集合中所有副本都完成了对offset=11的消息的同步,Leader副本会递增HW。
    在1~5步完成之后,offset=11的消息就对生产者可见了。

为什么Kafka要这么设计?在分布式存储中,冗余备份是常见的一种设计,常用的方案有同步复制和异步复制:

  • 同步复制要求所有能工作的Follower副本都复制完,这条消息才会被认为提交成功。一旦有一个Follower副本出现故障,就会导致HW无法完成递增,消息就无法提交,生产者获取不到消息。这种情况下,故障的Follower副本会拖慢整个系统的性能,甚至导致整个系统不可用。
  • 异步复制中,Leader副本收到生产者推送的消息后,就认为此消息提交成功。Follower副本则异步地从Leader副本同步消息。这种设计虽然避免了同步复制的问题,但同样也存在一定的风险。现在假设所有Follower副本的同步速度都比较慢,它们保存的消息量都远远落后于Leader副本,如图所示。

    此时Leader副本所在的Broker突然宕机,则会重新选举新的Leader副本,而新Leader副本中没有原来Leader副本的消息,这就出现了消息的丢失,而有些消费者则可能消费了这些丢失的消息,状态变得不可控。

Kafka权衡了同步复制和异步复制两种策略,通过引入了ISR集合,巧妙地解决了上面两种方案存在的缺陷:

当Follower副本的延迟过高时,Leader副本被踢出ISR集合,消息依然可以快速提交,生产者可以快速得到响应,避免高延时的Follower副本影响整个Kafka集群的性能。

当Leader副本所在的Broker突然宕机的时候,会优先将ISR集合中Follower副本选举为Leader副本,新Leader副本中包含了HW之前的全部消息,这就避免了消息的丢失。

值得注意是,Follower副本可以批量地从Leader副本复制消息,这就加快了网络I/O,Follower副本在更新消息时是批量写磁盘,加速了磁盘的I/O,极大减少了Follower与Leader的差距。

Cluster&Controller

多个Broker可以做成一个Cluster(集群)对外提供服务,每个Cluster当中会选举出一个Broker来担任Controller,Controller是Kafka集群的指挥中心,而其他Broker则听从Controller指挥实现相应的功能。

Controller负责管理分区的状状态、管理每个分区的副本状态、监听Zookeeper中数据的变化等工作。

Controller也是一主多从的实现,所有Broker都会监听Controller Leader的状态,当Leader Controller出现故障时则重新选举新的Controller Leader。

生产者

生产者(Producer)的主要工作是生产消息,并将消息按照一定的规则推送到Topic的分区中。这里选择分区的"规则"可以有很多种,例如:根据消息的key的Hash值选择分区,或按序轮训全部分区的方式。

消费者

消费者(Consumer)的主要工作是从Topic中拉取消息,并对消息进行消费。

某个消费者消费到Partition的哪个位置(offset)的相关信息,是Consumer自己维护的。在图中,三个消费者同时消费同一个分区,各自管理自己的消费位置。

Consumer Group

在Kafka中,多个Consumer可以组成一个Consumer Group,一个Consumer只能属于一个Consumer Group。

Consumer Group保证其订阅的Topic的每个分区只被分配给此Consumer Group中的一个消费者处理。

如果不同Consumer Group订阅了同一Topic,Consumer Group彼此之间不会干扰。

这样,如果要实现一个消息可以被多个消费者同时消费("广播")的效果,则将每个消费者放入单独的一个Consumer Group;

如果要实现一个消息只被一个消费者消费("独占")的效果,则将所有的Consumer放入一个Consumer Group中。

在Kafka官网的介绍中,将Consumer Group称为"逻辑上的订阅者"(logical subscriber),从这个角度看,是有一定道理的。

图展示了一个Consumer Group中消费者与分区之间的对应关系,其中,Consumer1和Consumer2分别消费Partition0和Partition1,而Partition2和Partition3同时分配给了Consumer3进行处理。

Consumer Group除了实现"独占"和"广播"模式的消息处理,Kafka还通过Consumer Group实现了消费者的水平扩展和故障转移。

当Consumer3的处理能力不足以处理两个Partition中的数据时,可以通过向Consumer Group中添加消费者的方式,触发Rebalance操作重新分配分区与消费者的对应关系,从而实现水平扩展。

如图所示,添加Consumer4之后,Consumer3只消费Partition3中的消息,Partition4中的消息则由Consumer4来消费。

下面来看消费者出现故障的场景,当Consumer4宕机时,Consumer Group会自动重新分配分区,如图所示,由Consumer3接管Consumer4对应的分区继续处理。

总结

介绍完Kafka的核心概念,我们通过图进行总结,并从更高的视角审视整个Kafka集群的架构。

相关推荐
zquwei5 小时前
SpringCloudGateway+Nacos注册与转发Netty+WebSocket
java·网络·分布式·后端·websocket·网络协议·spring
道一云黑板报9 小时前
Flink集群批作业实践:七析BI批作业执行
大数据·分布式·数据分析·flink·kubernetes
qq_5470261799 小时前
Kafka 常见问题
kafka
core5129 小时前
flink sink kafka
flink·kafka·sink
飞来又飞去11 小时前
kafka sasl和acl之间的关系
分布式·kafka
MZWeiei12 小时前
Zookeeper的监听机制
分布式·zookeeper
莹雨潇潇12 小时前
Hadoop完全分布式环境部署
大数据·hadoop·分布式
浩哲Zhe13 小时前
RabbitMQ
java·分布式·rabbitmq
明达技术13 小时前
分布式 IO 模块:赋能造纸业,革新高速纸机主传动
分布式
Allen Bright14 小时前
RabbitMQ中的Topic模式
分布式·rabbitmq