精品国产人成在线_亚洲高清无码在线观看_国产在线视频国产永久2021_国产AV综合第一页一个的一区免费影院黑人_最近中文字幕MV高清在线视频

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

runtime 的一些對(duì)比選型和應(yīng)用

jf_wN0SrCdH ? 來(lái)源:Rust語(yǔ)言中文社區(qū) ? 2023-05-26 15:48 ? 次閱讀

01

概述

盡管 Tokio 目前已經(jīng)是 Rust 異步運(yùn)行時(shí)的事實(shí)標(biāo)準(zhǔn),但要實(shí)現(xiàn)極致性能的網(wǎng)絡(luò)中間件還有一定距離。為了這個(gè)目標(biāo),CloudWeGo Rust Team 探索基于 io-uring 為 Rust 提供異步支持,并在此基礎(chǔ)上研發(fā)通用網(wǎng)關(guān)。

本文包括以下內(nèi)容:

介紹 Rust 異步 Runtime;

Monoio 的一些設(shè)計(jì)精要;

Runtime 對(duì)比選型與應(yīng)用。

02

Rust 異步機(jī)制

借助 Rustc 和 llvm,Rust 可以生成足夠高效且安全的機(jī)器碼。但是一個(gè)應(yīng)用程序除了計(jì)算邏輯以外往往還有 IO,特別是對(duì)于網(wǎng)絡(luò)中間件,IO 其實(shí)是占了相當(dāng)大比例的。

程序做 IO 需要和操作系統(tǒng)打交道,編寫(xiě)異步程序通常并不是一件簡(jiǎn)單的事情,在 Rust 中是怎么解決這兩個(gè)問(wèn)題的呢?比如,在 C++里面,可能經(jīng)常會(huì)寫(xiě)一些 callback ,但是我們并不想在 Rust 里面這么做,這樣的話會(huì)遇到很多生命周期相關(guān)的問(wèn)題。
Rust 允許自行實(shí)現(xiàn) Runtime 來(lái)調(diào)度任務(wù)和執(zhí)行 syscall;并提供了 Future 等統(tǒng)一的接口;另外內(nèi)置了 async-await 語(yǔ)法糖從面向 callback 編程中解放出來(lái)。

417ead60-fafc-11ed-90ce-dac502259ad0.png4186f2b8-fafc-11ed-90ce-dac502259ad0.png

Example

這里從一個(gè)簡(jiǎn)單的例子入手,看一看這套系統(tǒng)到底是怎么工作的。
當(dāng)并行下載兩個(gè)文件時(shí),在任何語(yǔ)言中都可以啟動(dòng)兩個(gè) Thread,分別下載一個(gè)文件,然后等待 thread 執(zhí)行結(jié)束;但并不想為了 IO 等待啟動(dòng)多余的線程,如果需要等待 IO,我們希望這時(shí)線程可以去干別的,等 IO 就緒了再做就好。
這種基于事件的觸發(fā)機(jī)制在 cpp 里面常常會(huì)以 callback 的形式遇見(jiàn)。Callback 會(huì)打斷我們的連續(xù)邏輯,導(dǎo)致代碼可讀性變差,另外也容易在 callback 依賴的變量的生命周期上踩坑,比如在 callback 執(zhí)行前提前釋放了它會(huì)引用的變量。
但在 Rust 中只需要?jiǎng)?chuàng)建兩個(gè) task 并等待 task 執(zhí)行結(jié)束即可。

418c9a4c-fafc-11ed-90ce-dac502259ad0.png

這個(gè)例子相比線程的話,異步 task 會(huì)高效很多,但編程上并沒(méi)有因此復(fù)雜多少。

第二個(gè)例子,現(xiàn)在 mock 一個(gè)異步函數(shù) do_http,這里直接返回一個(gè) 1,其實(shí)里面可能是一堆異步的遠(yuǎn)程請(qǐng)求;在此之上還想對(duì)這些異步函數(shù)做一些組合,這里假設(shè)是做兩次請(qǐng)求,然后把兩次的結(jié)果加起來(lái),最后再加一個(gè) 1 ,就是這個(gè)例子里面的 sum 函數(shù)。通過(guò) Async 和 Await 語(yǔ)法可以非常友好地把這些異步函數(shù)給嵌套起來(lái)。

#[inline(never)]
asyncfndo_http()->i32{
//dohttprequestinasyncway
1
}

pubasyncfnsum()->i32{
do_http().await+do_http().await+1
}

這個(gè)過(guò)程和寫(xiě)同步函數(shù)是非常像的,也就說(shuō)是在面向過(guò)程編程,而非面向狀態(tài)編程。利用這種機(jī)制可以避開(kāi)寫(xiě)一堆 callback 的問(wèn)題,帶來(lái)了編程的非常大的便捷性。

Async Await 背后的秘密

通過(guò)這兩個(gè)例子可以得知 Rust 的異步是怎么用的,以及它寫(xiě)起來(lái)確實(shí)非常方便。那么它背后到底是什么原理呢?

#[inline(never)]
asyncfndo_http()->i32{
//dohttprequestinasyncway
1
}

pubasyncfnsum()->i32{
do_http().await+do_http().await+1
}
41918dd6-fafc-11ed-90ce-dac502259ad0.png

剛才的例子使用 Async + Await 編寫(xiě),其生成結(jié)構(gòu)最終實(shí)現(xiàn) Future trait 。

