天天看點

那些必須要了解的Serverless時代的并發神器-Rust語言Tokio架構基礎

今天我們繼續高并發的話題,傳統的雲計算技術,本質上都是基于虛拟機的,雲平台可以将一些性能強勁的實體伺服器,拆分成若幹個虛拟機,提供給使用者使用,但在網際網路發展到今天,虛拟機還是太重了。即使是飛天叢集,新增部署虛拟機的時間也是以分鐘來計的。但是對于網際網路使用者來講20秒的等等就是就會千萬50%以上的使用者流失,不能忍受的煎熬,是以Docker秒級啟動的速度也不是個完美的解決方案,最終還是要Serverless極速的伸縮才能滿足客戶需求。

通俗的講,Serverless就是基建狂魔版的雲平台,雖然傳統的基建技術安全性更高,穩定性也更好,但是從頭修路、蓋房、裝修成本太高時間也太長,而Serverless本質上是一個比容器還小的最小運作環境的鏡像,隻要給點陽光就能野燦爛,而且用完以後想拆也很友善,是應對雲原生時代最新發展出的神器。

在我之前的部落格中也不止一次提到,在Serverless時代,服務冷啟動的速度與服務記憶體的消耗都是決定成敗的關鍵。無GC更不依賴JVM的Rust無論在冷啟速度還是在記憶體消耗上都比JAVA和GO更具優勢,而且相比C語言Rust的生産效率也更高,很多儲如從函數式語言借鑒而來的Future機制都非常先進,根據官方的測試結果,在性能方面Rust的網絡程式設計架構比JAVA和GO要好得多

那些必須要了解的Serverless時代的并發神器-Rust語言Tokio架構基礎
但是我意外的看到像Rust中Tokio這樣優秀的高并發網絡程式設計架構在中文技術社群卻沒有個完整的教程,是以筆者決定将這段時間探索Tokio的心得向大家分享一下,

初識Tokio

Tokio是基于Rust開發的異地網絡程式設計架構,用于執行異步代碼的多線程運作時。通過Future、async/await等機制,開發者可以讓代碼産生極高生産力的同時保持程式的性能基本與C語言一緻,基于Tokio的開發在編寫異步代碼時,開發者不能使用Rust标準庫提供的阻塞api,而必須使用由Tokio提供,鏡像了Rust标準庫的API。我們先來看一個Tokio的Helloworld程式

1.首先建立項目

cargo new my-tokio

指令建立一個my-tokio的項目

  1. 修改Cargo.toml

vi Cargo.toml

在依賴處添加以下内容

[dependencies]
tokio = { version = "1", features = ["full"] }      
那些必須要了解的Serverless時代的并發神器-Rust語言Tokio架構基礎
  1. 修改源代碼

vi src/main.rs

并将代碼替換為以下内容

async fn say_word() {
    println!("my tokio");
}
#[tokio::main]
async fn main() {
    let op = say_word();
    println!("hello");
    op.await;
}      
那些必須要了解的Serverless時代的并發神器-Rust語言Tokio架構基礎
  1. 編譯并執行

cargo build

cargo run

