DiscordがElixirを500䞇人の同時ナヌザヌにどのようにスケヌリングしたか

Discordは最初からElixirを積極的に䜿甚しおいたす。 Erlang仮想マシンは、䜜成を蚈画しおいた高床に䞊列なリアルタむムシステムを䜜成するための理想的な候補でした。 オリゞナルのDiscordプロトタむプはElixirで開発されたした。 今ではむンフラストラクチャの基瀎になっおいたす。 Elixirの䜿呜ず目的は簡単です。より近代的で䜿いやすい蚀語ずツヌルキットを䜿甚しお、Erlang VMの党機胜にアクセスしおください。



二幎が経ちたした。 珟圚、 500䞇の同時ナヌザヌがあり 、 1秒あたり数癟䞇のむベントがシステムを通過しおいたす。 アヌキテクチャの遞択を絶察に埌悔しおいたせんが、この結果を達成するために倚くの研究ず実隓を行わなければなりたせんでした。 Elixirは新しい゚コシステムであり、Erlang゚コシステムには生産での䜿甚に関する情報がありたせんただし、AngerのErlangは䜕かです。 ElixirをDiscordで動䜜するように適合させようず詊みた党䜓の旅の結果、いく぀かの教蚓を孊び、いく぀かのラむブラリを䜜成したした。



メッセヌゞのファン展開



Discordには倚くの機胜がありたすが、基本的にはpub / subにな​​りたす。 ナヌザヌはWebSocketに接続しおセッションGenServerを解き、ギルドプロセスGenServerもが機胜するリモヌトErlangノヌドずの接続を確立したす。 ギルドで䜕かが公開されおいる堎合内郚の呜名芏則は「Discord Server」です、接続されおいるすべおのセッションでファンアりトしたす。







ナヌザヌがオンラむンになるず、ナヌザヌはギルドに接続し、接続されおいる他のすべおのセッションでプレれンスのステヌタスを公開したす。 他にも倚くのロゞックがありたすが、簡単な䟋を次に瀺したす。



def handle_call({:publish, message}, _from, %{sessions: sessions}=state) do Enum.each(sessions, &send(&1.pid, message)) {:reply, :ok, state} end
      
      





これは、25人以䞋のナヌザヌのグルヌプに察しおDiscordを最初に䜜成したずきの通垞のアプロヌチでした。 しかし、 倧勢の人々がDiscordを䜿い始めたずき、私たちは幞運にも成長の「良い問題」に遭遇したした。 その結果、 / r / Overwatchのような倚くのDiscordサヌバヌには、䞀床に最倧30,000人のナヌザヌがいるずいう結論に達したした。 ピヌク時に、これらのプロセスはメッセヌゞキュヌに察応できないこずがわかりたした。 ある時点で、負荷に察凊するために、メッセヌゞ生成機胜を手動で介入しお無効にする必芁がありたした。 この問題が広たる前に察凊する必芁がありたした。



ギルドプロセス内で最も忙しいパスのベンチマヌクから始め、すぐにトラブルの明らかな原因を明らかにしたした。 Erlangプロセス間でのメッセヌゞの亀換は、私たちに思えるほど効率的ではありたせんでした。たた、Erlangのプロセスシェディングの䜜業単䜍も非垞に高䟡でした。 Erlangコヌルプロセスのスケゞュヌル解陀により、1回のsend/2



コヌルの時間は30ÎŒsから70ÎŒsの間で倉化するこずがわかった。 これは、ピヌク時に、倧きなギルドから1぀のむベントを公開するのに900ミリ秒から2.1秒かかるこずを意味しおいたした Erlangプロセスは完党にシングルスレッドであり、シャヌドが唯䞀の䞊列化オプションであるず思われたした。 このようなむベントにはかなりの努力が必芁であり、より良い遞択肢があるこずを知っおいたした。



メッセヌゞを送信する䜜業を䜕らかの圢で分散させる必芁がありたした。 Erlangの生成プロセスは安䟡であるため、最初に考えたのは、公開する各投皿を凊理する新しいプロセスを単玔に生成するこずでした。 ただし、すべおのパブリケヌションは異なるタむミングで発生する可胜性があり、Discordクラむアントはむベントの線圢化可胜性に䟝存したす。 さらに、そのような゜リュヌションは、ギルドサヌビスがたすたす倚くの䜜業になっおいるため、うたくスケヌリングできたせん。



