一、kafka简介
Kafka是最初由Linkedin公司开发,是一个分布式、分区的、多副本的、多订阅者,基于zookeeper协调的分布式日志系统(也可以当做MQ系统),常见可以用于web/nginx日志、访问日志,消息服务等等,Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目。
主要应用场景是:日志收集系统和消息系统。
Kafka主要设计目标如下:
-
以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间的访问性能。
-
高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条消息的传输。
-
支持Kafka Server间的消息分区,及分布式消费,同时保证每个partition内的消息顺序传输。
-
同时支持离线数据处理和实时数据处理。
二、整体架构
如上图所示,kafka由3种角色组成:消费者(produce)、代理服务器(broker)、消费者(consumer)
-
Broker:每一台机器叫一个Broker,多个Broker组成一个Kafka集群
-
Producer:日志消息生产者,负责往集群中写数据
-
Consumer:消息的消费者,负责从集群中读数据
-
Consumer Group:每个Consumer属于一个特定的Consumer Group
-
Topic:不同消费者去指定的Topic中读,不同的生产者往不同的Topic中写
-
Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列。
-
Message:日志消息,kafka集群内流通的基本单位
-
Segment:partition物理上由多个segment组成
-
Offset:每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中。partition中的每个消息都有一个连续的序列号叫做offset,用于partition唯一标识一条消息。
三、 Kafka高效文件存储设计
Kafka高效文件存储设计特点:
-
Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
-
通过索引信息(内存缓存)可以快速定位message和确定response的最大大小。
-
通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。
-
通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。
3.1 partition存储结构
-
Topic与Partition的关系:一个topic被分成多个partition,每个partition在磁盘上就是一个文件夹。
-
partition命名规则:topicName_(partitionNum-1)。
-
一个partition由多个segment组成,一个segment由两部分组成:一个index索引文件和一个log数据文件。
-
每个partiton只需要支持顺序读写就行了,segment文件生命周期由服务端配置参数决定。
-
segment命名规则:partition全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值。数值最大为64位long大小,19位数字字符长度,没有数字用0填充。
3.2 segment存储结构
-
-
index索引文件中保存的是messageId,offsetId的关系,采取稀疏索引存储方式
-
log数据文件保存的是全局messageId,offsetId的关系
其中以索引文件中元数据3,497为例,依次在数据文件中表示第3个message(在全局partiton表示第368772个message)、以及该消息的物理偏移地址为497。
-
从上述图中了解到segment data file由许多message组成,下面详细说明message物理结构如下:
参数说明:
关键字 | 解释说明 |
---|---|
8 byte offset | 在parition(分区)内的每条消息都有一个有序的id号,这个id号被称为偏移(offset),它可以唯一确定每条消息在parition(分区)内的位置。即offset表示partiion的第多少message |
4 byte message size | message大小 |
4 byte CRC32 | 用crc32校验message |
1 byte “magic" | 表示本次发布Kafka服务程序协议版本号 |
1 byte “attributes" | 表示为独立版本、或标识压缩类型、或编码类型。 |
4 byte key length | 表示key的长度,当key为-1时,K byte key字段不填 |
K byte key | 可选 |
value bytes payload | 表示实际消息数据。 |
3.3 通过offset查找message过程
由于kafka中设计由consumer来保存offset或指定offset来消费message,所以读取/查找的过程就是通过offset去查找message,举例读取offset=368776的message,需要通过下面2个步骤查找:
-
第一步查找segment file上述segment内部对应关系图为例,其中00000000000000000000.index表示最开始的文件,起始偏移量(offset)为0.第二个文件00000000000000368769.index的消息量起始偏移量为368770 = 368769 + 1.同样,其他后续文件依次类推,以起始偏移量命名并排序这些文件,只要根据offset 二分查找文件列表,就可以快速定位到具体文件。当offset=368776时定位到00000000000000368769.index|log
-
第二步通过segment file查找message通过第一步定位到segment file,当offset=368776时,依次定位到00000000000000368769.index的元数据物理位置和00000000000000368769.log的物理偏移地址,然后再通过00000000000000368769.log顺序查找直到offset=368776为止。
从上述图可知这样做的优点,segment index file采取稀疏索引存储方式,它减少索引文件大小,通过mmap(Linux内存映射)可以直接内存操作,稀疏索引为数据文件的每个对应message设置一个元数据指针,它比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。
3.4 数据顺序写入过程
因为硬盘是机械结构,每次读写都会寻址,写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最“讨厌”随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,Kafka就是使用顺序I/O。
每条消息都被append到该Partition中,属于顺序写磁盘,因此效率非常高。
message写入:
-
消息从java堆转入page cache(即物理内存)。
-
由异步线程刷盘,消息从page cache刷入磁盘。
3.5 数据zero-copy读取过程
即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以Kafka的数据并不是实时的写入硬盘,它充分利用了现代操作系统分页存储来利用内存提高I/O效率。
在Linux Kernal 2.2之后出现了一种叫做“零拷贝(zero-copy)”系统调用机制,就是跳过“用户缓冲区”的拷贝,建立一个磁盘空间和内存空间的直接映射,数据不再复制到“用户态缓冲区”系统上下文切换减少2次,可以提升一倍性能。
四、ISR副本机制
kafka通过消息副本机制提供高可用消息服务,其副本管理不是在topic消息队列,而是在topic的数据分片(partition)。在配置文件制动partition的副本个数,在多个副本中,只有一个主副本leader,其他均为次副本(slave)。所有针对这个数据分片的消息读写均由主副本响应,次副本从主副本同步数据,当主副本发生故障,选举次副本
ISR机制
将所有次级副本分成两个集合:ISR集合、非ISR集合,ISR集合的数据是即时与主副本数据保持一致的,非ISR集合允许落后于主副本数据,在主副本发生故障时,会从ISR集合选举主副本,维持一致性。数据写入时候,当主副本信息与ISR集合数据均写成功才算成功。假设ISR集合数为f+1,那么最多允许f个副故障。
副本同步机制
Producer在发布消息到某个Partition时,先通过ZooKeeper找到该Partition的Leader(主副本),然后无论该Topic的Replication Factor为多少,Producer只将该消息发送到该Partition的Leader(主副本)。Leader(主副本)会将该消息写入其本地Log。每个Follower(次级副本)都从Leader(主副本) pull数据。这种方式上,Follower(次级副本)存储的数据顺序与Leader保持一致。Follower(次级副本)在收到该消息并写入其Log后,向Leader(主副本)发送ACK。一旦Leader(主副本)收到了ISR中的所有Replica的ACK,该消息就被认为已经commit了,Leader(主副本)将增加HW并且向Producer发送ACK。
为了提高性能,每个Follower(次级副本)在接收到数据后就立马向Leader(主副本)发送ACK,而非等到数据写入Log中。因此,对于已经commit的消息,Kafka只能保证它被存于多个Replica的内存中,而不能保证它们被持久化到磁盘中,也就不能完全保证异常发生后该条消息一定能被Consumer消费。
Consumer读消息也是从Leader读取,只有被commit过的消息才会暴露给Consumer。