自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一個Rust小白發(fā)布生產(chǎn)級Rust應(yīng)用的進階之路

開發(fā) 前端
盡管開發(fā)Rust生產(chǎn)級應(yīng)用有那么多阻礙,我們目前已經(jīng)發(fā)布的Rust應(yīng)用已經(jīng)證明了,相比于付出,遷移Rust帶來的收益更大。

一、引言

二、Rust核心特性

    1. 所有權(quán)

    2. 生命周期和引用

三、用Rust構(gòu)建生產(chǎn)級應(yīng)用

    1. 合理利用引用減少數(shù)據(jù)拷貝

    2. FFI(Foreign Function Interface)

    3. Tokio

四、Rust應(yīng)用發(fā)布

    1. 上傳鏡像

    2. 發(fā)布

    3. 上監(jiān)控

五、結(jié)論

一、引 言

在流量日益增長的今天,隨著用戶需求的不斷增加和性能要求的提升,一個能夠更好地處理高并發(fā)、低延遲和資源有效利用的計算層是十分重要的。盡管在過去我們平臺使用Java開發(fā)的計算層提供了穩(wěn)定的服務(wù)支撐,但面對日益增長的流量和低延遲的需求,Java不可避免地開始顯現(xiàn)局限性:

  • 垃圾回收:Java 的自動內(nèi)存管理依賴于垃圾回收機制,而垃圾回收雖然簡化了開發(fā)工作,卻可能引入不可預(yù)測的延遲。
  • 內(nèi)存使用效率:Java 的內(nèi)存管理通常比手動管理的語言消耗更多的內(nèi)存,因為它必須保留足夠的空間來處理對象分配和回收。
  • 異步處理瓶頸:雖然Java近年來強化了異步編程支持,但在極限性能優(yōu)化方面,仍存在不可忽視的不足。

在此背景下,經(jīng)過調(diào)研和實驗驗證,我們發(fā)現(xiàn)了Rust這個計算層改造升級的語言選型。Rust語言以其出色的內(nèi)存管理、安全性和高效性能而聞名。Rust的所有權(quán)模型可以在編譯時捕捉大多數(shù)內(nèi)存錯誤,從而減少運行時錯誤,這對需要高可靠性和穩(wěn)定性的系統(tǒng)尤為重要。此外,Rust沒有垃圾回收機制,這意味著我們可以更好地預(yù)測和控制內(nèi)存使用,提高應(yīng)用程序的性能和資源利用率。

通過使用Rust對計算層改造升級,我們的系統(tǒng)獲得了如下的提升:

  • 相比于Java,減少了30%的CPU核數(shù)。
  • 高效內(nèi)存管理,減少了70%的內(nèi)存使用。
  • 服務(wù)更穩(wěn)定,Bug少。

二、Rust核心特性

Rust 能夠突破傳統(tǒng)編程語言的瓶頸,主要得益于其獨特的所有權(quán)、借用和生命周期機制。這些特性使 Rust 在編譯階段就能夠確保內(nèi)存安全和線程安全,從而最大程度地減少運行時錯誤和不確定性。接下來,我們將深入探討 Rust 在并發(fā)模型、所有權(quán)、生命周期和借用方面的優(yōu)勢。

所有權(quán)

Rust 的所有權(quán)(Ownership)是該語言獨特的內(nèi)存管理機制,它確保內(nèi)存安全性和并發(fā)性而不需要垃圾回收器。所有權(quán)機制通過編譯時檢查來保證安全性,避免絕大多數(shù)的運行時錯誤,例如空指針或數(shù)據(jù)競爭。

Rust所有權(quán)規(guī)則

Rust的所有權(quán)有三個主要規(guī)則:

  • 所有值(除Copy類型)有且只有一個擁有者。
  • 當(dāng)所有者離開作用域,值會被自動釋放,不需要手動回收。
  • 值的所有權(quán)可以被移動或者借用。

為了方便理解,這里展示Rust、C++和Java對象賦值的異同來理解所有權(quán)的運行機制。

圖片圖片