結果如下:    

Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/my-tokio`
hello
my tokio      
那些必須要了解的Serverless時代的并發神器-Rust語言Tokio架構基礎

這裡我們先解釋一下async和await的用法,我們看到async fn say_word()中,say_word()函數是被async關鍵詞修飾的,那麼也就是說這個函數在被調用時 let op = say_word();

,以上代碼是被立即傳回而沒有被執行的,而這時op實際是一個Future,也就是一個現在為空,在未來才會産生的值(有關Future的機制我們接下來解釋),而在調用op.await的時其實是在等到這個async異步操作執行完畢才傳回,是一個阻塞操作,是以最終輸出會是先列印hello,然後再列印my tokio

程式 程式員如何了解更像自然語言的Future

    在以下這段代碼中,網絡連接配接socket、請求發送request、響應接收response三個對象全部都是future類型的,也就是在代碼執行之後不會被執行也沒有值僅有占位的意義,當未來執行後才會有值傳回,and_then方法其實是在future對象執行成功後才會被調用的方法,比如read_to_end這行代碼就是在request對象執行成功後,調用read_to_end方法對讀取結果。

use futures::Future;
use tokio_core::reactor::Core;
use tokio_core::net::TcpStream;fn main() {
    let mut core = Core::new().unwrap();
     let addr = "127.0.0.1:8080".to_socket_addrs().unwrap().next().unwrap();
     let socket = TcpStream::connect(&addr, &core.handle());
     let request = socket.and_then(|socket|{
         tokio_core::io::write_all(socket, "Hello World".as_bytes())
     });
     let response = request.and_then(|(socket, _)| {
         tokio_core::io::read_to_end(socket, Vec::new())
     });
     let (_, data) = core.run(response).unwrap();
     println!("{}", String::from_utf8_lossy(&data));
 }      
那些必須要了解的Serverless時代的并發神器-Rust語言Tokio架構基礎

而想象一下如果是傳統程式設計所采用的方式,需要在網絡連接配接完成後調用請求發送的回調函數,然後再請求發送的響應處理方法中再注冊接收請求的回調函數,複雜不說還容易出錯。

上面的代碼就是建立Tcp連接配接,發送資料,最後讀取傳回,每個Future都是通過and_then建立關系,而future機制精髓之處在于,整個過程是通過core.run(response).unwrap();這行代碼運作起來的,也就是說在Future的幫助下,程式員隻需要關心最終的結果就可以了,整個鍊條通過poll機制串聯,從poll機制來看,這幾個子產品的傳遞機制如下:

那些必須要了解的Serverless時代的并發神器-Rust語言Tokio架構基礎

從建立網絡連接配接開始的調用鍊交給計算機去幫你完成,不但省去了回調所帶來的複雜性,最終的效率反而還會更高。

poll模制到底是什麼意思?

筆者看到不少部落客在介紹Rust的Future等異步程式設計架構時都提到了Rust的Future采用poll模式,不過到底什麼是poll模式卻大多語焉不詳,其實poll做的本質工作就是監測鍊條上前續Future的執行狀态。

那些必須要了解的Serverless時代的并發神器-Rust語言Tokio架構基礎

以上述情況為例,poll的方向是由response到request最後是socket,但是state和data的傳回方向是完全返過來的,也就是說response通過poll來擷取request的state,而request也同樣通過poll來擷取socket的state。

筆者還是這樣的觀點,程式員群體之是以覺得future機制難以了解,其關鍵在于思維模式被計算機的各種回調機制給束縛住了,而忘記了最簡單直接的方式。在解決這個問題之前我們先來問一個問題,假如讓我們自己設計一個類似于tokio這樣的異步Future管理器,應該如何入手?

最直接也是最容易想到的方案就是事件循環,定期周遊整個事件隊列,把狀态是ready的事件通知給對應的處理程式,這也是我們常說的select方案;另外一種做法是在事件poll管理器中直接拿到處理程式的句柄,不再周遊整個事件隊列,而是直接在中斷處理響應中把通知發給對應處理程序,比如上述例子中實際是按照poll的鍊條傳遞的處理程序句柄的,這就是Poll模式。而基于poll設計的如tokio架構進行應用開發時,程式員根本不必關心整個消息傳遞,隻需要用and_then、spawn等方法建立鍊條并讓系統工作起來就可以了。

而epoll(多路複用)是基于poll的另一種高并發機制,這種機制可以監視多個描述符,一旦某個描述符狀态變為就緒,能夠通知對應的handler進行後續操作。筆者在前文《這位創造了Github冠軍項目的老男人,堪稱10倍程式員本尊》中曾經介紹過Tdengine的定時器,其中就有這種多路複用的思想。由于作業系統timer的處理程式還不支援epoll的多路複用,是以每注冊一個timer就必須要啟動一個線程進行處理,資源浪費嚴重,是以Tdengine自己實作了一個多路複用的timer,可以做到一個線程同時處理多個timer,這些細節上的精巧設計也是Tdengine封神的原因之一。

後記

寫到這突然發現tokio架構的介紹一篇文章根本就不可能完成,那麼本文權當一個基礎介紹,為入門tokio做準備,如果後面讀者們再有強烈需求,我們再繼續聊這個話題。