基本简介
Apache Kafka是由LinkedIn采用Scala和Java开发的开源流处理软件平台,并捐赠给了Apache Software Foundation。
该项目旨在提供统一的、高吞吐量、低延迟的平台来处理实时数据流。
Kafka可以通过Kafka Connect连接到外部系统,并提供了Kafka Streams。
「Kafka的特性」
Kafka是一种分布式的,基于发布/订阅的消息系统,主要特性如下:
特性 | 分布式 | 「高性能」 | 「持久性和扩展性」 |
---|---|---|---|
描述 | 多分区 | 高吞吐量 | 数据可持久化 |
多副本 | 低延迟 | 容错性 | |
多订阅者 | 高并发 | 支持水平在线扩展 | |
基于ZooKeeper调度 | 时间复杂度为O(1) | 消息自动平衡 |
版本号
「Kafka版本命名」
我们在官网上下载Kafka时,会看到这样的版本:
前面的版本号是编译Kafka源代码的Scala编译器版本。
Kafka服务器端的代码完全由Scala语言编写,Scala同时支持面向对象编程和函数式编程,用Scala写成的源代码编译之后也是普通的.class
文件,因此我们说Scala是JVM系的语言。
真正的Kafka版本号实际上是2.1.1
。
❝那么这个2.1.1又表示什么呢?
❞
前面的2表示大版本号,即Major Version;中间的1表示小版本号或次版本号,即Minor Version;最后的1表示修订版本号,也就是Patch号。
Kafka社区在发布1.0.0版本后写过一篇文章,宣布Kafka版本命名规则正式从4位演进到3位,比如0.11.0.0版本就是4位版本号。
有个建议,不论用的是哪个版本,都请尽量保持服务器端版本和客户端版本一致,否则你将损失很多Kafka为你提供的性能优化收益。
「版本演进」
0.7版本:只提供了最基础的消息队列功能。
0.8版本:引入了副本机制,至此kafka成为了一个整整意义上完备的分布式可靠消息队列解决方案
0.9.0.0版本:增加了基础的安全认证/权限功能;使用Java重新了新版本消费者API;引入了Kafka Connect组件。
0.11.0.0版本:提供了幂等性Producer API以及事务API;对Kafka消息格式做了重构。
1.0和2.0版本:主要还是Kafka Streams的各种改进
基本概念
「主题」
发布订阅的对象是主题(Topic
),可以为每 个业务、每个应用甚至是每类数据都创建专属的主题
「生产者和消费者」
向主题发布消息的客户端应用程序称为生产者,生产者程序通常持续不断地 向一个或多个主题发送消息
订阅这些主题消息的客户端应用程序就被称为消费者,消费者也能够同时订阅多个主题的消息
「Broker」
集群由多个 Broker 组成,Broker
负责接收和处理客户端发送过来的请求,以及对消息进行持久化
虽然多个 Broker 进程能够运行在同一台机器上,但更常见的做法是将 不同的 Broker
分散运行在不同的机器上,这样如果集群中某一台机器宕机,即使在它上面 运行的所有 Broker 进程都挂掉了,其他机器上的 Broker
也依然能够对外提供服务
「备份机制」
备份的思想很简单,就是把相同的数据拷贝到多台机器上,而这些相同的数据拷贝被称为副本
定义了两类副本:领导者副本和追随者副本
前者对外提供服务,这里的对外指的是与 客户端程序进行交互;而后者只是被动地追随领导者副本而已,不能与外界进行交互
「分区」
分区机制指的是将每个主题划分成多个分区,每个分区是一组有序的消息日志
生产者生产的每条消息只会被发送到一个分区中,也就是说如果向一个双分区的主题发送一条消息,这条消息要么在分区 0 中,要么在分区 1 中
每个分区下可以配置若干个副本,其中只能有 1 个领 导者副本和 N-1 个追随者副本
生产者向分区写入消息,每条消息在分区中的位置信息叫位移
「消费者组」
多个消费者实例共同组成一个组来 消费一组主题
这组主题中的每个分区都只会被组内的一个消费者实例消费,其他消费者实例不能消费它
❝同时实现了传统消息引擎系统的两大模型:
❞
如果所有实例都属于同一个 Group
, 那么它实现的就是消息队列模型;
如果所有实例分别属于不 同的 Group
,那么它实现的就是发布/订阅模型
「Coordinator:协调者」
所谓协调者,它专门为Consumer Group服务,负责为Group执行Rebalance以及提供位移管理和组成员管理等。
具体来讲,Consumer端应用程序在提交位移时,其实是向Coordinator所在的Broker提交位移,同样地,当Consumer应用启动时,也是向Coordinator所在的Broker发送各种请求,然后由Coordinator负责执行消费者组的注册、成员管理记录等元数据管理操作。
所有Broker在启动时,都会创建和开启相应的Coordinator组件。
也就是说,「所有Broker都有各自的Coordinator组件」。
那么,Consumer Group如何确定为它服务的Coordinator在哪台Broker上呢?
通过Kafka内部主题__consumer_offsets
。
目前,Kafka为某个Consumer Group确定Coordinator所在的Broker的算法有2个步骤。
-
第1步:确定由
__consumer_offsets
主题的哪个分区来保存该Group数据:partitionId=Math.abs(groupId.hashCode() % offsetsTopicPartitionCount)
。 -
第2步:找出该分区Leader副本所在的Broker,该Broker即为对应的Coordinator。
首先,Kafka会计算该Group的group.id
参数的哈希值。
比如你有个Group的group.id
设置成了test-group
,那么它的hashCode值就应该是627841412。
其次,Kafka会计算__consumer_offsets
的分区数,通常是50个分区,之后将刚才那个哈希值对分区数进行取模加求绝对值计算,即abs(627841412 % 50) = 12
。
此时,我们就知道了__consumer_offsets
主题的分区12负责保存这个Group的数据。
有了分区号,我们只需要找出__consumer_offsets
主题分区12的Leader副本在哪个Broker上就可以了,这个Broker,就是我们要找的Coordinator。
「消费者位移:Consumer Offset」
消费者消费进度,每个消费者都有自己的消费者位移。
「重平衡:Rebalance」
消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。
Rebalance是Kafka消费者端实现高可用的重要手段。
「AR(Assigned Replicas)」:分区中的所有副本统称为AR。
所有消息会先发送到leader副本,然后follower副本才能从leader中拉取消息进行同步。
但是在同步期间,follower对于leader而言会有一定程度的滞后,这个时候follower和leader并非完全同步状态
「OSR(Out Sync Replicas)」:follower副本与leader副本没有完全同步或滞后的副本集合
「ISR(In Sync Replicas):「AR中的一个子集,ISR中的副本都」是与leader保持完全同步的副本」,如果某个在ISR中的follower副本落后于leader副本太多,则会被从ISR中移除,否则如果完全同步,会从OSR中移至ISR集合。
在默认情况下,当leader副本发生故障时,只有在ISR集合中的follower副本才有资格被选举为新leader,而OSR中的副本没有机会(可以通过unclean.leader.election.enable
进行配置)
「HW(High Watermark)」:高水位,它标识了一个特定的消息偏移量(offset),消费者只能拉取到这个水位 offset 之前的消息
下图表示一个日志文件,这个日志文件中只有9条消息,第一条消息的offset(LogStartOffset)为0,最有一条消息的offset为8,offset为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 之前的消息。
系统架构
「kafka设计思想」
一个最基本的架构是生产者发布一个消息到Kafka的一个Topic ,该Topic的消息存放于的Broker中,消费者订阅这个Topic,然后从Broker中消费消息,下面这个图可以更直观的描述这个场景:
「消息状态:」 在Kafka中,消息是否被消费的状态保存在Consumer中,Broker不会关心消息是否被消费或被谁消费,Consumer会记录一个offset值(指向partition中下一条将要被消费的消息位置),如果offset被错误设置可能导致同一条消息被多次消费或者消息丢失。
「消息持久化:」 Kafka会把消息持久化到本地文件系统中,并且具有极高的性能。
「批量发送:」 Kafka支持以消息集合为单位进行批量发送,以提高效率。
「Push-and-Pull:」 Kafka中的Producer和Consumer采用的是Push-and-Pull模式,即Producer向Broker Push消息,Consumer从Broker Pull消息。
「分区机制(Partition):」 Kafka的Broker端支持消息分区,Producer可以决定把消息发到哪个Partition,在一个Partition中消息的顺序就是Producer发送消息的顺序,一个Topic中的Partition数是可配置的,Partition是Kafka高吞吐量的重要保证。
「系统架构」
通常情况下,一个kafka体系架构包括「多个Producer」、「多个Consumer」、「多个broker」以及「一个Zookeeper集群」。
「Producer」:生产者,负责将消息发送到kafka中。
「Consumer」:消费者,负责从kafka中拉取消息进行消费。
「Broker」:Kafka服务节点,一个或多个Broker组成了一个Kafka集群
「Zookeeper集群」:负责管理kafka集群元数据以及控制器选举等。
生产者分区
「为什么分区?」
Kafka的消息组织方式实际上是三级结构:主题-分区-消息。
主题下的每条消息只会保存在某一个分区中,而不会在多个分区中被保存多份。
其实分区的作用就是提供负载均衡的能力,或者说对数据进行分区的主要原因,就是为了实现系统的高伸缩性(Scalability)。
不同的分区能够被放置到不同节点的机器上,而数据的读写操作也都是针对分区这个粒度而进行的,这样每个节点的机器都能独立地执行各自分区的读写请求处理,并且,我们还可以通过添加新的节点机器来增加整体系统的吞吐量。
「都有哪些分区策略?」
「所谓分区策略是决定生产者将消息发送到哪个分区的算法。」
Kafka为我们提供了默认的分区策略,同时它也支持你自定义分区策略。
「自定义分区策略」
如果要自定义分区策略,你需要显式地配置生产者端的参数partitioner.class
。
在编写生产者程序时,你可以编写一个具体的类实现org.apache.kafka.clients.producer.Partitioner
接口。
这个接口也很简单,只定义了两个方法:partition()和close(),通常你只需要实现最重要的partition方法。
我们来看看这个方法的方法签名:
int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
这里的topic、key、keyBytes、value和valueBytes都属于消息数据,cluster则是集群信息(比如当前Kafka集群共有多少主题、多少Broker等)。
Kafka给你这么多信息,就是希望让你能够充分地利用这些信息对消息进行分区,计算出它要被发送到哪个分区中。
只要你自己的实现类定义好了partition方法,同时设置partitioner.class
参数为你自己实现类的Full Qualified Name,那么生产者程序就会按照你的代码逻辑对消息进行分区。
「轮询策略」
也称Round-robin策略,即顺序分配。
比如一个主题下有3个分区,那么第一条消息被发送到分区0,第二条被发送到分区1,第三条被发送到分区2,以此类推。当生产第4条消息时又会重新开始,即将其分配到分区0
这就是所谓的轮询策略。轮询策略是Kafka Java生产者API默认提供的分区策略。
「轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一。」
「随机策略」
也称Randomness策略。所谓随机就是我们随意地将消息放置到任意一个分区上。
如果要实现随机策略版的partition方法,很简单,只需要两行代码即可:
List partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());
先计算出该主题总的分区数,然后随机地返回一个小于它的正整数。
本质上看随机策略也是力求将数据均匀地打散到各个分区,但从实际表现来看,它要逊于轮询策略,所以「如果追求数据的均匀分布,还是使用轮询策略比较好」。事实上,随机策略是老版本生产者使用的分区策略,在新版本中已经改为轮询了。
「按消息键保序策略」
Kafka允许为每条消息定义消息键,简称为Key。
这个Key的作用非常大,它可以是一个有着明确业务含义的字符串,比如客户代码、部门编号或是业务ID等;也可以用来表征消息元数据。
特别是在Kafka不支持时间戳的年代,在一些场景中,工程师们都是直接将消息创建时间封装进Key里面的。
一旦消息被定义了Key,那么你就可以保证同一个Key的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略
实现这个策略的partition方法同样简单,只需要下面两行代码即可:
List partitions = cluster.partitionsForTopic(topic);
return Math.abs(key.hashCode()) % partitions.size();
前面提到的Kafka默认分区策略实际上同时实现了两种策略:如果指定了Key,那么默认实现按消息键保序策略;如果没有指定Key,则使用轮询策略