一、kafka系统介绍
Kafka属于Apache组织,是一个高性能跨语言分布式发布订阅消息队列系统,采用Scala和Java语言编写,它提供了快速、可扩展的、分布式、分区的和可复制的日志订阅服务。它由Producer(消息生产者)、Broker(一个kafka消息接收、存储、分发到消费者 的节点)、Consumer(消息消费者)三部分构成。它的主要特点有:
以时间复杂度O(1)的方式提供消息持久化能力,并对大数据量能保证常数时间的访问性能;
高吞吐率,单台服务器可以达到每秒几十万的吞吐速率;
支持服务器间的消息分区,支持分布式消费,同时保证了每个分区内的消息顺序;
轻量级,支持实时数据处理和离线数据处理两种方式。
1.1 主要功能
根据官网的介绍,Apache Kafka是一个分布式流媒体平台,它主要有3种功能:
-
1:发布和订阅消息流,这个功能类似于消息队列,这也是kafka归类为消息队列框架的原因
-
2:以容错的方式记录消息流,kafka以文件的方式来存储消息流
-
3:可以在消息发布的时候进行处理
1.2 Kafka的特性
- 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。
- 可扩展性:kafka集群支持热扩展
- 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
- 容错性:允许集群中节点失败(若副本数量为2n+1,则允许n个节点失败)
- 高并发:支持数千个客户端同时读写
1.3 使用场景
-
1:在系统或应用程序之间构建可靠的用于传输实时数据的管道,消息队列功能
-
2:构建实时的流数据处理程序来变换或处理数据流,数据处理功能
二、角色介绍
1.broker
物理层面的机器节点。一台机器上部署了kafka,那么这台机器就是kafka集群的一个节点。一般情况下,一台机器只会部署一个kafka。
2.topic
用于消息分组的最基础的抽象概念。一个集群或某一个broker都可以容纳有多个topic。
3.partition
基于topic的分组,每一个topic又可以分为多个partition。partition之间没有关联,各自有各自的顺序和消息内容。俗称topic分区。
4.producer
生产者。发送消息到kafka的角色。需要指定topic。也可以指定partition。当不指定partition的时候,也可以根据自己的需求实现partition的“负载均衡”。 创建一个topic时,同时可以指定分区数目,分区数越多,其吞吐量也越大,但是需要的资源也越多,同时也会导致更高的不可用性。
在每个分区中,消息以顺序存储,最晚接收的的消息会最后被消费。
kafka中的message以topic的形式存在,topic在物理上又分为很多的partition,partition物理上由很多segment组成,segment是存放message的真正载体。
5.consumer
消费者。从kafka消费消息的角色。必须隶属于一个consumer group。
6.offset
记录consumer消费消息进度。consumer-topic-partition-offset。
7.consumer group
消费组。用于区分消费者们。
-
一个consumer group可以有多个consumer,但是一个consumer只能属于一个consumer group。
-
消费组与消费组之间无任何关联,可以消费相同的消息,offset也互相都不关联。
-
一个consumer group可以订阅多个topic。
-
每一个topic下的partition只能被一个consumer消费,不过可以被不同consumer group下的多个consumer消费。
-
一个consumer group里的一个consumer都能消费多个partition,但是同组的其他consumer不能同时消费一个partition。
-
一个consumer group里consumer数量最好小于等于topic下的partition数量,否则会有consumer被闲置。
8. partitioner
消息分区器,用于获取当前消息在指定topic下面的具体分区。
三、kafka生产者
话不多说,先上图,(此图摘自:https://www.jianshu.com/p/7feea4860a0f)
首先,创建ProducerRecord必须包含Topic和Value,key和partition可选。然后,序列化key和value对象为ByteArray,并发送到网络。
接下来,消息发送到partitioner。如果创建ProducerRecord时指定了partition,此时partitioner啥也不用做,简单的返回指定的partition即可。如果未指定partition,partitioner会基于ProducerRecord的key生成partition。producer选择好partition后,增加record到对应topic和partition的batch record。最后,专有线程负责发送batch record到合适的Kafka broker。
当broker收到消息时,它会返回一个应答(response)。如果消息成功写入Kafka,broker将返回RecordMetadata对象(包含topic,partition和offset);相反,broker将返回error。这时producer收到error会尝试重试发送消息几次,直到producer返回error。
实例化producer后,接着发送消息。这里主要有3种发送消息的方法:
-
立即发送:只管发送消息到server端,不care消息是否成功发送。大部分情况下,这种发送方式会成功,因为Kafka自身具有高可用性,producer会自动重试;但有时也会丢失消息;
-
同步发送:通过send()方法发送消息,并返回Future对象。get()方法会等待Future对象,看send()方法是否成功;
-
异步发送:通过带有回调函数的send()方法发送消息,当producer收到Kafka broker的response会触发回调函数
以上所有情况,一定要时刻考虑发送消息可能会失败,想清楚如何去处理异常。
通常我们是一个producer起一个线程开始发送消息。为了优化producer的性能,一般会有下面几种方式:单个producer起多个线程发送消息;使用多个producer。
消息均衡策略
生产者在向kafka集群发送消息的时候,可以通过指定分区来发送到指定的分区中
也可以通过指定均衡策略来将消息发送到不同的分区中
如果不指定,就会采用默认的随机均衡策略,将消息随机的存储到不同的分区中
四、kafka消费者
4.1 消费模式
kafka的消费模式总共有3种:最多一次,最少一次,正好一次。为什么会有这3种模式,是因为客户端处理消息,提交反馈(commit)这两个动作不是原子性。
- 1.最多一次:客户端收到消息后,在处理消息前自动提交,这样kafka就认为consumer已经消费过了,偏移量增加。
- 2.最少一次:客户端收到消息,处理消息,再提交反馈。这样就可能出现消息处理完了,在提交反馈前,网络中断或者程序挂了,那么kafka认为这个消息还没有被consumer消费,产生重复消息推送。
- 3.正好一次:保证消息处理和提交反馈在同一个事务中,即有原子性。
本文从这几个点出发,详细阐述了如何实现以上三种方式。
At-most-once(最多一次)
设置enable.auto.commit为ture
设置 auto.commit.interval.ms为一个较小的时间间隔.
client不要调用commitSync(),kafka在特定的时间间隔内自动提交。
At-least-once(最少一次)
方法一
设置enable.auto.commit为false
client调用commitSync(),增加消息偏移;
方法二
设置enable.auto.commit为ture
设置 auto.commit.interval.ms为一个较大的时间间隔.
client调用commitSync(),增加消息偏移;
Exactly-once(正好一次)
3.1 思路
如果要实现这种方式,必须自己控制消息的offset,自己记录一下当前的offset,对消息的处理和offset的移动必须保持在同一个事务中,例如在同一个事务中,把消息处理的结果存到mysql数据库同时更新此时的消息的偏移。
3.2 实现
设置enable.auto.commit为false
保存ConsumerRecord中的offset到数据库
当partition分区发生变化的时候需要rebalance,有以下几个事件会触发分区变化
1 consumer订阅的topic中的分区大小发生变化
2 topic被创建或者被删除
3 consuer所在group中有个成员挂了
4 新的consumer通过调用join加入了group
此时 consumer通过实现ConsumerRebalanceListener接口,捕捉这些事件,对偏移量进行处理。
consumer通过调用seek(TopicPartition, long)方法,移动到指定的分区的偏移位置。
4.2 消费组
在消费者消费消息时,kafka使用offset来记录当前消费的位置
在kafka的设计中,可以有多个不同的group来同时消费同一个topic下的消息,如图,我们有两个不同的group同时消费,他们的的消费的记录位置offset各不项目,不互相干扰。
对于一个group而言,消费者的数量不应该多余分区的数量,因为在一个group中,每个分区至多只能绑定到一个消费者上,即一个消费者可以消费多个分区,一个分区只能给一个消费者消费
因此,若一个group中的消费者数量大于分区数量的话,多余的消费者将不会收到任何消息。
4.3 分区重平衡
可以看到,当新的消费者加入消费组,它会消费一个或多个分区,而这些分区之前是由其他消费者负责的;另外,当消费者离开消费组(比如重启、宕机等)时,它所消费的分区会分配给其他分区。这种现象称为重平衡(rebalance)。重平衡是Kafka一个很重要的性质,这个性质保证了高可用和水平扩展。不过也需要注意到,在重平衡期间,所有消费者都不能消费消息,因此会造成整个消费组短暂的不可用。而且,将分区进行重平衡也会导致原来的消费者状态过期,从而导致消费者需要重新更新状态,这段期间也会降低消费性能。后面我们会讨论如何安全的进行重平衡以及如何尽可能避免。
消费者通过定期发送心跳(hearbeat)到一个作为组协调者(group coordinator)的broker来保持在消费组内存活。这个broker不是固定的,每个消费组都可能不同。当消费者拉取消息或者提交时,便会发送心跳。
如果消费者超过一定时间没有发送心跳,那么它的会话(session)就会过期,组协调者会认为该消费者已经宕机,然后触发重平衡。可以看到,从消费者宕机到会话过期是有一定时间的,这段时间内该消费者的分区都不能进行消息消费;通常情况下,我们可以进行优雅关闭,这样消费者会发送离开的消息到组协调者,这样组协调者可以立即进行重平衡而不需要等待会话过期。
在0.10.1版本,Kafka对心跳机制进行了修改,将发送心跳与拉取消息进行分离,这样使得发送心跳的频率不受拉取的频率影响。另外更高版本的Kafka支持配置一个消费者多长时间不拉取消息但仍然保持存活,这个配置可以避免活锁(livelock)。活锁,是指应用没有故障但是由于某些原因不能进一步消费。
五、kafka 消息存储方式
Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列。
Segment:partition物理上由多个segment组成
offset:每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中. partition中的每个消息都有一个连续的序列号叫做offset,用于partition唯一标识一条消息.
topic中partition存储分布
在Kafka文件存储中,同一个topic下有多个不同partition,每个partition为一个目录,partiton命名规则为topic名称+有序序号,第一个partiton序号从0开始,序号最大值为partitions数量减1。
├── data0
│ ├── cleaner-offset-checkpoint //存了每个log的最后清理offset
│ ├── client_mblogduration-35 //名为client_mblogduration的topic的35号分区的数据
│ │ ├── 00000000000004909731.index
│ │ ├── 00000000000004909731.log // 1G文件--Segment
│ │ ├── 00000000000005048975.index // 数字是Offset
│ │ ├── 00000000000005048975.log
│ ├── client_mblogduration-37 //名为client_mblogduration的topic的37号分区的数据
│ │ ├── 00000000000004955629.index
│ │ ├── 00000000000004955629.log
│ │ ├── 00000000000005098290.index
│ │ ├── 00000000000005098290.log
│ ├── __consumer_offsets-33
│ │ ├── 00000000000000105157.index
│ │ └── 00000000000000105157.log
│ ├── meta.properties //broker.id 信息
│ ├── recovery-point-offset-checkpoint //表示已经刷写到磁盘的记录。recoveryPoint以下的数据都是已经刷 到磁盘上的了。
│ └── replication-offset-checkpoint //用来存储每个replica的HighWatermark的(high watermark (HW),表示已经被commited的message,HW以下的数据都是各个replicas间同步的,一致的。)
partiton中文件存储方式
每个partion(目录)由多个大小相等segment(段)数据文件中。但每个段segment file消息数量不一定相等,这种特性方便old segment file快速被删除。
每个partiton只需要支持顺序读写就行了,segment文件生命周期由服务端配置参数决定。
segment file组成:由2大部分组成,分别为index file和data file,此2个文件一一对应,成对出现,后缀".index"和“.log”分别表示为segment索引文件、数据文件.
segment文件命名规则:partion全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值。数值最大为64位long大小,19位数字字符长度,没有数字用0填充。
segment中index<—->data file对应关系物理结构如下:
index与log映射关系
.index文件存放的是message逻辑相对偏移量(相对offset=绝对offset-base offset)与在相应的.log文件中的物理位置(position)。但.index并不是为每条message都指定到物理位置的映射,而是以entry为单位,每条entry可以指定连续n条消息的物理位置映射(例如:假设有20000~20009共10条消息,.index文件可配置为每条entry
指定连续10条消息的物理位置映射,该例中,index entry会记录偏移量为20000的消息到其物理文件位置,一旦该条消息被定位,20001~20009可以很快查到。)。每个entry大小8字节,前4个字节是这个message相对于该log segment第一个消息offset(base offset)的相对偏移量,后4个字节是这个消息在log文件中的物理位置。
Kafka高效文件存储设计特点
- topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
- 通过索引信息可以快速定位message和确定response的最大大小。
- 通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。
- 通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。
参考:https://www.cnblogs.com/byrhuangqiang/p/6364088.html
六、 Kafka集群副本容灾原理
如下图所示:
6.1 Kafka的副本机制简介
上图展示了一个名为topic1,分区数为3、副本数为3的话题的数据在机器数量为4的Kafka集群上的存储方式。因为分区数为3,所以消息被分为part0、part1、part3这三个区。因为副本数是3,所以每一个分区里面的数据都在集群里面存储了3份。默认创建topic的时候分区数和副本数都是1 。
上图可知,当前集群中的1号机器是该话题的0号分区的leader节点,则当该话题的Producer和Consumer在生产和消费0号分区的消息的时候都直接与1号节点交互,1号节点会将0号分区的相关信息同步到2号节点和3号节点中。此时如果1号节点故障,则:2号节点或3号节点会被选举为0号分区新的leader与Producer和Consumer交互,而1号分区和2号分区的交互不会受到任何影响,从而实现了集群容灾。
由上可知,Kafka是通过的副本的方式实现容灾。假设创建一个名为topic2,分区数为3、副本数为1的话题。则该话题数据的存储方式,就会类似于上图中去掉所有绿色框之后的样子。即。每个分区的数据都只存储一份。此时2号节点就是该话题的1号分区的数据的存储机器,其它机器上都没有1号分区的数据,如果此时2号节点故障,则该话题的整个数据存储和消费都会受到影响,应为其他机器上都没有1号分区的副本数据,无法恢复故障的机器数据。
在通常情况下,增加分区可以提供kafka集群的吞吐量。然而,也应该意识到集群的总分区数或是单台服务器上的分区数过多,会增加不可用及延迟的风险。
6.2 Kafka创建副本的2种模式—同步复制和异步复制
Kafka动态维护了一个同步状态的副本的集合(a set of In-Sync Replicas),简称ISR,在这个集合中的节点都是和leader保持高度一致的,任何一条消息只有被这个集合中的每个节点读取并追加到日志中,才会向外部通知说“这个消息已经被提交”。
只有当消息被所有的副本加入到日志中时,才算是“committed”,只有committed的消息才会发送给consumer,这样就不用担心一旦leader down掉了消息会丢失。
消息从leader复制到follower, 我们可以通过决定Producer是否等待消息被提交的通知(ack)来区分同步复制和异步复制。
同步复制流程:
1.producer联系zk识别leader
2.向leader发送消息
3.leadr收到消息写入到本地log
4.follower从leader pull消息
5.follower向本地写入log
6.follower向leader发送ack消息
7.leader收到所有follower的ack消息
8.leader向producer回传ack
异步复制流程:
1.producer联系zk识别leader
2.向leader发送消息
3.leadr收到消息写入到本地log,leader向producer回传ack
4.follower从leader pull消息
5.follower向本地写入log
6.follower向leader发送ack消息
和同步复制的区别在于:异步复制时,leader写入本地log之后, 直接向client回传ack消息,不需要等待所有follower复制完成。
由于Kafka支持副本模式,所以当其中一个Broker挂掉,一个新的leader就能通过ISR机制推选出来,继续处理读写请求。
6.3 broker节点存活条件
Kafka集群判断一个节点是否存活,需要满足已下2个条件:
-
- 节点必须可以维护和ZooKeeper的连接,Zookeeper通过心跳机制检查每个节点的连接。
-
- 如果节点是个follower,他必须能及时的同步leader的写操作,延时不能太久。
Leader会追踪所有“同步中”的节点,一旦一个down掉了,或是卡住了,或是延时太久,leader就会把它移除.
6.3 Kafka集群的容灾能力
首先,必须要明确:
- Kafka集群的leader选举是通过zookeeper实现的,而zookeeper的选举策略是需要 半数以上 的节点参与选举,才能产生新的leader。
eg1:假如有一个3个机器的集群,当leader机器故障之后,剩下的2个机器因为大于总数的一半,所以可以在他两中选举产生一个新的leader。此时新的leader节点再故障的话,剩下的1个正常节点无法独自选举出新的leader节点,因为没有超过总数的一半机器参与选举。此时容错能力为1。
eg2:假如有一个4个机器的集群,当leader机器故障之后,剩下的3个机器因为大于总数的一半,所以可以在他们3个中选举产生一个新的leader。此时新的leader节点再故障的话,剩下的2个正常节点无法独自选举出新的leader节点,因为没有超过总数的一半机器参与选举。此时容错能力为1。
以上2个例子说明,Kafka集群的2n+1和2n+2个机器的leader容灾能力是一样的,都是n。出于节约资源的考虑,大家部署集群的时候,都是部署奇数个机器。
eg3:假如有一个5个机器的集群,当leaderA机器故障之后,剩下的4个机器因为大于总数的一半,所以可以在他们4个中选举产生一个新的leaderB。此时新的leaderB节点再故障的话,剩下的3个正常节点还是大于集群总节点的一半,所以可以再次选举产生一个新的leaderC。此时新的leaderC节点再故障的话,剩下的2个正常节点无法独自选举出新的leader节点,因为没有超过总数的一半机器参与选举。此时容错能力为2。
上面说的都是leader节点故障的情况,但是当故障的全是flower节点时,情况就完全不一样了。因为flower节点即使故障了,也不会影响整个集群的正常工作,所以允许全部的flower节点都故障。
eg4:假如有一个5个机器的集群,只要leader节点正常,即使其他4个flower节点都故障了集群也是可以正常工作的。此时容错能力为4!
即:当故障的节点全是flower的时候,可以达到最大的容错能力n-1。