可以看到,將a賦值給b時,Java會將a指向的值的引用傳遞給b,而C++則會產(chǎn)生一個新的副本。從某種意義來說,在內(nèi)存管理上,Java和C++選擇了相反的權(quán)衡。代價是Java需要垃圾回收來管理內(nèi)存,而C++的賦值會消耗更多的內(nèi)存。不同于Java和C++,Rust選擇了另一種方案:移動所有權(quán)。即將a指向的堆內(nèi)存地址“移動到b上”,這時只有b可以訪問這段內(nèi)存,a則成為了未初始化狀態(tài)并禁止使用。

Rust的所有權(quán)概念內(nèi)置于語言本身,在編譯期間對所有權(quán)和借用規(guī)則進行檢查。這樣,程序員可以在運行之前解決錯誤,提高代碼的可靠性。

共享所有權(quán)

盡管Rust規(guī)定大多數(shù)值會有唯一的擁有者,但在某些情況下,我們很難為每個值都找到具有所需生命周期的單個擁有者,而是希望某個值在每個擁有者使用完后就自動釋放。簡單來說,就是可以在代碼的不同地方擁有某個值的所有權(quán),所有地方都使用完這個值后,會自動釋放內(nèi)存。對于這種情況,Rust提供了引用計數(shù)智能指針:Rc和Arc。

Rc和Arc非常相似,唯一的區(qū)別是Arc可以在多線程環(huán)境進行共享,代價是引入原子操作后帶來的性能損耗。Rc和Arc實現(xiàn)共享所有權(quán)的原理是,Rc和Arc內(nèi)部包含實際存儲的數(shù)據(jù)T和引用計數(shù),當(dāng)使用clone時不會復(fù)制存儲的數(shù)據(jù),而是創(chuàng)建另一個指向它的引用并增加引用計數(shù)。當(dāng)一個Rc或Arc離開作用域,引用計數(shù)會減一,如果引用計數(shù)歸零,則數(shù)據(jù)T會被釋放。這種機制也叫共享所有權(quán)機制。

圖片圖片

這時就有好奇的小伙伴問了,既然可以在多個地方共享所有權(quán),那不是違背了所有權(quán)的初衷,從而引入了數(shù)據(jù)競爭的問題?放心,Rust的開發(fā)者早就想到了這個問題,引用計數(shù)智能指針是內(nèi)部不可變的,即無法對共享的值進行修改。那這就又引入了一個問題:如果要對共享的值進行修改怎么辦?對于這種情況Rust也提供了解決方案,使用Mutex等同步原語即可避免數(shù)據(jù)競爭和未定義行為。以下是一個案例,如何在多線程訪問數(shù)據(jù),并安全的進行修改。

{
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];


    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // 鎖定 Mutex 以安全地訪問數(shù)據(jù)
            let mut num = counter_clone.lock().unwrap();
            *num += 1; // 修改數(shù)據(jù)
        });
        handles.push(handle);
    }


    // 等待所有線程完成
    for handle in handles {
        handle.join().unwrap();
    }


    // 獲取最終計數(shù)值
    println!("Final count: {}", *counter.lock().unwrap());
}

生命周期和引用

在 Rust 中,生命周期(lifetimes)引用(references)是兩個密切相關(guān)的概念,它們共同構(gòu)成了 Rust 的所有權(quán)系統(tǒng)的重要組成部分。生命周期用于確保引用在使用時是有效的,從而防止懸空引用和數(shù)據(jù)競爭等問題。

引用

前面提到,Rust值的所有權(quán)可以被借用,它允許在不獲取數(shù)據(jù)所有權(quán)的情況下訪問數(shù)據(jù)。Rust中有兩種類型的引用:

  • 不可變引用 (&T):允許你讀取數(shù)據(jù),但不允許修改。
  • 可變引用 (&mut T):允許你修改數(shù)據(jù)。

在使用引用的時候需要滿足以下規(guī)則:

  • 在同一時間只能有一個可變引用。
  • 多個不可變引用可以同時存在,但在可變引用存在時,不能有不可變引用。
  • 每個引用都有一個生命周期,表示該引用在程序中的有效范圍,且引用的生命周期不能超過被借用的值的生命周期。

