一個Rust小白發布生產級Rust應用的進階之路
一、引言
二、Rust核心特性
1. 所有權
2. 生命周期和引用
三、用Rust構建生產級應用
1. 合理利用引用減少數據拷貝
2. FFI(Foreign Function Interface)
3. Tokio
四、Rust應用發布
1. 上傳鏡像
2. 發布
3. 上監控
五、結論
一、引 言
在流量日益增長的今天,隨著用戶需求的不斷增加和性能要求的提升,一個能夠更好地處理高并發、低延遲和資源有效利用的計算層是十分重要的。盡管在過去我們平臺使用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代碼也非常簡單:
1. 聲明外部函數
extern "C" {
fn c_add(a: i32, b: i32) -> i32;
}
2. 在RUST中調用C函數
fn main() {
unsafe {
c_add(1, 2);
}
}
3. 將C程序編譯打包為靜態/動態鏈接庫
g++ -std=c++17 -shared -fPIC -o libhello.so hello.cpp
4. 然后編譯 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生態,目前仍然面臨如下挑戰:
1. 生態不完善
盡管 Rust 已經有一些非常優秀的庫和工具,但某些特定領域仍然缺乏成熟且廣泛使用的庫。這意味著開發者可能需要花費更多的時間來構建自己的解決方案或者整合不同語言的庫。
2. 學習曲線陡峭
Rust 語言引入了許多獨特的概念和特性,對于初學者和來自其他語言的開發者來說,這些特性可能需要一段時間來徹底掌握。
3. 開發進度
相比于自動內存管理類型語言的開發任務,Rust嚴格的編譯檢查會讓開發進度一度阻塞。
盡管開發Rust生產級應用有那么多阻礙,我們目前已經發布的Rust應用已經證明了,相比于付出,遷移Rust帶來的收益更大。希望大家都可以探索Rust的可行性,為節能減排和世界和平出一份力,也歡迎各位對Rust有興趣的同學一起交流。