一個 C 系程式設計師的 Rust 初體驗

語言: CN / TW / HK

引言:在工作裡使用 Rust 已經有兩個多月的時間了,談談我做為一名多年的 C 系(C、C++)程式設計師,對 Rust 的初體驗。

一個C系程式設計師的Rust初體驗

最近由於工作的原因,使用上了 Rust 語言,在此之前我有多年的 C、C++ 編碼經驗(以下將C、C++ 簡稱 C 系語言)。

使用 C 系語言編碼時,最經常面對的問題就是記憶體問題,諸如:

  • 野指標(Wild Pointe):使用了不可知的指標變數,如已經被釋放、未初始化、隨機,等等。

  • 記憶體地址由於訪問越界等原因被覆蓋(overflow),這不但是可能出錯的問題,還有可能成為程式的記憶體漏洞被利用。

  • 記憶體分配後未回收。

連 Chrome 的報告都指出,Chrome 中大約 70% 的安全漏洞都是記憶體問題。見:Memory safety[1]。

C 系語言發展到今天,已經有不少可以用於記憶體問題檢測的利器了,其中最好用的莫過於 AddressSanitizer[2],它的原理是在編譯時給程式加上一些資訊,一旦發生記憶體越界訪問、野指標等錯誤都會自動檢測出來。

但是即便有這些工具,記憶體問題也不好解決,其核心的原因在於:這些問題絕大部分都是執行時(Runtime)問題,即要在程式跑到特定場景的時候才會暴露出來,諸如上面提到的 AddressSanitizer 就是這樣。

都知道解決問題的第一步是能復現問題,而如果一個問題是執行時問題,這就意味著:復現問題可能會是一件很麻煩的事情,有時候還可能到生產環境去復現。

以我之前經歷的一個 Bug 來看這類工作的複雜度,見 線上儲存服務崩潰問題分析記錄 - codedump 的網路日誌[3],這是一個很典型的發生在生產環境上由於記憶體錯誤導致的崩潰問題:

  • 不好復現,因為跟特定的請求相關,還跟執行緒的排程有關;

  • 本質是由於使用了被釋放的記憶體導致的錯誤。

這個線上問題,記得當時花了一週時間來複現問題解決。

換言之,如果一個問題要等到執行時才能發現,那麼可以預見的是:一旦出現問題,要復現問題可能要花費大量的精力,以及需要很多經驗才行。如果一個問題還是在特定場景,或者使用者現場才出現的,那就更麻煩了,C 系程式設計師以往一般都是這樣來儲存“現場”:

  • 出現崩潰的時候儲存 core 檔案來檢視呼叫堆疊、變數等資訊。

  • 發明了各種複製流量重放的工具,比如 tcpcopy[4] 等。

總而言之,執行時問題一旦出現是很麻煩的,而解決這類問題的時間是難以預期的。

Rust 給這類記憶體問題的解決提供了另一個解決思路:

  • 一個記憶體地址同時只能被一個變數使用。

  • 不能使用未初始化的變數。

  • ...

簡而言之,凡是可能出現記憶體錯誤的地方,都在語言的語法層面給予禁止,換來的就是更多的編譯時間,因為要做這麼多檢查嘛,而需要更多的編譯時間反過來就需要更好的硬體。我想這也是 Rust 到了最近幾年才開始慢慢流行開來的原因之一,畢竟即便是現在,一些大型的 Rust 專案普通的機器編譯起來也還是很耗時。

“編譯時間(compile time)”是一個可以預期的固定時間,能通過增加硬體效能(比如買更好的機器來寫 Rust)來解決;而“執行時問題”一旦出現,查詢起來的時間、精力、場景(比如出現在使用者現場、幾百萬次才能重現一次等)不確定性可就很高了。

兩者權衡,我選擇解決“編譯時間”問題。而且,在我意識到有這樣的工具能夠在編譯期解決大部分記憶體問題時,反過來再看使用 C 系語言的專案,幾乎可以預期的是:只要程式碼和複雜度上了一定規模,那麼這類專案都要花上相當的一段時間才能穩定下來。原因在於:類似記憶體問題這樣的執行時問題,是需要場景去積累,才能暴露出來的,而場景的積累,就需要很多的小白鼠和執行時間了。

總結一下我的觀點:

  • C 系語言最多的問題就是各類記憶體問題,而這些問題大多是執行時問題。即便現在已經有了各種工具,解決其執行時問題也很困難。

  • Rust 解決這類問題的思路,是在語法層面禁止一切可能出現記憶體問題的操作,換來的代價就是更多的編譯時間。

  • 解決可預期的“編譯時間”和難預期的“執行時問題”,我選擇前者。

