一個 Rust 小白髮布生產級 Rust 應用的進階之路
一
引 言
在流量日益增長的今天,隨着用戶需求的不斷增加和性能要求的提升,一個能夠更好地處理高併發、低延遲和資源有效利用的計算層是十分重要的。儘管在過去我們平臺使用 Java 開發的計算層提供了穩定的服務支撐,但面對日益增長的流量和低延遲的需求,Java 不可避免地開始顯現侷限性:
-
垃圾回收:Java 的自動內存管理依賴於垃圾回收機制,而垃圾回收雖然簡化了開發工作,卻可能引入不可預測的延遲。
-
內存使用效率:Java 的內存管理通常比手動管理的語言消耗更多的內存,因爲它必須保留足夠的空間來處理對象分配和回收。
-
異步處理瓶頸:雖然 Java 近年來強化了異步編程支持,但在極限性能優化方面,仍存在不可忽視的不足。
在此背景下,經過調研和實驗驗證,我們發現了 Rust 這個計算層改造升級的語言選型。Rust 語言以其出色的內存管理、安全性和高效性能而聞名。Rust 的所有權模型可以在編譯時捕捉大多數內存錯誤,從而減少運行時錯誤,這對需要高可靠性和穩定性的系統尤爲重要。此外,Rust 沒有垃圾回收機制,這意味着我們可以更好地預測和控制內存使用,提高應用程序的性能和資源利用率。
通過使用 Rust 對計算層改造升級,我們的系統獲得瞭如下的提升:
-
相比於 Java,減少了 30% 的 CPU 核數。
-
高效內存管理,減少了 70% 的內存使用。
-
服務更穩定,Bug 少。
二
Rust 核心特性
Rust 能夠突破傳統編程語言的瓶頸,主要得益於其獨特的所有權、借用和生命週期機制。這些特性使 Rust 在編譯階段就能夠確保內存安全和線程安全,從而最大程度地減少運行時錯誤和不確定性。接下來,我們將深入探討 Rust 在併發模型、所有權、生命週期和借用方面的優勢。
所有權
Rust 的**所有權(Ownership)**是該語言獨特的內存管理機制,它確保內存安全性和併發性而不需要垃圾回收器。所有權機制通過編譯時檢查來保證安全性,避免絕大多數的運行時錯誤,例如空指針或數據競爭。
Rust 所有權規則
Rust 的所有權有三個主要規則:
-
所有值(除 Copy 類型)有且只有一個擁有者。
-
當所有者離開作用域,值會被自動釋放,不需要手動回收。
-
值的所有權可以被移動或者借用。
爲了方便理解,這裏展示 Rust、C++ 和 Java 對象賦值的異同來理解所有權的運行機制。
可以看到,將 a 賦值給 b 時,Java 會將 a 指向的值的引用傳遞給 b,而 C++ 則會產生一個新的副本。從某種意義來說,在內存管理上,Java 和 C++ 選擇了相反的權衡。代價是 Java 需要垃圾回收來管理內存,而 C++ 的賦值會消耗更多的內存。不同於 Java 和 C++,Rust 選擇了另一種方案:移動所有權。即將 a 指向的堆內存地址 “移動到 b 上”,這時只有 b 可以訪問這段內存,a 則成爲了未初始化狀態並禁止使用。
Rust 的所有權概念內置於語言本身,在編譯期間對所有權和借用規則進行檢查。這樣,程序員可以在運行之前解決錯誤,提高代碼的可靠性。
共享所有權
儘管 Rust 規定大多數值會有唯一的擁有者,但在某些情況下,我們很難爲每個值都找到具有所需生命週期的單個擁有者,而是希望某個值在每個擁有者使用完後就自動釋放。簡單來說,就是可以在代碼的不同地方擁有某個值的所有權,所有地方都使用完這個值後,會自動釋放內存。對於這種情況,Rust 提供了引用計數智能指針:Rc 和 Arc。
Rc 和 Arc 非常相似,唯一的區別是 Arc 可以在多線程環境進行共享,代價是引入原子操作後帶來的性能損耗。Rc 和 Arc 實現共享所有權的原理是,Rc 和 Arc 內部包含實際存儲的數據 T 和引用計數,當使用 clone 時不會複製存儲的數據,而是創建另一個指向它的引用並增加引用計數。當一個 Rc 或 Arc 離開作用域,引用計數會減一,如果引用計數歸零,則數據 T 會被釋放。這種機制也叫共享所有權機制。
這時就有好奇的小夥伴問了,既然可以在多個地方共享所有權,那不是違背了所有權的初衷,從而引入了數據競爭的問題?放心,Rust 的開發者早就想到了這個問題,引用計數智能指針是內部不可變的,即無法對共享的值進行修改。那這就又引入了一個問題:如果要對共享的值進行修改怎麼辦?對於這種情況 Rust 也提供瞭解決方案,使用 Mutex 等同步原語即可避免數據競爭和未定義行爲。以下是一個案例,如何在多線程訪問數據,並安全的進行修改。
{
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 以安全地訪問數據
let mut num = counter_clone.lock().unwrap();
*num += 1; // 修改數據
});
handles.push(handle);
}
// 等待所有線程完成
for handle in handles {
handle.join().unwrap();
}
// 獲取最終計數值
println!("Final count: {}", *counter.lock().unwrap());
}
生命週期和引用
在 Rust 中,**生命週期(lifetimes)和引用(references)**是兩個密切相關的概念,它們共同構成了 Rust 的所有權系統的重要組成部分。生命週期用於確保引用在使用時是有效的,從而防止懸空引用和數據競爭等問題。
引用
前面提到,Rust 值的所有權可以被借用,它允許在不獲取數據所有權的情況下訪問數據。Rust 中有兩種類型的引用:
-
不可變引用 (&T):允許你讀取數據,但不允許修改。
-
可變引用 (&mut T):允許你修改數據。
在使用引用的時候需要滿足以下規則:
-
在同一時間只能有一個可變引用。
-
多個不可變引用可以同時存在,但在可變引用存在時,不能有不可變引用。
-
每個引用都有一個生命週期,表示該引用在程序中的有效範圍,且引用的生命週期不能超過被借用的值的生命週期。
生命週期
在 Rust 編程語言中,生命週期用於確保引用在使用時是有效的。生命週期的存在使得 Rust 能夠在編譯時檢查引用的有效性,從而防止懸空引用。如下是一個 Rust 編譯器檢查生命週期的例子:
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
}
這裏編譯器將 r 的生命週期記爲'a,x 的生命週期記爲'b。可以明顯看出,內部塊的'b 比外部塊的'a 生命週期小,當 x 離開作用域被釋放時,r 仍然持有 x 的引用。所以當把生命週期爲'a 的 r 想引用生命週期爲'b 的 x 時,編譯器發現了這個問題,並拒絕通過編譯,保證了程序不會出現懸垂引用。
生命週期標註
正如我們看到的,Rust 的引用代表對值的一次借用,它們有着種種限制,所以,在函數中、在結構體中等等位置上使用引用時,你都要給 Rust 編譯器一些關於引用的提示,這種提示,就是生命週期標記。對於簡單的情況,聰明的 Rust 編譯器可以自動推斷出引用的生命週期。對於一些模棱兩可的情況,編譯器也無法推斷引用是否在程序運行期間始終有效,這時就需要我們提供生命週期標註來提示編譯器我們的代碼是正確的,放我過去吧。
生命週期標註並沒有改變傳入的值和返回的值的生命週期,我們只是向借用檢查器指出了一些用於檢查非法調用的一些約束而已,而借用檢查器並不需要知道 x、y 的具體存活時長。而事實上如果函數引用外部的變量,那麼單靠 Rust 確定函數和返回值的生命週期幾乎是不可能的事情。因爲函數傳遞什麼參數都是我們決定的,這樣的話函數在每次調用時使用的生命週期都可能發生變化,正因如此我們才需要手動對生命週期進行標註。
相信第一次看到生命週期的小夥伴們都感覺概念非常難理解,且寫出的代碼非常醜,簡直要逼死強迫症。但是有得就有舍,要寫出安全且高效的 Rust 代碼,就要學會理解和使用生命週期。如果實在不想用,那就多用 Rc 和 Arc 吧。
三
用 Rust 構建生產級應用
瞭解了 Rust 最核心的基本知識和特性後,你已經成爲了一個合格的 Rust 練習生,可以開始用 Rust 愉快的進行開發工作了。但是要使用 Rust 開發高性能的生產級應用,只瞭解到這種程序是不行的。當初筆者信心滿滿地將第一個 Rust 應用發佈到測試環境後,竟然發現效率比 Java 版本還低,於是開始了長期的瓶頸排查和調優,且調優時間遠大於編碼時間。最終我們的應用在相同吞吐量的條件下,CPU 使用率從高於 Java 20% 優化到低於 Java 40%。在這個過程中,也總結了一些經驗進行分享。
合理利用引用減少數據拷貝
相信很多剛接觸 Rust 的小夥伴在面對同一份數據需要在多處使用的情況時,爲了逃避複雜的生命週期問題,會傾向於使用 Clone 來創建數據副本。如果這樣做的話,一份數據在內存中重複出現多次,帶來的 cpu 和內存消耗會讓你會懷疑人生,爲什麼這麼相信 Rust 的性能而不相信自己能啃下生命週期這塊硬骨頭呢?
有一個應用場景,我們從數據源得到若干個源數據,根據業務邏輯聚合成 batch 並存儲到遠端或者本地。聚合的邏輯可以有兩種方式:
-
將源數據的所有權移動到 batch。
-
將源數據拷貝一份到 batch。
然而這兩種方式都不可取。第一種方式的問題是,我們不知道一份源數據是不是隻會被使用一次。而使用第二種方式則會消耗更多的 CPU,且佔用內存成倍上升。
前面提到,Rust 的值是可以借用的,如果在 batch 中不獲得所有權,而是存儲引用,那麼可以幾乎零消耗的實現需求。以上述應用場景爲例,這裏介紹我們是怎麼解決這個問題的。
首先給出源數據 Data 和 Batch 的定義:
struct Data {
condition: bool,
num: i32,
msg: String
}
struct Batch<'a> {
msgList: Vec<&'a str>
}
假設需求是將 Data 的 msg 字段在 Batch 裏存儲 num 次,我們很容易寫出這樣的代碼:
fn main() {
let batch: Batch = Batch:new(); // 初始化Batch
loop {
let data:Data = dataSource.getData(); // 從數據源獲得data
recordData(batch, &data);
if (batch.len() > 100) { // batch存儲的數據大於100條時,存儲並清空
save(batch);
batch.clear();
} // ------------------- data的生命週期到此結束
} // ------------------- batch的生命週期到此結束
}
fn record_data(batch: Batch, data: Data) {
if(condition) { // 根據條件將msg保存num次
for i in 0..data.num {
batch.msgList.push(&data.msg);
}
}
}
看起來是不是很合理,和其他語言也沒有什麼區別,當信心滿滿按下編譯後,會發現天空飄來五個字:編譯不通過。原因很簡單,因爲編譯器發現被引用對象 data 的生命週期小於 batch,data 的在當前循環結束後就會銷燬,batch 存儲的引用就變成了野指針。我們可以做如下修改:
fn() {
let batch: Batch = Batch:new(); // 初始化Batch
let dataList: Vec<Data> = Vec::new(); // dataList的生命週期和batch一樣
loop {
let data: Data = dataSource.getData(); // 從數據源獲得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) { // 根據條件將msg保存num次
for i in 0..data.num {
batch.msgList.push(&data.msg);
}
}
}
可以看到,我們對代碼做了一些小改動:
-
在循環外初始化了一個 Vec,並保存每次得到的 data。
-
record_data 函數上增加了生命週期標註。
爲什麼這麼做呢?我們已經知道最初版本是因爲 data 的生命週期小於 batch,導致 batch 不能存儲 data 的引用。解決這個問題的思路很簡單,提升 data 的生命週期不就完了。假設 batch 的生命週期是'a,data 的生命週期是'b,很明顯'a 是大於'b 的,因爲 batch 的生命週期是整個 main 函數,而 data 的生命週期僅僅在 loop 內。我們在 batch 同樣的作用域內定義一個容器,它的生命週期也是'a。在每次得到 data 後把它存入容器中,那 data 就不會在循環結束的時候被銷燬了。
同時,在 record_data 函數定義上,我們也要使用標註告訴編譯器 batch 和 data 的生命週期是相等的。如果 data 的生命週期大於 batch,我們也可以在參數中定義 data 的生命週期爲'a,因爲實際的生命週期和參數生命週期標註無需一致,只需要實際的生命週期大於參數生命週期就行了。如果你有強迫症,也可以在參數中標註實際的生命週期,只需要加上適當的生命週期約束就行了:
// 'b: 'a表示'b的生命週期能夠覆蓋'a
fn record_data<'a>(batch: Batch<'a>, data: &'b Data) where 'b: 'a {
......
}
經過這些小改動,你的應用會比粗暴的使用拷貝提升許多性能並且節約大量內存使用。經過我們的測試,在類似需求中將需要大量拷貝的操作替換成引用,可以節省一倍的內存,CPU 使用率也下降了 20%。
FFI
(Foreign Function Interface)
在一些情況下,我們項目使用的編程語言在實現一些功能時,想使用現成的依賴庫來實現複雜的邏輯,但是因爲生態不完善,導致缺少此類庫或者現存的依賴庫不成熟。在使用 Rust 時,這種現象尤其普遍。很多熱門組件沒有爲 Rust 提供官方 API,非官方實現功能和性能又得不到保證,且更新不穩定。難道 Rust 進階之路就要到此爲止?
Rust 很貼心地提供了跨語言交互能力,對 FFI 的良好支持可以讓開發者方便的在 Rust 代碼中調用 C 程序。如果我們需要的依賴庫剛好有 C/C++ 的實現,就能使 Rust 完成主要邏輯,把一些 Rust 不完善的功能通過 C/C++ 實現,而且性能也不會受到影響。在 Rust 程序調用 C 代碼也非常簡單:
- 聲明外部函數
extern "C" {
fn c_add(a: i32, b: i32) -> i32;
}
- 在 RUST 中調用 C 函數
fn main() {
unsafe {
c_add(1, 2);
}
}
- 將 C 程序編譯打包爲靜態 / 動態鏈接庫
g++ -std=c++17 -shared -fPIC -o libhello.so hello.cpp
- 然後編譯 Rust 文件並鏈接到鏈接庫
rustc main.rs hello.o
儘管用 Rust 調用 C 程序已經非常方便,但是仍需要注意這些問題:
-
處理數據類型:在 Rust FFI 中,需要特別注意數據類型的轉換和處理。Rust 和其他語言的數據類型可能存在差異,需要進行適當的轉換。例如,Rust 的 i32 和 C 的 int 可以直接相互轉換。而字符串的傳遞之所以需要特殊處理,是因爲 Rust 的字符串實現和 C/C++ 不一樣。C/C++ 的字符串指針只包含地址,且字符串後有 “\0” 作爲結尾,而 Rust 字符串的指針不僅包含地址,還包含字符串長度,且末尾沒有 “\0” 作爲結尾。
-
內存管理:儘管 Rust 是內存安全的語言,但是在使用 FFI 的情況下,Rust 無法保證調用的外部語言的安全性。作爲開發者,我們要自己管理外部語言的內存。
-
線程安全:在多線程環境下使用 Rust FFI 時,需要注意線程安全問題。某些外部函數可能不是線程安全的,需要在調用時進行適當的同步操作。
-
性能優化:在使用 Rust FFI 時,需要注意性能優化問題。由於涉及跨語言調用,可能會導致一定的性能損失。因此,需要對 FFI 調用的性能進行評估和優化。
Tokio
如果你想構建一個高性能的 Rust 服務器應用,那麼 Tokio 絕對是你的首選框架。Tokio 是一個用 Rust 編寫的異步運行時,旨在提供高性能的 I/O、任務調度和併發支持。雖說 Tokio 提供了強大的異步支持,要用好 Tokio 也不是一件容易得事,首先要了解 “異步” 的概念。在計算機編程中,“異步”是指一種不阻塞的操作方式,允許程序在等待某些操作(如 I/O 操作、網絡請求等)完成時繼續執行其他代碼。
Tokio 通過使用協程和 Future 機制來實現高效的併發處理。它將異步任務封裝爲 Future 對象,並通過運行時的調度器管理這些任務的執行狀態。當任務被調用時,運行時通過 poll 方法檢查其狀態,如果任務無法繼續執行(返回 Poll::Pending),則將其掛起並註冊一個 Waker 來在後續的某個時刻喚醒任務。一旦相關的 I/O 操作完成,Waker 會通知運行時重新調度該任務,從而實現非阻塞的併發執行。Tokio 支持多線程運行,可以充分利用多核 CPU 的能力,提高應用程序的性能和響應性。
Tokio 的使用非常簡單,使用 async 和 await 就可以很方便地創建異步任務,但是要使用 Tokio 寫出高性能的代碼不是一件簡單的事。剛剛接觸 Tokio 的開發者會經常發現代碼無故卡死或者性能低下,這是因爲沒有正確使用 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;
}
代碼結構很簡單,但是運行後會發現代碼似乎 hang 住了,檢查代碼結構也沒有發現問題。要解釋這個卡死的問題,要從 Tokio 的任務調度機制來分析:
Processor 獲取 Task 後,會開始執行這個 Task,在 Task 執行過程中,可能會產生很多新的 Task,第一個新 Task 會被放到 LIFO Slot 中,其他新 Task 會被放到 Local Run Queue 中,因爲 Local Run Queue 的大小是固定的,如果它滿了,剩餘的 Task 會被放到 Global Queue 中。
Processor 運行完當前 task 後,會嘗試按照以下順序獲取新的 Task 並繼續運行:
-
LIFO Slot.
-
Local Run Queue.
-
Global Queue.
-
其他 Processor 的 Local Run Queue。
如果 Processor 獲取不到 task 了,那麼其對應的線程就會休眠,等待下次喚醒。
在上面的例子中,我們首先 Spawn 了一個異步任務 Task-1,Task-1 被分配給了 Processor-1 執行。然後在 Task-1 裏 Spawn 了另一個異步任務 Task-2,Task-2 被放到了 Processor-1 的 LIFO Slot 中。
因爲 Task-1 繼續運行的條件依賴於 Task-2,所以 Task-1 被阻塞了。而且 Tokio 的協程是非搶佔式的,在 Task-1 沒有遇到. await 前無法讓出 CPU,Processor-1 無法去執行 Task-2。又因爲 Task-2 在 Processor-1 的 LIFO Slot 中,其他的 Processor 也無法偷取 Task-2 執行。於是,Task-2 永遠也不會有機會被執行,這兩個 Task 在循環等待中就永遠卡死了。
要解決這個問題,我們要將阻塞型的數據結構替換成 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 的非阻塞數據結構後,Task-1 在提交完 Task-2 後遇到 await 讓出了 CPU,Processor-1 就可以從 LIFO Slot 取出 Task-2 執行了,循環等待也就被打破了。
由這個例子可以看出,Tokio 的輕量級線程之間的關係是一種合作式的。合作式的意思就是同一個 CPU 核上的任務大家是配合着執行(不同 CPU 核上的任務是並行執行的)。我們可以設想一個簡單的場景,A 和 B 兩個任務被分配到了同一個 CPU 核上,A 先執行,那麼,只有在 A 異步代碼中碰到 .await 而且不能立即得到返回值的時候,纔會觸發掛起,進而切換到任務 B 執行。也就是說,在一個 task 沒有遇到 .await 之前,它是不會主動交出這個 CPU 核的,其他 task 也不能主動來搶佔這個 CPU 核。
所以在使用 Tokio 時,我們要注意兩點:
-
不要在異步代碼中執行阻塞操作,不然這個 OS 線程中的其他任務都會被阻塞。
-
Tokio 雖然適合網絡 I/O 型併發,但是也要在 I/O 任務裏小心地控制計算型代碼的時間,否則會導致運行時任務調度不均,從而長時間阻塞其它任務的運行。
四
Rust 應用發佈
通過 Cargo,開發者可以輕鬆創建、構建和共享 Rust 項目。但是因爲發佈系統只支持 Java 和 Golang 應用,要在發佈系統發佈 Rust 應用還是需要一些工作的。以下是我們發佈 Rust 應用的流程。
上傳鏡像
因爲公司平臺是沒有 Rust 應用的,所以我們需要自己製作鏡像並上傳,這樣才能在發佈平臺發佈我們的代碼。我們需要創建兩個 Docker 鏡像:一個用於構建(CI 鏡像),另一個用於運行(運行時鏡像)。
在 dockerfile 裏可以安裝自己想要的工具包,根據自己需求來定製。
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
# 創建 /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
發佈
建好集羣后,還需要對集羣進行一些配置:
-
修改編譯配置的鏡像爲自己上傳的鏡像。
-
將編譯命令設爲
cargo build --release
。 -
修改運行時鏡像。
-
修改發佈配置,改爲自己應用所需要的。
還需要注意的是,發佈平臺的編譯環境和運行環境是不同的,編譯完成後發佈平臺會將可執行文件移動到 / opt/apps 目錄下進行執行,而配置文件不會被打包。遇到這種情況可以使用rust-embed
庫,它允許將靜態文件(如 Yaml、Json、圖像等)打包到您的二進制文件中,從而簡化文件管理和部署。
上監控
雖說 Rust 應用主打的是穩定,但是發佈後持續對應用進行監控也是必須的,不然晚上能睡得着嗎。和發佈一樣,Rust 應埋的指標要被監控採集,需要額外的配置。在 KubeOne 平臺找到自己的集羣,在發佈配置里加上這兩項,監控平臺就可以採集到指標了。
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
通過上監控,可以實時觀察 Rust 服務的運行情況,並且根據自己的埋點分析系統的瓶頸。可以看到,Rust 應用運行非常平穩。相比於有 GC 的 Java 應用,Rust 明顯毛刺很少,非常平滑,而且內存佔用相比 Java 減少了 70%。
五
結 論
通過遷移到 Rust,我們的計算層能夠在處理高併發請求時顯著提高系統的吞吐量和響應能力,同時減少服務器資源的浪費。這不僅能降低運營成本,還能爲我們的用戶提供更流暢、更快速的體驗。
但是,如果要持續地擁抱 Rust 生態,目前仍然面臨如下挑戰:
- 生態不完善
儘管 Rust 已經有一些非常優秀的庫和工具,但某些特定領域仍然缺乏成熟且廣泛使用的庫。這意味着開發者可能需要花費更多的時間來構建自己的解決方案或者整合不同語言的庫。
- 學習曲線陡峭
Rust 語言引入了許多獨特的概念和特性,對於初學者和來自其他語言的開發者來說,這些特性可能需要一段時間來徹底掌握。
- 開發進度
相比於自動內存管理類型語言的開發任務,Rust 嚴格的編譯檢查會讓開發進度一度阻塞。
儘管開發 Rust 生產級應用有那麼多阻礙,我們目前已經發布的 Rust 應用已經證明了,相比於付出,遷移 Rust 帶來的收益更大。希望大家都可以探索 Rust 的可行性,爲節能減排和世界和平出一份力,也歡迎各位對 Rust 有興趣的同學一起交流。
文 / 小新
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/4LgoL2xJihs1s7cAxHQ0MQ