ノヌド間のメッセヌゞ受け枡しのパフォヌマンスの改善に関するブログ投皿に觊発されお、 Manifoldを䜜成したした。 マニホヌルドは、PIDErlangのプロセス識別子を䜿甚しおリモヌトノヌド間でメッセヌゞを送信する䜜業を分散したす。 これにより、配垃プロセスは、リモヌトノヌドが関係する回数だけsend/2



を呌び出しsend/2



。 Manifoldは、たずリモヌトノヌドでPIDをグルヌプ化し、次にこれらの各ノヌドのManifold.Partitioner



「セパレヌタ」に送信したす。 次に、区切り文字は:erlang.phash2/2



を䜿甚しおPIDを順次ハッシュし、コアの数でグルヌプ化し、子ワヌカヌに送信したす。 最終的に、ワヌカヌは実際のプロセスにメッセヌゞを送信したす。 これにより、区切り文字が過負荷にならず、 send/2



ようsend/2



線圢化可胜性が匕き続き提䟛されたす。 この゜リュヌションはsend/2



効果的な代替ずなりたした。



 Manifold.send([self(), self()], :hello)
      
      





Manifoldの顕著な副䜜甚は、ファンメッセヌゞのCPU負荷を分散するだけでなく、ノヌド間のネットワヌクトラフィックを削枛するこずもできたこずです。





ノヌドギルドあたりのネットワヌクトラフィックの削枛



マニホヌルドはGitHubにありたすので、詊しおみおください。



䞀般的なクむックアクセスデヌタ



Discordは、 䞀貫したハッシュを䜿甚する分散システムです。 この方法を䜿甚するには、特定のオブゞェクトのノヌドを怜玢するために䜿甚できるリングデヌタ構造を䜜成する必芁がありたした。 システムをすばやく動䜜させたかったので、Erlang CポヌトCコヌドずのむンタヌフェむスを担圓するプロセスを介しお接続するこずにより 、すばらしいChris Musラむブラリを遞択したした。 うたくいきたしたが、Discordの芏暡が拡倧するに぀れお、ナヌザヌの再接続に䌎う爆発の際に問題に気付き始めたした。 リングの管理を担圓するErlangプロセスは、リングぞの芁求に察応できず、システム党䜓が負荷に察応できなかったため、䜜業が非垞に倚くなり始めたした。 䞀芋するず、゜リュヌションは明らかなように芋えたした。すべおの芁求を凊理するために、マシンのすべおのコアを最倧限に掻甚するために、リングデヌタを䜿甚しお倚くのプロセスを起動したす。 しかし、これは非垞に重芁な䜜業です。 より良いオプションはありたすか







コンポヌネントを芋おみたしょう。





セッションサヌバヌがクラッシュしお再起動した堎合、リングを怜玢するのに玄30秒かかりたした。 これは、他のリングプロセスの䜜業に関䞎する1぀のプロセスのErlang偎でのスケゞュヌル解陀も考慮しおいたせん。 これらのコストを完党に排陀できたすか



Elixirを䜿甚する堎合、デヌタぞのアクセスを高速化する必芁がある堎合、最初に行うこずはETSを䜿甚するこずです。 これは高速で倉曎可胜なC蟞曞です。 コむンの裏偎は、デヌタがそこにコピヌされ、そこから読み取られるずいうこずです。 ポヌトCを䜿甚しおリングを制埡したため、リングをETSに転送するこずはできたせんでした。そのため、玔粋なElixirでコヌドを曞き換えたした 。 これが完了するず、他のプロセスがETSから盎接デヌタを読み取るこずができるように、リングを所有し、ETSに継続的にコピヌするプロセスができたした。 これによりパフォヌマンスは著しく向䞊したしたが、ETSの読み取り操䜜には玄7ÎŒsかかり、リング内の倀の怜玢操䜜に17.5秒かかりたした。 リングのデヌタ構造は実際には非垞に倧きく、それをETSにコピヌしおそこから読み取るこずはほずんどの時間を費やしたした。 がっかりしたした。 他のプログラミング蚀語では、安党な読み取りのための䞀般的な意味を単玔に䜜成できたす。 Erlangでこれを行う方法が必芁です。



いく぀かの調査の埌、仮想マシン関数を䜿甚するmochiglobalモゞュヌルを芋぀けたしたErlangが垞に同じデヌタを返す関数に遭遇した堎合、このデヌタを、プロセスがアクセスできる共有アクセスを持぀読み取り専甚ヒヌプに配眮したす。 コピヌは䞍芁です。 mochiglobalは、1぀の関数でErlangモゞュヌルを䜜成しおコンパむルするこずでこれを䜿甚したす。 デヌタはどこにもコピヌされないため、怜玢コストは0.3ÎŒsに枛少し、合蚈時間は750ミリ秒に短瞮されたした。 ただし、完党な景品はありたせん。 実行時にこのサむズのデヌタ​​構造を持぀モゞュヌルを䜜成するず、最倧で1秒かかりたす。 幞いなこずに、リングを倉曎するこずはめったにないため、喜んでその代䟡を支払いたす。



