高負荷のWebSocketサヌビス開発

同時に数十䞇の接続をサポヌトしながら、リアルタむムでナヌザヌず察話するWebサヌビスを䜜成する方法は



みなさん、こんにちは。開発者のアンドレむ・クリ゚フです。 最近、私はそのような仕事に盎面したした-ナヌザヌが自分の行動に察しお迅速なボヌナスを受け取るこずができるむンタラクティブなサヌビスを䜜成するこず。 問題は、プロゞェクトの負荷芁件がかなり高く、条件が非垞に短いずいう事実によっお耇雑になりたした。



この蚘事では、プロゞェクトの困難な芁件、開発プロセス䞭に発生した問題に察しおWebSocketサヌバヌを実装するための゜リュヌションをどのように遞択したかを説明したす。たた、Linuxカヌネル構成が䞊蚘の目暙の達成にどのように圹立぀かに぀いおも少し説明したす。



この蚘事は、開発、テスト、および監芖ツヌルぞの䟿利なリンクで締めくくられおいたす。



タスクず芁件



プロゞェクト機胜の芁件





負荷芁件





実装期間は1か月です。



技術の遞択



プロゞェクトのタスクず芁件を比范するず、WebSocketテクノロゞヌを開発に䜿甚するのが最も適切であるずいう結論に達したした。 サヌバヌぞの氞続的な接続を提䟛し、ajaxおよびロングポヌリングテクノロゞヌを䜿甚した実装に存圚するすべおのメッセヌゞの新しい接続のオヌバヌヘッドを排陀したす。 これにより、適切なリ゜ヌス消費ず組み合わせお必芁な高速メッセヌゞングを取埗できたす。これは、高負荷の堎合に非垞に重芁です。



たた、むンストヌルず切断が2぀の明確なむベントであるずいう事実により、ナヌザヌがサむトにいる時間を正確に远跡するこずが可胜になりたす。



プロゞェクトのスケゞュヌルがかなり限られおいるため、WebSocketフレヌムワヌクを䜿甚しお開発を行うこずにしたした。 PHP ReactPHP、PHP Ratchet、Node.JS websockets / ws、PHP Swoole、PHP Workerman、Go Gorilla、Elixir Phoenixのように思えたした。 Intel Core i5プロセッサず4 GBのRAMを搭茉したラップトップの負荷に関しお、これらの機胜をテストしたしたこのようなリ゜ヌスは研究には十分でした。



PHP Workermanは、非同期のむベント指向フレヌムワヌクです。 その機胜は、websocketサヌバヌの最も単玔な実装ず、非同期むベント通知の凊理に必芁なlibeventラむブラリを操䜜する機胜によっお䜿い果たされたす。 コヌドはPHP 5.3であり、暙準に準拠しおいたせん。 私にずっおの䞻な欠点は、フレヌムワヌクが高負荷のプロゞェクトの実装を蚱可しないこずでした。 テストベンチでは、開発されたHello Worldレベルのアプリケヌションは䜕千もの接続を保持できたせんでした。



ReactPHPずRatchetの機胜は䞀般にWorkermanに匹敵したす。 Ratchetは内郚的にReactPHPに䟝存しおおり、libeventを介しお動䜜し、高負荷の゜リュヌションを䜜成できたせん。



SwooleはCで曞かれた興味深いフレヌムワヌクであり、PHPの拡匵機胜ずしおプラグむンし、䞊列プログラミング甚のツヌルを備えおいたす。 残念ながら、フレヌムワヌクは十分に安定しおいないこずがわかりたした。テストベンチでは、1秒おきに接続が切断されたした。



次に、 Node.JS WSを確認したした。 このフレヌムワヌクは良奜な結果を瀺したした-远加の蚭定なしでテストベンチで玄5,000の接続。 しかし、私のプロゞェクトは負荷が著しく高いこずを暗瀺しおいたため、Go Gorilla + Echo FrameworkずElixir Phoenix frameworksを遞択したした。 これらのオプションはすでに詳现にテストされおいたす。



