You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

156 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 48 | DMA为什么Kafka这么快
过去几年里整个计算机产业界都在尝试不停地提升I/O设备的速度。把HDD硬盘换成SSD硬盘我们仍然觉得不够快用PCI Express接口的SSD硬盘替代SATA接口的SSD硬盘我们还是觉得不够快所以现在就有了傲腾Optane这样的技术。
但是无论I/O速度如何提升比起CPU总还是太慢。SSD硬盘的IOPS可以到2万、4万但是我们CPU的主频有2GHz以上也就意味着每秒会有20亿次的操作。
如果我们对于I/O的操作都是由CPU发出对应的指令然后等待I/O设备完成操作之后返回那CPU有大量的时间其实都是在等待I/O设备完成操作。
但是这个CPU的等待在很多时候其实并没有太多的实际意义。我们对于I/O设备的大量操作其实都只是把内存里面的数据传输到I/O设备而已。在这种情况下其实CPU只是在傻等而已。特别是当传输的数据量比较大的时候比如进行大文件复制如果所有数据都要经过CPU实在是有点儿太浪费时间了。
因此计算机工程师们就发明了DMA技术也就是**直接内存访问**Direct Memory Access技术来减少CPU等待的时间。
## 理解DMA一个协处理器
其实DMA技术很容易理解本质上DMA技术就是我们在主板上放一块独立的芯片。在进行内存和I/O设备的数据传输的时候我们不再通过CPU来控制数据传输而直接通过**DMA控制器**DMA Controller简称DMAC。这块芯片我们可以认为它其实就是一个**协处理器**Co-Processor
**DMAC最有价值的地方体现在当我们要传输的数据特别大、速度特别快或者传输的数据特别小、速度特别慢的时候。**
比如说我们用千兆网卡或者硬盘传输大量数据的时候如果都用CPU来搬运的话肯定忙不过来所以可以选择DMAC。而当数据传输很慢的时候DMAC可以等数据到齐了再发送信号给到CPU去处理而不是让CPU在那里忙等待。
好了现在你应该明白DMAC的价值知道了它适合用在什么情况下。那我们现在回过头来看。我们上面说DMAC是一块“协处理器芯片”这是为什么呢
注意这里面的“协”字。DMAC是在“协助”CPU完成对应的数据传输工作。在DMAC控制数据传输的过程中我们还是需要CPU的。
除此之外DMAC其实也是一个特殊的I/O设备它和CPU以及其他I/O设备一样通过连接到总线来进行实际的数据传输。总线上的设备呢其实有两种类型。一种我们称之为**主设备**Master另外一种我们称之为**从设备**Slave
想要主动发起数据传输必须要是一个主设备才可以CPU就是主设备。而我们从设备比如硬盘只能接受数据传输。所以如果通过CPU来传输数据要么是CPU从I/O设备读数据要么是CPU向I/O设备写数据。
这个时候你可能要问了那我们的I/O设备不能向主设备发起请求么可以是可以不过这个发送的不是数据内容而是控制信号。I/O设备可以告诉CPU我这里有数据要传输给你但是实际数据是CPU拉走的而不是I/O设备推给CPU的。
![](https://static001.geekbang.org/resource/image/99/bc/9998b67238044aad60d2aa0735b98ebc.jpeg)
不过DMAC就很有意思了它既是一个主设备又是一个从设备。对于CPU来说它是一个从设备对于硬盘这样的IO设备来说呢它又变成了一个主设备。那使用DMAC进行数据传输的过程究竟是什么样的呢下面我们来具体看看。
1.首先CPU还是作为一个主设备向DMAC设备发起请求。这个请求其实就是在DMAC里面修改配置寄存器。
2.CPU修改DMAC的配置的时候会告诉DMAC这样几个信息
* 首先是**源地址的初始值以及传输时候的地址增减方式**。
所谓源地址就是数据要从哪里传输过来。如果我们要从内存里面写入数据到硬盘上那么就是要读取的数据在内存里面的地址。如果是从硬盘读取数据到内存里那就是硬盘的I/O接口的地址。
我们讲过总线的时候说过I/O的地址可以是一个内存地址也可以是一个端口地址。而地址的增减方式就是说数据是从大的地址向小的地址传输还是从小的地址往大的地址传输。
* 其次是**目标地址初始值和传输时候的地址增减方式**。目标地址自然就是和源地址对应的设备,也就是我们数据传输的目的地。
* 第三个自然是**要传输的数据长度**,也就是我们一共要传输多少数据。
3.设置完这些信息之后DMAC就会变成一个空闲的状态Idle
4.如果我们要从硬盘上往内存里面加载数据这个时候硬盘就会向DMAC发起一个数据传输请求。这个请求并不是通过总线而是通过一个额外的连线。
5.然后我们的DMAC需要再通过一个额外的连线响应这个申请。
6.于是DMAC这个芯片就向硬盘的接口发起要总线读的传输请求。数据就从硬盘里面读到了DMAC的控制器里面。
7.然后DMAC再向我们的内存发起总线写的数据传输请求把数据写入到内存里面。
8.DMAC会反复进行上面第6、7步的操作直到DMAC的寄存器里面设置的数据长度传输完成。
9.数据传输完成之后DMAC重新回到第3步的空闲状态。
所以整个数据传输的过程中我们不是通过CPU来搬运数据而是由DMAC这个芯片来搬运数据。但是CPU在这个过程中也是必不可少的。因为传输什么数据从哪里传输到哪里其实还是由CPU来设置的。这也是为什么DMAC被叫作“协处理器”。
![](https://static001.geekbang.org/resource/image/c9/8e/c9ed34b47b0cd33867c581772d8eff8e.jpeg)
现在的外设里面很多都内置了DMAC
最早计算机里是没有DMAC的所有数据都是由CPU来搬运的。随着人们对于数据传输的需求越来越多先是出现了主板上独立的DMAC控制器。到了今天各种I/O设备越来越多数据传输的需求越来越复杂使用的场景各不相同。加之显示器、网卡、硬盘对于数据传输的需求都不一样所以各个设备里面都有自己的DMAC芯片了。
## 为什么那么快一起来看Kafka的实现原理
了解了DMAC是怎么回事儿那你可能要问了这和我们实际进行程序开发有什么关系呢有什么API我们直接调用一下就能加速数据传输减少CPU占用吗
你还别说过去几年的大数据浪潮里面还真有一个开源项目很好地利用了DMA的数据传输方式通过DMA的方式实现了非常大的性能提升。这个项目就是**Kafka**。下面我们就一起来看看它究竟是怎么利用DMA的。
Kafka是一个用来处理实时数据的管道我们常常用它来做一个消息队列或者用来收集和落地海量的日志。作为一个处理实时数据和日志的管道瓶颈自然也在I/O层面。
Kafka里面会有两种常见的海量数据传输的情况。一种是从网络中接收上游的数据然后需要落地到本地的磁盘上确保数据不丢失。另一种情况呢则是从本地磁盘上读取出来通过网络发送出去。
我们来看一看后一种情况从磁盘读数据发送到网络上去。如果我们自己写一个简单的程序最直观的办法自然是用一个文件读操作从磁盘上把数据读到内存里面来然后再用一个Socket把这些数据发送到网络上去。
```
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
```
[代码来源](https://developer.ibm.com/articles/j-zerocopy/)
这段伪代码来自IBM Developer Works上关于Zero Copy的文章
在这个过程中数据一共发生了四次传输的过程。其中两次是DMA的传输另外两次则是通过CPU控制的传输。下面我们来具体看看这个过程。
第一次传输是从硬盘上读到操作系统内核的缓冲区里。这个传输是通过DMA搬运的。
第二次传输需要从内核缓冲区里面的数据复制到我们应用分配的内存里面。这个传输是通过CPU搬运的。
第三次传输要从我们应用的内存里面再写到操作系统的Socket的缓冲区里面去。这个传输还是由CPU搬运的。
最后一次传输需要再从Socket的缓冲区里面写到网卡的缓冲区里面去。这个传输又是通过DMA搬运的。
![](https://static001.geekbang.org/resource/image/e0/d5/e0e85505e793e804e3b396fc50871cd5.jpg)
这个时候你可以回过头看看这个过程。我们只是要“搬运”一份数据结果却整整搬运了四次。而且这里面从内核的读缓冲区传输到应用的内存里再从应用的内存里传输到Socket的缓冲区里其实都是把同一份数据在内存里面搬运来搬运去特别没有效率。
像Kafka这样的应用场景其实大部分最终利用到的硬件资源其实又都是在干这个搬运数据的事儿。所以我们就需要尽可能地减少数据搬运的需求。
事实上Kafka做的事情就是把这个数据搬运的次数从上面的四次变成了两次并且只有DMA来进行数据搬运而不需要CPU。
```
@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
return fileChannel.transferTo(position, count, socketChannel);
}
```
如果你层层追踪Kafka的代码你会发现最终它调用了Java NIO库里的transferTo方法
Kafka的代码调用了Java NIO库具体是FileChannel里面的transferTo方法。我们的数据并没有读到中间的应用内存里面而是直接通过Channel写入到对应的网络设备里。并且对于Socket的操作也不是写入到Socket的Buffer里面而是直接根据描述符Descriptor写入到网卡的缓冲区里面。于是在这个过程之中我们只进行了两次数据传输。
![](https://static001.geekbang.org/resource/image/59/ab/596042d111ad9b871045d970a10464ab.jpg)
第一次是通过DMA从硬盘直接读到操作系统内核的读缓冲区里面。第二次则是根据Socket的描述符信息直接从读缓冲区里面写入到网卡的缓冲区里面。
这样我们同一份数据传输的次数从四次变成了两次并且没有通过CPU来进行数据搬运所有的数据都是通过DMA来进行传输的。
在这个方法里面我们没有在内存层面去“复制Copy”数据所以这个方法也被称之为**零拷贝**Zero-Copy
IBM Developer Works里面有一篇文章专门写过程序来测试过在同样的硬件下使用零拷贝能够带来的性能提升。我在这里放上这篇文章[链接](https://developer.ibm.com/articles/j-zerocopy/)。在这篇文章最后你可以看到无论传输数据量的大小传输同样的数据使用了零拷贝能够缩短65%的时间,大幅度提升了机器传输数据的吞吐量。想要深入了解零拷贝,建议你可以仔细读一读这篇文章。
## 总结延伸
讲到这里相信你对DMA的原理、作用和效果都有所理解了。那么我们一起来回顾总结一下。
如果我们始终让CPU来进行各种数据传输工作会特别浪费。一方面我们的数据传输工作用不到多少CPU核心的“计算”功能。另一方面CPU的运转速度也比I/O操作要快很多。所以我们希望能够给CPU“减负”。
于是工程师们就在主板上放上了DMAC这样一个协处理器芯片。通过这个芯片CPU只需要告诉DMAC我们要传输什么数据从哪里来到哪里去就可以放心离开了。后续的实际数据传输工作都会由DMAC来完成。随着现代计算机各种外设硬件越来越多光一个通用的DMAC芯片不够了我们在各个外设上都加上了DMAC芯片使得CPU很少再需要关心数据传输的工作了。
在我们实际的系统开发过程中利用好DMA的数据传输机制也可以大幅提升I/O的吞吐率。最典型的例子就是Kafka。
传统地从硬盘读取数据然后再通过网卡向外发送我们需要进行四次数据传输其中有两次是发生在内存里的缓冲区和对应的硬件设备之间我们没法节省掉。但是还有两次完全是通过CPU在内存里面进行数据复制。
在Kafka里通过Java的NIO里面FileChannel的transferTo方法调用我们可以不用把数据复制到我们应用程序的内存里面。通过DMA的方式我们可以把数据从内存缓冲区直接写到网卡的缓冲区里面。在使用了这样的零拷贝的方法之后呢我们传输同样数据的时间可以缩减为原来的1/3相当于提升了3倍的吞吐率。
这也是为什么Kafka是目前实时数据传输管道的标准解决方案。
## 推荐阅读
学完了这一讲之后我推荐你阅读一下Kafka的论文[Kakfa:a Distrubted Messaging System for Log Processing](http://notes.stephenholiday.com/Kafka.pdf)。Kafka的论文其实非常简单易懂是一个很好的让你了解系统、日志、分布式系统的入门材料。
如果你想要进一步去了解Kafka也可以订阅极客时间的专栏“[Kafka核心技术与实战](https://time.geekbang.org/column/intro/191)”。
## 课后思考
你可以自己尝试写一段使用零拷贝和不使用零拷贝传输数据的代码,然后看一看两者之间的性能差异。你可以看看,零拷贝能够带来多少吞吐量提升。
欢迎你把你运行程序的结果写在留言区和大家一起讨论、分享。你也可以把这个问题分享给你的朋友一起试一试看看DMA和零拷贝是否真的可以大幅度提升性能。