Async + Await 其實(shí)是語(yǔ)法糖,可以在 HIR 階段被展開(kāi)為 Generator 語(yǔ)法,然后 Generator 又會(huì)在 MIR 階段被編譯器展開(kāi)成狀態(tài)機(jī)。

419781c8-fafc-11ed-90ce-dac502259ad0.png

Future 抽象

Future trait 是標(biāo)準(zhǔn)庫(kù)里定義的。它的接口非常簡(jiǎn)單,只有一個(gè)關(guān)聯(lián)類型和一個(gè) poll 方法。

pubtraitFuture{
typeOutput;
fnpoll(self:Pin<&mut?Self>,cx:&mutContext<'_>)->Poll;
}

pubenumPoll{
Ready(T),
Pending,
}

Future 描述狀態(tài)機(jī)對(duì)外暴露的接口:

推動(dòng)狀態(tài)機(jī)執(zhí)行:Poll 方法顧名思義就是去推動(dòng)狀態(tài)機(jī)執(zhí)行,給定一個(gè)任務(wù),就會(huì)推動(dòng)這個(gè)任務(wù)做狀態(tài)轉(zhuǎn)換。

返回執(zhí)行結(jié)果:

遇到了阻塞:Pending

執(zhí)行完畢:Ready + 返回值

可以看出,異步 task 的本質(zhì)就是實(shí)現(xiàn) Future 的狀態(tài)機(jī)。程序可以利用 Poll 方法去操作它,它可能會(huì)告訴程序現(xiàn)在遇到阻塞,或者說(shuō)任務(wù)執(zhí)行完了并返回結(jié)果。

既然有了 Future trait,我們完全可以手動(dòng)地去實(shí)現(xiàn) Future。這樣一來(lái),實(shí)現(xiàn)出來(lái)的代碼要比 Async、Await 語(yǔ)法糖去展開(kāi)的要易讀。下面是手動(dòng)生成狀態(tài)機(jī)的樣例。如果用 Async 語(yǔ)法寫(xiě),可能直接一個(gè) async 函數(shù)返回一個(gè) 1 就可以;我們手動(dòng)編寫(xiě)需要自定義一個(gè)結(jié)構(gòu)體,并為這個(gè)結(jié)構(gòu)體實(shí)現(xiàn) Future。

//autogenerate
asyncfndo_http()->i32{
//dohttprequestinasyncway
1
}

//manuallyimpl
fndo_http()->DOHTTPFuture{DoHTTPFuture}

structDoHTTPFuture;
implFutureforDoHTTPFuture{
typeOutput=i32;
fnpoll(self:Pin<&mut?Self>,_cx:&mutContext<'_>)->Poll{
Poll::Ready(1)
}
}

Async fn 的本質(zhì)就是返回一個(gè)實(shí)現(xiàn)了 Future 的匿名結(jié)構(gòu),這個(gè)類型由編譯器自動(dòng)生成,所以它的名字不會(huì)暴露給我們。而我們手動(dòng)實(shí)現(xiàn)就定義一個(gè) Struct DoHTTPFuture,并為它實(shí)現(xiàn) Future,它的 Output 和 Async fn 的返回值是一樣的,都是 i32 。這兩種寫(xiě)法是等價(jià)的。

由于這里只需要立刻返回一個(gè)數(shù)字 1,不涉及任何等待,那么我們只需要在 poll 實(shí)現(xiàn)上立刻返回 Ready(1) 即可。前面舉了 sum 的例子,它做的事情是異步邏輯的組合:調(diào)用兩次 do http,最后再把兩個(gè)結(jié)果再加一起。這時(shí)候如果要手動(dòng)去實(shí)現(xiàn)的話,就會(huì)稍微復(fù)雜一些,因?yàn)闀?huì)涉及到兩個(gè) await 點(diǎn)。一旦涉及到 await,其本質(zhì)上就變成一個(gè)狀態(tài)機(jī)。

為什么是狀態(tài)機(jī)呢?因?yàn)槊看?await 等待都有可能會(huì)卡住,而線程此時(shí)是不能停止工作并等待在這里的,它必須切出去執(zhí)行別的任務(wù);為了下次再恢復(fù)執(zhí)行前面任務(wù),它所對(duì)應(yīng)的狀態(tài)必須存儲(chǔ)下來(lái)。這里我們定義了 FirstDoHTTP 和 SecondDoHTTP 兩個(gè)狀態(tài)。實(shí)現(xiàn) poll 的時(shí)候,就是去做一個(gè) loop,loop 里面會(huì) match 當(dāng)前狀態(tài),去做狀態(tài)轉(zhuǎn)換。

//autogenerate
asyncfnsum()->i32{
do_http().await+dohttp().await+1
}

//manuallyimpl
fnsum()->SumFuture{SumFuture::FirstDoHTTP(DoHTTPFuture)}

enumSumFuture{
FirstDoHTTP(DOHTTPFuture),
SecondDoHTTP(DOHTTPFuture,i32),
}

