关于kafka:从Kafka中学习高性能系统如何设计-京东云技术团队

9次阅读

共计 12319 个字符,预计需要花费 31 分钟才能阅读完成。

1 前言

置信各位小伙伴之前或多或少接触过音讯队列,比拟出名的蕴含 Rocket MQ 和 Kafka,在京东外部应用的是自研的消息中间件 JMQ,从 JMQ2 降级到 JMQ4 的也是带来了性能上的显著晋升,并且 JMQ4 的底层也是参考 Kafka 去做的设计。在这里我会给大家展现 Kafka 它的高性能是如何设计的,大家也能够学习相干方法论将其利用在理论我的项目中,兴许下一个顶级我的项目就在各位的代码中产生了。

2 如何了解高性能设计

2.1 高性能设计的”秘籍”

先抛开 kafka,咱们先来议论一下高性能设计的实质,在这里借用一下网上的一张总结高性能的思维导图:

从中能够看到,高性能设计的伎俩还是十分多,从”宏观设计”上的无锁化、序列化,到”宏观设计”上的缓存、存储等,能够说是形形色色,令人目迷五色。然而在我看来实质就两点:计算和 IO。上面将从这两点来浅析一下我认为的高性能的”道”。

2.2 高性能设计的”道法”

2.2.1 计算上的”道”

计算上的优化伎俩无外乎两种形式:1. 缩小计算量 2. 放慢单位工夫的计算量

  • 缩小计算量:比方用索引来取代全局扫描、用同步代替异步、通过限流来缩小申请处理量、采纳更高效的数据结构和算法等。(举例:mysql 的 BTree,redis 的跳表等)
  • 放慢单位工夫的计算量:能够利用 CPU 多核的个性,比方用多线程代替单线程、用集群代替单机等。(举例:多线程编程、分治计算等)

2.2.2 IO 上的”道”

IO 上的优化伎俩也能够从两个方面来体现:1. 缩小 IO 次数或者 IO 数据量 2. 放慢 IO 速度

  • 缩小 IO 次数或者 IO 数据量:比方借助零碎缓存或者内部缓存、通过零拷贝技术缩小 IO 复制次数、批量读写、数据压缩等。
  • 放慢 IO 速度:比方用磁盘程序写代替随机写、用 NIO 代替 BIO、用性能更好的 SSD 代替机械硬盘等。

3 kafka 高性能设计

了解了高性能设计的伎俩和实质之后,咱们再来看看 kafka 外面应用到的性能优化办法。各类消息中间件的实质都是一个生产者 - 消费者模型,生产者发送音讯给服务端进行暂存,消费者从服务端获取音讯进行生产。也就是说 kafka 分为三个局部:生产者 - 服务端 - 消费者,咱们能够依照这三个来别离演绎一下其对于性能优化的伎俩,这些伎俩也会涵盖在咱们之前梳理的脑图外面。

3.1 生产者的高性能设计

3.1.1 批量发送音讯

之前在下面说过,高性能的”道”在于计算和 IO 上,咱们先来看看在 IO 上 kafka 是如何做设计的。

IO 上的优化
kafka 是一个消息中间件,数据的载体就是音讯,如何将音讯高效的进行传递和长久化是 kafka 高性能设计的一个重点。基于此剖析 kafka 必定是 IO 密集型利用,producer 须要通过网络 IO 将消息传递给 broker,broker 须要通过磁盘 IO 将音讯长久化,consumer 须要通过网络 IO 将音讯从 broker 上拉取生产。

  • 网络 IO 上的优化:producer->broker 发送音讯不是一条一条发送的,kafka 模式会有个音讯发送提早机制,会将一批音讯进行聚合,一口气打包发送给 broker,这样就胜利缩小了 IO 的次数。除了传输音讯自身以外,还要传输十分多的网络协议自身的一些内容(称为 Overhead),所以将多条音讯合并到一起传输,可无效缩小网络传输的 Overhead,进而进步了传输效率。
  • 磁盘 IO 上的优化:大家晓得磁盘和内存的存储速度是不同的,在磁盘上操作的速度是远低于内存,然而在老本上内存是高于磁盘。kafka 是面向大数据量的消息中间件,也就是说须要将大批量的数据长久化,这些数据放在内存上也是不事实。那 kafka 是怎么在磁盘 IO 上进行优化的呢?在这里我先间接给出办法,具体细节在后文中解释(它是借助于一种磁盘程序写的机制来晋升写入速度)。

