網絡編程的未來,io_uring?

前言

熟悉Linux網絡或者存儲編程的開發人員,對于libaio [1] (Linux-native asynchronous I/O) 應該并不太陌生。Libaio提供了一套不同于POSIX接口的異步I/O接口,其目的是更加高效的利用I/O設備。在最近幾年的過程中,有很多Linux 開發人員試圖去優化libaio相關的實現,但是收效甚微。于是Jens [2] 開發了一套新的異步編程接口io_uring [3] ,主要是為了替代libaio,目前主要應用在存儲的場景中。相比使用libaio,在存儲中使用io_uring,那么應用的性能有了顯著的提升。比如說用FIO程序,采用不同的存儲引擎libaio和io_uring測試同樣的NVMe SSD(見[4]),io_uring的效果要好不少。為此也有一些文章宣稱,在某些場景下使用io_uring + Kernel NVMe的驅動,效果甚至要比使用SPDK 用戶態NVMe 驅動更好。當然在SPDK 項目中,用戶也進行如下的對比,使用FIO + SPDK bdev 測試同樣的NVMe 盤,可以采用以下的三種bdev進行性能比較: (1) 基于SPDK中用戶態NVMe驅動的bdev; (2) 基于SPDK中libaio實現的bdev;(3) 基于SPDK中io_uring實現的bdev。然后可以自行比較一下哪種方式性能最好。

在這篇文章中,我們主要是把io_uring使用在網絡中。為了使得大家更清晰的了解,我們將在本文中介紹如下內容:

  • SPDK socket API的現狀
  • SPDK中怎么高效利用kernel TCP/IP 棧
  • io_uring目前存在的一些問題
  • 在SPDK 中怎么使能io_uring

SPDK socket API 現狀

在SPDK的代碼庫,我們有一個socket API 位于(lib/sock)目錄下,這個SPDK socket API主要用于封裝各個不同的TCP/IP的socket實現,包括基于內核TCP/IP的socket, 以及用戶態的socket實現。圖1給出了目前代碼中實現的一些情況, SPDK 20.04 版本以及主分支上有三種sock封裝, POSIX(module/sock/posix), uring(module/sock/uring), vpp (module/sock/vpp) 。其中POSIX庫主要是使用了POSIX的接口去操縱kernel的TCP/IP 棧,uring主要是采用io_uring的接口去使用kernel TCP/IP的棧,VPP 的socket主要是整合了VPP的用戶態TCP/IP 棧。從圖1中我們可以看到,目前我們主要推薦POSIX的實現。因為uring socket的實現需要環境的支持,比如對Linux kernel版本的要求。目前Linux kernel對于io_uring的支持還在開發過程中,所以在SPDK 中基于io_uring的socket實現,需要不斷地完善和更新。但是不可否認,使用io_uring來高效地使用kernel TCP/IP 棧是未來的發展趨勢。

對于SPDK中VPP的整合,目前的狀況是在SPDK中的集成,將會在20.07停止,20.10中把VPP的sock實現從SPDK 中刪除。其主要原因是基于VPP的sock實現并沒有體現出相應的性能優勢,具體的聲明可以參考 [5] 。至于Seastar在SPDK 的集成工作,目前也處于停滯的階段, 具體patch請見[6]。現在SPDK socket API工作的重點在怎么高效利用kernel的TCP/IP棧實現。

高效使用kernel TCP/IP 棧

SPDK 的編程框架的總體思想是異步(asynchronous),數據通路無鎖化(lock free data path),以及基于用戶態的存儲協議棧。雖然kernel TCP/IP 棧在內核中的實現并不是無鎖的,但是應用程序可以盡可能高效地利用kernel TCP/IP 棧。為了配合SPDK的編程框架,我們采用了以下的方法在用戶態高效利用kernel TCP/IP棧,可以分為以下兩類,針對單socket以及針對多socket。

針對單socket的優化

1. 采用非阻塞的I/O。 對于一個SOCKET File Descriptor(簡稱為SOCK_FD)。我們需要通過系統調用設置相應的屬性O_NONBLOCK。這個主要是因為在發生調用的時候,諸如read或者write。我們不希望調用者阻塞在那里。因為根據SPDK的framework,我們還有其他任務去做(參考SPDK 編程framework這篇文章[7])。如果采用阻塞的I/O將會影響到后續其他任務的調度。當然采用非阻塞I/O,就必須要處理可能存在的部分讀或者寫的問題(partial read/write)。舉個例子如圖2 所示,用戶發起了一個writev操作,vector里面有三個元素,長度分別是4096, 8192,8192,總長度是16384。那么可能返回結果是,第一個元素的寫全部完成,第二個完成了一半,第三個什么都沒完成。很顯然我們需要處理這樣的情況,要么讓用戶自己處理,要么在SPDK 庫里面需要處理。

