1.2 Kafka 的设计与实现

1.3 Kafka 的设计与实现

下面我们会从3个角度分析Kafka的一些设计思路,并尝试回答下面3个问题。

  • 如何利用操作系统的优化技术来高效地持久化日志文件和加快数据传输效率?
  • Kafka的生产者如何批量地发送消息,消费者采用拉取模型带来的优点都有哪些?
  • Kafka的副本机制如何工作,当故障发生时,怎么确保数据不会丢失?

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

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

如图 1-6 (左)所示,应用程序写入数据到文件系统的一般做法是:在内存中保存尽可能多的数据,并在需要时将这些数据刷新到文件系统。 但这里我们要做完全相反的事情,右图中所有的数据都立即写入文件系统的持久化日志文件,但不进行刷新数据的任何调用 。数据会首先被传输到磁盘缓存,操作系统随后会将这些数据定期自动刷新到物理磁盘。

消息系统内的消息从生产者保存到服务端,消费者再从服务端读取出来,数据的传输效率决定了生产者和消费者的性能。 生产者如果每发送一条消息都直接通过网络发送到服务端,势必会造成过多的网络请求 。 如果我们能够将多条消息按照分区进行分组,并采用批量的方式一次发送一个消息集,并且对消息集进行压缩,就可以减少网络传输的带宽,进一步提高数据的传输效率

在这里插入图片描述

消费者要读取服务端的数据,需要将服务端的磁盘文件通过网络发送到消费者进程,而网络发送通常涉及不同的网络节点。 如图 1-7(左)所示,传统读取磁盘文件的数据在每次发送到网络时,都需要将页面缓存先保存到用户缓存,然后在读取消息时再将其复制到内核空间,具体步骤如下 。

  1. 操作系统将数据从磁盘中读取文件到内核空间里的页面缓存 。
  2. 应用程序将数据从内核空 间读人用户空间的缓冲区 。
  3. 应用程序将读到的数据写回内核空间并放入socket缓冲区 。
  4. 操作系统将数据从 socket缓冲区复制到网卡接口,此时数据才能通过网络发送归去 。

结合Kafka的消息有多个订阅者的使用场景,生产者发布的消息一般会被不同的消费者消费多次。如图 1-7(右)所示,使用“零拷贝技术”( zero-copy )只需将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的使用者时,都可以重复使用同一个页面缓存),避免了重复的复制操作。 这样,消息使用的速度基本上等同于网络连接的速度了 。
在这里插入图片描述

这里我们用一个示例来对比传统的数据复制和“零拷贝技术”这两种方案 。 假设有 10个消费者,传统复制方式的数据复制次数是4 × 10=40次,而“零拷贝技术”只需 1 + 10 =11 次 (一次表示从磁盘复制到页面缓存,另外 10次表示 10个消费者各自读取一次页面缓存)。 显然,“零拷贝技术”比传统复制方式需要的复制次数更少。 越少的数据复制,就越能更快地读取到数据;延迟越少,消费者的性能就越好。


1.3.2 生产者与消费者

Kafka的生产者将消息直接发送给分区主副本所在的消息代理节点,并不需要经过任何的中间路由层。 为了做到这一点,所有消息代理节点都会保存一份相同的元数据,这份元数据记录了每个主题分区对应的主副本节点。生产者客户端在发送消息之前,会向任意一个代理节点请求元数据,并确定每条消息对应的目标节点,然后把消息直接发送给对应的目标节点 。

如图1-8所示,生产者客户端有两种方式决定发布的消息归属于哪个分区:通过随机方式将请求负载到不同的消息、代理节点(图 1-8左),或者使用“分区语义函数”将相同键的所有消息发布到同一个分区(图 1-8右)。 对于分区语义, Kafka暴露了一个接口,允许用户指定消息的键如何参与分区。比如,我们可以将用户编号作为消息的键,因为对相同用户编号散列后的值是固定的,所以对应的分区也是固定的 。

