為什麼Discord從Go切換到Rust


為什麼Discord從Go切換到Rust

Rust正在成為各種領域的一流語言。 在Discord,我們已經看到Rust在客戶端和服務器端都取得了成功。 例如,我們在客戶端將其用於Go Live的視頻編碼管道,在服務器端將其用於Elixir NIF。 最近,我們通過將服務的實現從Go切換到Rust來極大地提高了服務的性能。 這篇文章解釋了為什麼重新實現服務對我們有意義,它是如何完成的,以及由此帶來的性能改進。

讀取狀態服務

Discord是一家專注於產品的公司,因此我們將從一些產品上下文入手。 我們從Go切換到Rust的服務是"讀取狀態"服務。 其唯一目的是跟蹤您已閱讀的頻道和消息。 每次您連接到Discord,每次發送消息和每次閱讀消息時,都會訪問"讀取狀態"。 簡而言之,"讀取狀態"正處於熱銷中。 我們要確保Discord始終都感覺超級敏捷,因此我們需要確保Read State快速。

通過Go實施,Read States服務不支持其產品要求。 在大多數情況下,速度很快,但是每隔幾分鐘,我們就會看到大量的延遲峰值,這不利於用戶體驗。 經過調查,我們確定峰值是由於Go的核心功能:其內存模型和垃圾收集器(GC)引起的。

為什麼Go無法達到我們的績效目標

為了說明Go無法達到我們的性能目標的原因,我們首先需要討論服務的數據結構,規模,訪問模式和體系結構。

我們用來存儲讀取狀態信息的數據結構通常稱為"讀取狀態"。 Discord擁有數十億個讀取狀態。 每個用戶每個通道有一個讀取狀態。 每個讀取狀態都有幾個需要自動更新的計數器,通常需要將其重置為0。例如,計數器之一是一個通道中有多少個提及。

為了獲得快速的原子計數器更新,每個讀取狀態服務器都具有讀取狀態的最近最少使用(LRU)緩存。 每個緩存中有數百萬個用戶。 每個緩存中有數千萬個讀取狀態。 每秒有數十萬個緩存更新。

為了保持持久性,我們使用Cassandra數據庫集群支持緩存。 在驅逐緩存鍵時,我們將您的讀取狀態提交到數據庫。 每當讀取狀態被更新時,我們還將在未來30秒內計劃數據庫提交。 每秒有數萬次數據庫寫入。

在下面的圖片中,您可以看到Go服務的峰值採樣時間範圍的響應時間和系統cpu。¹您可能會注意到,大約每2分鐘就會有延遲和CPU峰值。

為什麼Discord從Go切換到Rust

那麼為什麼要2分鐘峰值呢?

在Go中,在逐出緩存鍵時,不會立即釋放內存。 取而代之的是,垃圾收集器會如此頻繁地運行以查找沒有引用的任何內存,然後將其釋放。 換句話說,內存不再閒置後立即釋放,而是掛了一段時間,直到垃圾回收器可以確定它是否真正閒置為止。 在垃圾回收期間,Go必須做很多工作來確定哪些內存可用,這可能會使程序變慢。

這些延遲峰值肯定聞起來像垃圾回收性能影響,但是我們已經非常高效地編寫了Go代碼,並且分配很少。 我們並沒有創造很多垃圾。

深入研究Go的源代碼後,我們瞭解到Go將強制至少每2分鐘運行一次垃圾收集。 換句話說,如果垃圾收集2分鐘沒有運行,無論堆增長如何,go仍將強制執行垃圾收集。

我們認為我們可以調整垃圾收集器的發生頻率,以防止出現大的峰值,因此我們在服務上實現了一個端點,以動態更改垃圾收集器的GC百分比。 不幸的是,無論我們如何配置GC百分比,都沒有改變。 怎麼可能, 事實證明,這是因為我們分配內存的速度不夠快,無法迫使垃圾回收更頻繁地發生。

我們不斷進行挖掘,並瞭解到峰值之所以巨大,並不是因為有大量隨時可用的內存,而是因為垃圾收集器需要掃描整個LRU緩存以確定內存是否真正沒有引用。 因此,我們認為較小的LRU緩存會更快,因為垃圾收集器的掃描量更少。 因此,我們在服務中添加了另一項設置以更改LRU緩存的大小,並更改了體系結構以使每個服務器具有多個分區的LRU緩存。

沒錯 LRU緩存較小時,垃圾回收會導致較小的峰值。

不幸的是,降低LRU緩存的權衡取捨導致了第99個延遲時間的增加。 這是因為如果緩存較小,則用戶的讀取狀態位於緩存中的可能性較小。 如果它不在緩存中,那麼我們必須進行數據庫加載。

在對不同的緩存容量進行了大量的負載測試之後,我們發現了一個設置似乎還可以。 雖然不完全滿意,但足夠滿意,並且可以炸更大的魚,所以我們讓這種服務運行了一段時間。

在這段時間裡,Rust在Discord的其他部分獲得了越來越多的成功,我們共同決定要創建在Rust中完全構建新服務所需的框架和庫。 這項服務體積小且自包含,因此非常適合移植到Rust,但我們也希望Rust可以解決這些延遲峰值。 因此,我們承擔了將"讀取狀態"移植到Rust的任務,希望證明Rust是一種服務語言並改善用戶體驗。²

Rust中的內存管理

Rust速度極快且內存效率高:無需運行時或垃圾收集器,它可以為性能至關重要的服務提供支持,可以在嵌入式設備上運行,並且可以輕鬆地與其他語言集成。³