生命周期

在 Rust 編程語言中,生命周期用于確保引用在使用時是有效的。生命周期的存在使得 Rust 能夠在編譯時檢查引用的有效性,從而防止懸空引用。如下是一個Rust編譯器檢查生命周期的例子:

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}

這里編譯器將r的生命周期記為'a,x的生命周期記為'b。可以明顯看出,內(nèi)部塊的'b比外部塊的'a生命周期小,當(dāng)x離開作用域被釋放時,r仍然持有x的引用。所以當(dāng)把生命周期為'a的r想引用生命周期為'b的x時,編譯器發(fā)現(xiàn)了這個問題,并拒絕通過編譯,保證了程序不會出現(xiàn)懸垂引用。

生命周期標(biāo)注

正如我們看到的,Rust的引用代表對值的一次借用,它們有著種種限制,所以,在函數(shù)中、在結(jié)構(gòu)體中等等位置上使用引用時,你都要給Rust編譯器一些關(guān)于引用的提示,這種提示,就是生命周期標(biāo)記。對于簡單的情況,聰明的Rust編譯器可以自動推斷出引用的生命周期。對于一些模棱兩可的情況,編譯器也無法推斷引用是否在程序運行期間始終有效,這時就需要我們提供生命周期標(biāo)注來提示編譯器我們的代碼是正確的,放我過去吧。

生命周期標(biāo)注并沒有改變傳入的值和返回的值的生命周期,我們只是向借用檢查器指出了一些用于檢查非法調(diào)用的一些約束而已,而借用檢查器并不需要知道 x、y 的具體存活時長。而事實上如果函數(shù)引用外部的變量,那么單靠 Rust 確定函數(shù)和返回值的生命周期幾乎是不可能的事情。因為函數(shù)傳遞什么參數(shù)都是我們決定的,這樣的話函數(shù)在每次調(diào)用時使用的生命周期都可能發(fā)生變化,正因如此我們才需要手動對生命周期進行標(biāo)注。

相信第一次看到生命周期的小伙伴們都感覺概念非常難理解,且寫出的代碼非常丑,簡直要逼死強迫癥。但是有得就有舍,要寫出安全且高效的Rust代碼,就要學(xué)會理解和使用生命周期。如果實在不想用,那就多用Rc和Arc吧。

三、用Rust構(gòu)建生產(chǎn)級應(yīng)用

了解了Rust最核心的基本知識和特性后,你已經(jīng)成為了一個合格的Rust練習(xí)生,可以開始用Rust愉快的進行開發(fā)工作了。但是要使用Rust開發(fā)高性能的生產(chǎn)級應(yīng)用,只了解到這種程序是不行的。當(dāng)初筆者信心滿滿地將第一個Rust應(yīng)用發(fā)布到測試環(huán)境后,竟然發(fā)現(xiàn)效率比Java版本還低,于是開始了長期的瓶頸排查和調(diào)優(yōu),且調(diào)優(yōu)時間遠大于編碼時間。最終我們的應(yīng)用在相同吞吐量的條件下,CPU使用率從高于Java 20%優(yōu)化到低于Java 40%。在這個過程中,也總結(jié)了一些經(jīng)驗進行分享。

合理利用引用減少數(shù)據(jù)拷貝

相信很多剛接觸Rust的小伙伴在面對同一份數(shù)據(jù)需要在多處使用的情況時,為了逃避復(fù)雜的生命周期問題,會傾向于使用Clone來創(chuàng)建數(shù)據(jù)副本。如果這樣做的話,一份數(shù)據(jù)在內(nèi)存中重復(fù)出現(xiàn)多次,帶來的cpu和內(nèi)存消耗會讓你會懷疑人生,為什么這么相信Rust的性能而不相信自己能啃下生命周期這塊硬骨頭呢?

有一個應(yīng)用場景,我們從數(shù)據(jù)源得到若干個源數(shù)據(jù),根據(jù)業(yè)務(wù)邏輯聚合成batch并存儲到遠端或者本地。聚合的邏輯可以有兩種方式:

  • 將源數(shù)據(jù)的所有權(quán)移動到batch。
  • 將源數(shù)據(jù)拷貝一份到batch。