在这里插入图片描述
在 1.3.1 节中,生产者采用批量发送消息集的方式解决了网络请求过多的问题。生产者会尝试在内存中收集足够数据,并在一个请求中一次性发送一批数据。另外,我们还可以为生产者客户端设置“在指定的时间内收集不超过指定数量的消息” 。 比如,设置消息大小上限等于64字节 ,延迟时间等于100毫秒,表示在 100毫秒内消息大小达到64字节要立即发送;如果在 100毫秒时还没达到 64字节,也要把已经收集的消息发送州去。 客户端采用这种缓冲机制,在发送消息前会收集尽可能多的数据,通过每次牺牲一点点额外的延迟来换取更高的吞吐量。 相应地,服务端的l/O消耗也会大大降低。

如图1-9所示,消费者读取消息有两种方式。 第一种是消息代理主动地“推送”消息、给下游的消费者(图 1-9左图),由消息代理控制数据传输的速率,但是消息代理对下游消费者是否能及时处理不得而知。 如果数据的消费速率低于产生速率,消费者会处于超负荷状态,那么发送给消费者的消息就会堆积得越来越多。 而且,推送方式也难以应付不同类型的消费者,因为不同消费者的消费速率不一定都相同,消息代理要调整不同消费的传输速率,并让每个消费者充分利用系统的资源。 这种方式实现起来比较困难;
第二种读取方式是消费者从消息代理主动地“拉取”数据(图 1-9右),消息代理是无状态的,它不需要标记哪些消息被消费者处理过,也不需要保证一条消息只会被一个消费者处理。 而且,不同的消费者可以按照向己最大的处理能力来拉取数据,即使有时候某个消费者的处理速度稍微落后,它也不会影响其他的消费者,并且在这个消费者恢复处理速度后,仍然可以追赶之前落后的数据 。

在这里插入图片描述
因为消息系统不能作为严格意义上的数据库,所以保存在消息系统中的数据,在不用之后应该及时地删除掉并释放磁盘空间。 消息需要删除,其原因一般是消息被消费之后不会再使用了,大多数消息系统会在消息代理记录关于消息是否已经被消费过的状态 : 当消息从消息代理发送给消费者时(基于推送模型),消息代理会在本地记录这条消息“已经被消费过了” 。 但如果消费者没能处理这条消息(比如由于网络原因、请求超时或消费者挂掉),就会导致“消息丢失” 。 解决消息丢失的一种办法是添加应答机制,消息代理在发送完消息后只把消息标记为“已发送”,只有收到消费者返回的应答信息才表示“己消费” 。 但还是会存在一个问题:消费者处理完消息就失败了,导致应答没有返回给消息代理,这样消息代理又会重新发送消息,导致消息被重复处理。 这种方案还有一个缺点:消息代理需要保存每条消息的多种状态(比如,消息状态为“已发送”时,消息代理需要锁住这条消息,保证- 消息不会发送两次) , 这种方式需要在客户端和服务端做一些复杂的状态一致性保证 。

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

和传统方式需要跟踪每条消息的应答不同,Kafka的消费者会定时地将分区的消费进度保存成检查点文件,表示“这个位置之前的消息都已经被消费过了”。传统方式需要消费者发送每条消息的应答,服务端再对应答做出不同的处理;而Kafka只需要让消费者记录消费进度,服务端不需要记录消息的任何状态。 除此之外,让消费者记录分区的消费进度还有一个好处:消费者可以“故意”回退到某个旧的偏移量位置,然后重新处理数据。 虽然这种处理方式看起来违反了队列模型的规定(一条消息发送给队列的一个消费者之后,就不会被其他消费者再次处理),但在实际运用中,很多消费者都需要这种功能。比如,消费者的处理逻辑代码出现了问题,在部署并启动消费者后,需要处理之前的消息并重新计算

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


1.3.3 副本机制和容错处理