番外篇

rr

rr: lightweight recording&deterministic debugging[5] 也是出自 Mozilla 的另一款除錯 C 系程式的利器,rr 是 Record and Replay 的簡稱,目的還是為了解決各種執行時問題,由於執行時問題中存在著各種不確定的因素,包括:

  • 變數值。

  • 程序、執行緒環境,比如不同的執行緒排程順序可能導致了不同的結果。

  • 輸入不同的資料,能得到不同的結果。

於是,rr 要解決的核心問題,就是讓一個程式在執行時有一個固定的環境,它可以抓取程式執行的環境儲存下來。這樣在出現問題之後,就能使用它可以記錄下來程式執行時的環境,不停的重放來除錯解決問題。

但是,即便是這樣,rr 可能更適合於明確知道問題的情況下去抓取環境,不可能在線上直接開啟這個工具。所以又回到前面的結論了:除錯執行時問題可能面對的困難,包括場景、時間、使用者現場等等不確定因素。

rr 和 Rust 一樣,都出自 Mozilla,我想不是偶然的。Mozilla 和 chrome 等一樣,都是使用 C++ 編碼的超大型專案,而這裡一定遇到了各種執行時問題,不止於記憶體問題,所以才要使用各種工具來輔助解決這類問題。

吃上硬體升級的紅利了嗎?

前面提到過,Rust 目前較大的問題是編譯時間過長,這可能是導致它最近幾年才開始逐漸流行開來的原因。其實反過來說,在硬體升級之後,應該能儘量利用上硬體,在編譯期儘量多檢查出錯誤來,減少執行時發現問題的數量。這樣,才能吃上硬體升級的紅利,利用硬體來減少自己的犯錯。

一方面硬體升級給了程式語言能施展更大、更快的的“舞臺”,隨著舞臺的更新,就會有更新、更好的工具出現;另一方面做為從業者,也應該與時俱進,多學習跟進這些工具的演進。

我看到有一些人,強調自己多早就已經用 C 語言寫程式碼了,但是查記憶體問題還在用慢的不行的 Valgrind,沒聽過更不知道怎麼用 Address Sanitizer。想說如果技能點都已經不更新了,強調多早學的有什麼意義?好比 1950 年就會打算盤,有意義嗎?強調多早就用 C 語言類似的言論,在我看來就是“倚老賣老”,但是技術日新月異的領域,賣老的意義不大。

《Rust for Rustaceans》

推薦 Rust for Rustaceans[6] 作者 Jon Gjengset 的油管頻道:https://www.youtube.com/c/JonGjengset/playlists

有很多很有深度的 Rust 分享,比如:

  • Implementing TCP in Rust (part 1) - YouTube[7]

  • Porting Java's ConcurrentHashMap to Rust (part 1) - YouTube[8]

參考資料

[1]Memory safety: https://www.chromium.org/Home/chromium-security/memory-safety/

[2]AddressSanitizer: https://en.wikipedia.org/wiki/AddressSanitizer

[3]線上儲存服務崩潰問題分析記錄 - codedump的網路日誌: https://www.codedump.info/post/20190413-problem-fix/

[4]tcpcopy: https://github.com/session-replay-tools/tcpcopy

[5]rr: lightweight recording & deterministic debugging: https://rr-project.org/

[6]Rust for Rustaceans: https://rust-for-rustaceans.com/

[7]Implementing TCP in Rust (part 1) - YouTube: https://www.youtube.com/watch?v=bzja9fQWzdA&list=PLqbS7AVVErFivDY3iKAQk3_VAm8SXwt1X

[8]Porting Java's ConcurrentHashMap to Rust (part 1) - YouTube: https://www.youtube.com/watch?v=yQFWmGaFBjk&list=PLqbS7AVVErFj824-6QgnK_Za1187rNfnl

關於我們

Databend 是一款開源、彈性、低成本,基於物件儲存也可以做實時分析的新式數倉。期待您的關注,一起探索雲原生數倉解決方案,打造新一代開源 Data Cloud。

  • Databend 文件:https://databend.rs/

  • Twitter:https://twitter.com/Datafuse_Labs

  • Slack:https://datafusecloud.slack.com/

  • Wechat:Databend

  • GitHub :https://github.com/datafuselabs/databend

文章始發於公眾號:Databend