分布式消息中间件-KAFKA 原理

170 阅读11分钟

kafka 的相关特性

数据可靠性

partition 的高可用副本机制

kafka 的每个 topic 都可以分为多个 partition,并且多个 partition会均匀分布在集群的各个节点下。虽然这种方式能够有效的对数据进行分片,但是对于每个 partition 来说,都是单点的,当其中一个partition 不可用的时候,那么这部分消息就没办法消费。所以 kafka 为了提高 partition 的可靠性而提供了副本的概念(replica)通过副本机制来实现冗余备份。

每个分区可以有多个副本,并且在副本集合中会存在一个 leader 副本,所有的读写请求都是由 leader 副本来进行处理。其余的副本都作为 follower 副本,follower 副本会从 leader 副本同步消息日志。

一般情况下,同一个分区的多个副本会被均匀分配到集群总的不同 broker 上,当 leader 副本所在的 broker 出现故障后,可以重新选举新的 leader 副本继续对外提供服务。通过这样的副本机制来提高 kafka 集群的可用性。

副本分配算法

将所有 N Broker 和待分配的 i 个 Partition 排序.

将第 i 个 Partition 分配到第(i mod n)个 Broker 上.

将第 i 个 Partition 的第 j 个副本分配到第((i + j) mod n)个 Broker 上

数据一致性

kafka 提供了数据复制算法保证,如果 leader 发生故障或挂掉,一个新的 leader 被选举并被接受客户端的消息成功写入。kafka 确保从同步副本列表中选举一个副本为 leader;

leader 负责维护和跟踪 ISR(in-sync replicas, 副本同步队列)中所有 follower 之后的状态。当 producer 发送一条消息到 broker 后,leader 写入消息并复制到所有 follower。消息提交之后才被成功复制到所有的同步副本。

既然有副本机制,就一定涉及到数据同步的概念

kafka 副本机制中的几个概念

kafka 分区下有可能有很多个副本(replica)用于实现冗余,从而进一步实现高可用。副本根据角色的不同可以分为3类:

leader 副本:响应 clients 端读写请求的副本

follower副本:被动备份 leader 副本中的数据,不能响应 clients 端读写请求。

ISR 副本:包含了 leader 副本和所有与 leader 副本保持同步的 follower 副本(如何判断是否与 leader 同步后面会提到,每个 kafka 副本对象都有两个重要的属性:LEO 和 HW)

LEO:即日志末端位移(log end offset),记录了该副本底层日志(log)中下一条消息的位移值。注意是下一条消息 也就是说如果 LEO=10,那么表示该副本保存了10条消息,位移值范围是[0,9]

HW:即上面提到的水位值。对于同一个副本对象而言,其HW 值不会大于 LEO 值。小于等于 HW 值的所有消息都被认为是“已备份的”。同理,leader 副本和 follower 副本的 HW 更新是有区别的

副本协同机制

消息的读写操作只会由 leader 节点来接收和处理。follower 副本只负责同步数据以及当 leader 副本所在的 broker 挂了以后,会从 follower 副本中选举新的 leader。

image.png

写请求首先由 leader 副本处理,之后 follower 副本会从 leader 上拉取写入的消息,这个过程会有一定的延迟,导致 follower 副本中保存的消息略少于 leader 副本,但是只要没有超过阈值都可以容忍。但是如果一个 follower 副本出现异常,比如宕机、网络断开等原因长时间没有同步到消息,这个时候,leader 就会把它踢出去。kafka 通过 ISR 集合来维护一个分区副本信息。

ISR

ISR 表示目前“可用且消息量与 leader 相差不多的副本集合, 这是整个副本集合的一个子集”。怎么去理解可用和相差不多 这两个词呢?具体来说,ISR 集合中的副本必须满足两个条件

  1. 副本所在节点必须维持这 zookeeper 的连接
  2. 副本最后一条消息的 offset 和 leader 副本的最后一条信息的 offset 之间的差值不能超过指定的阈值(replica.lag.time.max.ms
    • replica.lag.time.max.ms: 如果该 follower 在此间隔内一直没有追上过 leader 的所有消息,则该 follower 就会被剔除 ISR 列表
    • ISR 数 据 保 存 在 Zookeeper 的 /brokers/topics//partitions//state 节点中。

HW&LEO

关于 follower 副本同步的过程中,还有两个关键的概念,HW(highWatermark)和 LEO(log end offset)。这两个参数跟 ISR 密切相关。HW 标记了一个特殊的 offset,当消费者处理消息的时候,只能拉取到 HW 之前的消息,HW 之后的消息对消费者来说是不可见的。也就是说,取 partition 对应 ISR 中最小的 LEO作为 HW,consumer 最多只能消费到 HW 所在的位置。每个 replica 都有HW,leader 和 follower 各自维护更新自己的 HW 状态。一条消息只有被 ISR 里所有 follower 都从 leader 复制过去了才会被认为已提交。这样就避免了部分数据被写进了 leader 还没来得及被任何 follower 复制就宕机了,而造成的数据丢失(consumer 无法消费这些数据)。而对于 producer 而言,它可以选择是否等待消息 commit,这可以通过 acks 来设置。这种机制确保了只要 ISR 有一个或以上的 follower,一条被 commit 的消息就不会丢失。

ISR 的设计原理

在所有的分布式存储中,冗余备份是一种常见的设计方式,而 常用的模式有同步复制和异步复制,按照 kafka 这个副本模型 来说

如果采用同步复制,那么需要要求所有能工作的 Follower 副 本都复制完,这条消息才会被认为提交成功,一旦有一个 follower 副本出现故障,就会导致 HW 无法完成递增,消息就 无法提交,消费者就获取不到消息。这种情况下,故障的 Follower 副本会拖慢整个系统的性能,设置导致系统不可用 如果采用异步复制,leader 副本收到生产者推送的消息后,就 认为次消息提交成功。follower 副本则异步从 leader 副本同 步。这种设计虽然避免了同步复制的问题,但是假设所有 follower 副本的同步速度都比较慢他们保存的消息量远远落后 于 leader 副本。而此时 leader 副本所在的 broker 突然宕机, 则会重新选举新的 leader 副本,而新的 leader 副本中没有原 来 leader 副本的消息。这就出现了消息的丢失。

kafka 权衡了同步和异步的两种策略,采用 ISR 集合,巧妙解 决了两种方案的缺陷:当 follower 副本延迟过高,leader 副本 则会把该 follower 副本提出 ISR 集合,消息依然可以快速提交。 当 leader 副本所在的 broker 突然宕机,会优先将 ISR 集合中 follower 副本选举为 leader,新 leader 副本包含了 HW 之前 的全部消息,这样就避免了消息的丢失。

高性能

顺序读写

kafka 的 message 是不断追加到本地磁盘文件末尾的,而不是随机的写入,这使得 kafka 写入吞吐量得到了显著提示。

在一定条件的测试下,磁盘是顺序读写可以达到 53.2 每秒,比内存的随机读写还要快。

文件索引

logSegment

假设 kafka 以 partition 为最小存储单位,那么我们可以想象当 kafka producer 不断发送消息,必然会引起 partition 文件的无限扩张,这样对于消息文件的维护以及被消费的消息的清理带来的非常大的挑战,所以 kafka 已 segment 为单位又把 partition 进行细分。每个 partition 相当于一个巨型文件被平均分配到多个大小相等的 segment 数据文件中(每个 segment 文件中的消息不一定相等),这种特性方便已经被消费的消息的清理,提高磁盘利用率。

  • log.segment.bytes=107370 (设置分段大小),默认是1GB,我们把这个值调小以后,可以看到日志分段的效果
  • 抽取其中3个分段来进行分析

image.png

segment file 由2大部分组成,分别是 index file 和 data file,此2个文件一一对应,成对出现,后缀为“.index”和“.log” 分别表示 segment 索引文件、数据文件

segment 文件命名规则:全局的第一个 segment 从 0 开始,后续每个 segment 文件名为上一个 segment 文件最后一条消息的 offset 进行递增。数值最大为 64 位 long 大小,20 位数字字符长度,没有数字用0填充。

segment 中 index 和log 的对应关系

从所有分段中,找一个分段进行分析 为了提高查找消息的性能,为每一个日志文件添加 2 个索 引索引文件:OffsetIndex 和 TimeIndex,分别对应*.index 以及*.timeindex, TimeIndex 索引文件格式:它是映射时间 戳和相对 offset

image.png 如图所示,index 中存储了索引以及物理偏移量。 log 存 储了消息的内容。索引文件的元数据执行对应数据文件中 message 的物理偏移地址。举个简单的案例来说,以 [4053,80899]为例,在 log 文件中,对应的是第 4053 条记 录,物理偏移量(position)为 80899. position 是 ByteBuffer 的指针位置

在 partition中如何通过 offset 查找 message

查找的算法是

  1. 根据 offset 的值,查找 segment 段中的 inedx 索引文件。由于索引文件名是以上一个文件的最后一个 offset 进行命名的,所以使用二分查找算法能够根据 offset 快速定位到指定的索引文件。
  2. 找到索引文件后,根据 offset 进行定位,找到索引文件中的符合范围的索引(kafka 采用稀疏索引的方式提供查找性能)
  3. 得到 position 以后,再到对应的 logs 文件,从 position 处开始查找 offset 对应的消息,将每条消息的 offset 与目标 offset 进行比较,直到找到消息

比如说,我们要查找 offset=2490 这条消息,那么先找到 00000000000000000000.index, 然后找到[2487,49111]这 个索引,再到 log 文件中,根据 49111 这个 position 开始 查找,比较每条消息的 offset 是否大于等于 2490。最后查 找到对应的消息以后返回

批量读写和文件压缩

kafka 还提供了“日志压缩(log compaction)”功能,通过这个功能可以有效的减少日志文件的大小,缓存磁盘紧张的情况。在很多实际场景中,消息的 key 和 value 的值之间的对应关系是不断变化的,就像数据库中的数据会不断修改一样,消费者只关心 key 对应的最新的 value。因此,我们可以开启 kafka 的日志压缩功能,服务端会在后台启动 cleaner 线程池,定期将相同的 key 进行合并,只保留最新的 value 值。日志压缩原理:

image.png

零拷贝

操作系统的虚拟内存分为了两块,一部分是内核空间,一部分是用户空间。这样就可以避免用户直接操作内核,保证内核安全。

如果用户要从磁盘读取数据(比如 kafka 消费消息),必须要先把数据从磁盘拷贝到内核缓冲区,然后从内核缓存区到用户缓存区,最后才能返回给用户。

image.png

通过"零拷贝"技术,可以使数据不经过用户缓冲区,直接把数据拷贝到网卡。减省了中间没必要的数据复制操作和上下文切换的次数。现代的 unix 操作系统提供一个优化的代码路径,用于将数据从页缓存传输到 socket;在linux 中,是通过 sendfile 系统调用来完成的。java 提供了访问这个系统调用的方法:fileChannel.transferToAPI

image.png

总结

本节大概说明了 kafka 数据可靠性和高性能的原理:

  • 数据可靠性是靠 partition 的副本机制及 ISR 机制来保证。
  • 高性能则因为使用到文件顺序读写、索引、压缩及"零拷贝"。