3.1.2 负载平衡

1.kafka 负载平衡设计

Kafka 有主题(Topic)概念,他是承载实在数据的逻辑容器,主题之下还分为若干个分区,Kafka 音讯组织形式实际上是三级构造:主题 - 分区 - 音讯。主题下的每条音讯只会在某一个分区中,而不会在多个分区中被保留多份。
Kafka 这样设计,应用分区的作用就是提供负载平衡的能力,对数据进行分区的次要目标就是为了实现零碎的高伸缩性(Scalability)。不同的分区可能放在不同的节点的机器上,而数据的读写操作也都是针对分区这个粒度进行的,每个节点的机器都能独立地执行各自分区读写申请。咱们还能够通过减少节点来晋升整体零碎的吞吐量。Kafka 的分区设计,还能够实现业务级别的音讯程序的问题。

2. 具体分区策略

  • 所谓的分区策略是指决定生产者将音讯发送到那个分区的算法。Kafka 提供了默认的分区策略是轮询,同时 kafka 也反对用户本人制订。
  • 轮询策略:也称为 Round-robin 策略,即程序调配。轮询的长处是有着优良的负载平衡的体现。
  • 随机策略:尽管也是谋求负载平衡,但总体体现差于轮询。
  • 音讯键划分策略:还要一种是为每条音讯配置一个 key,按音讯的 key 来存。Kafka 容许为每条音讯指定一个 key。一旦指定了 key,那么会对 key 进行 hash 计算,将雷同的 key 存入雷同的分区中,而且每个分区下的音讯都是有序的。key 的作用很大,能够是一个有着明确业务含意的字符串,也能够是用来表征音讯的元数据。
  • 其余的分区策略:基于地理位置的分区。能够从所有分区中找出那些 Leader 正本在某个地理位置所有分区,而后随机筛选一个进行音讯发送。

3.1.3 异步发送

1. 线程模型

之前曾经说了 kafka 是抉择批量发送音讯来晋升整体的 IO 性能,具体流程是 kafka 生产者应用批处理试图在内存中积攒数据,主线程将多条音讯通过一个 ProduceRequest 申请批量发送进来,发送的音讯暂存在一个队列 (RecordAccumulator) 中,再由 sender 线程去获取一批数据或者不超过某个延迟时间内的数据发送给 broker 进行长久化。

长处:

  • 能够晋升 kafka 整体的吞吐量,缩小网络 IO 的次数;
  • 进步数据压缩效率(个别压缩算法都是数据量越大越能靠近预期的压缩成果);

毛病:

  • 数据发送有肯定提早,然而这个提早能够由业务因素来自行设置。

3.1.4 高效序列化

1. 序列化的劣势
Kafka 音讯中的 Key 和 Value,都反对自定义类型,只须要提供相应的序列化和反序列化器即可。因而,用户能够依据理论状况选用疾速且紧凑的序列化形式(比方 ProtoBuf、Avro)来缩小理论的网络传输量以及磁盘存储量,进一步提高吞吐量。

2. 内置的序列化器

  • org.apache.kafka.common.serialization.StringSerializer;
  • org.apache.kafka.common.serialization.LongSerializer;
  • org.apache.kafka.common.serialization.IntegerSerializer;
  • org.apache.kafka.common.serialization.ShortSerializer;
  • org.apache.kafka.common.serialization.FloatSerializer;
  • org.apache.kafka.common.serialization.DoubleSerializer;
  • org.apache.kafka.common.serialization.BytesSerializer;
  • org.apache.kafka.common.serialization.ByteBufferSerializer;
  • org.apache.kafka.common.serialization.ByteArraySerializer;