Rust沒有垃圾回收,因此我們認為它不會像Go那樣具有相同的延遲峰值。

Rust使用相對獨特的內存管理方法,該方法結合了內存"所有權"的概念。 基本上,Rust跟蹤誰可以讀取和寫入內存。 它知道程序何時使用內存,並在不再需要時立即釋放內存。 它在編譯時強制執行內存規則,幾乎不可能出現運行時內存錯誤。bug您無需手動跟蹤內存。 編譯器會處理它。

因此,在Rust版本的"讀取狀態"服務中,當用戶的讀取狀態從LRU緩存中逐出時,會立即從內存中釋放出來。 讀取狀態內存不會等待垃圾回收器對其進行收集。 Rust知道它已不再使用,並立即釋放它。 沒有運行時過程來確定是否應釋放它。

異步Rust

但是Rust生態系統存在問題。 在重新實現該服務時,Rust stable對於異步Rust並沒有一個很好的故事。 對於網絡服務,必須進行異步編程。 有一些啟用異步Rust的社區庫,但是它們需要大量的儀式,並且錯誤消息非常晦澀。

幸運的是,Rust團隊努力使異步編程變得容易,並且在Rust不穩定的夜間頻道都可以使用。

Discord從未懼怕採用看起來很有前途的新技術。 例如,我們是Elixir,React,React Native和Scylla的早期採用者。 如果一項技術很有前途並給我們帶來優勢,那麼我們不介意處理前沿技術的固有困難和不穩定。 這是我們通過不到50名工程師迅速達到250+百萬用戶的一種方法。

每晚在Rust中使用新的異步功能是我們願意接受有前途的新技術的另一個例子。 作為一個工程團隊,我們認為每晚使用Rust值得,並且我們致力於每晚運行,直到在穩定狀態下完全支持異步為止。 我們共同處理了出現的任何問題,此時Rust穩定器支持異步Rust。

實施,負載測試和啟動

實際的重寫相當簡單。 它起初只是一個粗略的翻譯,然後我們將其精簡到合理的程度。 例如,Rust有一個很棒的類型系統,它對泛型提供了廣泛的支持,因此我們可以丟棄僅僅由於缺少泛型而存在的Go代碼。 另外,Rust的內存模型能夠推斷出線程間的內存安全性,因此我們能夠丟棄Go中所需的一些手動跨goroutine內存保護。

當我們開始負載測試時,我們立即對結果感到滿意。 Rust版本的延遲與Go一樣好,並且沒有延遲峰值!

值得注意的是,在編寫Rust版本時,我們僅將最基本的思想用於優化。 即使僅進行基本優化,Rust仍能勝過超級手動調整的Go版本。 與我們對Go進行的深入研究相比,這充分證明了用Rust編寫高效的程序是多麼容易。

但是,我們對滿足Go的性能並不滿意。 經過一些性能分析和性能優化後,我們在每個性能指標上都擊敗了Go。 在Rust版本中,延遲,CPU和內存都更好。

Rust性能優化包括:

· 在LRU緩存中更改為BTreeMap而不是HashMap以優化內存使用。

· 交換使用現代Rust併發性的初始指標庫。

· 減少我們正在執行的內存副本數量。

滿意後,我們決定推出該服務。

由於我們進行了負載測試,因此發佈是相當無縫的。 我們將其放到單個金絲雀節點上,找到了一些遺漏的邊緣案例,並進行了修復。 此後不久,我們將其推廣到整個艦隊。

以下是結果。

Go是紫色,Rusts是藍色。

為什麼Discord從Go切換到Rust

提高緩存容量

服務成功運行了幾天後,我們決定是時候重新提高LRU緩存容量了。 如上所述,在Go版本中,提高LRU緩存的上限會導致更長的垃圾回收。 我們不再需要處理垃圾回收,因此我們認為我們可以提高緩存的上限並獲得更好的性能。 我們增加了包裝盒的內存容量,優化了數據結構以使用更少的內存(用於娛樂),並將緩存容量增加到800萬個讀取狀態。

以下結果不言而喻。 請注意,現在平均時間以微秒為單位,max @mention以毫秒為單位。

為什麼Discord從Go切換到Rust

不斷髮展的生態系統

最後,Rust的另一個優點是它具有快速發展的生態系統。 最近,tokio(我們使用的異步運行時)發佈了0.2版。 我們進行了升級,並免費為我們提供了CPU好處。 從下面可以看到,從16號開始,CPU一直處於較低位置。

為什麼Discord從Go切換到Rust

總結思想

此時,Discord正在其軟件堆棧的許多地方使用Rust。 我們將其用於遊戲SDK,Go Live的視頻捕獲和編碼,Elixir NIF,若干後端服務等等。

在開始新項目或軟件組件時,我們考慮使用Rust。 當然,我們只在有意義的地方使用它。

除性能外,Rust對工程團隊還具有許多優勢。 例如,它的類型安全性和借位檢查器可以很容易地隨著產品需求的變化或發現有關該語言的新知識而重構代碼。 此外,生態系統和工具非常出色,並且背後蘊藏著巨大的動力。

如果到目前為止,您可能會對Rust感到很新,或者已經有一段時間了。 如果您想專業地使用Rust來解決有趣的問題,則應考慮在Discord工作。

還有一個有趣的事實:Rust團隊使用Discord進行協調。 甚至還有一個非常有用的Rust社區服務器,您可以不時發現我們正在聊天。 點擊這裡查看。

(本文翻譯自Jesse Howarth的文章《Why Discord is switching from Go to Rust》,參考:https://blog.discordapp.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f)


分享到:


相關文章: