一、kafka简介
kafka是分布式消息队列,具有高性能、持久化、多副本备份、横向扩展能力,其最大的特性是高吞吐量、低延迟,可以几乎实时的处理大量数据。这和其文件存储机制的设计有关,简单来说写入时为顺序写入,且有MMFile的机制,而在检索时,也做了分段和稀疏索引。同时,在整体架构上,对于同一个topic中的消息会均匀的分布在不同的partition上,也使得分布式消费成为可能。
二、鸟瞰kafka
总体来讲,kafka的数据流向如上图所示,简单描述流程大概是:
2.1 producer往topic上写消息,此时kafka会均匀的把message分布在不同的partition上(发送时不需要指定partition,kafka会自动的loan balance),同时写入时只写入到了leader上
2.2 leader发送给其他的follower
2.3 各个consumer线程可以组成一个consumer group,每个consumer group 均可以从头开始消费一个topic下的各个partition,即同一个topic的message,允许多个consumer group进行消费
2.4 在某一个consumer group 进行消费时,partition中每一条message只能被其中的一个consumer线程消费
2.5 消费时可是从leader进行的消费
2.6 Consumer处理partition里面的message的时候是顺序读取的。所以必须维护着上一次读到哪里的offsite信息
2.7 broker、topics、partitions的一些元信息用zk来存,监控和路由啥的也都会用到zk
好了,到这里一定会产生一些疑问,比如:
1.partition是干嘛的?
2.为什么不同的consumer group 可以同时消费同一个topic?如何确保消费顺序?
3.leader 和 follower的设计是有什么用?
4.消息的可靠性?
5.为什么说kafka的性能很好,读写很快?
一个个来看。
三、Topic & partition
Topic相当于传统消息系统MQ中的一个队列queue,producer端发送的message必须指定是发送到哪个topic,但是不需要指定topic下的哪个partition,因为kafka会把收到的message进行load balance,均匀的分布在这个topic下的不同的partition上( hash(message) % [broker数量] )。物理上存储上,这个topic会分成一个或多个partition,每个partiton相当于是一个子queue。在物理结构上,每个partition对应一个物理的目录(文件夹),文件夹命名是[topicname][partition][序号],一个topic可以有无数多的partition,根据业务需求和数据量来设置。在kafka配置文件中可随时更高num.partitions参数来配置更改topic的partition数量,在创建Topic时通过参数指定parittion数量。Topic创建之后通过Kafka提供的工具也可以修改partiton数量。
一般来说,(1)一个Topic的Partition数量大于等于Broker的数量,可以提高吞吐率。(2)同一个Partition的Replica尽量分散到不同的机器,高可用。
当add a new partition的时候,partition里面的message不会重新进行分配,原来的partition里面的message数据不会变,新加的这个partition刚开始是空的,随后进入这个topic的message就会重新参与所有partition的load balance。
四、消息写入及顺序消费
kafka中的Message是以topic为基本单位组织的,不同的topic之间是相互独立的。每个topic又可以分成几个不同的partition(每个topic有几个partition是在创建topic时指定的),每个partition存储一部分Message。
另外,因为硬盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最讨厌随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,kafka就是使用顺序I/O。
也就是说,当有消息写入时,实际的写入会类似下图所示
可以看到,message在partition中是顺序存储的,这种方式有一个很明显的缺陷——没有办法删除数据,所以kafka是不会删除数据的,它会把所有的数据都保留下来。所以消费时需要consumer通过offset来标记消费到了那一条数据,通过的consumer group中的consumer消费的offset可能不一样,如下图所示
五、leader选举机制
从前文的鸟瞰图我们可以知道,kafka的每个partition可以在其他的kafka broker节点上存副本,即对于同一个partition可以有一个leader多个follower,这是为了确保当某个broker宕机的时候不会影响这个kafka集群,如果是leader所在的broker宕机了,也会有新的follower站出来成为leader,也就是leader的重新选举机制。那kafka是如何进行leader的重新选举的呢?
1.kafka会在所有的broker中选出一个controller,所有partition的leader选举都由controller决定,controller在zk注册Watch,一旦有broker宕机,其在zk对应的znode会自动被删除,zk会fire controller注册的watch
2.此时controller会去重新读取一遍最新的幸存的broker,并决定partition集合(set_p),这个集合包含了宕机的broker上所有的partition
3.对于集合中的每个partition,controller会去读取该partition的ISR(副本同步队列),如果当前ISR中有至少一个replica(副本)还幸存,则选择其中一个作为新leader,新的ISR则包含当前ISR中所有幸存的replica(选举算法的实现类似于微软的PacificA)。如果该partition的所有replica都宕机了,则将新的leader设置为-1。
4.将新的leader,ISR和新的leader_epoch及controller_epoch写入/brokers/topics/[topic]/partitions/[partition]/state。
5.直接通过RPC向set_p相关的broker发送LeaderAndISRRequest命令。
如下图所示
从前文的鸟瞰图我们可以知道,producer发送消息时是先写到leader上,再由leader同步到follower,如果写到leader后还没同步时正好broker宕机了,此时重新选举后follower充当新的leader,则刚刚那条消息的数据就丢失了,也就是说kafka在一些少量场景下存在丢失数据的可能(消息队列消费时也同样存在丢失数据或者重复消费的场景),接下来就来看下kafka在消息可靠性上的设置
六、消息的可靠性
6.1生产者生产消息的可靠性
6.2消费者消费时可能存在的异常场景
6.2.1 consumer的delivery gurarantee,默认是读完message先commmit再处理message,autocommit默认是true,这时候先commit就会更新offsite+1,一旦处理失败,offsite已经+1,这个时候就会丢message
6.2.2 如前文所述,同一个topic支持被不同的consumer group消费,如果同样的业务存在两个consumer group(一般不会这样),在消息消费时就需要考虑重复消费的场景
6.3由于MMFile导致的消息丢失
kafka为了像操作内存一样写入数据,在写入数据时,除了前文提到的顺序写入外,也会有内存映射文件(Memory Mapped Files ,MMFile)这样的机制,它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上,如果在完成同步之前,服务器宕机了,则也会丢失一部分数据。
七、kafka如何做到快速写入和读取的
7.1 快速写入
关于写入的部分前文已经提到,主要是依靠顺序写入避免了最耗时的机械动作——寻址,以及上面刚刚提到的MMFile机制可以使kafka如同操作内存一样的写入。
7.2 快速读取
7.2.1 parition数据文件
为了弄明白kafka如何做到快速读取某一条消息的,首先要看一下partition文件的结构。
partition是以文件的形式存储在文件系统中。
Partition中的每条Message由offset来表示它在这个partition中的偏移量,这个offset不是该Message在partition数据文件中的实际存储位置,而是逻辑上一个值,它唯一确定了partition中的一条Message。因此,可以认为offset是partition中Message的id。partition中的每条Message包含了以下三个属性:
offset
MessageSize
data
其中offset为long型,MessageSize为int32,表示data有多大,data为message的具体内容
7.2.2 分段
由于consumer消费消息时一定是根据偏移量(offset)顺序消费的,弄清楚了partition文件的结构后,所以很容易想到kafka解决查询效率的手段之一是将数据文件分段。
比如有100条Message,它们的offset是从0到99。假设将数据文件分成5段,第一段为0-19,第二段为20-39,以此类推,每段放在一个单独的数据文件里面,数据文件以该段中最小的offset命名。这样在查找指定offset的Message的时候,用二分查找就可以定位到该Message在哪个段中。
7.2.3 索引
数据文件分段使得可以在一个较小的数据文件中查找对应offset的message了,但是这依然需要顺序扫描才能找到对应offset的message。为了进一步提高查找的效率,kafka为每个分段后的数据文件建立了索引文件。
索引包含两个部分(均为4个字节的数字),分别为相对offset和position。
相对offset:因为数据文件分段以后,每个数据文件的起始offset不为0,相对offset表示这条message相对于其所属分段的数据文件中最小的offset的大小。举例,分段后的一个数据文件的offset是从20开始,那么offset为25的message在index文件中的相对offset就是25-20 = 5。
position:表示该条message在数据文件中的绝对位置。只要打开文件并移动文件指针到这个position就可以读取对应的message了。
值得一提的是,index文件中并没有为数据文件中的每条message建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。这样避免了索引文件占用过多的空间,从而可以将索引文件保留在内存中。但缺点是没有建立索引的message也不能一次定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小了。
八、参考及引用的资料
8.1 Kafka史上最详细原理总结 ----看完绝对不后悔 https://blog.csdn.net/lingbo229/article/details/80761778
8.2 震惊了!原来这才是kafka! https://www.jianshu.com/p/d3e963ff8b70
8.3 kafka leader选举机制原理 https://www.jianshu.com/p/1f02328a4f2e
8.4 http://kafka.apache.org/documentation/