3.1.5 消息压缩

1. 压缩的目标
压缩秉承了用工夫换空间的经典 trade-off 思维,即用 CPU 的工夫去换取磁盘空间或网络 I / O 传输量,Kafka 的压缩算法也是出于这种目标。并且通常是:数据量越大,压缩成果才会越好。
因为有了批量发送这个后期,从而使得 Kafka 的消息压缩机制能真正施展出它的威力(压缩的实质取决于多音讯的重复性)。比照压缩单条音讯,同时对多条音讯进行压缩,能大幅缩小数据量,从而更大程度进步网络传输率。

2. 压缩的办法
想理解 kafka 消息压缩的设计,就须要先理解 kafka 音讯的格局:

  • Kafka 的音讯档次分为:音讯汇合(message set)和音讯(message);一个音讯汇合中蕴含若干条日志项(record item),而日志项才是真正封装音讯的中央。
  • Kafka 底层的消息日志由一系列音讯汇合 - 日志项组成。Kafka 通常不会间接操作具体的一条条音讯,他总是在音讯汇合这个层面上进行写入操作。

每条音讯都含有本人的元数据信息,kafka 会将一批音讯雷同的元数据信息给晋升到外层的音讯汇合外面,而后再对整个音讯汇合来进行压缩。批量音讯在长久化到 Broker 中的磁盘时,依然放弃的是压缩状态,最终是在 Consumer 端做理解压缩操作。
压缩算法效率比照
Kafka 共反对四种次要的压缩类型:Gzip、Snappy、Lz4 和 Zstd,具体效率比照如下:

3.2 服务端的高性能设计

3.2.1 Reactor 网络通信模型

kafka 相比其余消息中间件最出彩的中央在于他的高吞吐量,那么对于服务端来说每秒的申请压力将会微小,须要有一个优良的网络通信机制来解决海量的申请。如果 IO 有所钻研的同学,应该分明:Reactor 模式正是采纳了很经典的 IO 多路复用技术,它能够复用一个线程去解决大量的 Socket 连贯,从而保障高性能。Netty 和 Redis 为什么能做到十万甚至百万并发?它们其实都采纳了 Reactor 网络通信模型。

1.kafka 网络通信层架构

从图中能够看出,SocketServer 和 KafkaRequestHandlerPool 是其中最重要的两个组件:

  • SocketServer:次要实现了 Reactor 模式,用于解决内部多个 Clients(这里的 Clients 指的是狭义的 Clients,可能蕴含 Producer、Consumer 或其余 Broker)的并发申请,并负责将处理结果封装进 Response 中,返还给 Clients
  • KafkaRequestHandlerPool:Reactor 模式中的 Worker 线程池,外面定义了多个工作线程,用于解决理论的 I / O 申请逻辑。

2. 申请流程

  • Clients 或其余 Broker 通过 Selector 机制发动创立连贯申请。(NIO 的机制,应用 epoll)
  • Processor 线程接管申请,并将其转换成可解决的 Request 对象。
  • Processor 线程将 Request 对象放入共享的 RequestChannel 的 Request 队列。
  • KafkaRequestHandler 线程从 Request 队列中取出待处理申请,并进行解决。
  • KafkaRequestHandler 线程将 Response 放回到对应 Processor 线程的 Response 队列。
  • Processor 线程发送 Response 给 Request 发送方。

3.2.2 Kafka 的底层日志构造

根本构造的展现

Kafka 是一个 Pub-Sub 的音讯零碎,无论是公布还是订阅,都须指定 Topic。Topic 只是一个逻辑的概念。每个 Topic 都蕴含一个或多个 Partition,不同 Partition 可位于不同节点。同时 Partition 在物理上对应一个本地文件夹(也就是个日志对象 Log),每个 Partition 蕴含一个或多个 Segment,每个 Segment 蕴含一个数据文件和多个与之对应的索引文件。在逻辑上,能够把一个 Partition 当作一个十分长的数组,可通过这个“数组”的索引(offset)去拜访其数据。