implFutureforSumFuture{
typeOutput=i32;

fnpoll(self:Pin<&mut?Self>,cx:&mutContext<'?>)->Poll{
letthis=self.getmut();
loop{
matchthis{
SumFuture::FirstDoHTTP(f)=>{
letpinned=unsafe{Pin::new_unchecked(f)};
matchpinned.poll(cx){
Poll::Ready(r)=>{
*this=SumFuture::SecondDoHTTP(DOHTTPFuture,r);
}
Poll::Pending=>{
returnPol::Pending;
}
}
}
SumFuture::SecondDoHTTP(f,prev_sum)=>{
letpinned=unsafe{Pin::new_unchecked(f)};
returnmatchpinned.poll(cx){
Poll::Ready(r)=>Poll::Ready(*prev_sum+r+1),
Poll::Pending=>Pol::Pending,
};
}
}
}
}
}

Task, Future 和 Runtime 的關(guān)系

我們這里以 TcpStream 的 Read/Write 為例梳理整個(gè)機(jī)制和組件的關(guān)系。

首先當(dāng)我們創(chuàng)建 TCP stream 的時(shí)候,這個(gè)組件內(nèi)部就會(huì)把它注冊(cè)到一個(gè) poller 上去,這個(gè) poller 可以簡(jiǎn)單地認(rèn)為是一個(gè) epoll 的封裝(具體使用什么 driver 是根據(jù)平臺(tái)而異的)。

按照順序來(lái)看,現(xiàn)在有一個(gè) task ,要把這個(gè) task spawn 出去執(zhí)行。那么 spawn 本質(zhì)上就是把 task 放到了 runtime 的任務(wù)隊(duì)列里,然后 runtime 內(nèi)部會(huì)不停地從任務(wù)隊(duì)列里面取出任務(wù)并且執(zhí)行——執(zhí)行就是推動(dòng)狀態(tài)機(jī)動(dòng)一動(dòng),即調(diào)用它的 poll 方法,之后我們就來(lái)到了第2步。

41a12e26-fafc-11ed-90ce-dac502259ad0.png

我們執(zhí)行它的 poll 方法,本質(zhì)上這個(gè) poll 方法是用戶實(shí)現(xiàn)的,然后用戶就會(huì)在這個(gè) task 里面調(diào)用 TcpStream 的 read/write。這兩個(gè)函數(shù)內(nèi)部最終是調(diào)用 syscall 來(lái)實(shí)現(xiàn)功能的,但在執(zhí)行 syscall 之前需要滿足條件:這個(gè) fd 可讀/可寫(xiě)。如果它不滿足這個(gè)條件,那么即便我們執(zhí)行了 syscall 也只是拿到了 WOULD_BLOCK 錯(cuò)誤,白白付出性能。初始狀態(tài)下我們會(huì)設(shè)定新加入的 fd 本身就是可讀/可寫(xiě)的,所以第一次 poll 會(huì)執(zhí)行 syscall。當(dāng)沒(méi)有數(shù)據(jù)可讀,或者內(nèi)核的寫(xiě) buffer 滿了的時(shí)候,這個(gè) syscall 會(huì)返回 WOULD_BLOCK 錯(cuò)誤。在感知到這個(gè)錯(cuò)誤后,我們會(huì)修改 readiness 記錄,設(shè)定這個(gè) fd 相關(guān)的讀/寫(xiě)為不可讀/不可寫(xiě)狀態(tài)。這時(shí)我們只能對(duì)外返回 Pending。

之后來(lái)到第四步,當(dāng)我們?nèi)蝿?wù)隊(duì)列里面任務(wù)執(zhí)行完了,我們現(xiàn)在所有任務(wù)都卡在 IO 上了,所有的 IO 可能都沒(méi)有就緒,此時(shí)線程就會(huì)持續(xù)地阻塞在 poller 的 wait 方法里面,可以簡(jiǎn)單地認(rèn)為它是一個(gè) epoll_wait 一樣的東西。當(dāng)基于 io_uring 實(shí)現(xiàn)的時(shí)候,這可能對(duì)應(yīng)另一個(gè) syscall。

此時(shí)陷入 syscall 是合理的,因?yàn)闆](méi)有任務(wù)需要執(zhí)行,我們也不需要輪詢 IO 狀態(tài),陷入 syscall 可以讓出 CPU 時(shí)間片供同機(jī)的其他任務(wù)使用。如果有任何 IO 就緒,這時(shí)候我們就會(huì)從 syscall 返回,并且 kernel 會(huì)告訴我們哪些 fd 上的哪些事件已經(jīng)就緒了。比如說(shuō)我們關(guān)心的是某一個(gè) FD 它的可讀,那么這時(shí)候他就會(huì)把我們關(guān)心的 fd 和可讀這件事告訴我們。

我們需要標(biāo)記 fd 對(duì)應(yīng)的 readiness 為可讀狀態(tài),并把等在它上面的任務(wù)給叫醒。前面一步我們?cè)谧?read 的時(shí)候,有一個(gè)任務(wù)是等在這里的,它依賴 IO 可讀事件,現(xiàn)在條件滿足了,我們需要重新調(diào)度它。叫醒的本質(zhì)就是把任務(wù)再次放到 task queue 里,實(shí)現(xiàn)上是通過(guò) Waker 的 wake 相關(guān)方法做到的,wake 的處理行為是 runtime 實(shí)現(xiàn)的,最簡(jiǎn)單的實(shí)現(xiàn)就是用一個(gè) Deque 存放任務(wù),wake 時(shí) push 進(jìn)去,復(fù)雜一點(diǎn)還會(huì)考慮任務(wù)竊取和分配等機(jī)制做跨線程的調(diào)度。

