在這篇文章中,我們不會回答每個人在開始新專案時都會問的問題:我應該用 Rust 來做嗎?
相反,我們將探索在自信地回答「絕對! 」並開始主要使用 Rust 建立業務後遇到的陷阱和見解。
這篇文章旨在提供我們經驗的高層次概述,我們將在即將推出的系列中更深入地研究細節。
(在評論中為我們的下一篇文章投票🗳️)
為專案選擇正確的語言從來不是一個一刀切的決定。
關於我們的團隊和用例的幾句話:
我們是一個 6 人團隊,幾乎沒有 Rust 經驗,但擁有建立資料密集型應用程式的豐富 Scala/Java 背景
我們的 SaaS 是一個計費平台,專注於分析、即時資料和可操作的見解(想想 Stripe Billing 與 Profitwell 的結合,再加上一點 Posthog)。
我們的後端完全採用 Rust(分為 2 個模組和幾個工作線程),並使用 gRPC-web 與我們的 React 前端進行對話
我們是開源的!
您可以在這裡找到我們的儲存庫:https://github.com/meteroid-oss/meteroid
我們期待您的支持 ⭐ 和貢獻
因此,我們有一些不可協商的要求恰好非常適合 Rust:效能、安全性和並發性。
Rust 實際上消除了與記憶體管理相關的所有 bug 和 CVE,而它的並發原語非常有吸引力(並且沒有讓人失望)。
在 SaaS 中,所有這些功能在處理敏感或關鍵任務時尤其有價值,例如我們案例中的計量、發票計算和交付。
正如包括微軟在內的許多大型企業最近所承認的那樣,其記憶體使用量的顯著減少也是建立可擴展和永續平台的一大優勢。
來自戲劇性的、有時有毒的 Scala 社區,熱情且包容的Rust 生態系統也是一個重要的吸引力,為探索這個新領域提供了動力。
帶著這樣的厚望,讓我們開始我們的旅程吧!
學習 Rust 並不像學習另一種語言。所有權、借用和生命週期等概念一開始可能會讓人望而生畏,使得原本瑣碎的程式碼變得極其耗時。
儘管生態系統令人愉快(稍後會詳細介紹),但有時您不可避免地需要編寫較低層級的程式碼。
例如,考慮我們的 API (Tonic/Tower) 的一個相當基本的中間件,它只報告計算持續時間:
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for MetricService<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>, Error = BoxError>
+ Clone + Send + 'static,
S::Future: Send + 'static,
ReqBody: Send,
{
type Response = S::Response;
type Error = BoxError;
type Future = ResponseFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);
let started_at = std::time::Instant::now();
let sm = GrpcServiceMethod::extract(request.uri());
let future = inner.call(request);
ResponseFuture {
future,
started_at,
sm,
}
}
}
#[pin_project]
pub struct ResponseFuture<F> {
#[pin]
future: F,
started_at: Instant,
sm: GrpcServiceMethod,
}
impl<F, ResBody> Future for ResponseFuture<F>
where
F: Future<Output = Result<Response<ResBody>, BoxError>>,
{
type Output = Result<Response<ResBody>, BoxError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let res = ready!(this.future.poll(cx));
let finished_at = Instant::now();
let delta = finished_at.duration_since(*this.started_at).as_millis();
// this is the actual logic
let (res, grpc_status_code) = (...)
crate::metric::record_call(
GrpcKind::SERVER,
this.sm.clone(),
grpc_status_code,
delta as u64,
);
Poll::Ready(res)
}
}
是的,除了泛型類型、泛型生命週期和特徵約束之外,您最終還需要為簡單的服務中間件編寫自訂的 Future 實作。
請記住,這是一個有點極端的例子,旨在展示生態系統中存在的粗糙邊緣。在許多情況下,Rust 最終可以像任何其他現代語言一樣緊湊。
學習曲線可能會根據您的背景而有所不同。如果您習慣了 JVM 處理繁重的工作並像我們一樣使用更成熟、更廣泛的生態系統,那麼可能需要付出更多的努力來理解 Rust 的獨特概念和範例。
然而,一旦您掌握了這些概念和原語,它們就會成為您武器庫中極其強大的工具,即使您偶爾需要編寫一些樣板文件或宏,也可以提高您的工作效率。
值得一提的是, Google 在相當短的時間內成功地將團隊從 Go 和 C++ 過渡到 Rust,並且取得了積極的成果。
要平滑學習曲線,請考慮以下因素:
閱讀官方Rust Book 的封面。不要跳過章節。理解這些複雜的概念將變得容易得多。
練習,練習,練習!透過Rustlings練習來建立肌肉記憶並採用 Rust 思維方式。
參與Rust 社群。他們是一群令人難以置信的人,總是願意伸出援手。
利用 GitHub 的搜尋功能尋找其他專案並向其學習。生態系統仍在不斷發展,與其他人的合作至關重要(只需注意許可證並始終做出貢獻)。
我們將在下一篇文章中探討一些帶給我們啟發的專案。
Rust 的底層生態系統確實令人難以置信,擁有精心設計和維護的庫,並被社區廣泛採用。這些函式庫為建構高效能且可靠的系統奠定了堅實的基礎。
然而,當你在堆疊中向上移動時,事情可能會變得稍微複雜一些。
例如,在資料庫生態系統中,雖然針對關聯式資料庫存在像sqlx
和diesel
這樣的優秀函式庫,但對於許多非同步或非關聯式資料庫用戶端來說,情況會更加複雜。這些領域的高品質庫,即使被大公司使用,也往往只有單一維護者,導致開發速度較慢並且有潛在的維護風險。
對於分散式系統原語來說,挑戰更為明顯,您可能需要實現自己的解決方案。
這並不是 Rust 所獨有的,但與舊的/更成熟的語言相比,我們經常發現自己處於這種情況。
從好的方面來說, Rust 的生態系統對安全問題的反應令人印象深刻,補丁迅速傳播,確保了應用程式的穩定性和安全性。
到目前為止,圍繞 Rust 開發的工具也非常令人驚嘆。
我們將在以後的文章中深入探討我們選擇的函式庫以及我們所做的決定。
生態系統不斷發展,社區積極努力填補空白並提供強大的解決方案。準備好探索未知領域,並相應地分配資源以幫助維護,並回饋社區。
Metroid是一個現代化的開源計費平台,專注於商業智慧和可操作的見解。
我們需要你的幫助 !如果你有一分鐘時間,
您的支持對我們意義重大❤️
https://github.com/meteroid-oss/meteroid ⭐️ 在 Github 上為我們加註星標 ⭐️
當深入 Rust 的生態系統時,您很快就會意識到文件網站有時可能有點......好吧,稀疏。
但不要害怕!真正的寶藏往往存在於原始碼中。
許多庫都有非常詳細的方法記錄,並在程式碼註釋中包含全面的範例。如有疑問,請深入研究原始程式碼並進行探索。您經常會發現您尋求的答案,並對圖書館的內部運作有更深入的了解。
雖然具有使用指南的外部文件仍然很重要,並且可以節省開發人員的時間和挫折感,但在 Rust 生態系統中,準備好在必要時深入研究程式碼至關重要。
像docs.rs這樣的網站可以輕鬆存取公共 Rust 套件的基於程式碼的文件。或者,您可以使用 Cargo doc 在本機上產生所有依賴項的文件。這種方法一開始可能會令人困惑,但從長遠來看,花一些時間學習如何駕馭這個系統可能會非常有效。
不用說,另一個有用的技術是尋找範例(大多數庫在其存儲庫中都有一個/examples
資料夾)和使用您感興趣的庫的其他專案,並與這些社區互動。這些總是為如何使用該庫提供有價值的指導,並且可以作為您自己實施的起點。
當開始使用 Rust 時,人們很容易會努力爭取最慣用和最高效能的程式碼。
然而,大多數時候,以簡單性和生產力的名義進行權衡是可以的。
例如,使用clone()
或Arc
在執行緒之間共享資料可能不是最節省記憶體的方法,但它可以極大地簡化程式碼並提高可讀性。只要您意識到效能影響並做出明智的決策,優先考慮簡單性是完全可以接受的。
請記住,過早的優化是萬惡之源。首先專注於編寫乾淨、可維護的程式碼,然後在必要時進行最佳化。不要嘗試進行微優化(除非您確實需要)。 Rust 強大的類型系統和所有權模型已經為編寫高效、安全的程式碼提供了堅實的基礎。
當需要優化效能時,請專注於關鍵路徑並使用perf
和flamegraph
等分析工具來辨識程式碼中的真正效能熱點。對於工具和技術的全面概述,我可以推薦The Rust Performance Book 。
¹這適用於您的整個創業歷程,包括籌款
Rust 的錯誤處理非常優雅,具有Result
類型和?
運算符鼓勵明確的錯誤處理和傳播。然而,這不僅涉及處理錯誤;還涉及處理錯誤。它還涉及提供乾淨且資訊豐富的錯誤訊息以及可追蹤的堆疊追蹤。
無需大量樣板在錯誤類型之間進行轉換。
像thiserror
, anyhow
或snafu
函式庫對於這個目的來說是無價的。我們決定使用thiserror
,它可以簡化帶有資訊性錯誤訊息的自訂錯誤類型的建立。
在大多數 Rust 用例中,您不太關心底層錯誤類型堆疊跟踪,而是更喜歡將其直接映射到域中的訊息類型錯誤。
#[derive(Debug, Error)]
pub enum WebhookError {
#[error("error comparing signatures")]
SignatureComparisonFailed,
#[error("error parsing timestamp")]
BadHeader(#[from] ParseIntError),
#[error("error comparing timestamps - over tolerance.")]
BadTimestamp(i64),
#[error("error parsing event object")]
ParseFailed(#[from] serde_json::Error),
#[error("error communicating with client : {0}")]
ClientError(String),
}
投入時間製作清晰且資訊豐富的錯誤訊息可以大大增強開發人員的體驗並簡化偵錯。這是一個小小的努力,卻可以產生顯著的長期效益。
然而,有時,甚至在日誌位於使用者範圍之外的 SaaS 用例中,保留完整的錯誤鏈以及沿途可能有額外的上下文是很有意義的。
我們目前正在試驗error-stack
,這是一個由 hash.dev 維護的庫,它允許附加額外的上下文並將其保留在整個錯誤樹中。它作為thiserror
之上的一層效果很好。
它提供了一個慣用的 API,實際上將錯誤類型包裝在報告資料結構中,該資料結構保留了所有錯誤、原因和您可能加入的任何其他上下文的堆疊,在發生故障時提供大量資訊。
我們遇到了一些問題,但這篇文章已經太長了,更多內容將在後續文章中介紹!
使用 Rust 建立我們的 SaaS 一直是(而且仍然是)一段旅程。一開始是一段漫長而充滿挑戰的旅程,但也是一段非常有趣且有益的旅程。
當然。
或許。
可能不會。
Rust 促使我們以不同的方式思考我們的程式碼,接受新的範式,並不斷努力改進。
當然,Rust 也有其粗糙的一面。學習曲線可能很陡峭,而且生態系統仍在不斷發展。但這是令人興奮的一部分。
除了技術面之外, Rust 社群也絕對令人高興。熱情的氛圍、樂於助人的意願以及對語言的共同熱情使這趟旅程變得更加愉快。
因此,如果您有時間和意願去探索一個新的、蓬勃發展的生態系統,如果您願意接受挑戰並從中學習,如果您需要表現、安全性和並發性,那麼Rust 可能只是成為適合您的語言。
對我們來說,我們很高興能夠繼續使用 Rust 建立我們的 SaaS,不斷學習和成長,並看看這段旅程將帶我們走向何方。請繼續關注更深入的帖子,或在第一條評論中投票選出我們下一步應該做的事情。
如果您喜歡這篇文章並發現它有幫助,請不要忘記給我們的儲存庫一顆星!您的支持對我們來說意味著整個世界。
https://github.com/meteroid-oss/meteroid ⭐️ 流星星 ⭐️
下次見,祝您編碼愉快!
原文出處:https://dev.to/meteroid/5-lessons-learned-building-our-saas-with-rust-1doj