2.Partition 的并行处理能力

  • 一方面,topic 是由多个 partion 组成,Producer 发送音讯到 topic 是有个负载平衡机制,基本上会将音讯平均分配到每个 partion 外面,同时 consumer 外面会有个 consumer group 的概念,也就是说它会以组为单位来生产一个 topic 内的音讯,一个 consumer group 内蕴含多个 consumer,每个 consumer 生产 topic 内不同的 partion,这样通过多 partion 进步了音讯的接管和解决能力
  • 另一方面,因为不同 Partition 可位于不同机器,因而能够充分利用集群劣势,实现机器间的并行处理。并且 Partition 在物理上对应一个文件夹,即便多个 Partition 位于同一个节点,也可通过配置让同一节点上的不同 Partition 置于不同的 disk drive 上,从而实现磁盘间的并行处理,充分发挥多磁盘的劣势。

3. 过期音讯的革除

  • Kafka 的整个设计中,Partition 相当于一个十分长的数组,而 Broker 接管到的所有音讯程序写入这个大数组中。同时 Consumer 通过 Offset 程序生产这些数据,并且不删除曾经生产的数据,从而防止了随机写磁盘的过程。
  • 因为磁盘无限,不可能保留所有数据,实际上作为音讯零碎 Kafka 也没必要保留所有数据,须要删除旧的数据。而这个删除过程,并非通过应用“读 - 写”模式去批改文件,而是将 Partition 分为多个 Segment,每个 Segment 对应一个物理文件,通过删除整个文件的形式去删除 Partition 内的数据。这种形式革除旧数据的形式,也防止了对文件的随机写操作。

3.2.3 浮夸高效的索引

1. 稠密索引

能够从下面看到,一个 segment 蕴含一个.log 后缀的文件和多个 index 后缀的文件。那么这些文件具体作用是干啥的呢?并且这些文件除了后缀不同文件名都是雷同,为什么这么设计?

  • .log 文件:具体存储音讯的日志文件
  • .index 文件:位移索引文件,可依据音讯的位移值疾速地从查问到音讯的物理文件地位
  • .timeindex 文件:工夫戳索引文件,可依据工夫戳查找到对应的位移信息
  • .txnindex 文件:已停止事物索引文件
    除了.log 是理论存储音讯的文件以外,其余的几个文件都是索引文件。索引自身设计的原来是一种空间换工夫的概念,在这里 kafka 是为了减速查问所应用。kafka 索引 不会为每一条音讯建设索引关系,这个也很好了解,毕竟对一条音讯建设索引的老本还是比拟大的,所以它是一种稠密索引的概念,就好比咱们常见的跳表,都是一种稠密索引。
    kafka 日志的文件名 个别都是该 segment 写入的第一条音讯的起始位移值 baseOffset,比方 000000000123.log,这外面的 123 就是 baseOffset,具体索引文件外面纪录的数据是绝对于起始位移的绝对位移值 relativeOffset,baseOffset 与 relativeOffse 的加和即为理论音讯的索引值。假如一个索引文件为:00000000000000000100.index,那么起始位移值即 100,当存储位移为 150 的音讯索引时,在索引文件中的绝对位移则为 150 – 100 = 50,这么做的益处是应用 4 字节保留位移即可,能够节俭十分多的磁盘空间。(ps:kafka 真的是极致的压缩了数据存储的空间)

2. 优化的二分查找算法

kafka 没有应用咱们熟知的跳表或者 B +Tree 构造来设计索引,而是应用了一种更为简略且高效的查找算法:二分查找。然而绝对于传统的二分查找,kafka 将其进行了局部优化,集体感觉设计的十分奇妙,在这里我会进行详述。
在这之前,我先补充一下 kafka 索引文件的形成:每个索引文件蕴含若干条索引项。不同索引文件的索引项的大小不同,比方 offsetIndex 索引项大小是 8B,timeIndex 索引项的大小是 12B。

