Io_uring,干翻 Nio!
大家都知道BIO非常的低效,而網(wǎng)絡(luò)編程中的IO多路復用普遍比較高效。
現(xiàn)在,io_uring已經(jīng)能夠挑戰(zhàn)NIO的,功能非常強大。io_uring在2019加入了Linux內(nèi)核,目前5.1+的內(nèi)核,可以采用這個功能。
隨著一步步的優(yōu)化,系統(tǒng)調(diào)用這個大家伙,調(diào)用次數(shù)越來越少了。
一、性能耗費在哪里?
在Linux的性能指標里,有us和sy?兩個指標,使用top命令可以很方便的看到。
us?是用戶進程的意思,而sy是在內(nèi)核中所使用的cpu占比。如果進程在內(nèi)核態(tài)和用戶態(tài)切換的非常頻繁,那么效率大部分就會浪費在切換之上。
一次內(nèi)核態(tài)和用戶態(tài)切換的時間,普遍在微秒級別以上,可以說非常昂貴了。
cpu的性能是固定的,在無用的東西上浪費越小,在真正業(yè)務(wù)上的處理就效率越高。影響效率的有兩個方面。
- 進程或者線程的數(shù)量,引起過多的上下文切換。進程是由內(nèi)核來管理和調(diào)度的,進程的切換只能發(fā)生在內(nèi)核態(tài)。所以,如果你的代碼切換了線程,它必然伴隨著一次用戶態(tài)和內(nèi)核態(tài)的切換。
- IO的編程模型,引起過多的系統(tǒng)態(tài)和內(nèi)核態(tài)切換。比如同步阻塞等待的模型,需要經(jīng)過數(shù)據(jù)接收、軟中斷的處理(內(nèi)核態(tài)),然后喚醒用戶線程(用戶態(tài)),處理完畢之后再進入等待狀態(tài)(內(nèi)核態(tài))。
關(guān)于mmap,可以參考這篇文章。
《OS近距離:mmap給你想要的快!》
二、BIO
可以說,BIO這種模式,在線程數(shù)量上爆炸,編程模型古老,把性能低的原因全給占了。
通常情況下,BIO一條連接就對應(yīng)著一個線程。BIO的讀寫操作是阻塞的,線程的整個生命周期和連接的生命周期是一樣的,而且不能夠被復用。
如果連接有1000條,那就需要1000個線程。線程資源是非常昂貴的,除了占用大量的內(nèi)存,還會占用非常多的CPU調(diào)度時間,所以BIO在連接非常多的情況下,效率會變得非常低。
BIO的編程模型,也存在諸多缺陷。因為它是阻塞性編程模式,在有數(shù)據(jù)的時候,需要內(nèi)核通知它;在沒有數(shù)據(jù)的時候,需要阻塞wait在相應(yīng)的socket上。這兩個操作,都涉及到內(nèi)核態(tài)和用戶態(tài)的切換。如果數(shù)據(jù)報文非常頻繁,BIO就需要這么一直切換。
三、NIO
提到NIO,Java中使用的是Epoll,Netty使用的是改良后的Epoll,它們都是多路復用,只不過叫慣了,所以稱作NIO。
采用Reactor編程模型,可以采用非常少的線程,就能夠應(yīng)對海量的Socket連接。
一旦有新的事件到達,比如有新的連接到來,主線程就能夠被調(diào)度到,程序就能夠向下執(zhí)行。這時候,就能夠根據(jù)訂閱的事件通知,持續(xù)獲取訂閱的事件。
NIO是基于事件機制的,有一個叫做Selector的選擇器,阻塞獲取關(guān)注的事件列表。獲取到事件列表后,可以通過分發(fā)器,進行真正的數(shù)據(jù)操作。
熟悉Netty的同學可以看到,這個模型就是Netty設(shè)計的基礎(chǔ)。在Netty中,Boss線程對應(yīng)著對連接的處理和分派,相當于mainReactor;Work線程 對應(yīng)著subReactor,使用多線程負責讀寫事件的分發(fā)和處理。
通過Selector選擇器,NIO將BIO中頻繁的wait和notify操作,集中在了一起,大量的減少了內(nèi)核態(tài)和用戶態(tài)的切換。在網(wǎng)絡(luò)流量比較高的時候,Selector甚至都不會阻塞,它將一直處于處理數(shù)據(jù)的過程中。
這種模式將每個組件的職責分的更細,耦合度也更低,能有效的解決C10k問題。
四、io_uring
但是,NIO依然有大量的系統(tǒng)調(diào)用,那就是Epoll的epoll_ctl。另外,獲取到網(wǎng)絡(luò)事件之后,還需要把socket的數(shù)據(jù)進行存取,這也是一次系統(tǒng)調(diào)用。雖然相對于BIO來說,上下文切換次數(shù)已經(jīng)減少很多,但它仍然花費了比較多的時間在切換之上。
IO只負責對發(fā)生在fd描述符上的事件進行通知。事件的獲取和通知部分是非阻塞的,但收到通知之后的操作,卻是阻塞的。即使使用多線程去處理這些事件,它依然是阻塞的。
如果能把這些系統(tǒng)調(diào)用都放在操作系統(tǒng)里完成,那么就可以節(jié)省下這些系統(tǒng)調(diào)用的時間,io_uring就是干這個的。
如圖,用戶態(tài)和內(nèi)核態(tài)共享提交隊列(submission queue)和完成隊列(completion queue),這兩條隊列通過mmap共享,高效且安全。
(SQ)給內(nèi)核源源不斷的布置任務(wù),然后從另外一條隊列(CQ)獲取結(jié)果;內(nèi)核則按需進行 epoll(),并在一個線程池中執(zhí)行就緒的任務(wù)。
用戶態(tài)支持Polling模式,不會發(fā)生中斷,也就沒有系統(tǒng)調(diào)用,通過輪詢即可消費事件;內(nèi)核態(tài)也支持Polling模式,同樣不會發(fā)生上下文切換。
可以看出關(guān)鍵的設(shè)計在于,內(nèi)核通過一塊和用戶共享的內(nèi)存區(qū)域進行消息的傳遞,可以繞過Linux 的 syscall 機制。
rocksdb、ceph等應(yīng)用,已經(jīng)在嘗試這些功能,隨著內(nèi)核io_uring的成熟,相信網(wǎng)絡(luò)編程在效率上會更上一層樓。
作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎(chǔ)架構(gòu)和Linux。十年架構(gòu),日百億流量,與你探討高并發(fā)世界,給你不一樣的味道。