2.減少系統調用。我們知道使用內核TCP/IP棧使用網絡讀寫,需要利用系統調用。頻繁的系統調用會產生大量的上下文切換(進程和內核之間)。為此有必要減少系統調用。

  • 對讀系統調用的優化。對于讀的系統調用,我們可以采用以下的策略。比如在用戶態開辟一片較大的緩存,然后盡可能一次性讀取大量數據,如圖3 所示。這里我們可以看到在用戶態分配了一個大的臨時buffer,然后采用read或者readv進行操作,然后把讀到的內容分配拷貝到用戶自己邏輯的Buffer1,Buffer2或者Buffer3中。這里讀者不僅要問,為什么不能直接利用Buffer1-3 直接構建一個vector數組,進行readv讀寫。這個問題在于, 并不是在任何時候,我們都能明確知道要進行讀的地址。舉個例子,比如在NVMe-oF TCP的parse過程中,我們可以把PDU的header配置一個buffer,PDU所涉及到的數據采用另外一個buffer。但是并不是每個PDU 都有帶有數據。所以我們不能保證在進行讀的時候,知道所有buffer的地址。這么看來不能減少數據的拷貝。顯然用這樣的方法,我們可以減少讀操作的系統調用,但是增加了新的代價,即數據拷貝。為此我們必須做到平衡,意思是在減少系統調用的時候,盡可能采用一些其他硬件卸載數據拷貝的代價。比如在Intel平臺上我們可以使用CBDMA(Crystal beach DMA)或者DSA (Data Streaming Accelerator) 去進行copy操作,用于減輕CPU的工作。

  • 合并寫操作減少系統調用。對于寫的操作,為了減少系統調用,我們可以把多個writev操作合并成一個writev操作。其主要思路就是構建一個新的vector數組,把多個writev各自的vector數據的信息放入。這樣可以減少寫的系統調用。

3. 采用MESSAGE ZERO COPY。 在網絡編程中,寫數據可以采用zero copy。這個特性在Linux 4.1. 4以后支持。比如在SPDK POSIX sock的實現中,我們對于寫的IO, 在target 那端,采用了Message zero copy。這個需要調用setsockopt系統調用對SOCK_FD進行設置(使用這個key:SO_ZEROCOPY)。然后在對socket進行寫的時候,我們不在使用writev,而是采用sendmsg, 然后采用MSG_ZEROCOPY 對應的flag。這樣的好處是我們可以避免I/O copy。那么Linux網絡內核棧會Pin住用戶提供的內存,而不是將其復制到內核中的發送緩沖區。當然Pin住這些內存,需要有額外的代價。另外采用MSG_ZEROCOPY 后,會產生一些額外的事件, 即target端Message control path上的錯誤事件: MSG_ERRQUEUE。我們需要進行相應的處理。因為在POSIX實現中,我們也使用了異步的寫接口,為此必須等到那個寫操作已經完成了,然后調用相應的callback函數。

目前1 和2相應的實現已經存在于SPDK的POSIX和uring的socket實現中, 3的實現主要在POSIX中。

針對多socket的優化

除了針對單socket的優化,SPDK socket實現中采用了以下的機制,來保證更高效的運行;

1. 采用唯一的SPDK thread 管理一個socket 的生命周期。在SPDK 中,對于每個連接(可以對應到相應的socket)I/O 讀寫處理,我們都使用唯一的SPDK thread。這意味著,我們盡可能避免了多個CPU 在競爭處理同一個連接。為了做到這一點,當我們的Socket Listener得到一個新的連接的時候,我們就可以利用算法把這個connection的處理調度到一個特定的SPDK thread之上處理。

2. 基于組的高效異步I/O 處理。一個SPDK thread可以管理多個連接,為了更高效地處理每個I/O上發生的讀寫事件。當然用戶調用SPDK 基于組的polling的時候(函數是spdk_sock_group_poll) 我們采取了如下策略:

  • 一次性偵測組中所有socket的POLLIN 事件。 比如在POSIX SOCK實現中,我們采用epoll的機制。在每一輪中,一次性調用epoll_wait去偵測組中哪些socket有讀的事件。這樣相比用read或者readv去單獨偵測哪個socket有讀時間要好很多。舉個極端的例子,如果一個組中有100個連接,如果只有一個連接有讀的事件。那么采用epoll的方式,我們只用了兩個系統調用,epoll_wait以及read。但是不采用epoll,我們需要100個系統調用,即對每個連接調用read或者readv。
  • 減少整個組的"寫操作"的系統調用。正常來講,如果每一個socket都要調用"寫操作",那么一個組有N個連接,就需要有N個寫系統調用。為了更好的解決這個問題,我們引入了uring(當然采用libaio也可以),如圖4所示。我們可以看到在每一輪中,我們可以一次性對所有組中所有socket上的寫請求,進行提交。如果組中有N個連接,在每一輪我們可以減少N-1次系統調用。當然異步的寫,還會引起每個socket上部分寫的問題(類似圖2描述的那樣)。這個問題在SPDK POSIX以及uring的實現中,均已經解決。我們主要的思路是,在下一輪可以重建IO vector,然后進行在下一輪重新提交。當然我們在下一輪的寫請求提交中,不僅僅是提交上一輪中每個socket未完成的寫I/O,我們也會在合并在上一輪和這一輪中新產生的I/O。為此這個設計是非常高效的。