然而這兩種方式都不可取。第一種方式的問題是,我們不知道一份源數(shù)據(jù)是不是只會被使用一次。而使用第二種方式則會消耗更多的CPU,且占用內(nèi)存成倍上升。

前面提到,Rust的值是可以借用的,如果在batch中不獲得所有權(quán),而是存儲引用,那么可以幾乎零消耗的實現(xiàn)需求。以上述應(yīng)用場景為例,這里介紹我們是怎么解決這個問題的。

首先給出源數(shù)據(jù)Data和Batch的定義:

struct Data {
    condition: bool,
    num: i32,
    msg: String
}
struct Batch<'a> {
    msgList: Vec<&'a str>
}

假設(shè)需求是將Data的msg字段在Batch里存儲num次,我們很容易寫出這樣的代碼:

fn main() {
    let batch: Batch = Batch:new();    // 初始化Batch
    loop {                            
        let data:Data = dataSource.getData();    // 從數(shù)據(jù)源獲得data
        recordData(batch, &data);
        if (batch.len() > 100) {    // batch存儲的數(shù)據(jù)大于100條時,存儲并清空
            save(batch);
            batch.clear();
        }    // ------------------- data的生命周期到此結(jié)束
    }    // ------------------- batch的生命周期到此結(jié)束
}


fn record_data(batch: Batch, data: Data) {
    if(condition) {    // 根據(jù)條件將msg保存num次
        for i in 0..data.num {    
            batch.msgList.push(&data.msg);
        }
    }
}

看起來是不是很合理,和其他語言也沒有什么區(qū)別,當(dāng)信心滿滿按下編譯后,會發(fā)現(xiàn)天空飄來五個字:編譯不通過。原因很簡單,因為編譯器發(fā)現(xiàn)被引用對象data的生命周期小于batch,data的在當(dāng)前循環(huán)結(jié)束后就會銷毀,batch存儲的引用就變成了野指針。我們可以做如下修改:

fn() {
    let batch: Batch = Batch:new();    // 初始化Batch
    let dataList: Vec<Data> = Vec::new();    // dataList的生命周期和batch一樣
    loop {                            
        let data: Data = dataSource.getData();    // 從數(shù)據(jù)源獲得data
        dataList.push(data);    // 將data保存在dataList,提升生命周期
        if(batch.len() > 100) {
            for data_ref: &Batch in dataList.iter() {
                record_data(batch, data_ref);    // 此時data的生命周期和batch相等
            }
            save(batch);
            batch.clear();
            dataList.clear();
        }
    }
}


fn record_data<'a>(batch: Batch<'a>, data: &'a Data) {
    if(condition) {    // 根據(jù)條件將msg保存num次
        for i in 0..data.num {    
            batch.msgList.push(&data.msg);
        }
    }
}

可以看到,我們對代碼做了一些小改動:

  • 在循環(huán)外初始化了一個Vec,并保存每次得到的data。
  • record_data函數(shù)上增加了生命周期標(biāo)注。

為什么這么做呢?我們已經(jīng)知道最初版本是因為data的生命周期小于batch,導(dǎo)致batch不能存儲data的引用。解決這個問題的思路很簡單,提升data的生命周期不就完了。假設(shè)batch的生命周期是'a,data的生命周期是'b,很明顯'a是大于'b的,因為batch的生命周期是整個main函數(shù),而data的生命周期僅僅在loop內(nèi)。我們在batch同樣的作用域內(nèi)定義一個容器,它的生命周期也是'a。在每次得到data后把它存入容器中,那data就不會在循環(huán)結(jié)束的時候被銷毀了。

同時,在record_data函數(shù)定義上,我們也要使用標(biāo)注告訴編譯器batch和data的生命周期是相等的。如果data的生命周期大于batch,我們也可以在參數(shù)中定義data的生命周期為'a,因為實際的生命周期和參數(shù)生命周期標(biāo)注無需一致,只需要實際的生命周期大于參數(shù)生命周期就行了。如果你有強迫癥,也可以在參數(shù)中標(biāo)注實際的生命周期,只需要加上適當(dāng)?shù)纳芷诩s束就行了:

// 'b: 'a表示'b的生命周期能夠覆蓋'a
fn record_data<'a>(batch: Batch<'a>, data: &'b Data) where 'b: 'a {
    ......
}

經(jīng)過這些小改動,你的應(yīng)用會比粗暴的使用拷貝提升許多性能并且節(jié)約大量內(nèi)存使用。經(jīng)過我們的測試,在類似需求中將需要大量拷貝的操作替換成引用,可以節(jié)省一倍的內(nèi)存,CPU使用率也下降了20%。

FFI(Foreign Function Interface)

在一些情況下,我們項目使用的編程語言在實現(xiàn)一些功能時,想使用現(xiàn)成的依賴庫來實現(xiàn)復(fù)雜的邏輯,但是因為生態(tài)不完善,導(dǎo)致缺少此類庫或者現(xiàn)存的依賴庫不成熟。在使用Rust時,這種現(xiàn)象尤其普遍。很多熱門組件沒有為Rust提供官方API,非官方實現(xiàn)功能和性能又得不到保證,且更新不穩(wěn)定。難道Rust進階之路就要到此為止?

Rust很貼心地提供了跨語言交互能力,對FFI的良好支持可以讓開發(fā)者方便的在Rust代碼中調(diào)用C程序。如果我們需要的依賴庫剛好有C/C++的實現(xiàn),就能使Rust完成主要邏輯,把一些Rust不完善的功能通過C/C++實現(xiàn),而且性能也不會受到影響。在Rust程序調(diào)用C代碼也非常簡單:

1. 聲明外部函數(shù)

extern "C" {
    fn c_add(a: i32, b: i32) -> i32;
}

2. 在RUST中調(diào)用C函數(shù)

fn main() {
    unsafe {
        c_add(1, 2); 
    }
}

3. 將C程序編譯打包為靜態(tài)/動態(tài)鏈接庫

g++ -std=c++17 -shared -fPIC -o libhello.so hello.cpp

4. 然后編譯 Rust 文件并鏈接到鏈接庫

rustc main.rs hello.o

盡管用Rust調(diào)用C程序已經(jīng)非常方便,但是仍需要注意這些問題:

  • 處理數(shù)據(jù)類型:在 Rust FFI 中,需要特別注意數(shù)據(jù)類型的轉(zhuǎn)換和處理。Rust 和其他語言的數(shù)據(jù)類型可能存在差異,需要進行適當(dāng)?shù)霓D(zhuǎn)換。例如,Rust的i32和C的int可以直接相互轉(zhuǎn)換。而字符串的傳遞之所以需要特殊處理,是因為Rust的字符串實現(xiàn)和C/C++不一樣。C/C++的字符串指針只包含地址,且字符串后有“\0”作為結(jié)尾,而Rust字符串的指針不僅包含地址,還包含字符串長度,且末尾沒有“\0”作為結(jié)尾。
  • 內(nèi)存管理:盡管Rust是內(nèi)存安全的語言,但是在使用FFI的情況下,Rust無法保證調(diào)用的外部語言的安全性。作為開發(fā)者,我們要自己管理外部語言的內(nèi)存。
  • 線程安全:在多線程環(huán)境下使用 Rust FFI 時,需要注意線程安全問題。某些外部函數(shù)可能不是線程安全的,需要在調(diào)用時進行適當(dāng)?shù)耐讲僮鳌?/li>
  • 性能優(yōu)化:在使用 Rust FFI 時,需要注意性能優(yōu)化問題。由于涉及跨語言調(diào)用,可能會導(dǎo)致一定的性能損失。因此,需要對 FFI 調(diào)用的性能進行評估和優(yōu)化。

Tokio

