當(dāng)涉及到網(wǎng)絡(luò)編程和IO操作時(shí),數(shù)據(jù)拷貝是一個(gè)常見的性能瓶頸。傳統(tǒng)的數(shù)據(jù)拷貝過程中,數(shù)據(jù)需要從內(nèi)核緩沖區(qū)復(fù)制到用戶空間緩沖區(qū),然后再?gòu)挠脩艨臻g緩沖區(qū)復(fù)制到內(nèi)核緩沖區(qū),這個(gè)過程會(huì)耗費(fèi)大量的CPU時(shí)間和內(nèi)存帶寬,降低系統(tǒng)的性能和吞吐量。
為了解決這個(gè)問題,零拷貝技術(shù)應(yīng)運(yùn)而生。零拷貝技術(shù)是指在數(shù)據(jù)傳輸過程中,避免將數(shù)據(jù)從一塊內(nèi)存拷貝到另一塊內(nèi)存,從而減少了CPU的開銷和內(nèi)存帶寬的消耗,提高了系統(tǒng)的性能。
在JAVA后端開發(fā)中,使用零拷貝技術(shù)可以有效提升系統(tǒng)的性能和吞吐量。本文將介紹零拷貝技術(shù)的概念、實(shí)現(xiàn)原理以及在Java后端開發(fā)中的應(yīng)用,希望能夠?yàn)樽x者提供有價(jià)值的參考和幫助。
一、傳統(tǒng)I/O
在展開說零拷貝之前,我們先來回顧一下傳統(tǒng)IO的方式是怎么樣的。
早期的數(shù)據(jù)IO,由用戶進(jìn)程向CPU發(fā)起,應(yīng)用程序與磁盤之間的 I/O 操作都是通過 CPU 的中斷完成的。CPU還要負(fù)責(zé)將磁盤緩沖區(qū)拷貝到內(nèi)核緩沖區(qū)(pageCache),再?gòu)膬?nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū)。為了減少CPU占用,產(chǎn)生了DMA技術(shù),大大解放了CPU。
DMA 的全稱叫直接內(nèi)存存取(Direct Memory Access),是一種允許外圍設(shè)備(硬件子系統(tǒng))直接訪問系統(tǒng)主內(nèi)存的機(jī)制。目前大多數(shù)的硬件設(shè)備,包括磁盤控制器、網(wǎng)卡、顯卡以及聲卡等都支持 DMA 技術(shù)。
1.1傳統(tǒng)I/O的問題
我們以讀取一張圖片數(shù)據(jù)的過程為例來分析傳統(tǒng)IO有哪些問題。傳統(tǒng)的訪問方式是通過 write() 和 read() 兩個(gè)系統(tǒng)調(diào)用實(shí)現(xiàn)的,通過 read() 函數(shù)讀取圖片到到緩存區(qū)中,然后通過 write() 方法把緩存中的圖片輸出到網(wǎng)絡(luò)端口。
read操作:
當(dāng)應(yīng)用程序執(zhí)行 read 系統(tǒng)調(diào)用讀取一塊數(shù)據(jù)的時(shí)候,如果這塊數(shù)據(jù)已經(jīng)存在于用戶進(jìn)程的頁(yè)內(nèi)存中,就直接從內(nèi)存中讀取數(shù)據(jù)。
如果數(shù)據(jù)不存在,則先將數(shù)據(jù)從磁盤加載數(shù)據(jù)到內(nèi)核空間的讀緩存(read buffer)中,再?gòu)淖x緩存拷貝到用戶進(jìn)程的頁(yè)內(nèi)存中。
write操作:
當(dāng)應(yīng)用程序準(zhǔn)備好數(shù)據(jù),執(zhí)行 write 系統(tǒng)調(diào)用發(fā)送網(wǎng)絡(luò)數(shù)據(jù)時(shí),先將數(shù)據(jù)從用戶空間的頁(yè)緩存拷貝到內(nèi)核空間的網(wǎng)絡(luò)緩沖區(qū)(socket buffer)中,然后再將寫緩存中的數(shù)據(jù)拷貝到網(wǎng)卡設(shè)備完成數(shù)據(jù)發(fā)送。
從上圖中可以看出,整個(gè)IO的過程需要進(jìn)行兩次DMA拷貝,兩次CPU拷貝,四次上下文切換。總共四次拷貝,四次切換。這個(gè)代價(jià)確實(shí)有些大。
說完傳統(tǒng)IO,接下來我們看下零拷貝都做了哪些優(yōu)化。
二、零拷貝
2.1什么是零拷貝
零拷貝這個(gè)詞,在很多地方都出現(xiàn)過,比如Kafka、Nginx、Tomcat等等這些技術(shù)的底層都有用到零拷貝技術(shù),那么究竟什么是零拷貝呢?
零拷貝是指在數(shù)據(jù)傳輸過程中,避免了數(shù)據(jù)的多次拷貝,從而提高了數(shù)據(jù)傳輸?shù)男省T趥鹘y(tǒng)的IO模型中,數(shù)據(jù)從磁盤中讀取到內(nèi)核緩沖區(qū),然后再?gòu)膬?nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū),最后再?gòu)挠脩艟彌_區(qū)拷貝到應(yīng)用程序中。而在零拷貝模型中,數(shù)據(jù)可以直接從內(nèi)核緩沖區(qū)拷貝到應(yīng)用程序中,避免了數(shù)據(jù)的多次拷貝,提高了數(shù)據(jù)傳輸?shù)男省A憧截惣夹g(shù)可以通過mmap和sendfile等系統(tǒng)調(diào)用實(shí)現(xiàn)。
所以說零拷貝并不是說不拷貝,而是減少拷貝的次數(shù),因?yàn)閺拇疟P中拷貝數(shù)據(jù)到內(nèi)存,或者從內(nèi)存中的一塊兒區(qū)域拷貝到另一塊兒區(qū)域都是一個(gè)耗費(fèi)性能的操作。零拷貝技術(shù)的目的就是減少這種行為的發(fā)生次數(shù)以此來提高性能。
2.2零拷貝實(shí)現(xiàn)的幾種方式
對(duì)比開頭說到的傳統(tǒng)IO,我們可以在以下幾個(gè)方面進(jìn)行優(yōu)化
1. 用戶態(tài)可以直接操作讀寫,不需要在用戶態(tài)和內(nèi)核態(tài)之間反復(fù)橫跳。
2. 盡量減少拷貝次數(shù),盡量減少上下文切換次數(shù)。
3. 寫時(shí)復(fù)制,需要寫操作的時(shí)候再拷貝,只是讀操作沒必要拷貝
用戶態(tài)直接IO
用戶態(tài)直接 I/O 使得應(yīng)用進(jìn)程或運(yùn)行在用戶態(tài)(user space)下的庫(kù)函數(shù)直接訪問硬件設(shè)備。
用戶態(tài)直接 I/O 只能適用于不需要內(nèi)核緩沖區(qū)處理的應(yīng)用程序,這些應(yīng)用程序通常在進(jìn)程地址空間有自己的數(shù)據(jù)緩存機(jī)制,稱為自緩存應(yīng)用程序,如數(shù)據(jù)庫(kù)管理系統(tǒng) 就是一個(gè)代表。
其次,這種零拷貝機(jī)制會(huì)直接操作磁盤 I/O,由于 CPU 和磁盤 I/O 之間的執(zhí)行時(shí)間差距,會(huì)造成大量資源的浪費(fèi),解決方案是配合異步 I/O 使用。
寫時(shí)復(fù)制
寫時(shí)復(fù)制指的是當(dāng)多個(gè)進(jìn)程共享同一塊數(shù)據(jù)時(shí),如果其中一個(gè)進(jìn)程需要對(duì)這份數(shù)據(jù)進(jìn)行修改,那么就需要將其拷貝到自己的進(jìn)程地址空間中。
這樣做并不影響其他進(jìn)程對(duì)這塊數(shù)據(jù)的操作,每個(gè)進(jìn)程要修改的時(shí)候才會(huì)進(jìn)行拷貝,所以叫寫時(shí)拷貝。
減少拷貝次數(shù)
1. mmap+write零拷貝技術(shù)
以mmap+write的方式替代傳統(tǒng)的read+write的方式,減少了一次拷貝。
mmap 是 linux 提供的一種內(nèi)存映射文件方法,即將一個(gè)進(jìn)程的地址空間中的一段虛擬地址映射到磁盤文件地址使用 mmap 的目的是將內(nèi)核中讀緩沖區(qū)(read buffer)的地址與用戶空間的緩沖區(qū)(user buffer)進(jìn)行映射。從而實(shí)現(xiàn)內(nèi)核緩沖區(qū)與應(yīng)用程序內(nèi)存的共享,省去了將數(shù)據(jù)從內(nèi)核讀緩沖區(qū)(read buffer)拷貝到用戶緩沖區(qū)(user buffer)的過程。
整個(gè)拷貝過程會(huì)發(fā)生 4 次上下文切換,1 次 CPU 拷貝和 2 次 DMA 拷貝。mmap 主要的用處是提高 I/O 性能,特別是針對(duì)大文件。對(duì)于小文件,內(nèi)存映射文件反而會(huì)導(dǎo)致碎片空間的浪費(fèi)。
2. Sendfile零拷貝技術(shù)
通過 Sendfile 系統(tǒng)調(diào)用,數(shù)據(jù)可以直接在內(nèi)核空間內(nèi)部進(jìn)行 I/O 傳輸,從而省去了數(shù)據(jù)在用戶空間和內(nèi)核空間之間的來回拷貝。
將要讀取的文件緩沖區(qū)的文件 fd 和要發(fā)送的Socket緩沖區(qū)的Socket fd 傳給sendfile函數(shù),Sendfile 調(diào)用中 I/O 數(shù)據(jù)對(duì)用戶空間是完全不可見的。也就是說,這是一次完全意義上的數(shù)據(jù)傳輸過程。也就是說用戶程序不能對(duì)數(shù)據(jù)進(jìn)行修改,而只是單純地完成了一次數(shù)據(jù)傳輸過程。整個(gè)拷貝過程會(huì)發(fā)生 2 次上下文切換,1 次 CPU 拷貝和 2 次 DMA 拷貝。
3. Sendfile+DMA gather copy
它只適用于將數(shù)據(jù)從文件拷貝到 socket 套接字上的傳輸過程。
它將內(nèi)核空間的讀緩沖區(qū)(read buffer)中對(duì)應(yīng)的數(shù)據(jù)描述信息(內(nèi)存地址、地址偏移量)記錄到相應(yīng)的網(wǎng)絡(luò)緩沖區(qū)( socket buffer)中,由 DMA 根據(jù)內(nèi)存地址、地址偏移量將數(shù)據(jù)批量地從讀緩沖區(qū)(read buffer)拷貝到網(wǎng)卡設(shè)備中。
這樣 DMA 引擎直接利用 gather 操作將頁(yè)緩存中數(shù)據(jù)打包發(fā)送到網(wǎng)絡(luò)中即可,本質(zhì)就是和虛擬內(nèi)存映射的思路類似。
整個(gè)拷貝過程會(huì)發(fā)生 2 次上下文切換、0 次 CPU 拷貝以及 2 次 DMA 拷貝。
4.Splice零拷貝技術(shù)
Splice相當(dāng)于在Sendfile+DMA gather copy上的提升,Splice 系統(tǒng)調(diào)用可以在內(nèi)核空間的讀緩沖區(qū)(read buffer)和網(wǎng)絡(luò)緩沖區(qū)(socket buffer)之間建立管道(pipeline),從而避免了兩者之間的 CPU 拷貝操作。
基于 Splice 系統(tǒng)調(diào)用的零拷貝方式,整個(gè)拷貝過程會(huì)發(fā)生 2 次上下文切換,0 次 CPU 拷貝以及 2 次 DMA 拷貝。
2.3總結(jié)
無論是傳統(tǒng)I/O拷貝方式,還是引入了零拷貝的方式,2次DMA Copy都是必要的步驟,因?yàn)閮纱蜠MA都是依賴硬件完成的。
三、零拷貝的實(shí)際應(yīng)用
3.1JavaNIO基于零拷貝的實(shí)現(xiàn)
Java-NIO:主要有三個(gè)方面用到了零拷貝技術(shù):
MAppedByteBuffer.map():底層調(diào)用了操作系統(tǒng)的mmap()內(nèi)核函數(shù)。
DirectByteBuffer.allocateDirect():可以直接創(chuàng)建基于本地內(nèi)存的緩沖區(qū)。
FileChannel.transferFrom()/transferTo():底層調(diào)用了sendfile()內(nèi)核函數(shù)。
3.2主流技術(shù)中零拷貝的應(yīng)用
1..NETty中零拷貝的應(yīng)用
Netty中的零拷貝是一種用戶進(jìn)程級(jí)別的零拷貝體現(xiàn),主要也包含三方面:
1) Netty的發(fā)送、接收數(shù)據(jù)的ByteBuf緩沖區(qū),默認(rèn)會(huì)使用堆外本地內(nèi)存創(chuàng)建,采用直接內(nèi)存進(jìn)行Socket讀寫,數(shù)據(jù)傳輸時(shí)無需經(jīng)過二次拷貝。如果使用傳統(tǒng)的堆內(nèi)存進(jìn)行Socket網(wǎng)絡(luò)數(shù)據(jù)讀寫,JVM需要先將堆內(nèi)存中的數(shù)據(jù)拷貝一份到直接內(nèi)存,然后才寫入Socket緩沖區(qū)中,相較于堆外直接內(nèi)存,消息在發(fā)送過程中多了一次緩沖區(qū)的內(nèi)存拷貝。
2)Netty的文件傳輸采用了transferTo()/transferFrom()方法,它可以直接將文件緩沖區(qū)的數(shù)據(jù)發(fā)送到目標(biāo)Channel(Socket),底層就是調(diào)用了sendfile()內(nèi)核函數(shù),避免了文件數(shù)據(jù)的CPU拷貝過程。
3)Netty提供了組合、拆解ByteBuf對(duì)象的API,咱們可以基于一個(gè)ByteBuf對(duì)象,對(duì)數(shù)據(jù)進(jìn)行拆解,也可以基于多個(gè)ByteBuf對(duì)象進(jìn)行數(shù)據(jù)合并,這個(gè)過程中不會(huì)出現(xiàn)數(shù)據(jù)拷貝,這個(gè)是程序級(jí)別的零拷貝,實(shí)際上就是在原數(shù)據(jù)的基礎(chǔ)上用不同的引用表示而已。
2. 其他技術(shù)中的零拷貝技術(shù)應(yīng)用
Kafka底層基于java.nio包下的FileChannel.transferTo()實(shí)現(xiàn)零拷貝。Kafka Server基于FileChannel將文件中的消息數(shù)據(jù)發(fā)送到SocketChannel。
RocketMQ基于mmap + write的方式實(shí)現(xiàn)零拷貝。內(nèi)部實(shí)現(xiàn)基于nio提供的java.nio.MappedByteBuffer,基于FileChannel的map方法得到mmap的緩沖區(qū)。