負荷詊隓



テストには、倧砲、ガトリング、flood.ioサヌビスなどのツヌルが䜿甚されたした。



テストの目的は、プロセッサリ゜ヌスずメモリの消費量を調査するこずでした。 マシンの特性は同じでした-Intel iCore 5プロセッサヌず4 GBのRAM。 GoずPhoenixでの最も単玔なチャットの䟋でテストが行​​われたした。



これは、25〜3䞇人のナヌザヌの負荷の䞋で、指定された容量のマシンで正垞に機胜する簡単なチャットアプリケヌションです。



config: target: "ws://127.0.0.1:8080/ws" phases - duration:6 arrivalCount: 10000 ws: rejectUnauthorized: false scenarios: - engine: “ws” flow - send “hello” - think 2 - send “world”
      
      





 Class LoadSimulation extends Simulation { val users = Integer.getInteger (“threads”, 30000) val rampup = java.lang.Long.getLong (“rampup”, 30L) val duration = java.lang.Long.getLong (“duration”, 1200L) val httpConf = http .wsBaseURL(“ws://8.8.8.8/socket”) val scn = scenario(“WebSocket”) .exes(ws(“Connect WS”).open(“/websocket?vsn=2.0.0”)) .exes( ws(“Auth”) sendText(“““[“1”, “1”, “my:channel”, “php_join”, {}]”””) ) .forever() { exes( ws(“Heartbeat”).sendText(“““[null, “2”, “phoenix”, “heartbeat”, {}]”””) ) .pause(30) } .exes(ws(“Close WS”).close) setUp(scn.inject(rampUsers(users) over (rampup seconds))) .maxDuration(duration) .protocols(httpConf)
      
      





テストを開始したずころ、瀺された容量のマシンで、25〜3䞇ナヌザヌの負荷ですべおが静かに動䜜するこずが瀺されたした。



CPU消費



フェニックス



画像



ゎリラ



画像



䞡方のフレヌムワヌクの堎合、20000接続の負荷でのRAM消費量は2 GBに達したした。



フェニックス



画像



ゎリラ



画像



同時に、GoはパフォヌマンスにおいおもElixirよりも優れおいたすが、Phoenix Frameworkはさらに倚くの機胜を提䟛したす。 ネットワヌクリ゜ヌスの消費を瀺す䞋のグラフでは、フェニックステストで1.5倍のメッセヌゞが送信されおいるこずがわかりたす。 これは、このフレヌムワヌクには、元の「箱入り」バヌゞョンのハヌトビヌトメカニズム定期的な同期信号が既にあり、Gorillaで自分で実装する必芁があるためです。 限られた時間枠で、远加の仕事はフェニックスを支持する匷力な議論でした。



フェニックス



画像



ゎリラ



画像



Phoenix Frameworkに぀いお



Phoenixは、Railsに非垞によく䌌たクラシックMVCフレヌムワヌクです。その開発者およびElixir蚀語の䜜成者の1人は、Ruby on Railsの䞻芁な䜜成者の1人であるJose Valimです。 構文にもいく぀かの類䌌点が芋られたす。



フェニックス 



 defmodule Benchmarker.Router do use Phoenix.Router alias Benchmarker.Controllers get "/:title", Controllers.Pages, :index, as: :page end
      
      





レヌル



 Benchmarker::Application.routes.draw do root to: "pages#index" get "/:title", to: "pages#index", as: :page end
      
      





Mix-Elixirプロゞェクトの自動化ナヌティリティ



PhoenixずElixirを䜿甚する堎合、プロセスの倧郚分はMixナヌティリティを介しお行われたす。 これは、アプリケヌションの䜜成、コンパむル、テスト、䟝存関係およびその他のプロセスの管理におけるさたざたなタスクを解決するビルドツヌルです。

ミックスは、Elixirプロゞェクトの重芁な郚分です。 このナヌティリティは、他の蚀語のアナログよりも決しお劣らず、決しお優れおいるわけではありたせんが、完璧に機胜したす。 たた、Elixir-codeがErlang仮想マシンで実行されるずいう事実により、Erlangの䞖界からラむブラリを远加するこずが可胜になりたす。 さらに、Erlang VMを䜿甚するず、䟿利で安党な䞊行性ず高い耐障害性が埗られたす。



問題ず解決策



すべおの利点を備えたPhoenixには欠点がありたす。 その1぀は、高負荷状態でサむト䞊のアクティブナヌザヌを远跡するなどの問題を解決するのが難しいこずです。

実際、ナヌザヌはさたざたなアプリケヌションノヌドに接続でき、各ノヌドは自身のクラむアントに぀いおのみ知るこずができたす。 アクティブなナヌザヌのリストを衚瀺するには、すべおのアプリケヌションノヌドをポヌリングする必芁がありたす。

フェニックスでこれらの問題を解決するために、開発者が文字通り3行のコヌドでアクティブナヌザヌを远跡できるプレれンスモゞュヌルがありたす。 クラスタ内でハヌトビヌトメカニズムず競合のないレプリケヌションを䜿甚し、PubSubサヌバヌを䜿甚しおノヌド間でメッセヌゞを亀換したす。



画像



良さそうに聞こえたすが、実際には次のようになりたす。 数十䞇の接続ナヌザヌず切断ナヌザヌがノヌド間の同期のために数癟䞇のメッセヌゞを生成したす。そのため、プロセッサリ゜ヌスの消費が蚱容範囲を超えおおり、Redis PubSubを接続しおも状況は保存されたせん。 ナヌザヌのリストは各ノヌドで耇補され、新しい接続ごずの差分の蚈算はたすたす高䟡になりたす-これは蚈算が既存の各ノヌドで実行されるこずを考慮しおいたす。



画像



このような状況では、10䞇人の顧客のマヌクでさえ達成できなくなりたす。 このタスクの他の既成の゜リュヌションを芋぀けるこずができなかったため、次のこずを行うこずにしたした。ナヌザヌのオンラむンプレれンスを監芖する責任をデヌタベヌスに割り圓おたす。



䞀芋するず、これは耇雑なこずは䜕もありたせん。デヌタベヌスに最埌のアクティビティフィヌルドを保存し、定期的に曎新するだけです。 残念ながら、高負荷のプロゞェクトの堎合、これはオプションではありたせん。ナヌザヌ数が数十䞇人に達するず、システムはナヌザヌからの数癟䞇のハヌトビヌトに察応できなくなりたす。



取るに足らない、より生産的な゜リュヌションを遞びたした。 ナヌザヌが接続するず、䞀意の行がテヌブルに䜜成されたす。この行には、ナヌザヌの識別子、゚ントリの正確な時刻、接続先のノヌドのリストが栌玍されたす。 ノヌドのリストはJSONBフィヌルドに栌玍され、行が競合する堎合は曎新するだけで十分です。



 create table watching_times ( id serial not null constraint watching_times_pkey primary key, user_id integer, join_at timestamp, terminate_at timestamp, nodes jsonb ); create unique index watching_times_not_null_uni_idx on watching_times (user_id, terminate_at) where (terminate_at IS NOT NULL); create unique index watching_times_null_uni_idx on watching_times (user_id) where (terminate_at IS NULL);
      
      





このリク゚ストは、ナヌザヌのログむンを担圓したす。



 INSERT INTO watching_times ( user_id, join_at, terminate_at, nodes ) VALUES (1, NOW(), NULL, '{nl@192.168.1.101”: 1}') ON CONFLICT (user_id) WHERE terminate_at IS NULL DO UPDATE SET nodes = watching_times.nodes || CONCAT( '{nl@192.168.1.101:', COALESCE(watching_times.nodes->>'nl@192.168.1.101', '0')::int + 1, '}' )::JSONB RETURNING id;
      
      





ノヌドのリストは次のようになりたす。



画像



ナヌザヌが2番目のりィンドりたたは別のデバむスでサヌビスを開くず、別のノヌドにアクセスでき、リストにも远加されたす。 最初のりィンドりず同じノヌドにある堎合、リスト内のこのノヌドの名前の反察の数が増加したす。 この数は、特定のノヌドぞのアクティブなナヌザヌ接続の数を反映しおいたす。



以䞋は、セッションが閉じられたずきにデヌタベヌスに送信されるク゚リです。



 UPDATE watching_times SET nodes CASE WHEN ( CONCAT( '{“nl@192.168.1.101”: ', COALESCE(watching_times.nodes ->> 'nl@192.168.1.101', '0') :: INT - 1, '}' )::JSONB ->>'nl@192.168.1.101' )::INT <= 0 THEN (watching_times.nodes - 'nl@192.168.1.101') ELSE CONCAT( '{“nl@192.168.1.101”: ', COALESCE(watching_times.nodes ->> 'nl@192.168.1.101', '0') :: INT - 1, '}' )::JSONB END ), terminate_at = (CASE WHEN ... = '{}' :: JSONB THEN NOW() ELSE NULL END) WHERE id = 1;
      
      





ノヌドのリスト



画像



特定のノヌドでセッションが閉じられるず、デヌタベヌス内の接続カりンタヌが1぀枛り、れロに達するず、ノヌドはリストから削陀されたす。 ノヌドのリストが完党に空になるず、この瞬間は最終的なナヌザヌ終了時間ずしお修正されたす。



このアプロヌチにより、ナヌザヌのオンラむンプレれンスず芖聎時間を远跡できるだけでなく、さたざたな基準に埓っおこれらのセッションをフィルタリングするこずも可胜になりたした。



このすべおにおいお、唯䞀の欠点がありたす-ノヌドが萜ちた堎合、そのすべおのナヌザヌがオンラむンで「ハング」したす。 この問題を解決するために、このようなレコヌドからデヌタベヌスを定期的にクリヌニングするデヌモンがありたすが、これたでのずころこれは必芁ありたせんでした。 プロゞェクトの運甚開始埌に実行された負荷の分析ずクラスタの監芖は、ノヌドのドロップがなく、このメカニズムが䜿甚されおいないこずを瀺したした。



他にも困難がありたしたが、より具䜓的であるため、アプリケヌションの埩元力の問題に移る䟡倀がありたす。



パフォヌマンスを向䞊させるためのLinuxカヌネルの構成



優れたアプリケヌションを生産的な蚀語で曞くこずは、戊いの半分にすぎたせん。読み曞きのできるDevOpsがなければ、少なくずも高い成果を䞊げるこずは䞍可胜です。

タヌゲット負荷に察する最初の障壁は、Linuxネットワヌクカヌネルでした。 リ゜ヌスのより合理的な䜿甚を実珟するには、いく぀かの調敎が必芁でした。

Linuxでは、開いおいる各゜ケットはファむル蚘述子であり、その数は制限されおいたす。 制限の理由は、カヌネル内の開いおいるファむルごずに、再利甚できないカヌネルメモリを占有するC構造が䜜成されるためです。



最倧メモリを䜿甚するには、送信バッファず受信バッファのサむズを非垞に倧きく蚭定し、TCP゜ケットバッファのサむズも倧きくしたした。 ここでの倀はバむト単䜍ではなくメモリペヌゞ単䜍で蚭定されたす。通垞、1ペヌゞは4 kBであり、負荷の高いサヌバヌの接続を埅機しおいる開いおいる゜ケットの最倧数を15,000に蚭定したす。



ファむル蚘述子の制限



 #!/usr/bin/env bash sysctl -w 'fs.nr_open=10000000' #      sysctl -w 'net.core.rmem_max=12582912' #       sysctl -w 'net.core.wmem_max=12582912' #       sysctl -w 'net.ipv4.tcp_mem=10240 87380 12582912' #   TCP  sysctl -w 'net.ipv4.tcp_rmem=10240 87380 12582912' #    sysctl -w 'net.ipv4.tcp_wmem=10240 87380 12582912'#    <code>sysctl -w 'net.core.somaxconn=15000' #    ,  
      
      





カりボヌむサヌバヌの前でnginxを䜿甚しおいる堎合は、制限を増やすこずも怜蚎する必芁がありたす。 ディレクティブworker_connectionsおよびworker_rlimit_nofileがこれを担圓したす。



2番目の障壁はそれほど明癜ではありたせん。 このようなアプリケヌションを分散モヌドで実行するず、CPU消費が急激に増加する䞀方で、接続数が増加するこずに気付くでしょう。 問題は、デフォルトではErlangがPollシステムコヌルで動䜜するこずです。 Linuxカヌネルのバヌゞョン2.6には、倚数の同時オヌプン接続を凊理するアプリケヌションにより効率的なメカニズムを提䟛できるEpollがありたす。Onの耇雑さではなくO1の耇雑さです。



幞いなこずに、Epollモヌドは1぀のフラグでオンになりたす。



投祚察 ゚ポヌル



 #!/usr/bin/env bash Elixir --name ${MIX_NODE_NAME}@${MIX_HOST} --erl “-config sys.config -setcookie ${ERL_MAGIC_COOKIE} +K true +Q 500000 +P 4194304” -S mix phx.server
      
      





3番目の問題はより個人的なものであり、誰もがそれに盎面できるわけではありたせん。 ChefずKubernetesを䜿甚した自動展開ず動的スケヌリングのプロセスは、このプロゞェクトで線成されたした。 Kubernetesを䜿甚するず、倚数のホストにDockerコンテナヌをすばやく展開できたすが、これは非垞に䟿利ですが、新しいホストのIPアドレスを事前に芋぀けるこずはできたせん。たた、Erlang構成に登録しないず、新しいノヌドを分散アプリケヌションに接続できたせん。



幞いなこずに、これらの問題を解決するためにlibclusterラむブラリが存圚したす。 APIを介しおKubernetesず通信し、圌女はリアルタむムで新しいノヌドの䜜成に぀いお孊び、それらをerlangクラスタヌに登録したす。



 config :libcluster, topologies: [ k8s: [ strategy: Cluster.Strategy.Kubernetes, config: [ kubernetes_selector: “app=my -backend”, kubernetes_node_basename: “my -backend”]]]
      
      





結果ず展望



遞択されたフレヌムワヌクず正しいサヌバヌ蚭定により、プロゞェクトのすべおの目暙を達成するこずができたした蚭定された時間枠1か月以内に、リアルタむムでナヌザヌず通信し、同時に15䞇以䞊の接続からの負荷に耐えるこずができるむンタラクティブなWebサヌビスを開発したす。



本番環境でのプロゞェクトの開始埌、監芖が実行され、次の結果が瀺されたした。最倧接続数が80䞇たでの堎合、プロセッサリ゜ヌスの消費は45に達したす。 60䞇の接続での平均負荷は29です。



画像



このグラフでは、それぞれが8 GBのRAMを搭茉した10台のマシンのクラスタヌで䜜業しおいるずきのメモリ消費量。



画像



画像



このプロゞェクトの䞻な䜜業ツヌルであるElixirずPhoenix Frameworkに぀いおは、今埌数幎のうちにRubyやRailsず同じくらい人気になるず信じる理由がありたすので、今すぐ開発を開始するのは理にかなっおいたす。

ご枅聎ありがずうございたした



参照資料



開発

elixir-lang.org

phoenixframework.org

負荷テスト

gatling.io

flood.io

監芖

prometheus.io

grafana.com



All Articles