編者按:高可用架構(gòu)分享及傳播在架構(gòu)領(lǐng)域具有典型意義的文章,本文由唐劉在高可用架構(gòu)群分享。轉(zhuǎn)載請(qǐng)注明來(lái)自高可用架構(gòu)公眾號(hào)「 ArchNotes 」。
唐劉,PingCAP 首席架構(gòu)師,現(xiàn)致力于下一代分布式數(shù)據(jù)庫(kù) TiDB、分布式存儲(chǔ) TiKV 的開(kāi)發(fā)。開(kāi)源愛(ài)好者,Go、Rust 等語(yǔ)言愛(ài)好者和實(shí) 踐者。
大家好,我是 PingCAP 的唐劉,今天很榮幸跟大家來(lái)分享一下 Rust 相關(guān)知識(shí)以及我們團(tuán)隊(duì)使用 Rust 的實(shí)戰(zhàn)經(jīng)驗(yàn)。
為什么選擇Rust
首先來(lái)說(shuō)說(shuō)最近 Go 社區(qū)討論比較多的事情,Dropbox 將底層的類(lèi) S3 服務(wù)改用 Rust 來(lái)重寫(xiě)了,一下子讓 Rust 增加了很多知名度,誰(shuí)叫 Dropbox 這種公司通常都是技術(shù)架構(gòu)上面的風(fēng)向標(biāo)。大家普通關(guān)注的一個(gè)問(wèn)題:
為什么 Dropbox 不用 Go,反而用一門(mén)學(xué)習(xí)曲線比較陡峭的 Rust 來(lái)進(jìn)行開(kāi)發(fā)?
其實(shí)這個(gè)問(wèn)題也同樣適用于我們,一個(gè)自認(rèn)為 Go 經(jīng)驗(yàn)非常豐富的團(tuán)隊(duì),為什么不用 Go 反而要選擇 Rust?
介紹一下我們?cè)谧龅氖虑椤N覀儓F(tuán)隊(duì)從事的是下一代分布式數(shù)據(jù)庫(kù)的開(kāi)發(fā),也就是俗稱(chēng)的 NewSQL,整個(gè)理論基礎(chǔ)基于 Google 的 F1 以及 Spanner,所以自然我們的 NewSQL 也是分成了兩塊,一個(gè)是無(wú)狀態(tài)的 SQL 層,也就是現(xiàn)階段已經(jīng)開(kāi)源出來(lái)的 TiDB(http://dwz.cn/2XZkSm),另一個(gè)就是分布式 KV 層,我們叫做 TiKV,預(yù)計(jì)會(huì)在4月份開(kāi)源。
TiDB 是使用 Go 編寫(xiě)的,熟悉 Go 的同學(xué)都應(yīng)該知道,用 Go 來(lái)寫(xiě)分布式應(yīng)用那是非常的快捷方便的,而且我們團(tuán)隊(duì)的成員都有非常深厚的 Go 編程經(jīng)驗(yàn),但是在決定做 TiKV 的時(shí)候,我們沒(méi)有使用 Go,或者使用 C++,Java 這些更流行的靜態(tài)語(yǔ)言,反而是選擇了一門(mén)我們自身完全不熟悉的語(yǔ)言 Rust,Why?
先來(lái)說(shuō)說(shuō)使用 Go 會(huì)遇到的問(wèn)題:
GC,雖然 1.6 之后 Go 的 GC 已經(jīng)改善的很好了,而且我們覺(jué)得以后會(huì)越來(lái)越好,但是對(duì)于一個(gè)對(duì)性能要求非常高的分布式應(yīng)用,我們傾向選擇一門(mén)沒(méi)有 GC 的語(yǔ)言,這樣在內(nèi)存上面更加可控。
Cgo,TiKV 使用 RocksDB 作為其底層存儲(chǔ) engine,如果用 Go,我們需要使用 Cgo 來(lái)進(jìn)行 RocksDB的調(diào)用,而 Cgo 對(duì)性能還是有比較大的損耗的。
再來(lái)說(shuō)說(shuō)不選擇 C++ 或者 Java,C++ 主要是大規(guī)模開(kāi)發(fā)對(duì)整個(gè)團(tuán)隊(duì)要求非常高,而我們自己也覺(jué)得用 C++ 不可能 hold 住在短時(shí)間內(nèi)做出產(chǎn)品,所以自然放棄。而 Java,我們團(tuán)隊(duì)沒(méi)幾個(gè)人精通 Java,也直接放棄了。
所以,我們最終將目光落到了 Rust 上面,主要在于 Rust 有幾個(gè)很 cool 的 feature,就是 type safety,memory safety 以及 thread safety,這個(gè)后續(xù)詳細(xì)說(shuō)明。
Rust 的基礎(chǔ)入門(mén)
在 Rust 的官網(wǎng)上面,我們可以看到 Rust 的介紹,它是一門(mén)系統(tǒng)編程語(yǔ)言,性能好,同時(shí)在編譯階段就能幫你檢測(cè)出內(nèi)存,多線程數(shù)據(jù)訪問(wèn)等問(wèn)題,保證程序安全健壯。
也就是說(shuō),使用 Rust 寫(xiě)程序,如果能通過(guò)編譯,你就不用擔(dān)心類(lèi)似 C++ 里面很多 memory leak,segment faut,data race 的問(wèn)題了,但這一切都是有代價(jià)的。Rust 上手非常不容易,難度可以跟 C++ 媲美,如果是 Go,沒(méi)準(zhǔn)學(xué)習(xí)一個(gè)星期都能開(kāi)始給項(xiàng)目貢獻(xiàn)代碼,但換成 Rust,可能一個(gè)月都還在跟編譯器作斗爭(zhēng),研究為啥自己的代碼編譯不過(guò)。
因?yàn)槲也磺宄蠹矣卸嗌偃私佑|過(guò) Rust,所以這里列出來(lái)一些例子,讓大家對(duì) Rust 的語(yǔ)法有一個(gè)基本了解。
首先就是最通用的 Hello world:
(點(diǎn)擊圖片可全屏縮放圖片)
fn 是 Rust 的關(guān)鍵字,用來(lái)定義函數(shù),函數(shù)名字是 main, println! 是一個(gè) macro,在 rust style 里面,macro 都是在末尾使用“!”來(lái)表示的。
我們接下來(lái)定義幾個(gè)變量,下面就開(kāi)始顯示出 rust 跟其他語(yǔ)言的不一樣的地方了:
(點(diǎn)擊圖片可全屏縮放圖片)
上面我們聲明了一個(gè) u32 類(lèi)型的變量,但是沒(méi)有初始化,然后打印它的值,在一些語(yǔ)言里面,譬如 Go,會(huì)打印出 0,但是在 Rust 里面,這沒(méi)法編譯通過(guò),編譯器會(huì)提示:
(點(diǎn)擊圖片可全屏縮放圖片)
上面的錯(cuò)誤告訴我們使用了一個(gè)沒(méi)有初始化的變量,我們先來(lái)初始化一下,變成這樣:
(點(diǎn)擊圖片可全屏縮放圖片)
上面我們首先定義了一個(gè)初始值為 0 的變量,然后改成 10 打印,編譯,發(fā)現(xiàn)如下錯(cuò)誤:
(點(diǎn)擊圖片可全屏縮放圖片)
這次編譯器告訴我們對(duì)一個(gè) immutable 的變量進(jìn)行了更改。在 Rust 里面,變量是分為 immutable 和 mutable 的,我們必須顯示地定義這個(gè)變量到底能不能改動(dòng)。我們使用 mut 關(guān)鍵字來(lái)告訴 Rust 這個(gè)變量是 mutable 的,如下:
(點(diǎn)擊圖片可全屏縮放圖片)
這下就能正常編譯了。
通過(guò)上面一些簡(jiǎn)單的例子,大家應(yīng)該對(duì) Rust 有了一個(gè)初步的印象,更加詳細(xì)的了解可以去參考官網(wǎng)。
讓 Rust 開(kāi)發(fā)變 easy 的關(guān)鍵技術(shù)點(diǎn)
前面在為什么選擇 Rust 里面,我提到了因?yàn)?Rust 有幾個(gè)很 cool 的特性,能讓我們寫(xiě)出不容易出錯(cuò)的并發(fā)程序。而且我認(rèn)為,只要理解掌握了這個(gè)關(guān)鍵點(diǎn),用 Rust 進(jìn)行程序開(kāi)發(fā)就很 easy了。
Type safety
Rust 是一門(mén)嚴(yán)格要求類(lèi)型安全的語(yǔ)言,在 C/C++ 的世界里面,我們可以無(wú)拘無(wú)束的進(jìn)行類(lèi)型轉(zhuǎn)換,譬如:
(點(diǎn)擊圖片可全屏縮放圖片)
這種在 C/C++ 里面很常見(jiàn)的處理方式,在 Rust 里面是不被允許的,譬如:
我們會(huì)得到如下編譯錯(cuò)誤:
如果強(qiáng)制做這種內(nèi)存轉(zhuǎn)換,我們可以使用 unsafe,顯示的告訴 Rust 這么做是不安全的,但是你別管了:
(點(diǎn)擊圖片可全屏縮放圖片)
通常情況下面,Rust 是不允許寫(xiě)上面這樣的代碼的,所以它將 unsafe 的選擇權(quán)顯示的交給了程序員,而我們通過(guò) unsafe 也能清晰的知道哪里是不安全的代碼,需要注意的。
Memory safety
下面進(jìn)入 Rust 最令人抓狂的一個(gè)關(guān)鍵點(diǎn)了。Rust 是能夠保證程序的 memory safety 的,那么是如何保證的呢?首先我們需要了解的是 Rust 里面 ownership 以及 move 的概念。
Ownership + move
在Rust里面,任何資源只可能有一個(gè) ownership,譬如一個(gè)最簡(jiǎn)單的例子:
這里我們使用 let 將一個(gè) vector 給綁定到 a 這個(gè)變量上面了,我們就可以認(rèn)為 a 現(xiàn)在是這個(gè) vector 的 ownership。然后對(duì)于這個(gè) vector 的 resouce,同一個(gè)時(shí)間只允許一個(gè) ownership。我們來(lái)看下面這個(gè)代碼:
(點(diǎn)擊圖片可全屏縮放圖片)
在上面的例子中,我們將 a 賦值給了 b,同時(shí)繼續(xù)打印 a[0] 的值,這個(gè)在大多數(shù)語(yǔ)言都沒(méi)有任何問(wèn)題的操作,在 Rust 里面,是會(huì)報(bào)錯(cuò)的,如下:
為什么呢?在打印 a[0] 之前,我們進(jìn)行了 let b = a 這樣的操作,這個(gè)操作,在 Rust 里面叫做 move,含義就是把 a 對(duì) vector 的 ownership 移交給了 b,a 放棄了對(duì) vector 的 ownership。因?yàn)?a 已經(jīng)對(duì)這個(gè) vector 沒(méi)有 ownership 了,自然就不能訪問(wèn)相關(guān)的數(shù)據(jù)了。
ownership 和 move 的概念應(yīng)該算是學(xué)習(xí) Rust 第一個(gè)坑,我們也很容易寫(xiě)出如下代碼:
(點(diǎn)擊圖片可全屏縮放圖片)
這個(gè)代碼照樣是編譯不過(guò)的,因?yàn)?do_vec 這個(gè)函數(shù),a 已經(jīng)將對(duì) vector 的 ownership 給 move 了。
所以通常我們只要看到 let b = a 這樣的代碼,就表明 a move 掉了 ownership 了,但有一個(gè)例外,如果a的類(lèi)型實(shí)現(xiàn)了copy trait,let b =a 就不是move,而是 copy 了,下面的代碼是能正常編譯的:
上面的代碼里面,let b = a,a 并沒(méi)有 move,而是將自己的數(shù)據(jù) copy 了一份給 b 使用。通常基本的數(shù)據(jù)類(lèi)型都是實(shí)現(xiàn)了 copy trait,當(dāng)然我們也可以將我們自定義的類(lèi)型實(shí)現(xiàn) copy,只是就需要權(quán)衡下 copy 的性能問(wèn)題了。
Borrow
前面我們舉了一個(gè) do_vec 的例子,如果真的需要在調(diào)用這個(gè)函數(shù)之后繼續(xù)使用這個(gè) vector,怎么辦呢?
這里我們就開(kāi)始接觸到了第二個(gè)坑,borrow。上面說(shuō)了,move 是我給你了 ownership,而 borrow 則是我借給你這個(gè) resource 的使用權(quán),到時(shí)候還要還給我:
(點(diǎn)擊圖片可全屏縮放圖片)
上面的例子中,我們?cè)趨?shù)里面使用了 & 來(lái)表示 borrow,do_vec 這個(gè)函數(shù)只是借用了 a,然后函數(shù)結(jié)束之后,還了回來(lái),這樣后續(xù)我們就能繼續(xù)使用 a 了。
既然 borrow,當(dāng)然就能用了,于是我們就可能寫(xiě)這樣的代碼:
然后 Rust 又華麗麗的報(bào)錯(cuò)了,輸出:
因?yàn)槲覀兊?borrow 只是 immutable 的 borrow,并不能改數(shù)據(jù)。在前面我們也提到過(guò),如果要對(duì)一個(gè)變量進(jìn)行修改,必須顯示的用 mut 進(jìn)行聲明,borrow 也是一樣,如果要對(duì)一個(gè) borrow 的東西顯示修改,必須使用 mutable borrow,也就是這樣:
(點(diǎn)擊圖片可全屏縮放圖片)
borrow 還有 scope 的概念,有時(shí)候我們寫(xiě)這樣的代碼:
(點(diǎn)擊圖片可全屏縮放圖片)
發(fā)現(xiàn)編譯器又報(bào)錯(cuò)了,輸出:
因?yàn)槲覀冎坝?y 來(lái)對(duì) x 進(jìn)行了mutable 的 borrow,但是還沒(méi)還回去,所以后面 immutable的 borrow 就不允許。這個(gè)我們可以通過(guò) scope 來(lái)顯示的控制 mutable 的生存周期:
(點(diǎn)擊圖片可全屏縮放圖片)
這樣在 printIn! 進(jìn)行 immutable 的 borrow 的時(shí)候,y 這個(gè)mutable 的 borrow 已經(jīng)還回來(lái)了。
一個(gè)變量,可以同時(shí)進(jìn)行多個(gè) immutable 的 borrow,但只允許一個(gè) mutable 的 borrow,這個(gè)其實(shí)跟 read-write lock 很相似,同時(shí)允許多個(gè)讀鎖,但一次只允許一個(gè)寫(xiě)鎖。
Lifetime
在 C++ 里面,相信大家對(duì)野指針都印象深刻,有時(shí)候,我們會(huì)引用了一個(gè)已經(jīng)被 delete 的對(duì)象,然后再次使用的時(shí)候就 panic 了。在 Rust 里面,是通過(guò) lifetime 來(lái)解決這個(gè)問(wèn)題的,不過(guò)引入了 lifetime 之后,代碼看起來(lái)更丑了。一個(gè)簡(jiǎn)單的例子:
(點(diǎn)擊圖片可全屏縮放圖片)
我們定義了一個(gè) struct,里面的 field b 是對(duì)外面一個(gè) u32 變量的引用,而這個(gè)引用的 lifetime是 ’a。使用 lifetime 能保證b 引用的 u32 數(shù)據(jù)的生存周期一定是大于 A 的。
但是在上面的例子中,我們?cè)谝粋€(gè) scope 里面,讓 b 引用了 c,但是 c 在 scope 結(jié)束之后就沒(méi)了,這時(shí)候 b 就是一個(gè)無(wú)效的引用了,所以 Rust 會(huì)編譯報(bào)錯(cuò):
Thread safety
前面我們提到,Rust 使用 move,borrow 以及 lifetime 這些機(jī)制來(lái)保證 memory safety,雖然這幾個(gè)概念不怎么好理解,而且很容易大家寫(xiě)代碼的時(shí)候就會(huì)陷入與編譯器的斗爭(zhēng),但是我個(gè)人覺(jué)得只要理解了這些概念,寫(xiě) Rust 就不是問(wèn)題了。
好了,說(shuō)完了 memory safety,我們馬上進(jìn)入 thread safety了。大家都知道,多線程的程序很難寫(xiě),有且稍微不注意,就會(huì)出現(xiàn) data race 等情況,導(dǎo)致數(shù)據(jù)錯(cuò)誤,而且偏偏這樣的 bug 還能難查出來(lái)。
譬如在 Go 里面,我們可以這樣:
(點(diǎn)擊圖片可全屏縮放圖片)
上面的例子很極端,大家應(yīng)該也不會(huì)這面寫(xiě)代碼,但實(shí)際中,我們?nèi)匀豢赡軙?huì)面臨 data race 的問(wèn)題。雖然 Go 可以通過(guò) --race 打開(kāi) data race 的檢查,可通常只會(huì)用于 test,而不會(huì)在線上使用。
而 Rust 則是在源頭上面完全讓大家沒(méi)法寫(xiě)出 data race 的代碼。首先我們先來(lái)了解 Rust 兩個(gè)針對(duì)并發(fā)的 trait,Send和 Sync:
-
Send
當(dāng)一個(gè)類(lèi)型實(shí)現(xiàn)了 Send,我們就可以認(rèn)為這個(gè)類(lèi)型可以安全的從一個(gè)線程 move 給另一個(gè)線程去使用。
-
Sync
當(dāng)一個(gè)類(lèi)型實(shí)現(xiàn)了 Sync,我們就可以認(rèn)為這個(gè)類(lèi)型可以在多線程里面通過(guò) shared reference(也就是 Arc)安全的使用。
上面的概念看起來(lái)比較困惑,簡(jiǎn)單一點(diǎn)就是如果一個(gè)類(lèi)型實(shí)現(xiàn)了Send + Sync,那么這個(gè)就能在多線程下面安全的使用。
先來(lái)看一個(gè)簡(jiǎn)單的例子:
(點(diǎn)擊圖片可全屏縮放圖片)
上面就是一個(gè)典型的多線程 data race 的問(wèn)題了,Rust 是編譯不過(guò)的,報(bào)錯(cuò):
前面說(shuō)了,我們可以通過(guò) Arc 來(lái)保證自己的類(lèi)型在多線程下面安全使用,我們加上 Arc。
(點(diǎn)擊圖片可全屏縮放圖片)
現(xiàn)在我們的 vector 能多線程訪問(wèn)了,但是仍然出錯(cuò):
(點(diǎn)擊圖片可全屏縮放圖片)
因?yàn)槲覀儾还馐且嗑€程 read,而且還需要多線程去 write,自然 Rust 不允許,所以我們需要顯示的進(jìn)行加鎖保護(hù),如下:
(點(diǎn)擊圖片可全屏縮放圖片)
所以,如果我們要對(duì)一個(gè)數(shù)據(jù)進(jìn)行安全的多線程使用,最通用的做法就是使用 Arc<Mutex<T>> 或者 Arc<RwLock<T>>進(jìn)行封裝使用。
當(dāng)然,除了 Arc + Lock 之外,Rust 還提供了 channel 機(jī)制方便進(jìn)行線程之間的數(shù)據(jù)通訊,channel 類(lèi)似于 Go 的 channel,一端 send,一端 recv,這里就不詳細(xì)說(shuō)明了。
這里在提一點(diǎn),因?yàn)樵?Rust 里面,對(duì)于多線程使用的數(shù)據(jù),我們必須明確的進(jìn)行 lock 保護(hù),這個(gè)編程風(fēng)格直接帶到了后來(lái)我們寫(xiě) Go 。在 Go 里面,我以前寫(xiě) lock,通常會(huì)這樣:
(點(diǎn)擊圖片可全屏縮放圖片)
使用一個(gè) mutex 變量 m 來(lái)對(duì)數(shù)據(jù) v1,v2 進(jìn)行多線程保護(hù),但這種寫(xiě)法其實(shí)很容易就容易忘記這個(gè) lock 到底要保護(hù)哪些數(shù)據(jù)。自從受 Rust 影響了之后,我就喜歡寫(xiě)成這樣了:
(點(diǎn)擊圖片可全屏縮放圖片)
上面就是顯示的將 lock 以及需要保護(hù)的數(shù)據(jù)放到一個(gè) struct 里面,大家一看代碼就知道這個(gè) lock 要保護(hù)哪些數(shù)據(jù)了。
Rust 開(kāi)發(fā)實(shí)戰(zhàn)經(jīng)驗(yàn)
前面說(shuō)了是 Rust 的一些基本 feature,這里開(kāi)始說(shuō)下我們項(xiàng)目中用 Rust 的相關(guān)經(jīng)驗(yàn)。
Cargo
如果要用 Rust進(jìn)行項(xiàng)目開(kāi)發(fā),首先就需要了解的就是 Cargo,Cargo 是 Rust 一個(gè)構(gòu)建以及包管理工具,現(xiàn)在應(yīng)該已經(jīng)成了 Rust 開(kāi)發(fā)項(xiàng)目的規(guī)范了。Cargo 的使用還是很簡(jiǎn)單的,大家可以直接去看瀏覽官網(wǎng) (https://crates.io/)。
quick_error!
最開(kāi)始,我們?cè)趯?xiě) C 程序的時(shí)候,通過(guò)定義不同的 int 返回值來(lái)表示一個(gè)函數(shù)是不是有 error。然后到了 C++,我們就可以通過(guò) expection 來(lái)處理 error 了。不過(guò)到底采用哪種標(biāo)準(zhǔn),都是沒(méi)有定論的。
到了 Go,直接約定了函數(shù)最后一個(gè)返回參數(shù)是 error,官方還有一篇 blog 來(lái)介紹了 Go 的error handling (http://blog.golang.org/error-handling-and-go)。
在 Rust 里面,error 也有相應(yīng)的處理規(guī)范,就是 Result,Result 是一個(gè) enum,定義是這樣的:
(點(diǎn)擊圖片可全屏縮放圖片)
也就是說(shuō),我們的函數(shù)都可以返回 Result,外面去判斷,如果是 Ok,那么就是正確的處理,如果是 Err 則是錯(cuò)誤了。
這里有篇 error handling 的詳細(xì)說(shuō)明 (https://doc.rust-lang.org/book/error-handling.html)。
通常大家都會(huì)按照上面的規(guī)范來(lái)處理 error,也就是定義自己的 error,實(shí)現(xiàn)其他 error 轉(zhuǎn)成自己 error 的 from 函數(shù),然后在使用 try! 在代碼里面簡(jiǎn)化 error 的處理。
但是很快我們就發(fā)現(xiàn)了一個(gè)很?chē)?yán)重的問(wèn)題,定義自己的 error,以及將其他 error 轉(zhuǎn)成我們對(duì)應(yīng)的 error 是一件非常冗余復(fù)雜的事情,所以我們使用 quick_error!(http://dwz.cn/2XZpNo)來(lái)簡(jiǎn)化整個(gè)流程。
Clippy
當(dāng)我第一次寫(xiě) Go 代碼的時(shí)候,我對(duì) Go 的 fmt 印象特別深刻,以后再也不用擔(dān)心編碼風(fēng)格的爭(zhēng)論了,Rust 也有相關(guān)的 rust fmt,但是更令我驚奇的是 Rust 的 Clippy 這個(gè)工具。Clippy 已經(jīng)不是糾結(jié)于編碼風(fēng)格了,而是直接告訴你代碼要這么寫(xiě),那么寫(xiě)不對(duì)。
一個(gè)很簡(jiǎn)單的例子:
(點(diǎn)擊圖片可全屏縮放圖片)
這個(gè)代碼是能編譯通過(guò)的,但是如果我們打開(kāi)了 Clippy 的支持,直接會(huì)提示:
(點(diǎn)擊圖片可全屏縮放圖片)
也就是告訴你,別用 to_string,用 to_owned。
我們都知道,要開(kāi)發(fā)一個(gè)高性能的網(wǎng)絡(luò)服務(wù),通常的選擇就是 epoll 這種基于事件觸發(fā)的網(wǎng)絡(luò)模型,在 Rust,現(xiàn)階段成熟的庫(kù)就是 MIO。MIO是一個(gè)異步 IO 庫(kù),對(duì)不同的操作系統(tǒng)提供了統(tǒng)一抽象支持,譬如 Linux 下面就是 epoll,UNIX 下面就是 kqueue,Windows 下是 IOCP。不過(guò) MIO 為了統(tǒng)一擴(kuò)平臺(tái),在一些實(shí)現(xiàn)上面做了妥協(xié)。
MIO
譬如在 Linux 下面,系統(tǒng)直接提供了 event fd 的支持,但 MIO 為了兼容 UNIX,使用了傳統(tǒng)的 pipe 而不是 event fd 來(lái)進(jìn)行 event loop 的 awake 處理。
這里在單獨(dú)說(shuō)下 MIO 提供的另一種線程通訊 channel 機(jī)制,雖然我們可以用 Rust 自己的 thread channel 來(lái)進(jìn)行線程通訊,但如果引入 MIO,我更喜歡用 MIO 自己的 channel,主要原因是采用的 lock free queue,性能更好,但有 queue size 限制問(wèn)題,發(fā)送太頻繁但接受端沒(méi)處理過(guò)來(lái),就會(huì)造成發(fā)送失敗的問(wèn)題了。
Rust 語(yǔ)言的美中不足
我們團(tuán)隊(duì)已經(jīng)使用 Rust進(jìn)行了幾個(gè)月的開(kāi)發(fā),當(dāng)然也遇到了一些很不爽的地方。
首先就是庫(kù)的不完善,相比于 Go,Rust 的庫(kù)真的太不完備了。我覺(jué)得現(xiàn)階段 Rust 仍然沒(méi)有大規(guī)模的應(yīng)用,lib 不完備占了很大一個(gè)原因。
TiKV 是一個(gè)服務(wù)器程序,自然就會(huì)涉及到網(wǎng)絡(luò)編程,官方現(xiàn)階段的 net mod 里面,只有 block socket 的支持,這個(gè)完全沒(méi)法用來(lái)開(kāi)發(fā)高性能網(wǎng)絡(luò)程序的。幸好有 MIO,但光有 MIO 是遠(yuǎn)遠(yuǎn)不夠,在 Go 里面,我們很方便的使用 gRPC 來(lái)進(jìn)行 RPC 的編寫(xiě),但是在 Rust 里面,我覺(jué)得還得在等很長(zhǎng)一段時(shí)間,看能不能有開(kāi)源的實(shí)現(xiàn)。
再來(lái)就是在 Mac OS X 下面,panic 出來(lái)的堆棧完全沒(méi)法看,沒(méi)有 file 和 line number 的信息,根本沒(méi)法方便的查 bug。
當(dāng)然,畢竟 Rust 是一門(mén)比較新的語(yǔ)言,還在不斷的完善發(fā)展,我們還是很有信心它能越來(lái)越好的。
Q & A
1. Go 的 Cgo 在效率上面與 Rust FFI 有啥區(qū)別?
唐劉:我自己寫(xiě)過(guò)一個(gè)簡(jiǎn)單的測(cè)試,就是都循環(huán)調(diào)用 Snappy 的 MaxCompressedLength 這個(gè)函數(shù) 10000 次,發(fā)現(xiàn) Rust 的 FFI 比 Go 的 Cgo 要快上一個(gè)數(shù)量級(jí),雖然這么測(cè)試不怎么精確,但至少證明了 Rust 的 FFI 性能更好。
2. 在官方的介紹中,Rust 的首選平臺(tái)是 Windows, 那是否可以生成 dll. 你們的 IDE 用的是什么?
唐劉:我沒(méi)用過(guò) Windows,所以也不知道怎么生成 dll,開(kāi)發(fā) Rust 的 ide 也就是常用的那幾個(gè),譬如 Vim, Emacs, Sublime 這些,反正都有 Rust 的插件支持。
3. Rust 調(diào)用 C 的庫(kù)方便嗎?
唐劉:Rust 通過(guò) FFI 調(diào)用 C,很方便的,這里有相關(guān)文檔 (https://doc.rust-lang.org/book/ffi.html),但畢竟這涉及到跨語(yǔ)言,代碼寫(xiě)起來(lái)就不怎么好看了。而且 FFI 需要 unsafe保護(hù),所以通常我們會(huì)在外面在 wrap 一層 Rust 的函數(shù)。
4. Rust 性能指標(biāo)如何?
唐劉:Rust 性能這個(gè)不怎么好衡量,因?yàn)槲覀冎皇窃谝恍┨囟ǖ沫h(huán)境下面做過(guò)跟 Go 的對(duì)比,譬如 Cgo vs FFI 的測(cè)試,這方面性能是比 Go 要好的。另外,Rust 是一門(mén)靜態(tài)語(yǔ)言,沒(méi)有 GC,所以我覺(jué)得他的性能不會(huì)是問(wèn)題,不然 Dropbox 也不可能將類(lèi) S3 的應(yīng)用用 Rust 寫(xiě)了。
5. Rust 周邊生態(tài)如何? 如跟常用的 DBSQL/MQ 等第三方系統(tǒng)的 binding?
唐劉:Rust 的生態(tài)只能呵呵來(lái)形容了,跟 Go,Java 這些的沒(méi)法比。雖然有常用的 MySQL 等的 binding,但我沒(méi)用過(guò)。主要原因在于官方的網(wǎng)絡(luò) IO 是同步的,所以必須借助多線程來(lái)進(jìn)行這些處理,而用 MIO 這種異步模式,大家也知道寫(xiě)出來(lái)的代碼邏輯切割很厲害。所以通常我們也不會(huì)拿 Rust 來(lái)做這些復(fù)雜的業(yè)務(wù)系統(tǒng)開(kāi)發(fā),感覺(jué)還是 Go 更合適。
與C,C++不同,java規(guī)范中沒(méi)有“依賴(lài)具體實(shí)現(xiàn)”的地方,基本數(shù)據(jù)類(lèi)型大小以及有關(guān)算法都做了明確的說(shuō)明。例如,Java中int類(lèi)型永遠(yuǎn)為32位整數(shù),而C/C++中int的類(lèi)型大小有可能是16位,32位,也可能與編譯器的設(shè)置有關(guān)。在java中,數(shù)據(jù)類(lèi)型具有固定的大小,從而具有很好的可移植性。
本文轉(zhuǎn)載自 高可用架構(gòu)公眾號(hào)「 ArchNotes 」文章
原文:《Rust語(yǔ)言入門(mén)、關(guān)鍵技術(shù)與實(shí)戰(zhàn)經(jīng)驗(yàn)》
作者: 唐劉
鏈接:https://mp.weixin.qq.com/s/Wlz1G-eYgA9uEgO_63lK6Q
參考
(1)Rust 語(yǔ)言中文版-極客學(xué)院教程
http://wiki.jikexueyuan.com/project/rust/
(2)RUST官方網(wǎng)站
https://www.rust-lang.org/zh-CN/
https://www.rust-lang.org/zh-CN/learn
(3)標(biāo)準(zhǔn)文檔庫(kù)
https://doc.rust-lang.org/stable/std/index.html
補(bǔ)充
(1)GO和RUST的區(qū)別?
- go定位是云計(jì)算時(shí)代的C語(yǔ)言,主要是面向云計(jì)算領(lǐng)域的。而rust是系統(tǒng)級(jí)語(yǔ)言,更Low level一些
- 都是強(qiáng)類(lèi)型語(yǔ)言,go有GC(Garbage Collection,垃圾回收)而rust沒(méi)有;
3)go目前沒(méi)有模板,rust有,換言之,rust的編程范式更豐富一些 - go簡(jiǎn)單,上手快,rust因?yàn)橛凶兞康膌ifetime概念和內(nèi)存的borrow概念,上手難一些;
5)安全性。Rust語(yǔ)法引入所有權(quán)和生命期概念,在編譯期就能檢查出一部分內(nèi)存管理錯(cuò)誤,這是rust的一個(gè)殺手锏的特性。