如果你想構(gòu)建一個高性能的Rust服務(wù)器應(yīng)用,那么Tokio絕對是你的首選框架。Tokio 是一個用 Rust 編寫的異步運行時,旨在提供高性能的 I/O、任務(wù)調(diào)度和并發(fā)支持。雖說Tokio提供了強大的異步支持,要用好Tokio也不是一件容易得事,首先要了解“異步”的概念。在計算機編程中,“異步”是指一種不阻塞的操作方式,允許程序在等待某些操作(如 I/O 操作、網(wǎng)絡(luò)請求等)完成時繼續(xù)執(zhí)行其他代碼。

Tokio 通過使用協(xié)程和 Future 機制來實現(xiàn)高效的并發(fā)處理。它將異步任務(wù)封裝為Future對象,并通過運行時的調(diào)度器管理這些任務(wù)的執(zhí)行狀態(tài)。當(dāng)任務(wù)被調(diào)用時,運行時通過poll方法檢查其狀態(tài),如果任務(wù)無法繼續(xù)執(zhí)行(返回 Poll::Pending),則將其掛起并注冊一個Waker來在后續(xù)的某個時刻喚醒任務(wù)。一旦相關(guān)的I/O操作完成,Waker會通知運行時重新調(diào)度該任務(wù),從而實現(xiàn)非阻塞的并發(fā)執(zhí)行。Tokio支持多線程運行,可以充分利用多核CPU的能力,提高應(yīng)用程序的性能和響應(yīng)性。

圖片圖片

Tokio的使用非常簡單,使用async和await就可以很方便地創(chuàng)建異步任務(wù),但是要使用Tokio寫出高性能的代碼不是一件簡單的事。剛剛接觸Tokio的開發(fā)者會經(jīng)常發(fā)現(xiàn)代碼無故卡死或者性能低下,這是因為沒有正確使用Tokio。舉個例子,下面是一段運行后會卡死的代碼:

#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
async fn main() {
    let h = tokio::spawn(async {
        let (tx, rx) = std::sync::mpsc::channel::<String>();
        tokio::spawn(async move{
            let _ = tx.send("send message".to_string());
        });
        let ret = rx.recv().unwrap();
        println!("{}", ret)
    });
    h.await;
}

代碼結(jié)構(gòu)很簡單,但是運行后會發(fā)現(xiàn)代碼似乎hang住了,檢查代碼結(jié)構(gòu)也沒有發(fā)現(xiàn)問題。要解釋這個卡死的問題,要從Tokio的任務(wù)調(diào)度機制來分析:

圖片圖片

Processor 獲取 Task 后,會開始執(zhí)行這個 Task,在 Task 執(zhí)行過程中,可能會產(chǎn)生很多新的 Task,第一個新 Task 會被放到 LIFO Slot 中,其他新 Task 會被放到 Local Run Queue 中,因為 Local Run Queue 的大小是固定的,如果它滿了,剩余的 Task 會被放到 Global Queue 中。

Processor 運行完當(dāng)前 task 后,會嘗試按照以下順序獲取新的 Task 并繼續(xù)運行:

  1. LIFO Slot.
  2. Local Run Queue.
  3. Global Queue.
  4. 其他 Processor 的 Local Run Queue。

如果 Processor 獲取不到 task 了,那么其對應(yīng)的線程就會休眠,等待下次喚醒。

在上面的例子中,我們首先Spawn了一個異步任務(wù)Task-1,Task-1被分配給了Processor-1執(zhí)行。然后在Task-1里Spawn了另一個異步任務(wù)Task-2,Task-2被放到了Processor-1的LIFO Slot中。

因為Task-1繼續(xù)運行的條件依賴于Task-2,所以Task-1被阻塞了。而且Tokio的協(xié)程是非搶占式的,在Task-1沒有遇到.await前無法讓出CPU,Processor-1無法去執(zhí)行Task-2。又因為Task-2在Processor-1的LIFO Slot中,其他的Processor也無法偷取Task-2執(zhí)行。于是,Task-2永遠也不會有機會被執(zhí)行,這兩個Task在循環(huán)等待中就永遠卡死了。

要解決這個問題,我們要將阻塞型的數(shù)據(jù)結(jié)構(gòu)替換成Tokio的非阻塞式的:

#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
async fn main() {
    let handler = tokio::spawn(async {
        let (tx, mut rx) = tokio::sync::mpsc::channel(2);
        tokio::spawn(async move{
            let _ = tx.send("send message".to_string()).await;
        });
        let ret = rx.recv().await.unwrap();
        println!("{}", ret)
    });
    handler.await;
}

將channel替換成Tokio的非阻塞數(shù)據(jù)結(jié)構(gòu)后,Task-1在提交完Task-2后遇到await讓出了CPU,Processor-1就可以從LIFO Slot取出Task-2執(zhí)行了,循環(huán)等待也就被打破了。

由這個例子可以看出,Tokio 的輕量級線程之間的關(guān)系是一種合作式的。合作式的意思就是同一個 CPU 核上的任務(wù)大家是配合著執(zhí)行(不同 CPU 核上的任務(wù)是并行執(zhí)行的)。我們可以設(shè)想一個簡單的場景,A 和 B 兩個任務(wù)被分配到了同一個 CPU 核上,A 先執(zhí)行,那么,只有在 A 異步代碼中碰到 .await 而且不能立即得到返回值的時候,才會觸發(fā)掛起,進而切換到任務(wù) B 執(zhí)行。也就是說,在一個 task 沒有遇到 .await 之前,它是不會主動交出這個 CPU 核的,其他 task 也不能主動來搶占這個 CPU 核。

所以在使用Tokio時,我們要注意兩點:

  • 不要在異步代碼中執(zhí)行阻塞操作,不然這個OS線程中的其他任務(wù)都會被阻塞。
  • Tokio 雖然適合網(wǎng)絡(luò) I/O 型并發(fā),但是也要在 I/O 任務(wù)里小心地控制計算型代碼的時間,否則會導(dǎo)致運行時任務(wù)調(diào)度不均,從而長時間阻塞其它任務(wù)的運行。

四、Rust應(yīng)用發(fā)布

通過 Cargo,開發(fā)者可以輕松創(chuàng)建、構(gòu)建和共享 Rust 項目。但是因為發(fā)布系統(tǒng)只支持Java和Golang應(yīng)用,要在發(fā)布系統(tǒng)發(fā)布Rust應(yīng)用還是需要一些工作的。以下是我們發(fā)布Rust應(yīng)用的流程。

上傳鏡像

因為公司平臺是沒有Rust應(yīng)用的,所以我們需要自己制作鏡像并上傳,這樣才能在發(fā)布平臺發(fā)布我們的代碼。我們需要創(chuàng)建兩個 Docker 鏡像:一個用于構(gòu)建(CI 鏡像),另一個用于運行(運行時鏡像)。

圖片圖片

在dockerfile里可以安裝自己想要的工具包,根據(jù)自己需求來定制。

FROM repoin.shizhuang-inc.net/ci-build/rust:1.79.0


RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 871920D1991BC93C


# 創(chuàng)建 /etc/apt/sources.list
RUN echo "deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse" > /etc/apt/sources.list && \
    echo "deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse" >> /etc/apt/sources.list && \
    echo "deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse" >> /etc/apt/sources.list && \
    echo "deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse" >> /etc/apt/sources.list && \
    echo "deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse" >> /etc/apt/sources.list


# 更新包列表并安裝必要的工具
RUN apt-get install -y \
    protobuf-compiler \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*


# 驗證安裝
RUN protoc --version


RUN pwd


RUN ls -alh .


RUN ls -alh workspace

發(fā)布

建好集群后,還需要對集群進行一些配置:

  • 修改編譯配置的鏡像為自己上傳的鏡像。
  • 將編譯命令設(shè)為cargo build --release。
  • 修改運行時鏡像。
  • 修改發(fā)布配置,改為自己應(yīng)用所需要的。

還需要注意的是,發(fā)布平臺的編譯環(huán)境和運行環(huán)境是不同的,編譯完成后發(fā)布平臺會將可執(zhí)行文件移動到/opt/apps目錄下進行執(zhí)行,而配置文件不會被打包。遇到這種情況可以使用rust-embed庫,它允許將靜態(tài)文件(如 Yaml、Json、圖像等)打包到您的二進制文件中,從而簡化文件管理和部署。