當(dāng)該任務(wù)被 poll 時(shí),它內(nèi)部會(huì)再次做 TcpStream read,它會(huì)發(fā)現(xiàn) IO 是可讀狀態(tài),所以會(huì)執(zhí)行 read syscall,而此時(shí) syscall 就會(huì)正確執(zhí)行,TcpStream read 對(duì)外會(huì)返回 Ready。

Waker

剛才提到了 Waker,接下來(lái)介紹 waker 是如何工作的。我們知道 Future 本質(zhì)是狀態(tài)機(jī),每次推它轉(zhuǎn)一轉(zhuǎn),它會(huì)返回 Pending 或者 Ready ,當(dāng)它遇到 io 阻塞返回 Pending 時(shí),誰(shuí)來(lái)感知 io 就緒? io 就緒后怎么重新驅(qū)動(dòng) Future 運(yùn)轉(zhuǎn)?

pubtraitFuture{
typeOutput;
fnpoll(self:Pin<&mut?Self>,cx:&mutContext<'_>)->Poll;
}

pubstructContext<'a>{
//可以拿到用于喚醒Task的Waker
waker:&aWaker,
//標(biāo)記字段,忽略即可
_marker:PhantomData&'a()>,
}

Future trait 里面除了有包含自身狀態(tài)機(jī)的可變以借用以外,還有一個(gè)很重要的是 Context,Context 內(nèi)部當(dāng)前只有一個(gè) Waker 有意義,這個(gè) waker 我們可以暫時(shí)認(rèn)為它就是一個(gè) trait object ,由 runtime 構(gòu)造和實(shí)現(xiàn)。它實(shí)現(xiàn)的效果,就是當(dāng)我們?nèi)?wake 這個(gè) waker 的時(shí)候,會(huì)把任務(wù)重新加回任務(wù)隊(duì)列,這個(gè)任務(wù)可能立刻或者稍后被執(zhí)行。

舉另一個(gè)例子來(lái)梳理整個(gè)流程。

41a7d76c-fafc-11ed-90ce-dac502259ad0.png

用戶使用 listener.accept() 生成 AcceptFut 并等待:

fut.await 內(nèi)部使用 cx 調(diào)用 Future 的 poll 方法

poll 內(nèi)部執(zhí)行 syscall

當(dāng)前無(wú)連接撥入,kernel 返回 WOULD_BLOCK

將 cx 中的 waker clone 并暫存于 TcpListener 關(guān)聯(lián)結(jié)構(gòu)內(nèi)

本次 poll 對(duì)外返回 Pending

Runtime 當(dāng)前無(wú)任務(wù)可做,控制權(quán)交給 Poller

Poller 執(zhí)行 epoll_wait 陷入 syscall 等待 IO 就緒

查找并標(biāo)記所有就緒 IO 狀態(tài)

如果有關(guān)聯(lián) waker 則 wake 并清除

等待 accept 的 task 將再次加入執(zhí)行隊(duì)列并被 poll

再次執(zhí)行 syscall

12/13. kernel 返回 syscall 結(jié)果,poll 返回 Ready

Runtime

先從 executor 看起,它有一個(gè)執(zhí)行器和一個(gè)任務(wù)隊(duì)列,它的工作是不停地取出任務(wù),推動(dòng)任務(wù)運(yùn)行,之后在所有任務(wù)執(zhí)行完畢必須等待時(shí),把執(zhí)行權(quán)交給 Reactor。

Reactor 拿到了執(zhí)行權(quán)之后,會(huì)與 kernel 打交道,等待 IO 就緒,IO就緒好了之后,我們需要標(biāo)記這個(gè) IO 的就緒狀態(tài),并且把這個(gè) IO 所關(guān)聯(lián)的任務(wù)給喚醒。喚醒之后,我們的執(zhí)行權(quán)又會(huì)重新交回給 executor 。在 executor 執(zhí)行這個(gè)任務(wù)的時(shí)候,就會(huì)調(diào)用到 IO 組件所提供的一些能力。

IO 組件要能夠提供這些異步的接口,比如說(shuō)當(dāng)用戶想用 tcb stream 的時(shí)候,得用 runtime 提供的一個(gè) TcpStream, 而不是直接用標(biāo)準(zhǔn)庫(kù)的。第二,能夠?qū)⒆约旱?fd 注冊(cè)到 Reactor 上。第三,在 IO 沒(méi)有就緒的時(shí)候,我們能把這個(gè) waker 放到任務(wù)相關(guān)聯(lián)的區(qū)域里。

整個(gè) Rust 的異步機(jī)制大概就是這樣。

41afab2c-fafc-11ed-90ce-dac502259ad0.png

03

Monoio 設(shè)計(jì)

以下將會(huì)分為四個(gè)部分介紹 Monoio Runtime 的設(shè)計(jì)要點(diǎn):

基于 GAT(Generic associated types) 的異步 IO 接口;

上層無(wú)感知的 Driver 探測(cè)與切換;

如何兼顧性能與功能;

提供兼容 Tokio 的接口

基于 GAT 的純異步IO接口

首先介紹一下兩種通知機(jī)制。第一種是和 epoll 類似的,基于就緒狀態(tài)的一種通知。第二種是 io-uring 的模式,它是一個(gè)基于“完成通知”的模式。

41b917e8-fafc-11ed-90ce-dac502259ad0.png

在基于就緒狀態(tài)的模式下,任務(wù)會(huì)通過(guò) epoll 等待并感知 IO 就緒,并在就緒時(shí)再執(zhí)行 syscall。但在基于“完成通知”的模式下,Monoio 可以更懶:直接告訴 kernel 當(dāng)前任務(wù)想做的事情就可以放手不管了。

io_uring 允許用戶和內(nèi)核共享兩個(gè)無(wú)鎖隊(duì)列,submission queue 是用戶態(tài)程序?qū)?,?nèi)核態(tài)消費(fèi);completion queue 是內(nèi)核態(tài)寫(xiě),用戶態(tài)消費(fèi)。通過(guò) enter syscall 可以將隊(duì)列中放入的 SQE 提交給 kernel,并可選地陷入并等待 CQE。

在 syscall 密集的應(yīng)用中,使用 io_uring 可以大大減少上下文切換次數(shù),并且 io_uring 本身也可以減少內(nèi)核中數(shù)據(jù)拷貝。

41c08974-fafc-11ed-90ce-dac502259ad0.png

這兩種模式的差異會(huì)很大程度上影響 Runtime 的設(shè)計(jì)和 IO 接口。在第一種模式下,等待時(shí)是不需要持有 buffer 的,只有執(zhí)行 syscall 的時(shí)候才需要 buffer,所以這種模式下可以允許用戶在真正調(diào)用 poll 的時(shí)候(如 poll_read)傳入 &mut Buffer;而在第二種模式下,在提交給 kernel 后,kernel 可以在任何時(shí)候訪問(wèn) buffer,Monoio 必須確保在該任務(wù)對(duì)應(yīng)的 CQE 返回前 Buffer 的有效性。

如果使用現(xiàn)有異步 IO trait(如 tokio/async-std 等),用戶在 read/write 時(shí)傳入 buffer 的引用,可能會(huì)導(dǎo)致 UAF 等內(nèi)存安全問(wèn)題:如果在用戶調(diào)用 read 時(shí)將 buffer 指針推入 uring SQ,那么如果用戶使用 read(&mut buffer) 創(chuàng)建了 Future,但立刻 Drop 它,并 Drop buffer,這種行為不違背 Rust 借用檢查,但內(nèi)核還將會(huì)訪問(wèn)已經(jīng)釋放的內(nèi)存,就可能會(huì)踩踏到用戶程序后續(xù)分配的內(nèi)存塊。

所以這時(shí)候一個(gè)解法,就是去捕獲它的所有權(quán),當(dāng)生成 Future 的時(shí)候,把所有權(quán)給 Runtime,這時(shí)候用戶無(wú)論如何都訪問(wèn)不到這個(gè) buffer 了,也就保證了在 kernel 返回 CQE 前指針的有效性。這個(gè)解法借鑒了 tokio-uring 的做法。

Monoio 定義了 AsyncReadRent 這個(gè) trait。所謂的 Rent ,即租借,相當(dāng)于是 Runtime 先把這個(gè) buffer 從用戶手里拿過(guò)來(lái),待會(huì)再還給用戶。這里的 type read future 是帶了生命周期泛型的,這個(gè)泛型其實(shí)是 GAT 提供了一個(gè)能力,現(xiàn)在 GAT 已經(jīng)穩(wěn)定了,已經(jīng)可以在 stable 版本里面去使用它了。當(dāng)要實(shí)現(xiàn)關(guān)聯(lián)的 Future 的時(shí)候,借助 TAIT 這個(gè) trait 可以直接利用 async-await 形式來(lái)寫(xiě),相比手動(dòng)定義 Future 要方便友好很多,這個(gè) feature 目前還沒(méi)穩(wěn)定(現(xiàn)在改名叫 impl trait in assoc type 了)。

當(dāng)然,轉(zhuǎn)移所有權(quán)會(huì)引入新的問(wèn)題。在基于就緒狀態(tài)的模式下,取消 IO 只需要 Drop Future 即可;這里如果 Drop Future 就可能導(dǎo)致連接上數(shù)據(jù)流錯(cuò)誤(Drop Future 的瞬間有可能 syscall 剛好已經(jīng)成功),并且一個(gè)更嚴(yán)重的問(wèn)題是一定會(huì)丟失 Future 捕獲的 buffer。針對(duì)這兩個(gè)問(wèn)題 Monoio 支持了帶取消能力的 IO trait,取消時(shí)會(huì)推入 CancelOp,用戶需要在取消后繼續(xù)等待原 Future 執(zhí)行結(jié)束(由于它已經(jīng)被取消了,所以會(huì)預(yù)期在較短的時(shí)間內(nèi)返回),對(duì)應(yīng)的 syscall 可能執(zhí)行成功或失敗,并返還 buffer。

上層無(wú)感知的 Driver 探測(cè)和切換

第二個(gè)特性是支持上層無(wú)感知的 Driver 探測(cè)和切換。

traitOpAble{
fnuring_op(&mutself)->io_uring::Entry;
fnlegacy_interest(&self)->Option<(ready::Diirection,?usize)>;
fnlegacy_call(&mutself)->io::Result;
}

通過(guò) Feature 或代碼指定 Driver,并有條件地做運(yùn)行時(shí)探測(cè)

暴露統(tǒng)一的 IO 接口,即 AsyncReadRent 和 AsyncWriteRent