这里以 offsetIndex 为例子来详述 kafka 的二分查找算法:
1)一般二分查找
offsetIndex 每个索引项大小是 8B,但操作系统拜访内存时的最小单元是页,个别是 4KB,即 4096B,会蕴含了 512 个索引项。而找出在索引中的指定偏移量,对于操作系统拜访内存时则变成了找出指定偏移量所在的页。假如索引的大小有 13 个页,如下图所示:

因为 Kafka 读取音讯,个别都是读取最新的偏移量,所以要查问的页就集中在尾部​,即第 12 号页上。依据二分查找,将顺次拜访 6、9、11、12 号页。

当随着 Kafka 接管音讯的减少,索引文件也会减少至第 13 号页,这时依据二分查找,将顺次拜访 7、10、12、13 号页。

能够看出拜访的页和上一次的页齐全不同。之前在只有 12 号页的时候,Kafak 读取索引时会频繁拜访 6、9、11、12 号页,而因为 Kafka 应用了​mmap​来进步速度,即读写操作都将通过操作系统的 page cache,所以 6、9、11、12 号页会被缓存到 page cache 中,防止磁盘加载。然而当增至 13 号页时,则须要拜访 7、10、12、13 号页,而因为 7、10 号页长时间没有被拜访(古代操作系统都是应用 LRU 或其变体来治理 page cache),很可能曾经不在 page cache 中了,那么就会造成​缺页中断​(线程被阻塞期待从磁盘加载没有被缓存到 page cache 的数据)。在 Kafka 的官网测试中,这种状况会造成几毫秒至 1 秒的提早。

2)kafka 优化的二分查找
Kafka 对二分查找进行了改良。既然个别读取数据集中在索引的尾部。那么​将索引中最初的 8192B(8KB)划分为“热区”(刚好缓存两页数据),其余部分划分为“冷区”,别离进行二分查找。这样做的益处是,在频繁查问尾部的状况下,尾部的页根本都能在 page cahce 中,从而防止缺页中断。
上面咱们还是用之前的例子来看下。因为每个页最多蕴含 512 个索引项,而最初的 1024 个索引项所在页会被认为是热区。那么当 12 号页未满时,则 10、11、12 会被断定是热区;而当 12 号页刚好满了的时候,则 11、12 被断定为热区;当增至 13 号页且未满时,11、12、13 被断定为热区。假如咱们读取的是最新的音讯,则在热区中进行二分查找的状况如下:

当 12 号页未满时,顺次拜访 11、12 号页,当 12 号页满时,拜访页的状况雷同。当 13 号页呈现的时候,顺次拜访 12、13 号页,不会呈现拜访长时间未拜访的页,则能无效防止缺页中断。

3.mmap 的应用

利用稠密索引,曾经根本解决了高效查问的问题,然而这个过程中依然有进一步的优化空间,那便是通过 mmap(memory mapped files)读写下面提到的稠密索引文件,进一步提高查问音讯的速度。

到底如何了解 mmap?后面提到,惯例的文件操作为了进步读写性能,应用了 Page Cache 机制,然而因为页缓存处在内核空间中,不能被用户过程间接寻址,所以读文件时还须要通过零碎调用,将页缓存中的数据再次拷贝到用户空间中。

1)惯例文件读写

  • app 拿着 inode 查找读取文件
  • address_space 中存储了 inode 和该文件对应页面缓存的映射关系
  • 页面缓存缺失,引发缺页异样
  • 通过 inode 找到磁盘地址,将文件信息读取并填充到页面缓存
  • 页面缓存处于内核态,无奈间接被 app 读取到,因而要先拷贝到用户空间缓冲区,此处产生内核态和用户态的切换