上監(jiān)控

雖說Rust應(yīng)用主打的是穩(wěn)定,但是發(fā)布后持續(xù)對應(yīng)用進行監(jiān)控也是必須的,不然晚上能睡得著嗎。和發(fā)布一樣,Rust應(yīng)埋的指標(biāo)要被監(jiān)控采集,需要額外的配置。在KubeOne平臺找到自己的集群,在發(fā)布配置里加上這兩項,監(jiān)控平臺就可以采集到指標(biāo)了。

labels:
    - key: http://dewu.com/qos
      value: LS
    - key: http://duapp.kubernetes.io/metrics-scraped
      value: metrics
containerPorts:
    - containerPort: "2892"
      name: http-metrics
      protocol: TCP

通過上監(jiān)控,可以實時觀察Rust服務(wù)的運行情況,并且根據(jù)自己的埋點分析系統(tǒng)的瓶頸??梢钥吹剑琑ust應(yīng)用運行非常平穩(wěn)。相比于有GC的Java應(yīng)用,Rust明顯毛刺很少,非常平滑,而且內(nèi)存占用相比Java減少了70%。

圖片圖片

五、結(jié) 論

通過遷移到Rust,我們的計算層能夠在處理高并發(fā)請求時顯著提高系統(tǒng)的吞吐量和響應(yīng)能力,同時減少服務(wù)器資源的浪費。這不僅能降低運營成本,還能為我們的用戶提供更流暢、更快速的體驗。

但是,如果要持續(xù)地擁抱Rust生態(tài),目前仍然面臨如下挑戰(zhàn):

1. 生態(tài)不完善

盡管 Rust 已經(jīng)有一些非常優(yōu)秀的庫和工具,但某些特定領(lǐng)域仍然缺乏成熟且廣泛使用的庫。這意味著開發(fā)者可能需要花費更多的時間來構(gòu)建自己的解決方案或者整合不同語言的庫。

2. 學(xué)習(xí)曲線陡峭

Rust 語言引入了許多獨特的概念和特性,對于初學(xué)者和來自其他語言的開發(fā)者來說,這些特性可能需要一段時間來徹底掌握。

3. 開發(fā)進度

相比于自動內(nèi)存管理類型語言的開發(fā)任務(wù),Rust嚴格的編譯檢查會讓開發(fā)進度一度阻塞。

盡管開發(fā)Rust生產(chǎn)級應(yīng)用有那么多阻礙,我們目前已經(jīng)發(fā)布的Rust應(yīng)用已經(jīng)證明了,相比于付出,遷移Rust帶來的收益更大。希望大家都可以探索Rust的可行性,為節(jié)能減排和世界和平出一份力,也歡迎各位對Rust有興趣的同學(xué)一起交流。

責(zé)任編輯:武曉燕 來源: 得物技術(shù)
相關(guān)推薦

2024-06-07 08:59:35

2023-07-11 13:34:19

Rust開發(fā)軟件

2017-04-18 14:25:54

Excel實戰(zhàn)數(shù)據(jù)

2023-04-18 08:14:27

ElixirRustWebRTC

2021-01-03 16:30:34

Rust編程語言

2021-05-12 12:49:42

Rust開發(fā)團隊版本

2021-09-29 08:59:49

Rust編程語言

2024-07-10 08:51:29

2024-02-27 07:33:32

搜索引擎Rust模型

2022-04-10 23:02:08

GoRust語言

2021-07-06 14:36:05

RustLinux內(nèi)核模塊

2024-04-22 08:06:34

Rust語言

2024-06-27 11:08:45

2024-01-09 18:00:22

Rust后端slvelte

2023-08-30 19:06:58

2023-05-29 16:25:59

Rust函數(shù)

2024-11-08 09:19:28

2022-12-30 11:05:40

Rust代碼

2024-06-17 09:00:08

2024-02-28 07:48:05

Rust項目框架
點贊
收藏

51CTO技術(shù)棧公眾號