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读取数据,就是由此实现。

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

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

4 总结

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

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

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

作者:京东物流 李鹏

起源:京东云开发者社区