tips:这一过程实际上产生了四次数据拷贝。首先通过零碎调用将文件数据读入到内核态 Buffer(DMA 拷贝),而后应用程序将内存态 Buffer 数据读入到用户态 Buffer(CPU 拷贝),接着用户程序通过 Socket 发送数据时将用户态 Buffer 数据拷贝到内核态 Buffer(CPU 拷贝),最初通过 DMA 拷贝将数据拷贝到 NIC Buffer。同时,还随同着四次上下文切换。

2)mmap 读写模式

  • 调用内核函数 mmap(),在页表 (类比虚拟内存 PTE) 中建设了文件地址和虚拟地址空间中用户空间的映射关系
  • 读操作引发缺页异样,通过 inode 找到磁盘地址,将文件内容拷贝到用户空间,此处不波及内核态和用户态的切换

tips:采纳 mmap 后,它将磁盘文件与过程虚拟地址做了映射,并不会导致零碎调用,以及额定的内存 copy 开销,从而进步了文件读取效率。具体到 Kafka 的源码层面,就是基于 JDK nio 包下的 MappedByteBuffer 的 map 函数,将磁盘文件映射到内存中。只有索引文件的读写才用到了 mmap。

3.2.4 音讯存储 - 磁盘程序写

对于咱们罕用的机械硬盘,其读取数据分 3 步:

  1. 寻道;
  2. 寻找扇区;
  3. 读取数据;

前两个,即寻找数据地位的过程为机械运动。咱们常说硬盘比内存慢,次要起因是这两个过程在拖后腿。不过,硬盘比内存慢是相对的吗?其实不然,如果咱们能通过程序读写缩小寻找数据地位时读写磁头的挪动间隔,硬盘的速度还是相当可观的。一般来讲,IO 速度层面,内存程序 IO > 磁盘程序 IO > 内存随机 IO > 磁盘随机 IO。这里用一张网上的图来比照一下相干 IO 性能:

Kafka 在程序 IO 上的设计分两方面看:

  1. LogSegment 创立时,一口气申请 LogSegment 最大 size 的磁盘空间,这样一个文件外部尽可能散布在一个间断的磁盘空间内;
  2. .log 文件也好,.index 和.timeindex 也罢,在设计上都是只追加写入,不做更新操作,这样防止了随机 IO 的场景;

3.2.5 Page Cache 的应用

为了优化读写性能,Kafka 利用了操作系统自身的 Page Cache,就是利用操作系统本身的内存而不是 JVM 空间内存。这样做的益处有:

  • 防止 Object 耗费:如果是应用 Java 堆,Java 对象的内存耗费比拟大,通常是所存储数据的两倍甚至更多。
  • 防止 GC 问题:随着 JVM 中数据一直增多,垃圾回收将会变得复杂与迟缓,应用零碎缓存就不会存在 GC 问题

相比于应用 JVM 或 in-memory cache 等数据结构,利用操作系统的 Page Cache 更加简略牢靠。

  • 首先,操作系统层面的缓存利用率会更高,因为存储的都是紧凑的字节构造而不是独立的对象。
  • 其次,操作系统自身也对于 Page Cache 做了大量优化,提供了 write-behind、read-ahead 以及 flush 等多种机制。
  • 再者,即便服务过程重启,JVM 内的 Cache 会生效,Page Cache 仍然可用,防止了 in-process cache 重建缓存的过程。

通过操作系统的 Page Cache,Kafka 的读写操作基本上是基于内存的,读写速度失去了极大的晋升。

3.3 生产端的高性能设计

3.3.1 批量生产

生产者是批量发送音讯,音讯者也是批量拉取音讯的,每次拉取一个音讯 batch,从而大大减少了网络传输的 overhead。在这里 kafka 是通过 fetch.min.bytes 参数来管制每次拉取的数据大小。默认是 1 字节,示意只有 Kafka Broker 端积攒了 1 字节的数据,就能够返回给 Consumer 端,这切实是太小了。咱们还是让 Broker 端一次性多返回点数据吧。
并且,在生产者高性能设计目录外面也说过,生产者其实在 Client 端对批量音讯进行了压缩,这批音讯长久化到 Broker 时,依然放弃的是压缩状态,最终在 Consumer 端再做解压缩操作。

