編譯 | 言征
去年,Web開發公司Mainmatter對Web版 Rust 進行了戰略押注,并發起了 EuroRust 會議,加入了 Rust 基金會,同時正在內部以及開源領域從事許多 Rust 項目。
Mainmatter非常樂觀地認為 Rust 將在未來幾個月和幾年內在Web和云空間中起飛,并認為Rust 是邁向 Web 開發新時代的第一步,開發人員可以利用這項技術,在不放棄開發人員經驗和生產力的情況下,達到更高的、以前難以想象的效率、穩定性、可靠性和可維護性水平。
這篇文章意在分享為什么Mainmatter有信心作這一押注,以及為什么我們相信 Rust 在Web和云領域擁有美好的未來。
一、大廠偏愛,Rust的未來
Rust 自從大約十年前登臺以來,就受到了很多開發人員的關注和喜愛。不僅開發人員喜歡這門語言,大公司的決策者也認為 Rust 是一項偉大的技術,并且在過去幾年里,該語言在整個行業得到了廣泛采用。
AWS在其平臺上大量使用 Rust ,Google在 Android 中使用它,Microsoft在 Windows 中使用它。從本質上講,Rust 有望在以前使用的許多領域取代 C 和 C++:系統編程、操作系統、各種嵌入式系統、低級工具以及游戲和游戲引擎。
當然,除了以上這些,未來具有更大潛力的領域是Web和云。Rust給這兩個領域帶來了無限想象的后端提升空間。一旦后端的開發提升到一個新的水平,就能讓團隊能夠訪問以前無法實現的功能。
盡管 Rust 還很年輕,但已經看到很多公司在Web和云中成功使用 Rust,比如:Truelayer、Discord、Temporal、Nando's、svix、Wingback等等。
值得一提的是,谷歌多年來也一直大力采用 Rust,最近表示,與他們使用的任何其他語言相比,他們并沒有真正看到 Rust 的生產力損失。
二、Rust做Web的雄心
雖然相對年輕,畢竟距離1.0 發布,Rust的生態系統也只走過了 8 年。但Rust以及其Web生態已經達到了一定的成熟度,足夠使其成為構建真實應用程序的可行選擇。
正如arewewebyet.org所證實的,Rust 顯然已經為Web做好了準備:
首先,有tokio,一個異步運行時,它是Web應用程序的堅實且高性能的基礎;其次,最重要的是,Rust已經有了成熟且維護良好的 Web 框架,例如axum和actix-web。所有相關數據存儲以及 ORM 都有成熟的驅動程序;最后,可以找到涵蓋構建 Web 應用程序的所有其他相關方面的庫,例如(反)序列化、國際化、模板化、可觀察性等。
總的來說,Rust的雄心勃勃,構建 Web 后端提供了堅實而穩定的構建塊。
三、有必要換Rust做Web?
當然,有人可能會問:我為什么要換Rust?對于已經使用 Ruby、Java、Elixir、TypeScript、Go 或其他任何語言的團隊而言,換 Rust有哪些好處嗎?
有兩個關鍵方面使 Rust 成為 Web 構建的絕佳選擇:一是它的效率和性能;二是其類型系統帶來的可靠性和可維護性帶來的好處。
1.效率和性能
Rust 以其高效和高性能而聞名。它將輕松超越 Web 應用程序常用的 JavaScript、Ruby、Python 等語言幾個數量級。其他語言可能具有更高的性能上限(例如 Java 或 C# 或 Go),但你需要投入大量的工程精力才能接近 Rust 工具包開箱即用提供的性能水平。
此外,Rust 還有一個關鍵優勢:它不捆綁垃圾收集器。垃圾收集語言可以很快,但它們不能始終一致性地表現出色。垃圾收集器將引入暫停(pause)以釋放未使用的內存,從而對應用程序的尾部延遲產生負面影響。而Rust 不存在這個問題:它可以提供一致的性能,而不會出現這些峰值。
C 和 C++ 是唯一能夠實現如此穩定和一致性能的其他語言。不幸的是,這兩種語言往往搬起石頭砸自己腳,處處是陷阱,特別是在手動內存管理時。正如 Linux 的創始人 Linus Torvalds 所說:
“它離硬件太近了,你可以用它做任何事情。這很危險。就像玩雜耍電鋸一樣。我還發現它確實有很多陷阱,而且很容易被忽視。
由于 C 和 C++ 的這些危險,除了這兩種語言的專家或擁有專家團隊時才能使用。否則,你得到的就是一個不穩定且充滿安全漏洞的系統。
同時,別忘了,在 Web 領域,很少人具備這種專業知識,因為每個人大多都使用非常不同的語言,如 JavaScript、Python、Ruby、Elixir 等。反而Rust 就不會遇到同樣的陷阱,使開發人員能夠以以前的效率水平構建軟件。
Rust 通常會比用于構建 Web 后端的其他技術的性能好幾個數量級,同時保持顯著較低的內存占用。
當然,如果與其他技術相比, Rust Web 服務器可以在一小部分時間內響應請求,這也意味著它可以用更少的服務器響應相同數量的請求,這又意味著更少的托管成本。
這對于中小型產品和公司來說,減少托管的云服務器數量,就意味每月就可以輕松減少不菲的費用。
我們的 Python 服務平均約為 50 個請求/秒,NodeJS 約為 100 個請求/秒,Rust 約為 690 個請求/秒。我們可以在通常托管單個 Python 服務的 k8 EKS 節點上安裝 4 個 Rust 服務。——Reddit某用戶
然而,成本節省還只是好處之一,使用更少的服務器也意味著使用更少的能源。盡管使用可再生能源運行數據中心固然很好,但最綠色的能源仍然是我們不使用的能源。Rust 或許不能解決氣候危機,但這里要承認的是,運行我們編寫的軟件,也會真真切切地消耗資源,從而對現實世界產生影響。軟件行業往往會忘記這一點——如果我們能夠更有效地利用資源,并以更少的投入獲得相同的產出,這是選擇技術時的一個重要考慮。
2.可靠性和可維護性
雖然性能和效率很重要,但在許多情況下,可能更相關的原因則是 Rust 的強類型系統所帶來的可靠性和可維護性收益。
像這樣的代碼片段對于 Web 應用程序(Ruby on Rails)來說是相當典型的:
class User
attr :name
attr :active
attr :activation_date
def activate(activation_date)
self.active = true
self.activation_date = activation_date
save
end
def save
…
end
end
…
user.activate(Time.now)
雖然這段代碼非常簡潔且易于閱讀,并且編寫這樣的代碼可以讓你快速實現目標,但也存在問題。在這個的示例中,雖然我們可以看到用戶的屬性,但我們不知道這些屬性周圍可能有什么規則(例如,如果active是true,則activation_date可能也必須設置?如果active是false,則大概activation_date應該是nil?)。為了驗證這些假設,我們必須研究該activate方法的實現,這意味著需要付出相對較高的努力才能獲取信息。
查看該activate方法的調用,我們無法知道它是否會引發錯誤,或者我們應該在哪個時區中度過時間。雖然 Ruby 可能有點極端,但考慮到其眾所周知的靈活性,許多這些問題在其他語言中也存在。讓我們以 Java 為例。我們仍然無法在類型系統中對圍繞active和activation_date屬性的規則進行編碼,即使可以null,我們也有NullPointerException在運行時獲取 s 的風險。
隨著代碼庫的增長和開發團隊的壯大,或者只是隨著一些人離開和加入而發生變化。從事代碼庫工作的每個人都對整個應用程序以及整個代碼庫中所做的所有隱式假設都有一個完美的心智模型,但這很難做到,相反,理解這些概念需要人們認真閱讀遺留代碼。這不僅降低了效率,而且還可能導致生產中的錯誤率增加。
與上面相同的代碼片段,但在 Rust 中更加清晰和富有表現力:
enum User {
Inactive {
name: String,
},
Active {
name: String,
active: bool,
activation_date: DateTime<Utc>,
},
}
impl User {
fn activate(&self, activation_date: DateTime<Utc>) -> Result<(), DBError> {
match self {
User::Inactive { name } => {
let new_user = User::Active {
name: name.clone(),
active: true,
activation_date: activation_date,
};
new_user.save()
}
User::Active { .. } => Err(Error::default()),
}
}
fn save(&self) -> Result<(), DBError> {
…
}
}
首先,對于用戶模型,我們可以使用 Rust 的enum關聯數據。這樣,就可以完全清楚非活躍用戶和活躍用戶是什么樣子,以及在什么場景下可以設置哪些屬性——事實上,活躍用戶和非活躍用戶甚至不具有相同的屬性,但每個用戶都只具有對其有意義的屬性。它們代表各自的用戶狀態。此外,屬性的類型也被明確定義——不僅 Rust 是類型化的,而 Ruby 顯然是非類型化的,而且類型也非常精確,例如對于字段,activation_date預期的時區在類型中也是正確的。
該函數的簽名activate還顯式地編碼了 Rails 示例中隱含的許多信息。同樣,預期的時區activation_date在類型中是正確的,并且該函數返回Rust 的時區Result,這清楚地表明調用它時可能會發生錯誤。Result事實上,Rust 編譯器將要求處理的成功和錯誤變體,以便不會發生未處理的運行時異常。
此外,activation_date當調用函數時,函數的參數總是保證有一個值,因為 Rust 沒有隱式可空性的概念(與 Java 不同)。如果activation_date 可能在其計算位置沒有值,則它可能無法Option<DateTime<Utc>>傳遞給函數activate,因為它的類型與預期的不同DateTime<Utc>。Rust 編譯器只允許Some其變體的代碼路徑Option導致方法的調用activate,以便activation_date保證在函數運行時有一個值。
雖然這顯然是一個相當簡單的示例,但它很好地說明了 Rust 的兩個主要優點:
(1)Rails 示例中隱含的許多概念和規則都是通過 Rust 代碼中的類型顯式傳達的。可以清楚地區分活躍用戶和非活躍用戶,對于日期字段,甚至預期的時區也被編碼在類型中。這種表現力使代碼更容易理解,特別是對于代碼庫的新手來說,從而提高了可維護性。
(2)Rust 還大大提高了可靠性,因為其他語言(包括 Java 或 Go 等類型化語言)中常見的整類錯誤將在編譯時而不是運行時檢測到。編譯器保證函數activation_date的參數activate具有值以及要處理的函數可能返回的任何錯誤。
總體而言,當每個人都關注 Rust 的性能時,Rust 帶來的可靠性和可維護性方面的改進常常被忽視。然而,這些好處對于項目的長期成功可能比純粹的績效數字更相關。
四、Rust先苦后甜
由于 Rust 的主要優點是可靠性、可維護性、效率和性能,因此該語言的用例顯然是與這四個方面特別相關的用例。但是,好處的代價是需要考慮在內。
總體而言,Rust 仍然需要比其他技術更高的前期投資,特別是與 Web 項目中常用的技術相比:
雖然像 JavaScript 和 Ruby 這樣的語言是為了快速獲得結果而設計的,但 Rust 則沒有留下太多的自由度,并且要求程序在獲得工作結果之前通過所有編譯器的檢查。與這些語言相比,使用 Rust 就需要付出更多的初始工作。此外,人們在使用 Rust 之前還需要翻越一座山——那就是掌握 Rust 獨特的所有權系統。
然而,當跨過項目的初始階段并將視野擴展到更長的時間范圍時,可維護性、可靠性和穩定性等方面變得極其重要,一開始使用 Rust 時進行的額外投資會隨著時間的推移而帶來回報——
Rust 應用程序更可靠,因此需要更少的時間投入到錯誤修復上,并且更易于維護,因此更容易與不斷增長和變化的團隊一起有效地工作。
最后就會呈現出:Rust工作量先大后小,先苦后甜。對于其他語言來說,情況往往是相反的:隨著時間的推移,隨著團隊的成長,可靠性和可維護性挑戰的影響變得更大、成本更高,工作量也會增加。
五、用Rust前的幾個問題
根據 Rust 的優勢和投入曲線,每當評估是否針對特定情況選擇 Rust 時,需要回答的主要問題是:
(1)團隊是否已經具備 Rust 專業知識(許多不使用 Rust 的團隊實際上已經擁有專業知識,因為很多開發人員在空閑時間使用 Rust 編寫代碼)?
(2)可靠性方面有哪些要求?
(3)長期維護計劃是什么?
(4)系統構建的規模有多大,Rust 在托管方面可以節省多少錢?
(5)根據以上問題的答案,額外的初始投資值得嗎?
雖然在某些情況下,結論是額外的初始投資不值得,但在某些情況下,評估顯然對 Rust 有利。我們看到的一些典型用例包括:
(1)對于實現產品關鍵業務邏輯的核心業務系統來說,可靠性、長期可維護性等方面是首要考慮的問題。
(2)對于金融系統來說,通常對錯誤的容忍度很低,而 Rust 帶來的穩定性的提高可能是一個決定性因素。另外,性能是一項關鍵要求,在特定場景(例如交易系統)中具有明顯的財務影響。
(3)任何必須能夠提供高吞吐量和性能的系統顯然都會從 Rust 中受益。位于多個微服務前面的代理服務器等系統必須具有最小的開銷和一致的性能。在這些情況下,垃圾收集語言及其不可靠的性能特征通常不是一個選擇。
(4)最后,對于任何大規模運行的系統,在托管成本方面都有很大的節省潛力。
一旦做出了使用 Rust 的決定,就有兩種主要的采用路徑——要么用 Rust 從頭開始(重新)編寫整個應用程序,要么考慮與其他技術一起逐步采用。篇幅原因,就不再展開了。
原文鏈接:https://mainmatter.com/blog/2023/08/14/the-case-for-rust-on-the-web/#why