內(nèi)部利用 OpAble 統(tǒng)一組件實(shí)現(xiàn)(對(duì) Read、Write 等 Op 做抽象)

具體來(lái)說(shuō),比如想做 accept、connect 或者 read、write 之類的,這些 op 是實(shí)現(xiàn)了 OpAble 的,實(shí)際對(duì)應(yīng)這三個(gè) fn :

uring_op:生成對(duì)應(yīng) uring SQE

legacy_interest:返回其關(guān)注的讀寫(xiě)方向

legacy_call:直接執(zhí)行syscall

41c9c106-fafc-11ed-90ce-dac502259ad0.png

整個(gè)流程會(huì)將一個(gè)實(shí)現(xiàn)了 opable 的結(jié)構(gòu) submit 到的 driver 上,然后會(huì)返回一個(gè)實(shí)現(xiàn)了 future 的東西,之后它 poll 的時(shí)候和 drop 的時(shí)候具體地會(huì)分發(fā)到兩個(gè) driver 實(shí)現(xiàn)中的一個(gè),就會(huì)用這三個(gè)函數(shù)里面的一個(gè)或者兩個(gè)。

性能

性能是 Monoio 的出發(fā)點(diǎn)和最大的優(yōu)點(diǎn)。除了 io_uring 帶來(lái)的提升外,它設(shè)計(jì)上是一個(gè) thread-per-core 模式的 Runtime。

所有 Task 均僅在固定線程運(yùn)行,無(wú)任務(wù)竊取。

Task Queue 為 thread local 結(jié)構(gòu)操作無(wú)鎖無(wú)競(jìng)爭(zhēng)。

高性能其實(shí)主要源于兩個(gè)方面:

Runtime內(nèi)部高性能:基本等價(jià)于裸對(duì)接syscall

用戶代碼高性能:結(jié)構(gòu)盡量 thread local 不跨線程

任務(wù)竊取和 thread-per-core 兩種機(jī)制的對(duì)比:

如果用 tokio 的話,可能某一個(gè)線程上它的任務(wù)非常少,可能已經(jīng)空了,但是另一個(gè)線程上任務(wù)非常多。那么這時(shí)候比較閑的線程就可以把任務(wù)從比較忙的任務(wù)上偷走,這一點(diǎn)和 Golang 非常像。這種機(jī)制可以較充分的利用 CPU,應(yīng)對(duì)通用場(chǎng)景可以做到較好的性能。

但跨線程本身會(huì)有開(kāi)銷,多線程操作數(shù)據(jù)結(jié)構(gòu)時(shí)也會(huì)需要鎖或無(wú)鎖結(jié)構(gòu)。但無(wú)鎖也不代表沒(méi)有額外開(kāi)銷,相比純本線程操作,跨線程的無(wú)鎖結(jié)構(gòu)會(huì)影響緩存性能,CAS 也會(huì)付出一些無(wú)效 loop。除此之外,更重要的是這種模式也會(huì)影響用戶代碼。

舉個(gè)例子,我們內(nèi)部需要一個(gè) SDK 去收集本程序的一些打點(diǎn),并把這些打點(diǎn)聚合之后去上報(bào)。在基于 tokio 的實(shí)現(xiàn)下,要做到極致的性能就比較困難。如果在 thread-per-core 結(jié)構(gòu)的 Runtime 上,我們完全可以將聚合的 Map 放在 thread-local 中,不需要任何鎖,也沒(méi)有任何競(jìng)爭(zhēng)問(wèn)題,只需要在每個(gè)線程上啟動(dòng)一個(gè)任務(wù),讓這個(gè)任務(wù)定期清空并上報(bào) thread local 中的數(shù)據(jù)。而在任務(wù)可能跨線程的場(chǎng)景下,我們就只能用全局的結(jié)構(gòu)來(lái)聚合打點(diǎn),用一個(gè)全局的任務(wù)去上報(bào)數(shù)據(jù)。聚合用的數(shù)據(jù)結(jié)構(gòu)就很難不使用鎖。

所以這兩種模式各有各的優(yōu)點(diǎn),thread-per-core 模式下對(duì)于可以較獨(dú)立處理的任務(wù)可以達(dá)到更好的性能。共享更少的東西可以做到更好的性能。但是 thread-per-core 的缺點(diǎn)是在任務(wù)本身不均勻的情況下不能充分利用 CPU。對(duì)于特定場(chǎng)景,如網(wǎng)關(guān)代理等,thread-per-core 更容易充分利用硬件性能,做到比較好的水平擴(kuò)展性。當(dāng)前廣泛使用 nginx 和 envoy 都是這種模式。

41d028e8-fafc-11ed-90ce-dac502259ad0.png

我們做了一些 benchmark,Monoio 的性能水平擴(kuò)展性是非常好的。當(dāng) CPU 核數(shù)增加的時(shí)候,只需要增加對(duì)應(yīng)的線程就可以了。

功能性

Thread-per-core 不代表沒(méi)有跨線程能力。用戶依舊可以使用一些跨線程共享的結(jié)構(gòu),這些和 Runtime 無(wú)關(guān);Runtime 提供了跨線程等待的能力。

任務(wù)在本線程執(zhí)行,但可以等待其他線程上的任務(wù),這個(gè)是一個(gè)很重要的能力。舉例來(lái)說(shuō),用戶需要用單線程去拉取遠(yuǎn)程配置,并下發(fā)到所有線程上?;谶@個(gè)能力,用戶就可以非常輕松地實(shí)現(xiàn)這個(gè)功能。