3.3.2 零拷贝 - 磁盘音讯文件的读取

1.zero-copy 定义
零拷贝并不是不须要拷贝,而是缩小不必要的拷贝次数。通常是说在 IO 读写过程中。
零拷贝字面上的意思包含两个,“零”和“拷贝”:

  • “拷贝”:就是指数据从一个存储区域转移到另一个存储区域。
  • “零”:示意次数为 0,它示意拷贝数据的次数为 0。

实际上,零拷贝是有狭义和广义之分,目前咱们通常听到的零拷贝,包含下面这个定义缩小不必要的拷贝次数都是狭义上的零拷贝。其实理解到这点就足够了。
咱们晓得,缩小不必要的拷贝次数,就是为了提高效率。那零拷贝之前,是怎么的呢?

2. 传统 IO 的流程
做服务端开发的小伙伴,文件下载性能应该实现过不少了吧。如果你实现的是一个 web 程序,前端申请过去,服务端的工作就是:将服务端主机磁盘中的文件从已连贯的 socket 收回去。要害实现代码如下:

while((n = read(diskfd, buf, BUF_SIZE)) > 0)
    write(sockfd, buf , n);

传统的 IO 流程,包含 read 和 write 的过程。

  • read:把数据从磁盘读取到内核缓冲区,再拷贝到用户缓冲区
  • write:先把数据写入到 socket 缓冲区,最初写入网卡设施
    流程图如下:
  • 用户利用过程调用 read 函数,向操作系统发动 IO 调用,上下文从用户态转为内核态(切换 1)
  • DMA 控制器把数据从磁盘中,读取到内核缓冲区。
  • CPU 把内核缓冲区数据,拷贝到用户利用缓冲区,上下文从内核态转为用户态(切换 2),read 函数返回
  • 用户利用过程通过 write 函数,发动 IO 调用,上下文从用户态转为内核态(切换 3)
  • CPU 将用户缓冲区中的数据,拷贝到 socket 缓冲区
  • DMA 控制器把数据从 socket 缓冲区,拷贝到网卡设施,上下文从内核态切换回用户态(切换 4),write 函数返回

从流程图能够看出,传统 IO 的读写流程,包含了 4 次上下文切换(4 次用户态和内核态的切换),4 次数据拷贝(两次 CPU 拷贝以及两次的 DMA 拷贝 ),什么是 DMA 拷贝呢?咱们一起来回顾下,零拷贝波及的操作系统知识点。

**3. 零拷贝相干知识点
1)内核空间和用户空间 **
操作系统为每个过程都调配了内存空间,一部分是用户空间,一部分是内核空间。内核空间是操作系统内核拜访的区域,是受爱护的内存空间,而用户空间是用户应用程序拜访的内存区域。以 32 位操作系统为例,它会为每一个过程都调配了 4G (2 的 32 次方)的内存空间。

  • 内核空间:次要提供过程调度、内存调配、连贯硬件资源等性能
  • 用户空间:提供给各个程序过程的空间,它不具备拜访内核空间资源的权限,如果应用程序须要应用到内核空间的资源,则须要通过零碎调用来实现。过程从用户空间切换到内核空间,实现相干操作后,再从内核空间切换回用户空间。

2)用户态 & 内核态

  • 如果过程运行于内核空间,被称为过程的内核态
  • 如果过程运行于用户空间,被称为过程的用户态。

3)上下文切换
cpu 上下文

CPU 寄存器,是 CPU 内置的容量小、但速度极快的内存。而程序计数器,则是用来存储 CPU 正在执行的指令地位、或者行将执行的下一条指令地位。它们都是 CPU 在运行任何工作前,必须的依赖环境,因而叫做 CPU 上下文。