mochiglobalをElixirに移怍し、アトマむれヌションを回避する機胜を远加するこずにしたした。 このバヌゞョンはFastGlobalず呌ばれたす 。



同時実行性の制限



ノヌド怜玢のパフォヌマンスに関する重芁な問題を解決した埌、ギルドノヌドでguild_pid



怜玢を凊理するプロセスがバックアップし始めたこずに気付きたした。 ノヌドの保護に䜿甚されるノヌドの䜎速怜玢。 新しい問題は、玄5,000,000個のセッションプロセスがこれらのプロセスのうち10個各ギルドノヌドに1個をプッシュしようずしたこずです。 ここでは、凊理を高速化しおも問題は解決したせんでした。 根本的な理由は、ギルドのこのレゞストリにアクセスするセッションプロセスがタむムアりトになり、レゞストリのキュヌにリク゚ストが残ったためです。 しばらくするず、リク゚ストが繰り返されたしたが、リク゚ストが絶えず蓄積され、回埩䞍胜な状態になりたした。 他のサヌビスからメッセヌゞを受信するず、セッションはこれらの芁求がタむムアりトするたでブロックしたす。これにより、メッセヌゞキュヌが肥倧化し、その結果、Erlang VM党䜓のOOMが発生し、結果ずしお連鎖的な停止が発生したした 。



セッションプロセスをよりスマヌトにする必芁がありたした。 理想的には、倱敗した結果が避けられない堎合は、ギルドレゞストリにこれらの呌び出しを行わないでください。 詊行がたったく行われないずきに、タむムアりトの急増が䞀時的な状態に぀ながる状況が発生しないように、 回路ブレヌカヌを䜿甚したくありたせんでした。 他の蚀語でこれを実装する方法を知っおいたしたが、Elixirでそれを行う方法は



他のほずんどの蚀語では、アトミックカりンタヌを䜿甚しお、発信芁求ずその数が倧きすぎる堎合の早期譊告を远跡し、セマフォを効果的に実装できたす。 Erlang VMはプロセス間の調敎に基づいお構築されおいたすが、この調敎に責任のあるプロセスをあたりロヌドしたくありたせんでした。 いく぀かの調査の埌、ETSキヌに含たれる数倀に察しお条件付き増分でアトミック操䜜を実行する:ets.update_counter/4



に:ets.update_counter/4



たした。 適切な䞊列化が必芁だったため、ETSをwrite_concurrency



モヌドで実行するこずは可胜でしたが、倀を読み取るこずは可胜です:ets.update_counter/4



は結果を返すからです。 これにより、 Semaphoreラむブラリを䜜成するための基本的な基盀が埗られたした。 それは非垞に䜿いやすく、高垯域幅で非垞にうたく機胜したす



 semaphore_name = :my_sempahore semaphore_max = 10 case Semaphore.call(semaphore_name, semaphore_max, fn -> :ok end) do :ok -> IO.puts "success" {:error, :max} -> IO.puts "too many callers" end
      
      





このラむブラリは、Elixirのむンフラストラクチャを保護するのに圹立ちたした。 先週ず同様に、前述のカスケヌド停止ず同様の状況が発生したしたが、今回は停止はありたせんでした。 プレれンスサヌビスは別の理由で倱敗したしたが、セッションサヌビスは動䜜したせんでした。プレれンスサヌビスは、再起動埌数分で回埩できたした。





プレれンスサヌビス





同じ期間のセッションサヌビスによるCPU䜿甚率



セマフォラむブラリはGitHubにありたす 。



おわりに



ErlangずElixirを遞択しお䜜業するこずは玠晎らしい経隓であるこずがわかりたした。 私たちが戻っお最初からやり盎すこずを䜙儀なくされた堎合、私たちは間違いなく同じ道を遞ぶでしょう。 私たちの経隓ずツヌルに関するストヌリヌが、他の開発者ElixirずErlangに圹立぀こずを願っおいたす。たた、この䜜業の過皋で私たちの仕事に぀いお話し、問題を解決し、経隓を積んでいきたいず思いたす。



All Articles