Efficient data transfer through zero copy
Zero Copy I: User-Mode Perspective
0. 前言
在閱讀RocketMQ的官方文檔時,發現Chapter6.1中關於零拷貝的敘述中有點不理解,因此查閱了相關資料,來解釋文中的說法。
Consumer消費消息過程,使用了零拷貝,零拷貝包含以下兩種方式
- 使用mmap + write方式 優點:即使頻繁調用,使用小塊文件傳輸,效率也很高 缺點:不能很好的利用DMA方式,會比sendfile多消耗CPU,內存安全性控制復雜,需要避免JVM Crash問題。
- 使用sendfile方式 優點:可以利用DMA方式,消耗CPU較少,大塊文件傳輸效率高,無內存安全新問題。 缺點:小塊文件效率低於mmap方式,只能是BIO方式傳輸,不能使用NIO。
RocketMQ選擇了第一種方式,mmap+write方式,因為有小塊數據傳輸的需求,效果會比sendfile更好。
為什么mmap會多消耗CPU?
為什么mmap比sendfile內存安全性控制復雜,為什么mmap會引起JVM Crash?
為什么sendfile只能是BIO的,不能使用NIO?
為什么mmap對於小塊數據傳輸的需求效果更好?
- 這個問題其實很好解答,如果上一個說法成立,mmap支持NIO,sendfile只能BIO傳輸,那么NIO的特性本身就會對數據塊小、請求個數多的傳輸需求有很好的支持。
1. 零拷貝 Zero copy
1.1 no zero-copy
Web應用程序通常提供大量靜態內容,例如使用聊天工具向好友發送了一張本地圖片,應用程序需要從磁盤讀取圖片數據,並將完全相同的數據寫到響應socket中,通過網絡發送給對方。這個操作看起來貌似不需要占用過多的CPU資源,因為沒有計算的需求,但仍然效率較低:內核從磁盤讀取數據並將其推送到應用程序,然后應用程序將其推回到內核寫到套接字。這種場景下,應用程序充當了一個低效的中介,它將數據從磁盤文件獲取到套接字。
1.2 zero copy
每次數據經過用戶內核邊界時,都必須復制一次數據,這會消耗CPU周期和內存帶寬。zero-copy技術的出現就是通過減少復制次數來消除這些副本。使用零拷貝請求的應用程序,內核將數據直接從磁盤文件復制到套接字,而不用 無需通過應用程序。零拷貝極大地提高了應用程序性能,並減少了內核和用戶模式之間的上下文切換次數。
1.3 Java中的zero copy
Java類庫通過java.nio.channels.FileChannel
中的transferTo()
方法在Linux和UNIX系統上支持零拷貝。使用transferTo()
法將字節直接從調用它的通道傳輸到另一個可寫字節通道,而不需要數據流經應用程序。
本文先解釋下傳統復制,然后介紹zero copy
的幾種機制,最后解釋前言中的疑問。
1.4 類比舉例
在干貨之前,先喝口湯壓壓驚。舉個通俗點的例子來類比描述傳統的 no zero-copy的做法,A用左手拿筷子要吃飯,B告訴你A,你需要用右手拿筷子,然后A把筷子從左手給了B,然后B又把筷子塞到A的右手里,A開始吃飯。
A和B這里可以看成兩個上下文,A的左手傳遞筷子給B之后,切換到了B的上下文,B傳遞給A的右手,又切換回A的上下文,這個代價其實是非常昂貴的。
為了減少這種昂貴的代價,我們可以想象一些場景來逐步降低事情的復雜度。
最直觀最簡單的方法,B只需要告訴A,也就是說B發出一條指令,A接收指令之后,自己把筷子從左手換到右手,就可以既減少了上下文的切換,減輕了B的壓力,又減少了傳遞的次數和溝通代價。然而這需要A具備這樣的功能,計算機中某些硬件可以提供這樣的支持,但是如果A是一個不滿3歲的孩子,他可能聽不懂你的話,又或者不明白如何把筷子從左手轉到右手。
這個時候B只需要扶住A的左手,幫他把筷子換到右手里。這樣,也縮減了這個過程中的代價。
2. 傳統傳輸方式
Linux標准訪問文件方式
在Linux中,訪問文件的方式是通過兩個系統調用實現的:read()
和write()
。
當應用程序調用read()
系統調用讀取一塊數據的時候:
- 如果該塊數據已經在內存中,就直接從內存中讀取數據並返回給應用程序;
- 如果該塊數據不在內存中,name數據會被從磁盤上讀取到頁緩存中,再從頁緩存中拷貝到用戶地址空間中去。
如果一個進程讀取某個文件,那么其他進程就都不可以讀取或者更改該文件;對於寫操作,當一個進程調用了write()
系統調用往某個文件中寫數據的時候,數據會先從用戶地址空間拷貝到操作系統內核地址空間的頁緩存中,然后才被寫到磁盤上。
對於這種標准的訪問文件方式,在數據被寫到頁緩存中時,write()
系統調用就算執行完成,並不會等數據完全寫入到磁盤上。
Linux在這里采用的是延遲寫機制。
一般情況下,應用程序采取的寫操作機制有三種:
- 同步寫(Synchronous Writes),數據會立即從緩存頁寫回磁盤,應用程序會一直等待到寫入磁盤的結束。
- 異步寫(Asynchronous Writes),數據寫入緩存頁后,操作系統會定期將頁緩存中的數據刷到磁盤上,在寫入磁盤結束后,系統會通知應用程序寫入已完成。
- 延遲寫(Deferred Writes),數據寫入緩存頁后立即返回應用程序寫入成功,操作系統定期將頁緩存中是數據刷入磁盤,由於寫入頁緩存時已經返回吸入成功,寫入磁盤之后不會通知應用程序。因此延遲寫機制是存在數據丟失的風險的。
2.1 傳輸過程
圖1. 傳統方式數據傳輸示意圖
如上圖所示,數據按箭頭的方向流動,從本地終端的硬盤存儲中讀取數據,經過4次copy,最終到達NIC buffer
,通過網卡再發送給其他終端。
這種傳輸方式實際上是一種經過優化的設計,雖然看起來效率比較低下,但是內核緩沖區
的存在使得整個流程的性能得到了提升。內核緩沖區
的引入充當了預讀緩存
的角色,使得數據並不是直接從硬盤到用戶緩沖區,而是允許應用程序在未請求的情況下,內核緩沖區
中已經存在了相應的數據。
內核空間內,內存與硬件存儲之間的數據傳輸使用了DMA直接內存存取
的復制方式,這種方式不需要CPU的參與,並且提高讀取速度。CPU也因此可以趁機去完成其他的工作。
例如用戶緩沖區大小為4K,內核緩沖區大小為8K,文件總大小為40K,每次用戶請求讀取4K數據時,內核緩沖區中已經預讀存入了相應的數據。
硬盤到內存(內核緩沖區)的數據傳輸速度是比較慢的,尤其是SSD應用之前,而內核緩沖區到用戶緩沖區這種內存到內存的復制相對較快,用戶緩沖區就不用等待硬盤數據傳輸到內存。廣義上也是一種空間換時間的做法。
然而,一些情況下內核緩沖區
也無法完全跟上應用程序的步伐,比如用戶緩沖區的大於內核緩沖區的大小。預讀的數據無法滿足需要,仍然需要等待硬盤到內存的緩慢傳輸。此時性能將會大打折扣。
盡管做了很多優化,如圖所示,數據已經被復制了至少四次,並且執行了多次的用戶和內核上下文的切換。實際上這個過程比圖示要復雜得多。
2.2 上下文切換和數據復制過程
圖2.上下文切換和數據復制
圖2所示是傳統方式數據傳輸(圖1)時上下文切換和數據復制的過程。上半部分表示上下文切換,下半部分表示數據復制流程。
step 1
系統調用讀操作時,上下文會從用戶模式切換到內核模式。在內核空間中,DMA引擎執行了第一次數據拷貝,將數據從硬盤等其他存儲設備上導入到內核緩沖區。
step 2
數據從內核緩沖區拷貝到用戶緩沖區,系統調用讀操作結束並返回。調用的返回會導致又一次的上下文切換,上下文從內核又切換到用戶模式。
step 3
系統寫操作開始調用,進行第三次數據復制,將數據從用戶緩沖區寫回內核的socket緩沖區,此時回引起一次從用戶模式到內核模式的上下文切換。
step 4
寫操作的系統調用返回,引起第四次上下文切換。開始第四次數據復制,數據從內核緩沖區復制到協議引擎。這次復制是異步並且獨立的,系統不保證數據一定會傳輸,這次返回只是任務提交成功,數據包進入了隊列,等待傳輸。就像線程池模型中任務提交時,任務只是成功提交到任務隊列,何時開始執行上游調用程序並不知情。
summary
傳統的傳輸方式會存在大量的數據復制和上下文切換,如果這些重復可以消除一部分,就可以減少開銷並提升性能。某些硬件可以繞過主存儲器將數據直接傳輸到另一個設備,但是如1.4中的類比描述一樣,並不是所有硬件都支持這項功能,而且這項功能的實現遠非這么簡單。
為了降低開銷,我們可以減少復制,而不是直接消除復制。
系統為了減少復制所采用的所有方式中,最多的就是讓這些傳輸盡可能不跨越用戶空間和內核空間的邊界,因為每次跨越邊界就意味着一次復制。
3. zero-copy mmap
圖3. mmap方式零拷貝數據傳輸示意圖
mmap系統調用的方式如下:
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
這種方式用到兩種系統調用,mmap+write。
這種傳輸方式使用mmap()
代替了read()
,磁盤上的數據會通過DMA
被拷貝到內核緩沖區,然后操作系統會把這塊內核緩沖區與應用程序共享,這樣就避免了跨越邊界的一次復制。應用程序再調用write()
直接將內核緩沖區的內容拷貝到socket緩沖區,最后系統把數據從socket緩沖區傳輸到網卡。
mmap
減少了一次拷貝,提升了效率。但是mmap
也可能遇到一些隱藏的問題。例如,當應用程序map
了一個文件,但是這個文件被另一個進程截斷時,write()
系統調用會因為訪問非法地址而被SIGBUS
信號終止。SIGBUS
信號默認會殺死你的進程並產生一個coredump
。
解決mmap
上述問題的方式通常有兩種:
- 增加對
SIGBUS
信號的處理程序
當遇到SIGBUS
信號時,處理程序可以直接去調用return,這樣,write
調用在被中斷之前返回已經寫入的字節數並且將errno
設置為success
。但是這么處理顯得較為粗糙。 - 使用文件租借鎖
在文件描述符上使用租借鎖,這樣當有進程要截斷這個文件時,內核會立刻發送一個RT_SIGNAL_LEASE
信號,這樣在程序訪問非法內存之前,中斷write
調用,返回已經寫入的字節數,並將errno
設置為success
,而不必等到write
被SIGBUS
殺死再做處理。
圖4 mmap上下文切換與數據傳輸
如上圖所示,mmap
+write
的復制減少了文件的復制,但是上下文切換的次數和read
+write
的方式是一樣的。
4. sendfile
圖5 sendfile數據傳輸示意圖
Linux的內核版本2.1之后,系統引入了sendfile
來簡化文件傳輸到網絡的工作,這種方式不僅減少了拷貝次數,也減少了上下文的切換。
使用sendfile代替了read
+write
操作。
圖6 sendfile上下文切換與數據傳輸
數據發生三次拷貝,首先sendfile
系統調用,通過DMA引擎將文件復制到內核緩沖區。
在內核區,內核將數據復制到socket
緩沖區。
最后,DMA引擎將數據從內核socket緩沖區傳遞到協議引擎中(網卡)。
sendfile
是否會遇到和mmap
同樣的隱藏問題?
- 如果另一個進程截斷了使用
sendfile
傳輸的文件,sendfile
在沒有任何信號處理程序的情況下,會返回被中斷前傳輸的字節數,並且errno
被設置為success
。 - 如果使用了文件租借鎖,sendfile可以獲得
RT_SIGNAL_LEASE
信號,並給出和沒有使用文件租借鎖同樣的返回。
5. 使用DMA gather copy的sendfile
在內核2.4版本之后,sendfile
可以在硬件支持的情況下實現更高效的傳輸。
圖7 使用DMA gather copy的sendfile數據傳輸示意圖
在硬件的支持下,不再從內核緩沖區的數據拷貝到socket緩沖區,取而代之的僅僅是緩沖區文件描述符和數據長度的拷貝。
這樣DMA引擎直接將頁緩存中數據打包發送到網絡中即可。
圖8 DMA gather copy的sendfile上下文切換與數據傳輸
這種方式避免了最后一次拷貝,並且減輕了CPU的負擔,省去了頁緩存到socket緩沖區的CPU Copy
。這種sendfile是Linux中真正的零拷貝,雖然依然需要磁盤到內存的復制,但是內核空間和用戶空間內已經不存在任何多余的復制。
這種方式的前提是硬件和相關驅動程序支持DMA Gather Copy
。
6.總結
通過以上描述,可以解答文章開始的幾個問題。
為什么mmap會多消耗CPU?
mmap
沒有完全消除內存中的文件復制,從頁緩存到socket緩沖區需要進行CPU Copy
,並且上下文的切換次數和傳統的read
+write
方式一樣。
因此,相對於sendfile
,mmap
會占用更多的CPU資源。
為什么mmap比sendfile內存安全性控制復雜,為什么mmap會引起JVM Crash?
mmap
沒有提供被其他進程截斷時的處理,需要添加對SIGBUS
信號中斷的處理。由於截斷后,mmap
訪問了非法內存,SIGBUS
信號會導致JVM Crash
的問題。
為什么sendfile只能是BIO的,不能使用NIO?(個人理解,未驗證)
sendfile
在使用DMA gather copy
的情況下,降低了CPU資源的占用,減少了文件復制和上下文切換次數,但是由於socket緩沖區中拿到的只是文件描述符和數據長度,並沒有拿到真正的文件,因此並不能執行write等相關操作異步寫或者延遲寫,只能進行同步寫。這也是減少上下文切換付出的代價,接收到sendfile后,都在內核態執行,缺少應用程序的干預因此可控性也較差。所以sendfile
只能使用BIO
這種同步阻塞的IO。
However, 在大文件的傳輸上,sendfile
依然是最佳的方式。
Java中NIO的類庫通過java.nio.channels.FileChannel
中的transferTo()
依賴的零拷貝是sendfile
,因此實質上transferTo
並不支持真正意義上的NIO。
而mmap
+write
的方式,使真正的文件被復制到socket緩沖區,從socket緩沖區到網卡的復制過程是可以異步的,但是這種操作意味着更多的CPU消耗。
RocketMQ中更多的需求是小文件的傳輸,而NIO的特性可以更快更高效的應對這種場景。在這種權衡考量下,犧牲部分CPU資源來換取更高的文件傳輸效率的選擇顯然是一種更優的方案。