cpu 上下文切换

它是指,先把前一个工作的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,而后加载新工作的上下文到这些寄存器和程序计数器,最初再跳转到程序计数器所指的新地位,运行新工作。

个别咱们说的上下文切换,就是指内核(操作系统的外围)在 CPU 上对过程或者线程进行切换。过程从用户态到内核态的转变,须要通过零碎调用 来实现。零碎调用的过程,会产生 CPU 上下文的切换。

4)DMA 技术

DMA,英文全称是 Direct Memory Access,即间接内存拜访。DMA 实质上是一块主板上独立的芯片,容许外设设施和内存存储器之间间接进行 IO 数据传输,其过程不须要 CPU 的参加。

咱们一起来看下 IO 流程,DMA 帮忙做了什么事件。

能够发现,DMA 做的事件很清晰啦,它次要就是帮忙 CPU 转发一下 IO 申请,以及拷贝数据。
之所以须要 DMA,次要就是效率,它帮忙 CPU 做事件,这时候,CPU 就能够闲下来去做别的事件,进步了 CPU 的利用效率。

4.kafka 生产的 zero-copy
1)实现原理
零拷贝并不是没有拷贝数据,而是缩小用户态 / 内核态的切换次数以及 CPU 拷贝的次数。零拷贝实现有多种形式,别离是

  • mmap+write
  • sendfile

在服务端那里,咱们曾经晓得了 kafka 索引文件应用的 mmap 来进行零拷贝优化的,当初通知你 kafka 消费者在读取音讯的时候应用的是 sendfile 来进行零拷贝优化。

linux 2.4 版本之后,对 sendfile 做了优化降级,引入 SG-DMA 技术,其实就是对 DMA 拷贝退出了 scatter/gather 操作,它能够间接从内核空间缓冲区中将数据读取到网卡。应用这个特点搞零拷贝,即还能够多省去一次 CPU 拷贝。
sendfile+DMA scatter/gather 实现的零拷贝流程如下:

  • 用户过程发动 sendfile 零碎调用,上下文(切换 1)从用户态转向内核态。
  • DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
  • CPU 把内核缓冲区中的文件描述符信息(包含内核缓冲区的内存地址和偏移量)发送到 socket 缓冲区
  • DMA 控制器依据文件描述符信息,间接把数据从内核缓冲区拷贝到网卡
  • 上下文(切换 2)从内核态切换回用户态,sendfile 调用返回。

能够发现,sendfile+DMA scatter/gather 实现的零拷贝,I/ O 产生了 2 次用户空间与内核空间的上下文切换,以及 2 次数据拷贝。其中 2 次数据拷贝都是包 DMA 拷贝。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。

2)底层实现
Kafka 数据传输通过 TransportLayer 来实现,其子类 PlaintextTransportLayer 通过 Java NIO 的 FileChannel 的 transferTo 和 transferFrom 办法实现零拷贝。底层就是 sendfile。消费者从 broker 读取数据,就是由此实现。

@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {return fileChannel.transferTo(position, count, socketChannel);
}

tips:transferTo 和 transferFrom 并不保障肯定能应用零拷贝。实际上是否能应用零拷贝与操作系统相干,如果操作系统提供 sendfile 这样的零拷贝零碎调用,则这两个办法会通过这样的零碎调用充分利用零拷贝的劣势,否则并不能通过这两个办法自身实现零拷贝。

4 总结

文章第一部分为大家解说了高性能常见的优化伎俩,从”秘籍”和”道法”两个方面来诠释高性能设计之路该如何走,并引申出计算和 IO 两个优化方向。

文章第二局部是 kafka 外部高性能的具体设计——别离从生产者、服务端、消费者来进行全方位解说,包含其设计、应用及相干原理。

心愿通过这篇文章,可能使大家不仅学习到相干方法论,也能明确其方法论具体的落地计划,一起学习,一起成长。

作者:京东物流 李鹏

起源:京东云开发者社区

正文完
 0