使用Rust和Elixir實現高效的下發好友列表

2021-09-24 02:32:17 字數 3211 閱讀 7564

去年,discord的後端基礎設施團隊努力提高核心實時通訊基礎設施的可擴充套件性和效能。

我們進行的乙個大專案是改變我們更新公會成員列表的方式(螢幕右側的那些漂亮的頭像)。我們可以直接傳送會員列表中可見部分的更新(分頁),而不是為會員列表中的每個人都傳送更新。這樣做的好處很明顯,例如網路流量更少,cpu使用率更低,電池壽命更長等等。

然而,這給伺服器端造成了乙個大問題:我們需要乙個能夠容納數十萬個元素的資料結構,以一種可以處理大量更新的方式進行排序,並且可以上報會員的位置索引新增和刪​​除。

elixir是一種函式式語言,它的資料結構是不可變的。這對推理**並支撐大量併發性都非常好。不可變資料結構是把雙刃劍。現有的資料結構的更新是通過建立全新資料結構來實現的,該全新資料結構是將該操作應用於現有的資料結構的結果。

這意味著當有人加入伺服器(內部稱為公會)並擁有100,000名成員的成員列表時,我們必須構建乙個包含100,001名成員的新列表。 beam vm非常快速,並且每天都在變得更快。elixir試圖在可能的情況下利用persistent data structure。但是在我們的運營規模下,這樣的更新效率是無法被接受的。

兩位工程師接受了製作純elixir資料結構的挑戰,該資料結構可以容納大型sorted sets並支援快速更新操作。這說起來容易做起來難。

elixir有乙個名為mapset的set實現。 mapset是構建在map資料結構之上的通用資料結構。它對許多set操作很有用,但它不能保證有序,但這是成員列表的關鍵要求。排除mapset。

考慮一下list型別:對list做一層封裝,強制保證唯一性並在插入新元素後對列表進行排序。這種方法的壓測資料表明,對於小型列表(5,000個元素) ,插入時間在500μs和3,000μs之間。這太慢了,不可行。更糟糕的是,插入的效能與列表的大小和列表中的位置深度成正比。在250,000個元素的末尾新增乙個新元素,大約170,000μs:基本上是恆定的。

接下來再看看。

erlang有乙個名為ordsets的模組。 ordsets是有序sets,所以聽起來我們找到了解決問題的方法:讓我們壓測一下。當列表很小時,效能看起來相當不錯,範圍在0.008μs和288μs之間。遺憾的是,當測試的大小增加到250,000時,最壞情況下的效能提高到27,000μs,這比我們的自定義list的實現速度提高了五倍,但仍然不夠快。

嘗試了語言附帶的所有候選者,粗略地搜尋了開源lib,看看其他人是否已經解決了這個問題並開源。看了一些lib,但它們都沒有提供所需的屬性和效能。值得慶幸的是,電腦科學領域一直在優化用於儲存和分類資料的演算法和資料結構。

ordset在小資料下表現非常出色。也許有一些方法可以將一堆非常小的ordsets鏈結在一起,並在訪問特定位置時快速訪問正確的ordset。這類似於乙個skiplist。

這個新資料結構的第乙個版本非常簡單。 orderedset是乙個cell列表的封裝,每個cell內部都是乙個小的ordset:ordset的第一項,ordset的最後一項,以及count。這允許orderedset快速遍歷cells列表以找到適當的cell,然後執行非常快速的ordset操作。在250,000專案列表的末尾插入專案從27,000μs降至5,000μs,比原始ordsets快5倍,比原始list實現快34​​倍。

效能有所提公升,但是在列表的頭部cell建立250,000個元素,單個插入時間仍為19,000μs。

這是有道理的。當你在orderedset的前面插入乙個專案時,它會在第乙個cell中結束,但是cell已經滿了,所以它將最後乙個專案驅逐到下乙個cell,但是cell已經滿了,所以它將最後乙個專案驅逐到下乙個cell,依此類推。這樣的情況,我們稱之為級聯。

在小列表時,這個新的orderedset可以在列表中的任何點執行4μs和34μs之間的插入,很不錯。我們將大小調整到250,000。在列表的開頭插入,第乙個插入為4μs,後面會逐慚變慢。最終在列表末尾插入乙個專案需要640μs,看起來還行。

上面的解決方案適用於高達250,000名成員的公會,但我們想要更多!discord一直在使用rust來讓事情變得更快,我們可以使用rust來加快速度嗎?

rust不是一種函式式語言,可以使用可變資料結構。它也沒有執行時並提供「zero-cost abstractions」。如果我們用rust,它可能會表現得更好。

我們的核心服務不是用rust編寫的,它們是基於elixir的。 elixir非常適合呼叫rust,幸運的是,beam vm還有另乙個漂亮的技巧。 beam vm有三種型別的函式:

用erlang或elixir編寫的函式。這些是簡單的使用者空間函式。

內置於語言中的函式,充當使用者空間函式的構建塊。這些被稱為bif或內建函式。

nif或native函式。這些是使用c或rust構建並編譯到beam vm中的函式。呼叫這些函式就像呼叫bif一樣,但是你可以控制它的功能。

有乙個名為rustler的elixir專案。它為elixir和rust提供了很好的支援,可以建立乙個表現良好的安全的nif,並保證使用rust不會vm崩潰或記憶體洩漏。

我們預留了乙個星期,看看這是否值得付出努力。到本週末,我們給出乙個非常有限的驗證資料。壓測資料看上去很有希望,與orderedset的4μs至640μs相比,向sortedset新增元素的最佳情況是0.4μs,最差情況為2.85μs。這只是使用integer來測試,但它足以證明優於elixir的實現。

有了資料支撐,我們決定繼續擴充套件程式支援更多的elixir資料型別。最後我們的測試資料如下:

我們將數量一直增加到1,000,000。最後列印出結果:sortedset最佳情況為0.61μs,最差情況為3.68μs。結果是基於多種大小的sets,從5,000到1,000,000。

我們使最壞的情況與先前的最佳情況一樣好!rust支援的nif提供了巨大的效能優勢,而無需犧牲易用性或記憶體。

今天,rust版的sortedset為每乙個discord公會提供支援:從計畫到日本旅行的3人公會到享受最新、有趣的遊戲的20萬人公會。

自部署sortedset以來,我們已經看到效能全面提公升,不會對記憶體壓力產生影響。我們了解到rust和elixir可以並肩工作。我們仍然可以將我們的核心實時通訊邏輯保留在更高階別的elixir中,它具有出色的保護和簡單的併發實現,同時在需要時可以使用rust。

Rust 列舉的使用

參考 列舉型別的簡單使用。self 就是實現當前 trait 的型別的別名。enum veryverboseenumofthingstodowithnumbers impl veryverboseenumofthingstodowithnumbers fn main println subtract...

rust實現wss訪問 Rust入坑指南 居安思危

任何事情都是相對的,就像rust給我們的印象一直是安全 快速,但實際上,完全的安全是不可能實現的。因此,rust中也是會有不安全的 的。嚴格來講,rust語言可以分為safe rust和unsafe rust。unsafe rust是safe rust的超集。在unsafe rust中並不會禁用任何...

Rust 程式和記憶體

乙個程式執行,主流作業系統 windows linux 將執行程式作為程序載入到記憶體中,和其他程序一起共享cpu和記憶體 作業系統為每個程序分配各自虛擬位址空間,每個程序間的虛擬位址空間不同,每個程序具有各自的記憶體檢視。如下為乙個程序記憶體布局的基本展示 1.文字段 包含已編譯的二進位制檔案中執...