41d69e94-fafc-11ed-90ce-dac502259ad0.png

跨線程等待的本質(zhì)是在別的線程喚醒本線程的任務(wù)。實(shí)現(xiàn)上我們?cè)?Waker 中標(biāo)記任務(wù)的所屬權(quán),如果當(dāng)前線程并不是任務(wù)所屬線程,那么 Runtime 會(huì)通過(guò)無(wú)鎖隊(duì)列將任務(wù)發(fā)送到其所屬線程上;如果此時(shí)目標(biāo)線程處于休眠狀態(tài)(陷入 syscall 等待 IO),則利用事先安插的 eventfd 將其喚醒。喚醒后,目標(biāo)線程會(huì)處理跨線程 waker 隊(duì)列。

除了提供跨線程等待能力外,Monoio 也提供了 spawn_blocking 能力,供用戶執(zhí)行較重的計(jì)算邏輯,以免影響到同線程的其他任務(wù)。

兼容接口

需要允許用戶以兼容方式使用,即便付出一些性能代價(jià)。由于目前很多組件(如 hyper 等)綁定了 tokio 的 IO trait,而前面講了由于地層 driver 的原因這兩種 IO trait 不可能統(tǒng)一,所以生態(tài)上會(huì)比較困難。對(duì)于一些非熱路徑的組件,需要允許用戶以兼容方式使用,即便付出一些性能代價(jià)。

41df0eb2-fafc-11ed-90ce-dac502259ad0.png

//tokioway
lettcp=tokio:connect("1.1.1.1.1:80").await.unwrap();
//monoioway(withmonoio-compat)
lettcp=monoio_compat::new(monoio_tcp);
letmonoio_tcp=monoio::connect("1.1.1.1:80").await.unwrap();
//bothofthemimplementstokio::io::AsyncReaddandtokio::io:AsyncWrite

我們提供了一個(gè) Wrapper,內(nèi)置了一個(gè) buffer,用戶使用時(shí)需要多付出一次內(nèi)存拷貝開(kāi)銷。通過(guò)這種方式,我們可以為 monoio 的組件包裝出 tokio 的兼容接口,使其可以使用兼容組件。

04

Runtime 對(duì)比 & 應(yīng)用

這部分介紹 runtime 的一些對(duì)比選型和應(yīng)用。

前面已經(jīng)提到了關(guān)于均勻調(diào)度和 thread-per-core 的一些對(duì)比,這里主要說(shuō)一下應(yīng)用場(chǎng)景。對(duì)于較大量的輕任務(wù),thread-per-core 模式是適合的。特別是代理、網(wǎng)關(guān)和文件 IO 密集的應(yīng)用,使用 Monoio 就非常合適。
還有一點(diǎn),Tokio 致力于一個(gè)通用跨平臺(tái),但是 Monoio 設(shè)計(jì)之初就是為了極致性能,所以是期望以 io_uring 為主的。雖然也可以支持 epoll 和 kqueue,但僅作 fallback。比如 kqueue 其實(shí)就是為了讓用戶能夠在 Mac 上去開(kāi)發(fā)的便利性,其實(shí)不期望用戶真的把它跑在這(未來(lái)將支持 Windows)。

生態(tài)部分,Tokio 的生態(tài)是比較全的,Monoio 的比較缺乏,即便有兼容層,兼容層本身是有開(kāi)銷的。Tokio 有任務(wù)竊取,可以在較多的場(chǎng)景表現(xiàn)很好,但其水平擴(kuò)展性不佳。Monoio 的水平擴(kuò)展就比較好,但是對(duì)這個(gè)業(yè)務(wù)場(chǎng)景和編程模型其實(shí)是有限制的。所以 Monoio 比較適合的一些場(chǎng)景就是代理、網(wǎng)關(guān)還有緩存數(shù)據(jù)聚合等。以及還有一些會(huì)做文件 io 的,因?yàn)?io_uring 對(duì)文件 io 非常好。如果不用 io_uring 的話,在 Linux 下其實(shí)是沒(méi)有真異步的文件 io 可以用的,只有用 io_uring 才能做到這一點(diǎn)。還適用于這種文件 io 比較密集的,比如說(shuō)像 DB 類型的組件。

41e6f4ba-fafc-11ed-90ce-dac502259ad0.png

Tokio-uring 其實(shí)是一個(gè)構(gòu)建在 tokio 之上的一層,有點(diǎn)像是一層分發(fā)層,它的設(shè)計(jì)比較漂亮,我們也參考了它里面的很多設(shè)計(jì),比如說(shuō)像那個(gè)傳遞所有權(quán)的這種形式。但是它還是基于 tokio 做的,在 epoll 之上運(yùn)行 uring,沒(méi)有做到用戶透明。當(dāng)組件在實(shí)現(xiàn)時(shí),只能在使用 epoll 和使用 uring 中二選一,如果選擇了 uring,那么編譯產(chǎn)物就無(wú)法在舊版本 linux 上運(yùn)行。而 Monoio 很好的支持了這一點(diǎn),支持動(dòng)態(tài)探測(cè) uring 的可用性。

Monoio 應(yīng)用