io_uring目前使用的一些問題

SPDK uring的實現完全基于io_uring,目前在滿足版本需求的Linux kernel上可以正常工作。當然在SPDK uring的整個開發過程中,我們也碰到了一些問題,有些問題liburing庫相關的問題,有些問題是Linux kernel中io_uring實現局限。我們把這些問題反饋給io_uring的開發者Jens,得到了非常正面的反饋,也得到了相應的支持。目前角度看來,尚且存在以下的一些問題, 諸如:

  • Fixed buffer 支持。io_uring可以注冊固定的緩存,使得Linux內核可以Pin住這次內存,用于提高效率。但是目前io_uring 的固定緩存的支持比較有限。比如目前支持的是:io_uring_prep_write_fixed 以及io_uring_prep_read_fixed。但是并沒有支持io_uring_prep_writev_fixed以及io_uring_prep_readv_fixed。對于sendmsg以及recvmsg中的message填充的IOV似乎也沒有支持fixed 的buffer。這個無疑是一個缺陷。目前這個特性的改進,估計會出現在以后的版本中。
  • Control message的異步處理。目前io_uring中 io_uring_prep_sendmsg以及io_uring_prep_recvmsg對于數據通道相關的message,均可以支持異步處理。但是對于control message不能支持異步。比如在SPDK POSIX實現中的Message zero copy, 我們并不可以采用異步的recvmsg進行檢查。
  • IORING_SETUP_IOPOLL對于網絡設備的支持。 目前創建uring的時候,這個選項僅僅可以作用于O_DIRECT模式打開的文件或者設備,才可以讓用戶選擇在用戶態自己進行輪詢。顯而易見,網絡的描述符并不滿足這個特性。

SPDK 中使能io_uring

  • 升級Linux 內核,至少滿足內核版本大于5.4.3. 當然越高版本的內核對于io_uring的支持越好,比如Linux kernel 版本5.7-rc1以上的特性應該豐富,諸如支持IORING_FEAT_FAST_POLL 。
  • 下載和安裝liburing的庫:https://github.com/axboe/liburing
  • 編譯SPDK, 打開如下開關:./configure --with-uring 。如果liburing沒有安裝在系統指定的目錄,需要自己指定。這樣編譯出的SPDK 可執行文件,會優先使用SPDK的uring socket實現,而不是POSIX。比如啟動SPDK NVMe-oF tcp target, 就會采用SPDK uring 的socket實現。

總結

本文介紹了SPDK 項目中socket實現的一些現狀,以及在SPDK 中我們是怎樣高效利用內核的TCP/IP 棧,并在POSIX以及uring的socket 模塊中實現。在我們的后續開發過程中,我們會繼續不斷地完善SPDK uring socket的實現。

References
[1] https://pagure.io/libaio
[2] https://en.wikipedia.org/wiki/Jens_Axboe
[3] Efficient IO with io_uring. https://kernel.dk/io_uring.pdf
[4]https://www.flashmemorysummit.com/Proceedings2019/08-07-Wednesday/20190807_SOFT-202-1_Verma.pdf
[5]https://lists.01.org/hyperkitty/list/spdk@lists.01.org/thread/L7FST3E5CKUCUK4SX24IYXMDWBHH4VAA/
[6]https://review.gerrithub.io/c/spdk/spdk/+/466629

文章轉載自DPDK與SPDK開源社區


  • 本站原創文章僅代表作者觀點,不代表SDNLAB立場。所有原創內容版權均屬SDNLAB,歡迎大家轉發分享。但未經授權,嚴禁任何媒體(平面媒體、網絡媒體、自媒體等)以及微信公眾號復制、轉載、摘編或以其他方式進行使用,轉載須注明來自 SDNLAB并附上本文鏈接。 本站中所有編譯類文章僅用于學習和交流目的,編譯工作遵照 CC 協議,如果有侵犯到您權益的地方,請及時聯系我們。
  • 本文鏈接http://www.taian720.com/24474.html
分享到:
相關文章
條評論

登錄后才可以評論

大臉肥飛貓 發表于20-09-28
0