Kafka是分布式的,基于发布订阅的消息系统。
1 特点
- 同时为发布和订阅提供高吞吐量 Kafka每秒可生产约25万条消息(50 MB),每秒处理55万条消息(110 MB)
- 持久化 默认就将消息持久化到磁盘,因此可用于批量消费,例如ETL,以及实时应用程序。通过将数据持久化到硬盘以及防止数据丢失。以O(1)时间复杂度提供消息持久化能力,即使对TB级以上数据也能保证常数时间复杂度的访问性能。
- 分布式系统,易于向外扩展 所有的producer、broker和consumer都会有多个,均为分布式的。无需停机即可扩展机器。消息被处理的状态是在consumer端维护,而不是由server端维护。当失败时能自动平衡
- 支持online和offline场景
Kafka发布订阅的对象是主题(Topic),可为每个业务、每个应用甚至是每类数据都创建专属的主题。
Message
消息,是通信的基本单位,每个producer可以向一个topic(主题)发布一些消息
1 Producer & Consumer(客户端)
- 生产者(Producer) 向主题发布消息的客户端应用。生产者程序通常持续不断向一或多个主题发消息。
- 消费者(Consumer) 订阅这些主题消息的客户端应用程序。消费者也能同时订阅多个主题消息。
生产者和消费者统称为客户端(Clients)。可同时运行多个生产者和消费者实例,这些实例会不断向Kafka集群中的多个主题生产和消费消息。
2 Broker(服务器端)
Kafka的服务器端由被称为Broker的服务进程构成,即一个Kafka集群由多个Broker组成。
Broker负责接收和处理客户端发送过来的请求,以及对消息进行持久化。
虽然多个Broker进程能够运行在同一机器,但更常见的做法是将不同的Broker分散运行在不同机器。
这样如集群中某一机器宕机,即使在它上面运行的所有Broker进程都挂,其他机器的Broker也依然能够对外提供服务。
这也是Kafka高可用的手段之一
。
3 Partitioning(分区)
partition,物理上的概念,有序不可变的record序列,partition中的record会被分配一个自增长id(offset)。
一个topic中的消息数据按多个分区组织,partition是kafka消息队列组织的最小单位,一个partition可看做是一个队列。
虽然副本机制可保证数据持久化以不丢消息,但未解决Scalability伸缩性问题。
- 虽现在有leader、follower副本,但若leader副本积累太多数据以至单台Broker无法容纳,咋办?
Kafka会把数据分割成多份,保存在不同Broker(即分区)。原理类似其他分布式系统的分片、分区域等说法,如
- MongoDB和ES中的Sharding
- HBase中的Region
但Partitioning才是最标准的名称。
Kafka分区,就是将每个topic划成多个分区(Partition),每个Partition内是一组顺序消息日志。 生产者生产的每条消息只会被发送到一个分区,即向一个双分区的主题发送一条消息,该消息要么在分区0,要么在分区1(分区编号从0开始)。
副本与分区
副本是在分区级别定义的
。每个分区下可配置N个副本,但只能有1个领导者副本和N-1个追随者副本。
生产者向分区写入消息,每条消息在分区中的位置信息由位移(Offset)数据来表征。 分区位移从0开始,假设一个生产者向一个空分区写入10条消息,则这10条消息的位移依次是0、1、2、…、9。
4 Topic
一个业务即一个Topic。 每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽保存于一或多个broker,但用户只需指定消息的Topic即可生产或消费数据,不必关心数据存于何处。
数据主题,是Kafka中用来代表一个数据流的一个抽象,Kafka处理的消息源(feeds of messages)的不同分类。
发布数据时,可用topic对数据分类,也作为订阅数据时的主题。 一个Topic同时可有多个producer、consumer。 Topic可理解为一个队列,生产者和消费者面向的都是同一topic。
3 Replication - 副本
实现高可用的另一个手段。
为保证分布式可靠性,kafka 0.8开始对每个分区的数据进行备份(不同Broker上),防止其中一个Broker宕机而造成分区数据不可用。
每个partition被复制到其它服务器作为replication,这是一种冗余备份策略:
- 同一partition的多个replication不允许在同一broker
- 每个partition的replication中,有一个leader,零或多个follower
- leader处理此分区所有读、写请求
- follower仅被动的复制数据
- leader宕机后,会从follower中选举出新leader
副本数量可配置,副本保存着相同数据,却也有不同:
3.1 副本的分类
Kafka定义了两类副本
- 领导者副本(Leader Replica) 对外提供服务,与客户端程序交互
- 追随者副本(Follower Replica) 只被动地追随领导者副本,不与外界交互
在很多其他系统中追随者副本是可以对外提供服务的,比如MySQL的从库是可以处理读操作的,但是在Kafka中追随者副本不会对外提供服务。
3.2 副本的工作机制
- 生产者总是向领导者副本写消息
- 而消费者总是从领导者副本读消息
至于追随者副本,它只做一件事:向领导者副本发送请求,请求领导者把最新生产的消息发给它,这样它能保持与领导者的同步。
6 Record
每条记录都有key、value、 timestamp三个信息
分区id offset才可确定数据位置 分区内才是有序的!
ConsumerGroup - 消费组
每个Consumer属于一个特定的Consumer Group (可为每个Consumer 指定 group name, 若不指定 group name则属于默认的group)
- 消费者可使用相同的
group.id
加入一个组 - 每个Consumer实例属于一个ConsumerGroup
- 组的最大并行度是组中的消费者数量 ← 没有partition
- Kafka将topic的partition分配给组中的消费者,以便每个分区仅由组中的一个消费者使用
- Kafka保证消息只能由该组中的单个消费者读取。消费者可按存储在日志中的顺序查看消息
- 每个ConsumerGroup都有一个Coordinator(协调者),负责分配Consumer和Partition的对应关系,当Partition或是Consumer发生变更时,会触发reblance(重新分配),重新分配Consumer与Partition的对应关系
Coordinator
Consumer维护与Coordinator之间的心跳,这样Coordinator就能感知到Consumer的状态,在Consumer故障的时候及时触发rebalance。
Kafka的三层消息架构
- 第一层是主题层,每个主题可以配置M个分区,而每个分区又可以配置N个副本
- 第二层是分区层,每个分区的N个副本中只能有一个充当领导者角色,对外提供服务;其他N-1个副本是追随者副本,只是提供数据冗余之用
- 第三层是消息层,分区中包含若干条消息,每条消息的位移从0开始,依次递增
最后,客户端程序只能与分区的领导者副本进行交互。
讲完了消息层次,来说说Kafka Broker是如何持久化数据的。总的来说,Kafka使用消息日志(Log)来保存数据,一个日志就是磁盘上一个只能追加写(Append-only)消息的物理文件。因为只能追加写入,故避免了缓慢的随机I/O操作,改为性能较好的顺序I/O写操作,这也是实现Kafka高吞吐量特性的一个重要手段。不过如果你不停地向一个日志写入消息,最终也会耗尽所有的磁盘空间,因此Kafka必然要定期地删除消息以回收磁盘。怎么删除呢?简单来说就是通过日志段(Log Segment)机制。在Kafka底层,一个日志又近一步细分成多个日志段,消息被追加写到当前最新的日志段中,当写满了一个日志段后,Kafka会自动切分出一个新的日志段,并将老的日志段封存起来。Kafka在后台还有定时任务会定期地检查老的日志段是否能够被删除,从而实现回收磁盘空间的目的。
这里再重点说说消费者。 点对点模型(Peer to Peer,P2P)和发布订阅模型。这里面的点对点指的是同一条消息只能被下游的一个消费者消费,其他消费者则不能染指。在Kafka中实现这种P2P模型的方法就是引入了消费者组(Consumer Group)。所谓的消费者组,指的是多个消费者实例共同组成一个组来消费一组主题。这组主题中的每个分区都只会被组内的一个消费者实例消费,其他消费者实例不能消费它。为什么要引入消费者组呢?主要是为了提升消费者端的吞吐量。多个消费者实例同时消费,加速整个消费端的吞吐量(TPS)。我会在专栏的后面详细介绍消费者组机制,所以现在你只需要了解消费者组是做什么的即可。另外这里的消费者实例可以是运行消费者应用的进程,也可以是一个线程,它们都称为一个消费者实例(Consumer Instance)。
消费者组里面的所有消费者实例不仅“瓜分”订阅主题的数据,而且更酷的是它们还能彼此协助。假设组内某个实例挂掉了,Kafka能够自动检测到,然后把这个Failed实例之前负责的分区转移给其他活着的消费者。这个过程就是Kafka中大名鼎鼎的“重平衡”(Rebalance)。嗯,其实既是大名鼎鼎,也是臭名昭著,因为由重平衡引发的消费者问题比比皆是。事实上,目前很多重平衡的Bug社区都无力解决。
每个消费者在消费消息的过程中必然需要有个字段记录它当前消费到了分区的哪个位置上,这个字段就是消费者位移(Consumer Offset)。注意,这和上面所说的位移完全不是一个概念。上面的“位移”表征的是分区内的消息位置,它是不变的,即一旦消息被成功写入到一个分区上,它的位移值就是固定的了。而消费者位移则不同,它可能是随时变化的,毕竟它是消费者消费进度的指示器嘛。另外每个消费者有着自己的消费者位移,因此一定要区分这两类位移的区别。我个人把消息在分区中的位移称为分区位移,而把消费者端的位移称为消费者位移。
8 总结
消息:Record。Kafka是消息引擎嘛,这里的消息就是指Kafka处理的主要对象。 主题:Topic。主题是承载消息的逻辑容器,在实际使用中多用来区分具体的业务。 分区:Partition。一个有序不变的消息序列。每个主题下可以有多个分区。 消息位移:Offset。表示分区中每条消息的位置信息,是一个单调递增且不变的值。 副本:Replica。Kafka中同一条消息能够被拷贝到多个地方以提供数据冗余,这些地方就是所谓的副本。副本还分为领导者副本和追随者副本,各自有不同的角色划分。副本是在分区层级下的,即每个分区可配置多个副本实现高可用。 生产者:Producer。向主题发布新消息的应用程序。 消费者:Consumer。从主题订阅新消息的应用程序。 消费者位移:Consumer Offset。表征消费者消费进度,每个消费者都有自己的消费者位移。 消费者组:Consumer Group。多个消费者实例共同组成的一个组,同时消费多个分区以实现高吞吐。 重平衡:Rebalance。消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。Rebalance是Kafka消费者端实现高可用的重要手段。
9 为什么Kafka不像MySQL允许追随者副本对外提供读服务?
不从follower读几个原因:
- kafka的分区已经让读是从多个broker读从而负载均衡,不是MySQL的主从,压力都在主上
- kafka保存的数据和数据库的性质有实质的区别就是数据具有消费的概念,是流数据,kafka是消息队列,所以消费需要位移,而数据库是实体数据不存在这个概念,如果从kafka的follower读,消费端offset控制更复杂
- 生产者来说,kafka可以通过配置来控制是否等待follower对消息确认的,如果从上面读,也需要所有的follower都确认了才可以回复生产者,造成性能下降,如果follower出问题了也不好处理
首先明确一下:主从分离与否没有绝对的优劣,它仅仅是一种架构设计,各自有适用的场景。
Redis和MySQL都支持主从读写分离,这和它们的使用场景有关。对于那种读操作很多而写操作相对不频繁的负载类型而言,采用读写分离是非常不错的方案——我们可以添加很多follower横向扩展,提升读操作性能。反观Kafka,它的主要场景还是在消息引擎而不是以数据存储的方式对外提供读服务,通常涉及频繁地生产消息和消费消息,这不属于典型的读多写少场景,因此读写分离方案在这个场景下并不太适合。
Kafka副本机制使用的是异步消息拉取,因此存在leader和follower之间的不一致性。如果要采用读写分离,必然要处理副本lag引入的一致性问题,比如如何实现read-your-writes、如何保证单调读(monotonic reads)以及处理消息因果顺序颠倒的问题。相反地,如果不采用读写分离,所有客户端读写请求都只在Leader上处理也就没有这些问题了——当然最后全局消息顺序颠倒的问题在Kafka中依然存在,常见的解决办法是使用单分区,其他的方案还有version vector,但是目前Kafka没有提供。最后、社区正在考虑引入适度的读写分离方案,比如允许某些指定的follower副本(主要是为了考虑地理相近性)可以对外提供读服务。当然目前这个方案还在讨论中。
Kafka的数据会保存到leader副本的log文件中并写入磁盘,随后follower副本会对数据进行同步。
参考
- 《Apache Kafka实战》
- https://www.zhihu.com/question/327925275/answer/705690755
- https://kafka.apache.org/documentation