2018年4月1日,Cloudflare發(fā)布了1.1.1.1公共DNS解析器。過去幾年來,我們在平臺上不斷添磚加瓦,包括用于故障排除的調(diào)試頁面,緩存清除,Cloudflare區(qū)域的0 TTL,上游TLS,以及1.1.1.1 for Families。本文想分享一些幕后的細(xì)節(jié)和變化。
項(xiàng)目啟動時,Knot Resolver被選定為DNS解析器。我們開始在其基礎(chǔ)上構(gòu)建一個完整的系統(tǒng),以便它適合Cloudflare的用例。擁有一個經(jīng)過實(shí)戰(zhàn)考驗(yàn)的DNS遞歸解析器,以及一個DNSSEC驗(yàn)證器非常棒,這樣我們可以把精力花在其他地方,而不用擔(dān)心DNS協(xié)議的實(shí)現(xiàn)。
就其基于Lua的插件系統(tǒng)而言,Knot Resolver非常靈活。它允許我們快速擴(kuò)展核心功能,以支持各種產(chǎn)品特性,如DoH/DoT、日志記錄、基于bpf的攻擊緩解、緩存共享和迭代邏輯覆蓋。隨著流量增長,我們達(dá)到了一定的限制。
我們吸取的教訓(xùn)
在深入討論之前,讓我們先鳥瞰一下簡化的Cloudflare數(shù)據(jù)中心設(shè)置,這可以幫助我們理解后面將要討論的內(nèi)容。Cloudflare的每臺服務(wù)器都是相同的:運(yùn)行在一臺服務(wù)器上的軟件棧與另一臺服務(wù)器上的軟件棧完全相同,僅配置可能不同。這種設(shè)置大大降低了維護(hù)的復(fù)雜性。
圖1:數(shù)據(jù)中心布局
解析器以后臺進(jìn)程kresd的形式運(yùn)行,而且它并不是單獨(dú)工作的。請求,特別是DNS請求,由Unimog進(jìn)行負(fù)載平衡后分配給數(shù)據(jù)中心內(nèi)的服務(wù)器。DoH請求在我們的TLS終止器終止。配置和其他小塊數(shù)據(jù)可以通過Quicksilver在幾秒鐘內(nèi)傳遞到世界各地。憑借以上所有幫助,解析器可以專注于自己的目標(biāo)——解析DNS查詢,而不用擔(dān)心傳輸協(xié)議的細(xì)節(jié)。現(xiàn)在讓我們來談?wù)勎覀兿胍倪M(jìn)的3個關(guān)鍵領(lǐng)域:插件阻塞I/O,更有效地使用緩存空間,以及插件隔離。
阻塞事件循環(huán)的回調(diào)函數(shù)
Knot Resolver有一個非常靈活的插件系統(tǒng)來擴(kuò)展它的核心功能。這些插件被稱為模塊,它們是基于回調(diào)的。在請求處理期間的某些點(diǎn),這些回調(diào)將通過當(dāng)前查詢上下文調(diào)用。這為某個模塊提供檢查、修改和甚至生成請求/響應(yīng)的能力。從設(shè)計(jì)來看,這些回調(diào)應(yīng)該是簡單的,以避免阻塞底層的事件循環(huán)。這一點(diǎn)很重要,因?yàn)榉?wù)是單線程的,而事件循環(huán)負(fù)責(zé)同時處理多個請求。因此,即使只有一個請求在回調(diào)中被擱置,也意味著在回調(diào)完成之前,其他并發(fā)請求也無法被處理。
這種設(shè)置一直工作得足夠好,直到我們需要進(jìn)行阻塞操作,例如,在響應(yīng)客戶端前從Quicksilver拉取數(shù)據(jù)。
緩存效率
由于對一個域的請求可能落在數(shù)據(jù)中心內(nèi)的任何節(jié)點(diǎn)上,在另一個節(jié)點(diǎn)已經(jīng)有答案的情況下重復(fù)解析查詢將是一種浪費(fèi)。根據(jù)直覺,如果緩存可以在服務(wù)器之間共享,則延遲可以得到改善,因此我們創(chuàng)建了一個緩存模塊,該模塊對新添加的緩存條目進(jìn)行組播。然后,同一數(shù)據(jù)中心內(nèi)的節(jié)點(diǎn)可以訂閱事件并更新其本地緩存。
Knot Resolver的默認(rèn)緩存實(shí)現(xiàn)是LMDB。對于中小型部署來說,它快速又可靠。但是在我們的例子中,緩存清除很快成為一個問題。緩存本身不跟蹤任何TTL、流行度等。緩存填滿時,它會清除所有條目并重新開始。像分區(qū)枚舉這樣的場景可能會用以后不太可能被檢索的數(shù)據(jù)填充緩存。
此外,我們的組播緩存模塊導(dǎo)致情況進(jìn)一步惡化,將不太有用的數(shù)據(jù)放大到所有節(jié)點(diǎn),并使它們同時達(dá)到緩存高水位。然后我們看到了一個延遲峰值,因?yàn)樗械墓?jié)點(diǎn)都放棄了緩存,并在同一時間重新開始。
模塊隔離
隨著Lua模塊列表增加,調(diào)試問題變得越來越困難。這是因?yàn)閱蝹€Lua狀態(tài)在所有模塊之間共享,因此一個行為不當(dāng)?shù)哪K可能會影響另一個模塊。例如,當(dāng)Lua狀態(tài)內(nèi)部出現(xiàn)問題時,例如有太多的協(xié)程,或者內(nèi)存不足,程序崩潰已經(jīng)算是幸運(yùn)了,但是所產(chǎn)生的堆棧跟蹤很難讀取。強(qiáng)制拆除或升級運(yùn)行中的模塊也很困難,因?yàn)樗贚ua運(yùn)行時中不僅有狀態(tài),還有FFI,因此內(nèi)存安全無法保證。
BigPineapple應(yīng)運(yùn)而生
我們找不到任何現(xiàn)有軟件能滿足我們這個有點(diǎn)小眾的要求,因此最終我們自己動手打造了一個。第一個嘗試是用Rust編寫的一個瘦服務(wù)包裹Knot Resolver的核心(修改版edgedns)。
由于必須不斷地在存儲和C/FFI類型之間進(jìn)行轉(zhuǎn)換,以及一些其他的問題(例如,從緩存中查找記錄的ABI期望返回的記錄在下一次調(diào)用或讀事務(wù)結(jié)束之前是不可變的),這樣做證明是很困難的。但是,從嘗試實(shí)現(xiàn)這種分離功能——其中主機(jī)(服務(wù))向客機(jī)(解析器核心庫)提供一些資源,以及如何使接口變得更好,但我們從中學(xué)到了很多。
在之后的迭代中,我們用一個基于異步運(yùn)行時的新庫替換了整個遞歸庫;并添加了一個重新設(shè)計(jì)的模塊系統(tǒng),隨著時間的推移,隨著我們換出越來越多的組件,逐漸將服務(wù)重寫為Rust。異步運(yùn)行時就是tokio,其提供了簡潔的線程池接口,用于運(yùn)行非阻塞和阻塞任務(wù),以及一個與其他部件(Rust庫)一起工作的良好生態(tài)系統(tǒng)。
在那以后,隨著futures combinators變得繁瑣,我們開始將一切轉(zhuǎn)換為async/await。這是async/await特性通過Rust 1.39提供以前,導(dǎo)致我們使用了一段時間的nightly(Rust測試版),遇到了一些問題。當(dāng)async/await穩(wěn)定后,它讓我們能夠高效地編寫請求處理例程,類似于Go。
所有的任務(wù)都可以并發(fā)運(yùn)行,某些I/O繁重的任務(wù)可以被分解成更小的部分,從而受益于更細(xì)粒度的調(diào)度。由于運(yùn)行時在線程池(而不是單個線程)上執(zhí)行任務(wù),它還可以受益于工作竊?。╳ork stealing)。這避免了我們之前遇到的一個問題,即單個請求需要花費(fèi)大量時間來處理,阻塞事件循環(huán)中的所有其他請求。
圖2:組件概覽
最后,我們打造了一個自己滿意的平臺,稱之為BigPineapple。上圖顯示了平臺的主要組件及組件之間數(shù)據(jù)流的概覽。在BigPineapple內(nèi)部,服務(wù)器模塊從客戶端獲取入站請求,驗(yàn)證并將其轉(zhuǎn)換為統(tǒng)一的幀流,然后由worker模塊處理。worker模塊有一組worker,它們的任務(wù)是為請求中的問題找到答案。每個worker與緩存模塊交互,以檢查答案是否存在并且仍然有效,否則將驅(qū)動遞歸模塊進(jìn)行遞歸迭代查詢。遞歸器不做任何I/O,當(dāng)它需要任何東西時,它將子任務(wù)委托給conductor模塊。然后,conductor使用出站查詢從上游名稱服務(wù)器獲取信息。在整個過程中,一些模塊可以與沙盒模塊交互,通過運(yùn)行里面的插件來擴(kuò)展它的功能。
讓我們更詳細(xì)地看看其中的一些模塊,看看它們是如何幫助我們克服以前遇到的問題的。
更新的I/O架構(gòu)
DNS解析器可以看作是客戶端和幾個權(quán)威名稱服務(wù)器之間的代理:它從客戶端接收請求,遞歸地從上游名稱服務(wù)器獲取數(shù)據(jù),然后組合響應(yīng)并將它們發(fā)送回客戶端。因此,它同時有入站和出站流量,分別由服務(wù)器和conductor組件處理。
服務(wù)器使用不同傳輸協(xié)議偵聽一個接口列表。這些隨后被抽象為“幀”流。每一幀都是一個DNS消息的高級表示,帶有一些額外的元數(shù)據(jù)。在底層,它可以是UDP包、TCP流的一段或HTTP請求的有效載荷,但它們都以相同的方式處理。然后,幀被轉(zhuǎn)換成一個異步任務(wù),接著由一組負(fù)責(zé)解析這些任務(wù)的worker接收。完成的任務(wù)被轉(zhuǎn)換回響應(yīng),并發(fā)送回客戶端。
這種對協(xié)議及其編碼的“幀”抽象簡化了用于規(guī)范幀源的邏輯,例如加強(qiáng)公平性以防止“餓死”,并控制節(jié)奏以保護(hù)服務(wù)器不被壓垮。我們從之前的實(shí)踐中了解到的一件事是,對于一個向公眾開放的服務(wù),I/O峰值性能的重要性低于公平調(diào)度客戶端的能力。這主要是因?yàn)槊總€遞歸請求的時間和計(jì)算成本有很大的不同(例如緩存命中/不命中),并且很難事先猜測。遞歸服務(wù)中的緩存不命中不僅消耗Cloudflare的資源,還消耗被查詢的權(quán)威名稱服務(wù)器的資源,因此我們需要注意這一點(diǎn)。
服務(wù)器的另一方面是conductor,其管理所有出站連接。它幫助在向上游連接之前回答一些問題:就延遲而言,連接到哪個名稱服務(wù)器最快?如果所有的名稱服務(wù)器都不可達(dá)該怎么辦?連接使用什么協(xié)議,以及是否有更好的選項(xiàng)?conductor能夠通過跟蹤上游服務(wù)器的指標(biāo)(例如RTT、QoS等)來做出這些決策。了解這些情況,它也可以猜測像上游容量、UDP包丟失等信息,并進(jìn)行必要的操作,例如,在認(rèn)為此前的UDP包沒有到達(dá)上游時進(jìn)行重試。
圖3:I/O conductor
圖3顯示了關(guān)于conductor的簡化數(shù)據(jù)流。它被上面提到的交換器調(diào)用,以上游請求作為輸入。這些請求將首先進(jìn)行去重:這意味著在一個小窗口中,如果有很多請求來到這個conductor并詢問相同的問題,只有一個會通過,其他請求被放入一個等待隊(duì)列中。這在緩存條目過期時很常見,可以減少不必要的網(wǎng)絡(luò)流量。然后,根據(jù)請求和上游指標(biāo),連接instructor要么選擇一個可用的已打開連接,要么生成一組參數(shù)。使用這些參數(shù),I/O執(zhí)行器就可以直接連接到上游,甚至可以使用我們的Argo Smart Routing技術(shù),采取一條經(jīng)另一個Cloudflare數(shù)據(jù)中心的路線。
緩存
在遞歸服務(wù)中進(jìn)行緩存是非常關(guān)鍵的,因?yàn)榉?wù)器可以在1毫秒內(nèi)返回緩存的響應(yīng),而緩存不命中時則需要數(shù)百毫秒來進(jìn)行響應(yīng)。由于內(nèi)存是有限的資源(在Cloudflare的架構(gòu)中也是共享資源),更有效地使用緩存空間是我們想要改進(jìn)的關(guān)鍵領(lǐng)域之一。新的緩存使用一種緩存替代數(shù)據(jù)結(jié)構(gòu)(ARC),而非KV存儲。這樣可以很好地利用單個節(jié)點(diǎn)上的空間,因?yàn)闊衢T程度較低的條目會逐步被剔除,而且該數(shù)據(jù)結(jié)構(gòu)可抗掃描。
此外,與我們之前使用組播在整個數(shù)據(jù)中心復(fù)制緩存不同,BigPineapple知道它在同一個數(shù)據(jù)中心中的對等節(jié)點(diǎn),如果它在自己的緩存中找不到條目,就將查詢從一個節(jié)點(diǎn)轉(zhuǎn)發(fā)給另一個節(jié)點(diǎn)。這是通過將查詢一致地散列到每個數(shù)據(jù)中心的健康節(jié)點(diǎn)來實(shí)現(xiàn)的。例如,針對相同注冊域的查詢將通過相同的節(jié)點(diǎn)子集,這不僅提高了緩存命中率,而且有助于基礎(chǔ)架構(gòu)緩存,后者存儲有關(guān)名稱服務(wù)器性能和特性的信息。
圖4:更新的數(shù)據(jù)中心布局
異步遞歸庫
遞歸庫是BigPineapple的DNS大腦,它知道如何為查詢中的問題找到答案。它從根開始,將客戶端查詢分解為子查詢,并使用它們從互聯(lián)網(wǎng)上的各種權(quán)威名稱服務(wù)器中遞歸地收集知識。這個過程的結(jié)果就是答案。得益于async/await,它可以被抽象為這樣的函數(shù):
async fn resolve(Request,Exchanger)→Result;
該函數(shù)包含對給定請求生成響應(yīng)需要的所有邏輯,但它自己不做任何I/O。相反,我們傳遞一個Exchanger trait(Rust接口),它知道如何與上游權(quán)威名稱服務(wù)器異步交換DNS消息。exchanger通常在不同的await被調(diào)用——例如,當(dāng)遞歸開始時,它最初做的事情之一就是為該域查找最近的緩存委托。如果它在緩存中沒有最終的委托,則需要詢問哪些名稱服務(wù)器負(fù)責(zé)這個域,并等待響應(yīng),然后才能繼續(xù)進(jìn)行下去。
得益于這種設(shè)計(jì),將“等待某些響應(yīng)”部分從遞歸DNS邏輯中分離出來,通過提供exchanger的模擬實(shí)現(xiàn),測試起來就容易得多。此外,它使遞歸迭代代碼(以及,特別是DNSSEC驗(yàn)證邏輯)更具可讀性,因?yàn)樗前错樞蚓帉懙?,而不是分散在許多回調(diào)中。
有趣的事實(shí):從頭開始寫一個DNS遞歸解析器一點(diǎn)都不好玩!
不僅因?yàn)镈NSSEC驗(yàn)證的復(fù)雜性,還因?yàn)楦鞣N不兼容RFC的服務(wù)器、轉(zhuǎn)發(fā)器、防火墻等提供必須的“變通方法”。因此,我們將deckard移植到Rust中來幫助測試它。此外,當(dāng)我們開始遷移到這個新的異步遞歸庫時,我們首先在“影子”模式下運(yùn)行它:處理來自生產(chǎn)服務(wù)的真實(shí)世界查詢樣本,并比較差異。我們過去在Cloudflare的權(quán)威DNS服務(wù)上也這樣做過。遞歸服務(wù)要稍微困難一些,因?yàn)檫f歸服務(wù)必須在互聯(lián)網(wǎng)上查找所有數(shù)據(jù),而且,由于本地化、負(fù)載平衡等原因,權(quán)威名稱服務(wù)器通常會對相同的查詢給出不同的答案,導(dǎo)致許多誤報。
2019年12月,我們終于在一個公共測試端點(diǎn)上啟用了新服務(wù)(查看公告)以解決剩余的問題,然后慢慢將生產(chǎn)端點(diǎn)遷移到新服務(wù)。即使做了所有這些工作之后,我們繼續(xù)檢測到了DNS遞歸(特別是DNSSEC驗(yàn)證)的邊緣情況,但由于庫的新架構(gòu),修復(fù)和再現(xiàn)這些問題變得更容易了。
沙盒中的插件
動態(tài)擴(kuò)展核心DNS功能的能力對我們來說很重要,因此BigPineapple重新設(shè)計(jì)了它的插件系統(tǒng)。以前,Lua插件運(yùn)行在與服務(wù)本身相同的內(nèi)存空間中,通常可以自由地做它們想做的事情。這很方便,因?yàn)槲覀兛梢允褂肅/FFI在服務(wù)和模塊之間自由傳遞內(nèi)存引用。例如,直接從緩存讀取響應(yīng),而不必先復(fù)制到緩沖區(qū)。但這也很危險,因?yàn)槟K可以讀取未初始化的內(nèi)存、使用錯誤的函數(shù)簽名調(diào)用主機(jī)ABI、阻塞本地套接字或做其他不希望做的事情,此外,服務(wù)沒有辦法限制這些行為。
所以我們考慮用JavaScript或原生模塊替換嵌入式的Lua運(yùn)行時,WebAssembly(簡稱Wasm)的嵌入式運(yùn)行時開始出現(xiàn)了。WebAssembly程序的兩個優(yōu)點(diǎn)是,它允許我們用與服務(wù)其他部分相同的語言編寫程序,以及它們在一個隔離的內(nèi)存空間中運(yùn)行。因此,我們開始圍繞WebAssembly模塊的限制對客機(jī)/主機(jī)接口進(jìn)行建模,以了解其工作原理。
BigPineapple的Wasm運(yùn)行時目前由Wasmer驅(qū)動。開始時,我們嘗試了幾種不同的運(yùn)行時,例如Wasmtime和WAVM,發(fā)現(xiàn)對我們的用例而言,使用Wasmer更簡單。該運(yùn)行時允許每個模塊在其自己的實(shí)例中運(yùn)行,具有隔離的內(nèi)存和信號陷阱,這自然解決了我們前面描述的模塊隔離問題。除此之外,我們還可以同時運(yùn)行同一個模塊的多個實(shí)例。通過仔細(xì)控制,應(yīng)用可以從一個實(shí)例熱交換到另一個,而不會錯過任何一個請求!這很好,因?yàn)閼?yīng)用程序可以在不重啟服務(wù)器的情況下進(jìn)行動態(tài)升級。由于Wasm程序是通過Quicksilver分發(fā)的,BigPineapple的功能幾秒鐘內(nèi)就能在全球范圍內(nèi)安全地改變!
要更好地理解WebAssembly沙盒,首先需要介紹幾個術(shù)語:
主機(jī):運(yùn)行Wasm運(yùn)行時的程序。與內(nèi)核類似,它可以通過該運(yùn)行時完全控制客機(jī)應(yīng)用程序。
客機(jī)應(yīng)用程序:沙盒內(nèi)的Wasm程序。在一個受限環(huán)境中,它只能訪問由運(yùn)行時提供的自有內(nèi)存空間,并調(diào)用導(dǎo)入的主機(jī)調(diào)用。我們將其簡稱為“應(yīng)用”。
主機(jī)調(diào)用:在主機(jī)中定義、可被客機(jī)調(diào)用的函數(shù)。與系統(tǒng)調(diào)用類似,這是客機(jī)應(yīng)用訪問沙盒之外資源的唯一方式。
客機(jī)運(yùn)行時:一個讓客機(jī)應(yīng)用程序輕松與主機(jī)交互的庫。它實(shí)現(xiàn)了一些常見的接口,以便應(yīng)用可以只使用異步、套接字、日志和跟蹤,而不知道底層的細(xì)節(jié)。
現(xiàn)在我們可以深入介紹一下沙盒了。首先,讓我們從客機(jī)側(cè)開始,看看一個普通應(yīng)用的生命周期是什么樣子的。在客機(jī)運(yùn)行時的幫助下,可以編寫與常規(guī)程序類似的客機(jī)應(yīng)用。因此,像其他可執(zhí)行文件一樣,應(yīng)用以start函數(shù)作為入口點(diǎn)開始,在加載時由主機(jī)調(diào)用。它還會被提供一些參數(shù),如同來自命令行。此時,該實(shí)例通常會進(jìn)行一些初始化,更重要的是,為不同的查詢階段注冊回調(diào)函數(shù)。這是因?yàn)椋谶f歸解析器中,查詢必須經(jīng)過幾個階段,才能收集足夠的信息來產(chǎn)生響應(yīng),例如,緩存查找,或生成子請求來解析域的委托鏈,因此能夠連接到這些階段是應(yīng)用在不同用例中發(fā)揮作用的必要條件。start函數(shù)還可以運(yùn)行一些后臺任務(wù)來補(bǔ)充階段回調(diào),并存儲全局狀態(tài)。例如——報告指標(biāo),或者從外部數(shù)據(jù)源預(yù)取共享數(shù)據(jù),等等。同樣,一如我們寫一個普通程序。
但是程序參數(shù)來自何處呢?客機(jī)應(yīng)用如何發(fā)送日志和指標(biāo)呢?答案是外部函數(shù)。
圖5:基于Wasm的沙盒
在圖5中,我們可以看到中間有一個屏障,即沙盒邊界,它將客機(jī)與主機(jī)分開。一側(cè)要到達(dá)另一側(cè),唯一途徑是通過由對方預(yù)先導(dǎo)出的一組函數(shù)。如圖所示,“hostcalls”(主機(jī)調(diào)用)由主機(jī)導(dǎo)出,由客機(jī)導(dǎo)入和調(diào)用;而“trampoline”是主機(jī)知道的客機(jī)函數(shù)。
后者稱為trampoline,是因?yàn)樗糜谡{(diào)用客機(jī)實(shí)例中未導(dǎo)出的函數(shù)或閉包。階段回調(diào)是我們?yōu)槭裁葱枰猼rampoline函數(shù)的一個例子:每個回調(diào)返回一個閉包,因此不能在實(shí)例化時導(dǎo)出。客機(jī)應(yīng)用要注冊一個回調(diào),調(diào)用一個帶有回調(diào)地址“hostcall_register_callback(pre_cache,#30987)”的主機(jī)調(diào)用,當(dāng)需要調(diào)用回調(diào)時,主機(jī)不能直接調(diào)用該指針,因?yàn)樗赶蚩蜋C(jī)的內(nèi)存空間。取而代之,它利用前面提到的trampoline之一,并提供回調(diào)閉包的地址:“trampoline_call(#30987)”。
隔離開銷
就像硬幣有兩面一樣,新的沙盒確實(shí)也會帶來一些額外的開銷。WebAssembly提供的可移植性和隔離性帶來了額外代價。這里,我們將列出兩個例子。
首先,客機(jī)應(yīng)用不允許讀取主機(jī)內(nèi)存。其工作的方式是,客機(jī)通過一個主機(jī)調(diào)用提供一個內(nèi)存區(qū)域,然后主機(jī)將數(shù)據(jù)寫入客機(jī)內(nèi)存空間。這會引入一個內(nèi)存副本,如果我們在沙盒之外,則不需要該內(nèi)存副本。壞消息是,在我們的用例中,客戶應(yīng)用程序應(yīng)該對查詢和/或響應(yīng)進(jìn)行一些操作,因此它們幾乎總是需要在每次請求時從主機(jī)讀取數(shù)據(jù)。另一方面,好消息是在請求的生命周期中,數(shù)據(jù)不會改變。因此,我們在客機(jī)應(yīng)用實(shí)例化后立即在客機(jī)內(nèi)存空間中預(yù)分配大量內(nèi)存。分配的內(nèi)存并不會被使用,而是用于在地址空間中占一個坑。一旦主機(jī)獲得了地址的詳細(xì)信息,它就會將一個包含客機(jī)所需公共數(shù)據(jù)的共享內(nèi)存區(qū)域映射到客戶空間中。當(dāng)客機(jī)代碼開始執(zhí)行時,它只需要訪問共享內(nèi)存覆蓋層中的數(shù)據(jù),而不需要復(fù)制。
我們遇到的另一個問題是,我們想在BigPineapple中添加對現(xiàn)代協(xié)議oDoH的支持。它的主要工作是解密客戶端查詢,解析它,然后在發(fā)送回之前加密答案。從設(shè)計(jì)上講,它不屬于核心DNS,應(yīng)該使用Wasm應(yīng)用進(jìn)行擴(kuò)展。然而,WebAssembly指令集沒有提供一些密碼學(xué)原語,例如AES和SHA-2,這使得它無法獲得主機(jī)硬件的好處。有一些進(jìn)行中的工作通過WASI-crypto將這一功能引入Wasm。在那以前,我們對此的解決方案是簡單地通過主機(jī)調(diào)用將HPKE委托給主機(jī),與在Wasm中執(zhí)行相比,我們已經(jīng)看到了4倍的性能提升。
Wasm中的異步
還記得我們之前討論過的回調(diào)函數(shù)可能阻塞事件循環(huán)的問題嗎?本質(zhì)上,問題在于如何異步運(yùn)行沙盒中的代碼。因?yàn)闊o論請求處理回調(diào)函數(shù)有多復(fù)雜,只要它能返回結(jié)果,我們就可以設(shè)定允許阻塞的時間上限。幸運(yùn)的是,Rust的異步框架既優(yōu)雅又輕量。它讓我們有機(jī)會使用一組客機(jī)調(diào)用來實(shí)現(xiàn)“Future”。
在Rust中,F(xiàn)uture是異步計(jì)算的基礎(chǔ)組件。從用戶的角度來看,為了創(chuàng)建一個異步程序,必須考慮兩件事:實(shí)現(xiàn)一個驅(qū)動狀態(tài)轉(zhuǎn)換的可輪詢函數(shù),并放置一個waker作為回調(diào)函數(shù),當(dāng)可輪詢函數(shù)因某些外部事件(例如時間經(jīng)過,套接字變得可讀,等等)而應(yīng)被再次調(diào)用時,用來喚醒自己。前者是為了能夠逐步推進(jìn)程序,例如從I/O讀取緩沖的數(shù)據(jù),并返回一個表示任務(wù)狀態(tài)的新狀態(tài):finished或yielded。后者在任務(wù)放棄的情況下很有用,因?yàn)楫?dāng)任務(wù)等待的條件滿足時,它會觸發(fā)Future被輪詢,而不是一直忙著循環(huán)直到任務(wù)完成。
讓我們看看這是如何在我們的沙盒中實(shí)現(xiàn)的。對于客機(jī)需要執(zhí)行一些I/O操作的場景,它必須通過主機(jī)調(diào)用來完成,因?yàn)樗幱谝粋€受限制的環(huán)境中。假設(shè)主機(jī)提供了一組簡化的主機(jī)調(diào)用,它們反映了基本的套接字操作:打開、讀取、寫入和關(guān)閉,那么客戶可按如下定義其偽輪詢器:
fn poll(&mut self, wake: fn()) -> Poll {match hostcall_socket_read(self.sock, self.buffer) { HostOk => Poll::Ready, HostEof => Poll::Pending,}}
在這里,主機(jī)調(diào)用從套接字讀取數(shù)據(jù)到緩沖區(qū),根據(jù)其返回值,函數(shù)可以將自己移動到我們前面提到的狀態(tài)之一:finished(就緒)或yielded(等待)。神奇的事情發(fā)生在主機(jī)調(diào)用中。還記得在圖5中,這是訪問資源的唯一方法嗎?客機(jī)應(yīng)用并不擁有套接字,但它可以通過“hostcall_socket_open”獲得一個“句柄”,這將在主機(jī)側(cè)創(chuàng)建一個套接字,并返回一個句柄。理論上,句柄可以是任何東西,但實(shí)際上,使用整數(shù)套接字句柄可以很好地映射到主機(jī)側(cè)的文件描述符,或vector或slab中的索引。通過引用返回的句柄,客機(jī)應(yīng)用能夠遠(yuǎn)程控制真正的套接字。由于主機(jī)側(cè)是完全異步的,它可以簡單地將套接字狀態(tài)轉(zhuǎn)發(fā)給客戶端。如果您注意到上面沒有使用waker函數(shù),非常棒!這是因?yàn)楫?dāng)調(diào)用主機(jī)調(diào)用時,它不僅開始打開一個套接字,還注冊了當(dāng)前的waker,以便在套接字打開時調(diào)用(或者不調(diào)用)。因此,當(dāng)套接字就緒時,主機(jī)任務(wù)將被喚醒,它將從其上下文中找到相應(yīng)的客機(jī)任務(wù),并使用trampoline函數(shù)將其喚醒,如圖5所示。在其他情況下,客機(jī)任務(wù)需要等待另一個客機(jī)任務(wù),例如一個異步互斥。這里的機(jī)制類似:使用主機(jī)調(diào)用來注冊waker。
以上復(fù)雜操作都封裝在我們的客戶異步運(yùn)行時中,提供易于使用的API,以便客機(jī)應(yīng)用可以訪問常規(guī)的異步函數(shù),而不必考慮底層的細(xì)節(jié)。
寫在最后
希望本文能讓您對支持1.1.1.1的創(chuàng)新平臺有一個大致的了解。這個平臺仍在發(fā)展。截至今天,我們的幾個產(chǎn)品都是由BigPineapple上運(yùn)行的客機(jī)應(yīng)用支持,例如1.1.1.1 for Families、AS112和Gateway DNS。我們期待著將新技術(shù)引入其中。如果您有任何想法,請通過社區(qū)或電子郵件告訴我們。