io_uring,幹翻 nio!

原創:小姐姐味道(微信公衆號 ID:xjjdog),歡迎分享,非公衆號轉載保留此聲明。

大家都知道 BIO 非常的低效,而網絡編程中的 IO 多路複用普遍比較高效。

現在,io_uring 已經能夠挑戰 NIO 的,功能非常強大。io_uring 在 2019 加入了 Linux 內核,目前 5.1 + 的內核,可以採用這個功能。

隨着一步步的優化,系統調用這個大傢伙,調用次數越來越少了。

一、性能耗費在哪裏?

在 Linux 的性能指標裏,有ussy兩個指標,使用top命令可以很方便的看到。

us是用戶進程的意思,而sy是在內核中所使用的 cpu 佔比。如果進程在內核態和用戶態切換的非常頻繁,那麼效率大部分就會浪費在切換之上。

一次內核態和用戶態切換的時間,普遍在微秒級別以上,可以說非常昂貴了。

cpu 的性能是固定的,在無用的東西上浪費越小,在真正業務上的處理就效率越高。影響效率的有兩個方面。

  1. 進程或者線程的數量,引起過多的上下文切換。進程是由內核來管理和調度的,進程的切換隻能發生在內核態。所以,如果你的代碼切換了線程,它必然伴隨着一次用戶態和內核態的切換。

  2. IO 的編程模型,引起過多的系統態和內核態切換。比如同步阻塞等待的模型,需要經過數據接收、軟中斷的處理(內核態),然後喚醒用戶線程(用戶態),處理完畢之後再進入等待狀態(內核態)。

關於 mmap,可以參考這篇文章。

《OS 近距離:mmap 給你想要的快!》

二、BIO

可以說,BIO 這種模式,在線程數量上爆炸,編程模型古老,把性能低的原因全給佔了。

通常情況下,BIO 一條連接就對應着一個線程。BIO 的讀寫操作是阻塞的,線程的整個生命週期和連接的生命週期是一樣的,而且不能夠被複用。

如果連接有 1000 條,那就需要 1000 個線程。線程資源是非常昂貴的,除了佔用大量的內存,還會佔用非常多的 CPU 調度時間,所以 BIO 在連接非常多的情況下,效率會變得非常低。

BIO 的編程模型,也存在諸多缺陷。因爲它是阻塞性編程模式,在有數據的時候,需要內核通知它;在沒有數據的時候,需要阻塞 wait 在相應的 socket 上。這兩個操作,都涉及到內核態和用戶態的切換。如果數據報文非常頻繁,BIO 就需要這麼一直切換。

三、NIO

提到 NIO,Java 中使用的是 Epoll,Netty 使用的是改良後的 Epoll,它們都是多路複用,只不過叫慣了,所以稱作 NIO。

採用 Reactor 編程模型,可以採用非常少的線程,就能夠應對海量的 Socket 連接。

一旦有新的事件到達,比如有新的連接到來,主線程就能夠被調度到,程序就能夠向下執行。這時候,就能夠根據訂閱的事件通知,持續獲取訂閱的事件。

NIO 是基於事件機制的,有一個叫做 Selector 的選擇器,阻塞獲取關注的事件列表。獲取到事件列表後,可以通過分發器,進行真正的數據操作。

熟悉 Netty 的同學可以看到,這個模型就是 Netty 設計的基礎。在 Netty 中,Boss 線程對應着對連接的處理和分派,相當於 mainReactor;Work 線程 對應着 subReactor,使用多線程負責讀寫事件的分發和處理。

通過 Selector 選擇器,NIO 將 BIO 中頻繁的 wait 和 notify 操作,集中在了一起,大量的減少了內核態和用戶態的切換。在網絡流量比較高的時候,Selector 甚至都不會阻塞,它將一直處於處理數據的過程中。

這種模式將每個組件的職責分的更細,耦合度也更低,能有效的解決C10k問題。

四、io_uring

但是,NIO 依然有大量的系統調用,那就是 Epoll 的 epoll_ctl。另外,獲取到網絡事件之後,還需要把 socket 的數據進行存取,這也是一次系統調用。雖然相對於 BIO 來說,上下文切換次數已經減少很多,但它仍然花費了比較多的時間在切換之上。

IO 只負責對發生在 fd 描述符上的事件進行通知。事件的獲取和通知部分是非阻塞的,但收到通知之後的操作,卻是阻塞的。即使使用多線程去處理這些事件,它依然是阻塞的。

如果能把這些系統調用都放在操作系統裏完成,那麼就可以節省下這些系統調用的時間,io_uring 就是幹這個的。

如圖,用戶態和內核態共享提交隊列(submission queue)和完成隊列(completion queue),這兩條隊列通過 mmap 共享,高效且安全。

(SQ)給內核源源不斷的佈置任務,然後從另外一條隊列(CQ)獲取結果;內核則按需進行 epoll(),並在一個線程池中執行就緒的任務。

用戶態支持 Polling 模式,不會發生中斷,也就沒有系統調用,通過輪詢即可消費事件;內核態也支持 Polling 模式,同樣不會發生上下文切換。

可以看出關鍵的設計在於,內核通過一塊和用戶共享的內存區域進行消息的傳遞,可以繞過 Linux 的 syscall 機制。

rocksdb、ceph 等應用,已經在嘗試這些功能,隨着內核 io_uring 的成熟,相信網絡編程在效率上會更上一層樓。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Mcv7meWsNAPyyyAOHDlFRg