Kafka的副本机制会在多个服务端节点(简称节点 即消息代理节点 )上对每个主题分区的日志进行复制。当集群中的某个节点出现故障时,访问故障节点的请求会被转移到其他正常节点的副本上。副本的单位是主题的分区, Kafka每个主题的每个分区都有一个主副本以及0个或多个备份副本。 备份副本会保持和主副本的数据同步,用来在主副本失效时替换为主副本。

如图1-10所示,所有的读写请求总是路由到分区的主副本。 虽然生产者可以通过负载均衡策略将消息分配到不同的分区,但如果这些分区的主副本都在同一个服务器上(1-10左图),就会存在数据热点问题。 因此,分区的主副本应该均匀地分配到各个服务器上(见图 1-10右图)。通常,分区的数量要比服务器多很多,所以每个服务器都可以成为一些分区的主副本,也能同时成为一些分区的备份副本

在这里插入图片描述

备份副本始终尽量保持与主副本的数据同步。 备份副本的日志文件和主副本的日志总是相同的,它们都有相同的偏移量和相同顺序的消息 。 备份副本从主副本消费消息的方式和普通的消费者一样,只不过备份副本会将消息运用到自己的本地日志文件(备份副本和主副本都在服务端,它们都会将收到的分区数据持久化成日志文件)。 普通的消费者客户端拉取到消息后并不会持久化,而是直接处理。

分布式系统处理故障容错时,需要明确地定义节点是否处于存活状态 。kafka对节点的存活定义有两个条件:

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

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

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

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

1.4 快速开始

如表 1-1所示,我们会在单机环境下事先创建好不同副本数、不同分区数的几个主题。

在这里插入图片描述

我们的实验会分成单机模式与分布式模式,不同的模式会操作不同的主题。


1.4.1 单击模式

如果是一个服务器,只需要在不同的终端分别启动ZK和KafkaServer服务端进程:

$ cd kafka_2.10-0.10.0.0
$ bin/zookeeper-server-start.sh config/zookeeper.properties
$ bin/kafka-server-start.sh config/server.properties
1 一个副本一个分区

通过c「eate~项创建一个名称为test的主题,副本数为 1 ,分区数为 1 ,并连接本地的ZK。 通过list选项,可以查看集群的主题列表。 创建主题后,日志目录下将创建以“主题名称-分区编号”命名的文件夹,用来存放主题分区的消息 。 相关代码如下:

$ bin/kafka-topi.cs.sh --create --zookeeper localhost:2181 \
--replication-factor 1 --partitions 1 --topipc test
Created topipc ” test".
$ bin/kafka-topipcs.sh --list --zookeeper localhost:2181
test
2 一个副本多个分区

创建多个分区的my-partitioned-topic主题,然后使用describe命令查看指定主题的详细信息,验证
一共有3个分区 , 并且每个分区都有以下5个属性。

  • Topipc 。主题名称,如果没有事先创建主题, Kafka也可以帮我们自动创建主题 。
  • partitions分区编号,从0开始 。 为了简洁起见,本书会用 P0表示 partitions:0。
  • Leader当前分区负责读写的节点,只有主副本才会接受消息的读写 。
  • replicas分区的复制节点列表,它与主题的副本数量有关,默认只有一个副本,即主副本。
  • Isr同步状态的副本,是Replicas 的子集,必须是存活的,并且都能赶上主副本 。

1.4.2 分布式模式

在本地模拟启动4个Kafka服务 , 更改每个节点配置文件中的消息代理编号、端口号、日志文件目录。 除了上面已经启动的一个Kafka服务外,还需要再启动3个服务(执行脚本与下面的方式类似)


1.4.3 消费组示例

默认的控制 台消费者在启动时,都会分配到一个随机的消费组编号, 即一个消费组只有一个消费者。 为了模拟一个消费组下有多个消费者的情况, 通过指定消费者的配置文件,并在配置文件中配置消费组的编号, 比如这里会设置group.i.d等于test-consumer-group


1.5 环境准备

  1. 直接下载编译后的kafka文件
  2. 自己编译需要版本的kafka(看源码方便)

具体看这里

相关推荐
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页