Kafka消息队列原理总结

时间:2022-12-13 09:03:14

最近在测试kafka的读写性能,所以借这个机会了解了kafka的一些设计原理,既然作为分布式系统,我们还是按照分布式的套路进行分析。

 

Kafka的逻辑数据模型:

生产者发送数据给服务端时,构造的是ProducerRecord<Integer, String>(String topic, Integer key,String value)对象并发送,从这个构造函数可以看到,kafka的表面逻辑数据模型是key-value。当然api再发送前还会在这个基础上加入若干校验信息,不过这个对用户而言是透明的。

 

Kafka的分发策略:

跟很多分布式多备份系统类似,kafka的基本网络结构如下:

Kafka消息队列原理总结

一个节点(Broker)中存有不同partition的备份,一个parittion存在多份备份保存在不同节点上并且选举出一个作为leader跟客户端交互,一个topic拥有多个parittion。

默认的kafka分发算法是hash(key)%numPartitions,简单来就是哈希再取模。当然这个算法可以自定义,只要重写相关接口。如上图在一个四台主机上创建了一个有两个备份,四个分区partion的话题topic,但生产者需要发送某个key-value对象到消息队列里面时,创建连接时通过访问zookeeper,获取到一份leader partion列表(Broker1. Partition-0, Broker2. Partition1, Broker3. Partition-2, Broker4.Partition-3),再根据分发算法计算出这个对象应该要发送到哪个leader partion中。

 

Kafka的物理存储模型和查找数据的设计:

Kafka的物理存储模型比较简单,在kafka的物理持久化的存储中有分Segment的概念,每个Segment有两种类型的文件:索引文件***.index和日志文件(数据文件)***.log。两者的命名规则都是以这个Segment的第一条的消息逻辑偏移量作为文件名。索引是稀疏索引,目的在于减少索引文件的数据量,其文件的内容是key-value结构,key是消息的偏移量offeset(就是一个自增的序列号),value是对应的log文件的实际物理磁盘偏移量。

值得一提的是,跟其他正常分布式不一样,kafka并不支持根据给定的key查找该key对应的value值的能力,某种意义而言,逻辑数据模型中的key只是用来实现分发计算用的,所以使用kafka查找数据只能以指定消息的偏移量的放松实现。

整个查找过程:当要查找offset=888及后续的消息时,kafka先到该节点上找到对应的Segment。通过该Segment的index文件上用二分查找的方法找到最接近offset=888的纪录,比如886,然后找到886对应的物理磁盘偏移量999,这样就从log的磁盘偏移量找起,连续遍历了两个消息后就能找到888这个消息的数据(log文件中保留了每条消息的逻辑偏移量,长度和数据)。

 

Kafka的持久化策略设计:

Kafka的持久化设计是非常有特色的,和其他分布式系统不同,它没有自己维护一套缓存机制,而是直接使用了操作系统的文件系统(操作系统的文件系统自带pagecache)。这样的好处是减少了一次内存拷贝的消耗。其他分布式系统比如cassandra,自己在服务端维护了一份数据缓冲内存块datacache,当需要持久化时再调用操作系统的文件系统写入到文件中,这样就多了一次datacache到pagecache的拷贝消耗。这样的话,kafka的持久化管理关键是管理文件系统的pagecache的刷盘。

由于kafka采用了这种特别的持久化策略,所以在kafka中并没有其他分布式系统的重做日志。所以kafka在出现故障后的数据恢复策略有自己的一套:首先,kafka会通过配置文件配置pagecache定时或者定量刷盘的频率以保证即使出现故障也能把丢失的数据降低到最少。其次,pageche本身是操作系统管理维护的,跟kafka自身的服务进程没有关系,如果是kafka本身挂了的话,重启后还是能访问到pageche中的数据的。最后如果很不幸是kafka所在的一个节点的主机挂掉的话,那么重启主机和kafka后也可以从其他备份节点重新同步丢失的数据。

Kafka高性能的和持久化策略关系非常密切,这部分内容,也是整个kafka设计的精髓所在:

传统的观念认为磁盘的读写是非常低效的,所以一般系统都会自己管理一块内存datacache充当磁盘的缓存,只有需要的时候才去和磁盘交互。但是实际上,磁盘的低效的原因不在于磁盘io,而在于磁头的随机寻址。如果数据是顺序读写的话(也就是一次磁头寻址,连续io),其实速度是非常快的((Raid-5,7200rpm):顺序 I/O: 600MB/s)。而在传统的设计中虽然加入了内存作为缓存,但是为了保证数据的安全性还是得提供一份重做日志(每次的修改操作都要记录在重做日志redo.log中,以保证内存丢失后能根据重做日志进行恢复),并且当datacache里面的数据达到一定容量时刷新到磁盘的data文件中。但是kafka并没有使用这套常规设计,并没有自己维护一套datacache而是另辟蹊径,直接使用操作系统中的文件系统,并利用文件系统原有的pagecache作为数据缓存。减少了datacache到pagecache的拷贝消耗。并且顺序地进行磁盘io,这样大大提高了kafka写数据时持久化的效率。对于kafka的读数据这块,kafka也使用了Sendfile技术来提高读的效率,传统的读方案是读取磁盘的数据到pagecache中,然后从pagecache拷贝一份到用户进程的datacache中,datacache再拷贝到内核的socket缓存区中,最后从socket缓存区拷贝数据到网卡中发送。而Sendfile技术跳过了用户进程的datacache这一环节,直接读取磁盘的数据到pagecache中,然后从pagecache拷贝一份到socket缓存区中,最后从socket缓存区拷贝数据到网卡中发送。整个过程减少了两次拷贝消耗。


 

 

Kafka的节点间的数据一致性策略设计:

对于任何多节点多备份的分布式系统而言,数据的一致性问题都是绕不开的难点,一般的选择是要么优先考虑效率,这样可能就造成数据不一致甚至是数据丢失,要么选择保障数据一致性和数据安全性牺牲效率。在kafka的身上也存在这样的矛盾。

Kafka是一种分partion,多节点多备份的分布式系统,每个partion都可以存在多份备份,每个备份在不同的节点上。多个备份中会根据zookpeer的注册信息通过算法选举出其中一份作为leader,这个leader负责和客户端的读写访问进行交互。其他备份不参与跟客户端的交互。而是去跟leader partion交互同步数据。这样一来就可能出现主备之间数据不一致的情况。Kafka在客户端提供了一个配置选项props.put("acks", "all");--其中all表示生产者等待确认所有的备份数据都写入pagecache后再返回。可以设置为0(不等待任何确认),1(leader确认)或者其他小于备份数的数字。其他备份节点会异步去同步leader partion的数据,保持一致,当然如果在同步的过程中,leader partion出现数据丢失,那么这部分数据将永远丢失。

 

Kafka的备份和负载均衡:

Kafka的备份很明显,上文已经说过是通过讨论一致性问题已经交待清楚,至于Kafka的负载均衡,个人发现是严重依赖于zookeeper上的注册信息,通过一套算法来选取leader partion来实现kafka多节点的负载均衡。Zookeeper中保存了kafka几乎一切的重要信息,比如topic,每个topic下面的多个partion信息,主机节点信息(包括ip和端口),每个节点下的多个partion信息,每个partion的主备份信息,消费客户端的group_id分组信息,每个消费者信息等。通过这一堆信息进行算法计算最后得出负载均衡的方案,主要体现是选出让kafka效率性能达到最好的每个partion的leader。并且在zookeeper中注册监视器,一旦发现上述信息有变动则更新负载均衡方案。