Monoio Gateway: 基于 Monoio 生態(tài)的網(wǎng)關(guān)服務(wù),我們優(yōu)化版本 Benchmark 下來(lái)性能優(yōu)于 Nginx;

Volo: CloudWeGo Team 開(kāi)源的 RPC 框架,目前在集成中,PoC 版本性能相比基于 Tokio 提升 26%

我們也在內(nèi)部做了一些業(yè)務(wù)業(yè)務(wù)試點(diǎn),未來(lái)我們會(huì)從提升兼容性和組件建設(shè)上入手,就是讓它更好用。

審核編輯:彭靜
聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 文件
    +關(guān)注

    關(guān)注

    1

    文章

    561

    瀏覽量

    24695
  • 機(jī)器碼
    +關(guān)注

    關(guān)注

    0

    文章

    12

    瀏覽量

    8307
  • runtime
    +關(guān)注

    關(guān)注

    0

    文章

    17

    瀏覽量

    2161

原文標(biāo)題:字節(jié)開(kāi)源 Monoio :基于 io-uring 的高性能 Rust Runtime

文章出處:【微信號(hào):Rust語(yǔ)言中文社區(qū),微信公眾號(hào):Rust語(yǔ)言中文社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    總結(jié)了一些元器件選型資料

    一些元件選型資料 希望對(duì)大家有幫助
    發(fā)表于 10-16 19:08

    CAM 350一些基本操作

    CAM 350一些基本操作 G
    發(fā)表于 01-25 11:26 ?2213次閱讀

    一些電子公司的簡(jiǎn)稱

    一些電子公司的簡(jiǎn)稱
    發(fā)表于 07-10 14:21 ?20次下載

    Autium_designer的一些經(jīng)驗(yàn)

    Autium_designer的一些經(jīng)驗(yàn)
    發(fā)表于 02-28 21:16 ?0次下載

    一些制作1969的分享經(jīng)驗(yàn)

    一些制作1969的分享經(jīng)驗(yàn)
    發(fā)表于 03-04 18:25 ?37次下載

    關(guān)于Runtime的應(yīng)用

    可以參看Apple開(kāi)源的Runtime代碼 和Rumtime編程指南 。 本文總結(jié)一些其常用的方法。 1 新建測(cè)試Demo 我們先創(chuàng)建個(gè)測(cè)試Demo如下圖,其中TestClass是
    發(fā)表于 09-25 15:10 ?0次下載
    關(guān)于<b class='flag-5'>Runtime</b>的應(yīng)用

    VICOR模塊的一些基本應(yīng)用

      VICOR模塊的一些基本應(yīng)用
    發(fā)表于 11-24 11:42 ?17次下載

    虛擬貨幣臨著一些嚴(yán)重的安全問(wèn)題

    虛擬貨幣臨著一些嚴(yán)重的安全問(wèn)題,如虛擬幣錢(qián)包的安全性、二次支付,對(duì)比特幣交易的復(fù)雜攻擊以及瘋狂的挖礦賊。以下這些顧慮對(duì)比特幣和其他加密幣都是極具破壞性的比特幣錢(qián)包在面對(duì)黑客攻擊和竊賊的時(shí)候相當(dāng)脆弱。
    的頭像 發(fā)表于 03-17 10:03 ?9788次閱讀

    一些簡(jiǎn)單趣味小電子制作教程

    一些簡(jiǎn)單趣味小電子制作教程
    發(fā)表于 09-26 14:05 ?28次下載

    介紹一些大功率IGBT模塊應(yīng)用中的一些技術(shù)

    PPT主要介紹了大功率IGBT模塊應(yīng)用中的一些技術(shù),包括參數(shù)解讀、器件選型、驅(qū)動(dòng)技術(shù)、保護(hù)方法以及失效分析等。
    發(fā)表于 09-05 11:36 ?781次閱讀

    get與post的請(qǐng)求一些區(qū)別

    今天再次看到這個(gè)問(wèn)題,我也有了一些新的理解和感觸,臨時(shí)回顧了下 get 與 post 的請(qǐng)求的一些區(qū)別。
    的頭像 發(fā)表于 09-07 10:00 ?1365次閱讀

    INCA的一些用法

    INCA的一些用法
    的頭像 發(fā)表于 11-10 15:32 ?8624次閱讀

    電阻選型技巧---根據(jù)電阻的參數(shù)

    給大家分享一些關(guān)于電阻選型考慮哪些因素?電阻選型技巧的一些知識(shí)。
    的頭像 發(fā)表于 12-20 10:13 ?2192次閱讀

    分享一些SystemVerilog的coding guideline

    本文分享一些SystemVerilog的coding guideline。
    的頭像 發(fā)表于 11-22 09:17 ?683次閱讀
    分享<b class='flag-5'>一些</b>SystemVerilog的coding  guideline

    分享一些常見(jiàn)的電路

    理解模電和數(shù)電的電路原理對(duì)于初學(xué)者來(lái)說(shuō)可能比較困難,但通過(guò)一些生動(dòng)的教學(xué)方法和資源,可以有效地提高學(xué)習(xí)興趣和理解能力。 下面整理了一些常見(jiàn)的電路,以動(dòng)態(tài)圖形的方式展示。 整流電路 單相橋式整流
    的頭像 發(fā)表于 11-13 09:28 ?197次閱讀
    分享<b class='flag-5'>一些</b>常見(jiàn)的電路