去年,Discord的後端基礎設施團隊努力提升核心實時通訊基礎設施的可擴展性和性能。git
咱們進行的一個大項目是改變咱們更新公會成員列表的方式(屏幕右側的那些漂亮的頭像)。咱們能夠直接發送會員列表中可見部分的更新(分頁),而不是爲會員列表中的每一個人都發送更新。這樣作的好處很明顯,例如網絡流量更少,CPU使用率更低,電池壽命更長等等。github
然而,這給服務器端形成了一個大問題:咱們須要一個可以容納數十萬個元素的數據結構,以一種能夠處理大量更新的方式進行排序,而且能夠上報會員的位置索引添加和刪除。算法
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的實現速度提升了五倍,但仍然不夠快。app
嘗試了語言附帶的全部候選者,粗略地搜索了開源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,依此類推。這樣的狀況,咱們稱之爲級聯。
問題在於,當元素填滿時,操做會從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有三種類型的函數:
有一個名爲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。
若是你須要一個高效更新的SortedSet,咱們已經開源了SortedSet。