2021-11-07

Kafka 设计思路


前言

当我们设计Kafka时,我们需要考虑以下几个问题:
1.如何利用操作系统的优化技术来高效地持久化日志文件和加快数据传输效率?
2.Kafka 生产者如何批量地发送消息,消费者采用拉取模型带来的优点都有哪些?
3.Kafka的副本机制如何运作,当故障发生时,怎么确保数据不会丢失?


一、文件系统的持久化与数据传输效率

人们普遍认为一旦涉及磁盘的访问,读写的性能就严重下降 实际上,现代的操作系统针对磁盘的读写已经做了 些优化方案来加快磁盘的访问速度 比如,预读( read-ahead )会提前将 个比较大的磁盘块读人内存 后写 rite-behind 会将很多小的逻辑写操作合并起来组合成一个大的物理写操作 并且,操作系统还会将主内存剩余的所有 闲内存 间都用作磁盘握存 disk cache/page cache), 所有的磁盘读写操作都会经过统 的磁盘缓存(除了直接νo会绕过磁盘缓存) 综合这几点优化特点,如果是针对磁盘的顺序访问,某些情况下它可能比随机的内存访问都要快,甚至可以和网络的速度相无几。
在这里插入图片描述

如图所示,应用程序写人数据到文件系统的做法是:在内存中保存尽可能多的数据,并在需要时将这些数据刷新到文件系统 但这里我们要做完全相反的事情,图中所有的数据都写入文件系统的持久化日志文件,但不进行刷新数据的任何调用 数据会首先被传输到磁盘缓存,操作系统随后会将这些数据定期自动刷新到物理磁盘消息系统内的消息从生产者保存到服务端,消费者再从服务端读取出来,数据的传输效 决定了生产者和消费者的性能 生产者如果每发送一条消息都直接通过网络发送到服务端,势必会造成过多的网络请求 如果我们能够将多条消息按照分区进行分组,并采用批量的方式 次发送 个消息集,并且对消息集进行压缩,就可以减少网络传输的带宽,进一步提高数据的传输效率。

消费者要读取服务端的数据,需要将服务端的磁盘文件通过网络发送到消费者进程,而网络发送通常涉及不同的网络节点,如图所示,传统读取磁盘文件的数据在每次发送到网络时,都需要将页面缓存先保存到用户缓存,然后在读取消息时再将其复制到内核空间,具体步骤如下:
(1) 操作系统将数据从磁盘中读取文件到内核空间里的页面缓存
(2)应用程序将数据从内核 间读人用户 间的缓冲区
(3)应用程序将读到的数据写回内核 间并放入socket缓冲区
(4)操作系统将数据从socket缓冲区复制到网卡接口,此时数据才能通过网络发送归。
在这里插入图片描述
结合Kafka 的消息有多个订阅者的使用场景,生产者发布的消息一般会被不同的消费者消费多次。如下图所示,使用“零拷贝技术”( zero-copy )只需将磁盘文件的数据复制到页面缓存中 次,然后将数据从页面缓存直接发送到网络中(发送给不同的使用者时,都可以重复使用同一个页面缓存),避免了重复的复制操作 这样,消息使用的速度基本上等同于网络连接的速度。
在这里插入图片描述
这里我们用一个示例来对比传统的数据复制和“零拷贝技术”这两种方案 假设有 10个消费者,传统复制方式的数据复制次数是 4*10=40次,而“零拷贝技术”只需 1+ 10 = 11 (一次表示从磁盘复制到页面缓存,另外10 表示 10个消费者各自读取一次页面缓存) 显然,“零拷贝技术”比传统复制方式需要的复制次数更少 越少的数据复制,就越能更快地读取到数据;延迟越少,消费者的性能就越好。

二、生产者与消费者

Kafka 生产者将消息直接发送给分区主副本所在的消息代理节点,并不需要经过任何的中间路由层 为了做到这一点,所有消息代理节点都会保存一份相同的元数据,这份元数据记录了每个主题分区对应的主副本节点 生严者客户端在发送消息之前,会向任意一个代理节点请求元数据,井确定每条消息对应的目标节点 然后把消息直接发送给对应的目标节点。
在这里插入图片描述
如图所示,生产者客户端有两种方式决定发布的消息归属于哪个分区:通过随机方式将请求负载到不同的消息、代理节点,或者使用“分区语义函数”将相同键的所有消息发布到同一个分区分区语义, Kafka暴露了一个接口,允许用户指定消息的键如何参与分区比如,我们可以将用户编号作为消息的键,因为对相同用户编号散列后的值是罔定的,所以对应的分区也是固定的。

读取消息推送模式

在这里插入图片描述
消息代理主动地“推送”消息、给下游的消费者,由消息代理控制数据传输的速率,但是消息代理对下游消费者是否能及时处理不得而知。如果数据的消费速率低于产生速率,消费者会处于超负荷状态,那么发送给消费者的消息就会堆积得越来越多 而且,推送方式也难以应付不同类型的消费者,因为不同消费者的消费速率不一都相同,消息代理要调整不同消费者者的传输速率,并让每个消费者充分利用系统的资源 。

读取消息拉取模式

在这里插入图片描述
消费者从消息代理主动地“拉取”数据,消息代理是无状态的,它不需要标记哪些消息被消费者处理过,也不需要保证一条消息只会被一个消费者处理 而且,不同的消费者可以按照向己最大的处理能力来拉取数据,即使有时候某个消费者的处理速度稍微落后,它也不会影响其他的消费者,并且在这个消费者恢复处理速度后,仍然可以追赶之前落后的数据。

Kafka采用了基于拉取模型的消费状态处理,它将主题分成多个有序的分区,任何时刻每个分区都只被 个消费者使用 并且,消费者会记录每个分区的消费进度( 即偏移量) 每个消费者只需要为每个分区记录一个整数值 ,而不需要像其他消息系统那样记录每条消息的状态 假设有 10000条消息,传统方式需要记录10000条消息的状态 ;如果用Kafka的分区机制,假设有 10个分区,每个分区 1000条消息,总共只需要记录 分区的消费状态(需要保存的状态数据少了很多,而且也没有了锁)。

和生产者采用批量发送消息类似,消费者拉取消息也可以一次拉取一批消息 消费者客户端拉取消息,然后处理这一批消息,这个过程一般套在一个死循环里,表示消费者永远处于消费消息的状态(因为消息系统的消息总是一直产生数据,所以消费者也要一直消费消息) 消费者采用拉取方式消费消息有一个缺点 :如果消息代理没有数据或者数据量很少,消费者可能需要不断地轮询,并等待新数据的到来(拉取模式主动权在消费者手里,但是消费者并不知道消息代理有没有新的数据;如果是推送模式,只有新数据产生时,消息代理才会发送数据给消费者,就不存在这种问题) 解决这个问题的方案是:允许消费者的拉取请求以阻塞式、长轮询的方式等待,直到有新的数据到来 我们可以为消费者客户端设置“指定的字节数量”,表示消息代理在还没有收集足够的数据时,客户端的拉取请求就不会立即返回。

三、副本机制和容错处理

Kafka的副本机制会在多个服务端节点上对每个主题分区的日志进行复制,当集群中的某个节点出现故障时,访问故障节点的请求会被转移到其他正常节点的副本上。
副本的单位是主题的分区, Kafka每个主题的每个分区都有1个主副本以及0个或多个备份副本。备份副本会保持和主副本的数据同步,用来在主副本失效时替换为主副本,所有的读写请求总是路由到分区的主副本。
虽然生产者可以通过负载均衡策略将消息分配到不同的分区,但如果这些分区的主副本都在同一个服务器上,就会存在数据热点问题 。
在这里插入图片描述
因此,分区的主副本应该均匀地分配到各个服务器上。通常,分区的数量要比服务器多很多,所以每个服务器都可以成为 些分区的主副本,也能同时成为一些分区的备份副本。
在这里插入图片描述
备份副本始终尽量保持与主副本的数据同步 备份副本的日志文件和主副本的日志总是相同的,它们都有相同的偏移量和相同顺序的消息 备份副本从主副本消费消息的方式和普通的消费者一样,只不过备份副本会将消息运用到自己的本地日志文件(备份副本和主副本都在服务端,它们都会将收到的分区数据持久化成日志文件) 普通的消费者客户端拉取到消息后并不会持久化,而是直接处理。
分布式系问故障容错盹需要跚地时点是否处于存呐 山俏的存活定义固有两个条件:

  • 节点必须和ZK保持会话;
  • 如果这个节点是某个分区的备份副本,它必须对分区主副本的写操作进行复制,并且复制的进度不能落后太多。

满足这两个条件,叫作“正在同步中”( in-sync 每个分区的主副本会跟踪正在同步中的备份副本节点( In Sync Replicas ,即ISR 如果一个备份副本掉、没有响应或者落后太多,主副本就会将其从同步副本集合中移除 反之,如果备份副本重新赶上主副本,它就会加入到主副本的同步集合中。

Kafka 中, 条消息只有被ISR集合的所有副本都运用到本地的日志文件,才会认为消息被成功提交了 任何时刻,只要ISR至少有一个副本是存活的,Kafka就可以保证“→条消息一旦被提交,就不会丢失” 只有已经提交的消息才能被消费者消费,因此消费者不用担心会看到因为主副本失败而丢失的消息 下面我们举例分析Kafka的消息提交机制如何保证消费者看到的数据是一致的。

  • 生产者发布了 rn条消息,但都还没有提交(没有完全复制到ISR中的所有副本)如果没有提交机制,消息写到主副本的节点就对消费者立即可见,即消费者可以 即看到这 10条消息但之后主副本挂掉了,这 10条消息实际上就丢失了 而消费者之前能看到这 10 条丢失的数据,在主副本挂掉后就看不到了,导致消费者看到的数据出现了不一致。
  • 如果有提交机制的保证,并且生产者发布的 10条消息还没有提交,则对消费者不可见 即使10条消息都已经写入主副本,但是它们在还没有来得及复制到其他备份副本之前,主副本就挂掉了 那么,这 10条消息就不算写入成功,生产者会重新发送这 10条消息 当这 10条消息成功地复制到ISR 的所有副本后,它们才会认为是提交的,即对消费者才是可见的 在这之后,即使主副本挂掉了也没有关系,因为原先消费者能看到主副本的 10条消息,在新的主副本上也能看到这10条消息,不会出现不一致的情况。

四、基础架构与流程

Kafka的基础架构和工作流程参考如下:
在这里插入图片描述
在这里插入图片描述
Kafka中消息是以topic进行分类的,生产者生产消息,消费者消费消息,都是面向topic的。

Topic是逻辑上的概念,而partition(分区)是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是producer生产的数据。Producer生产的数据会被不断追加到该log文件末端,且每条数据都有自己的offset。消费者组中的每个消费者,都会实时记录自己消费到哪个offset,以便出错恢复时,从上次的位置继续消费。参考如下:
在这里插入图片描述
由于生产者生产的消息会不断追加到log文件末尾,为防止log文件过大导致数据定位效率低下,Kafka采取了分片和索引的机制,将每个partition分为多个segment。(由log.segment.bytes决定,控制每个segment的大小,也可通过log.segment.ms控制,指定多长时间后日志片段会被关闭)每个segment对应两个文件——“.index”文件和“.log”文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic名称+分区序号。例如:bing这个topic有3个分区,则其对应的文件夹为:bing-0、bing-1和bing-2。

索引文件和日志文件命名规则:每个 LogSegment 都有一个基准偏移量,用来表示当前 LogSegment 中第一条消息的 offset。偏移量是一个 64位的长整形数,固定是20位数字,长度未达到,用 0 进行填补。

index和log文件以当前segment的第一条消息的offset命名。index文件记录的是数据文件的offset和对应的物理位置,正是有了这个index文件,才能对任一数据写入和查看拥有O(1)的复杂度,index文件的粒度可以通过参数log.index.interval.bytes来控制,默认是是每过4096字节记录一条index。下图为index文件和log文件的结构示意图:
在这里插入图片描述
查找message的流程(比如要查找offset为170417的message):

1.首先用二分查找确定它是在哪个Segment文件中,其中0000000000000000000.index为最开始的文件,第二个文件为0000000000000170410.index(起始偏移为170410+1 = 170411),而第三个文件为0000000000000239430.index(起始偏移为239430+1 = 239431)。所以这个offset = 170417就落在第二个文件中。其他后续文件可以依此类推,以起始偏移量命名并排列这些文件,然后根据二分查找法就可以快速定位到具体文件位置。
2.用该offset减去索引文件的编号,即170417 - 170410 = 7,也用二分查找法找到索引文件中等于或者小于7的最大的那个编号。可以看出我们能够找到[4,476]这组数据,476即offset=170410 + 4 = 170414的消息在log文件中的偏移量。
3.打开数据文件(0000000000000170410.log),从位置为476的那个地方开始顺序扫描直到找到offset为170417的那条Message。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值