このスライドはYAPC::Okinawa ONNASON 2018で行ったトークです。
トーク概要 http://yapcjapan.org/2018okinawa/talks.html#/detail/6
High (Availability|Performance)High (Availability|Performance)WebSocket for Perl Real-TimeWebSocket for Perl Real-TimeApplicationApplicationmacopy a.k.a @mackee_wYAPC::Okinawa 2018 ONNASON
View Slide
自己紹介自己紹介インターネット及び会社ではマコピーと呼ばれています所属:面白法人カヤック主な職業:スマホゲームのサーバサイドエンジニアゲーム開発,サーバ運用,ワークフロー構築 etc...
「トーカナイザの守護霊」興味があること:リアルタイム通信, 3Dプリンタ,自作キーボード最近の出来事:右肘を骨折,リハビリ中Twitter: @mackee_w GitHub: @mackeePAUSE: MACOPY
Perl Hackers HubにDBを使ったテストの話などを寄稿させていただきました
予告編の内容予告編の内容課題と背景について説明しますサーバプッシュを従来の同期型Preforkアーキテクチャで扱った場合の問題点何故kuiperbeltが生まれたのかの説明スマホゲームの場合の事例です
このトークの内容このトークの内容背景があった上でどう解決したかを解説永続接続を扱うスケーラブルなミドルウェアの設計そのミドルウェアを趣味で作って運用に持っていく話Proof of Conceptから Production Readyになるまで
!!! CAUTION!!!このトークでのHigh AvailabilityHigh Availability高可用性 これほどコンテキストによって変わる高ナントカはあまりないと思うSPOFがない片方落ちたときにクライアントの再接続でもう片方でサービス継続が可能リトライ可能
人間諦めが肝心人間諦めが肝心信頼性が期待されているミドルウェアは、何を提供するミドルウェアでどこからどこまでを保証するかをはじめに決めるはじめにルールを決めておけば信頼性を損ないかねない機能追加の要望を説明して突っぱねられる代替案は提案した方がいいけれど
!!! CAUTION!!!このトークでのHigh PerformanceHigh PerformanceWebSocket同時接続数 10k~ 100k上記数字は実際の本番での運用での数字横に並べられる ボトルネックになることを避ける設計もっといけるとは思う
!!! CAUTION!!!このトークでのリアルタイムリアルタイム「リアルタイムっていうからリアルタイムOSとかのリアルタイムかと思った」ごめんなさい違います。なのでそういうの言うだけのブクマはしないで欲しいWebおよびスマホゲームで使われる言葉。技術的観点からはサーバプッシュを用いた機能
第1章第1章リアルタイム通信がめんどくリアルタイム通信がめんどくさい件さい件
リアルタイム通信とはリアルタイム通信とはあるユーザの操作時間経過やゲーム内オブジェクトの状態によって引き起こされるイベントが別のユーザに即座に伝わるアーキテクチャ/ゲームシステム のことを指す
例:ターン制カードゲーム例:ターン制カードゲーム相手のターンから自分のターンになる時に場に出すカードを選択するなどしてターンを終了する
例:ターン制カードゲーム例:ターン制カードゲーム既にターンが終了しているかどうかを出来るだけ素早く知るには?今の場の状態をサーバに定期的に問い合わせる(=ポーリング)サーバから情報が降ってくるのを待つ(=サーバプッシュ)
ターン終了時の通信ターン終了時の通信
ポーリングポーリング
サーバプッシュサーバプッシュ
実現するためには実現するためにはクライアントからのリクエスクライアントからのリクエスト無しでサーバから情報を送ト無しでサーバから情報を送る必要があるる必要がある=>サーバプッシュ=>サーバプッシュ
本物と擬似本物と擬似従来のクライアント起点のリクエストでも似たような体験を実現出来なくはないこれがポーリングクライアントが定期的にサーバに新しい情報がないかを取りに来る。更新されていたら画面とかも更新する疑似リアルタイムと呼ばれる
ポーリング サーバプッシュリアルタイム性 低 高通信量 大 小サーバリソース 大 小技術的飛躍度 低 高枯れている感 高 低
モバイルの場合モバイルの場合ポーリング サーバプッシュ電池消費 小 大サーバプッシュはサーバに対してクライアントが通信を繋ぎっぱなしになるため電池を消費しがちポーリングは通信が間欠的になるため比較的電池消費をしない
メリット・デメリットあるけメリット・デメリットあるけれど本物が必要なアプリケーれど本物が必要なアプリケーションは多いので本物のリアションは多いので本物のリアルタイム技術を採用せざるをルタイム技術を採用せざるを得ない今日このごろ得ない今日このごろ
本物のリアルタイムでもラグ本物のリアルタイムでもラグはあるはある驚くべきことに光は1msに300kmしか進まない驚くべきことに通信回線の資源や計算資源は有限驚くべきことにインターネットはコンピュータのバケツリレーで出来ている
あなたはそばに相手がいると思うかもしれないが、それはインターネットの中だけで実は地球の反対側にいるかもしれない。それがインターネットの良いところ
ラグを見せないゲームデザイン[CEDEC 2010]ネットゲームの裏で何が起こっているのか。ネットワークエンジニアから見た,ゲームデザインの大原則行動パターンが送られてこない場合でも,それを誤魔化す小さいアクション(火を吹くなど)が用意されているとのこと
Web以外のゲームではWeb以外のゲームでは家庭用ゲーム機/オンラインゲームの世界もっぱら生TCP/生UDPの世界各々がそれぞれやっている(シリアライズ形式とか)
スマホゲーム界ではスマホゲーム界ではノウハウを持ってない会社が参入してきたのでミドルウェアを提供する会社が出てきたPhoton Server / Photon Cloud (Exit Games, Inc)モノビットエンジン (モノビット株式会社)
一方我々は一方我々は面白法人カヤックって出自がWebの会社なんですよね最近はゲームやったりハードウェアを設計する人もいるけれど使われる技術スタックはWeb由来のものいきなり家庭用ゲーム機やPCゲーム由来の技術を使おうとしてもノウハウがないPhoton使っているゲームもある
Webの技術Webの技術ニアリーイコールブラウザで使える技術ブラウザで使える技術
HTTPの世界観HTTPの世界観リクエストとレスポンスは1対1レスポンスが生まれるにはリクエストが必要この時点でサーバプッシュとは矛盾している
HTTPはステートレスなプロトコルステートレスなプロトコルあるリクエスト/レスポンスの対と、別のあるリクエスト/レスポンスの対は独立している
重要な点重要な点一般的にWebサービスの1リクエストは長くても数秒以内にレスポンスが返ってくるこのへんが後々効いてくる大事な話です
HTTPの特性を踏まえた上でのHTTPの特性を踏まえた上でのWebでのサーバプッシュはどWebでのサーバプッシュはどうやる?うやる?
ロングポーリング/Cometロングポーリング/CometふつうにHTTPリクエスト投げるサーバは送りたい情報が出るまでレスポンスを送らず焦らす送りたい情報が生まれたらレスポンスを返すクライアントは処理したらまたリクエストを投げる
ロングポーリング/Cometロングポーリング/Cometメリット:普通のHTTP通信が長くなっただけデメリット:一個情報を受け取るのに一回リクエスト送るオーバーヘッド,レスポンスを受け取ってから次にリクエストを送るまでは情報をリアルタイムに受け取れない
Sever-Sent Events(SSE)Sever-Sent Events(SSE)ふつうにリクエスト投げるサーバはコネクションを切らずにだらだら1行ずつ書くクライアントは1行データが送られてきたら読む、コネクションは切らない
Sever-Sent Events(SSE)Sever-Sent Events(SSE)メリット:ただのHTTPのアクロバティック的使用法デメリット:一方通行,ライブラリ実装の少なさ
WebSocketWebSocketUpgradeヘッダが入ったリクエストを投げるサーバがWebSocketを介したいんだなって思って101 SwitchingProtocolsを返すHTTPとは違う独自のWebSocketプロトコルによる通信が開始される
WebSocketWebSocketメリット:現代においては広く使われている,標準化もされている,双方向デメリット: HTTPSじゃないWebSocketだとフォワードプロキシがある環境で使えないことがある,デバッグ方法が通常のHTTPの作法と違う
他にも未来の技術達他にも未来の技術達HTTP2 Server Pushhtmlを取りに来たクライアントにこれも必要だから持ってけって言ってJSやら画像を送りつけることが出来るやつゲームなどのサーバプッシュに使えるかどうかはあまりわからない……この前のセッションでトークした人達に聞いてみましょう
他にも未来の技術達他にも未来の技術達WebRTCブラウザでビデオチャットとかするやつ条件が合えばP2P通信ができる。また画像や音声以外にもデータも送れる。さらにUDPを選択できるのでオーバーヘッドも少ない。ただしNAT越え……gRPCみたいなWebの技術に乗っかっているサーバプッシュの仕組みを持ったものもあるが、ブラウザでは使えない
WebサーバアーキテクチャのWebサーバアーキテクチャの話話サーバプッシュを適用するにはアーキテクチャの変更が必要になる場合がある……
太古の昔: CGI太古の昔: CGIHTTPリクエストが飛んできたらサーバ内のコマンドを起動してリクエストを起動したプロセスに渡すプロセスが吐いた出力をレスポンスとして返す
太古の昔: CGI太古の昔: CGIメリット:簡単,分かりやすい,デバッグしやすいデメリット:プロセスの起動ってコストがかかるんや……
時代が移ろいでいきPerl界ではmod_perlperlインタプリタの起動とモジュールの読み込みは予めしておくFastCGIApacheからの解脱, lighttpdってのがあってだなPSGIもうperlがHTTPをしゃべっちゃうPerlウェブ開発の中世~CGIと Plackの間~ - YAPC::Kansai 2017
しかしプロセスの使われ方はしかしプロセスの使われ方は変わっていない変わっていない
同期的Preforkモデル同期的PreforkモデルPre(=事前に)forkあらかじめリクエストを処理するプログラムのプロセスを複数立てておき、リクエストが来たら暇しているプロセスに配る1プロセスだと1度に一つのリクエストしかさばけないが、Preforkモデルはforkしたプロセス分さばける
Preforkモデルは他のLLでも使Preforkモデルは他のLLでも使われているわれているPerl/PSGI - starman, starletRuby/Rack - unicornPython/WSGI - gunicorn
同期的Preforkモデルが使われ同期的Preforkモデルが使われる理由る理由->簡単->簡単
マルチスレッドではないのでデータ競合で変数壊れるとか無いそもそも並行プログラミングを考えなくて良い、コードも上から下に読んでいけば良いLBとインスタンスの関係と同じなので同じ考え方が適用できる共有リソースがないのでサーバを横に簡単に並べられる=>スケールアウト
しかし同期的Preforkモデルにしかし同期的Preforkモデルにも限界がも限界がもし、一つのHTTPリクエストが数十秒、数百秒かかっていたら?Preforkモデルはforkしたプロセス分さばける
Preforkしたプロセス以上のリクエストがHTTPリクエストの寿命の間に殺到したときに破滅する
刹那的な場合はプロセス数以上のリクエストが来てもなんとかさばける
ここに永続的な場合にプロセス数以上のリクエストが来るとレスポンスが詰まる
破滅です破滅です同時接続数が少ないアプリケーションでは破滅しないこともあります
Q.何故、同期的PreforkモデルQ.何故、同期的Preforkモデルが成り立っていたかが成り立っていたかA.普通のHTTP通信は刹那的なA.普通のHTTP通信は刹那的なプロトコルだからプロトコルだから
今までのHTTPでは1つのリク今までのHTTPでは1つのリクエストが1つのプロセスを専エストが1つのプロセスを専有する時間が短い。だからこ有する時間が短い。だからこれでやってこれたれでやってこれた
刹那的接続から永続的接続刹那的接続から永続的接続へ……へ……ロングポーリング, SSE, WebSocket...これらはすべて1回のHTTPリクエストの寿命が長い従来のアーキテクチャではプロセスが枯渇する
C10Kの話をしていますC10Kの話をしていますC10K = Connection 10,000 Problemサーバリソースはあるのにアーキテクチャの問題でリクエストをさばくことが出来ない問題本来はHTTP Keep-Aliveの文脈からも論ぜられますが、Keep-Aliveはnginxとかが維持してくれるし、刹那的HTTPに変換可能なのでアプリケーションの変更はいらない
C10Kの解法並行プログラミングの導入並行プログラミングの導入イベント駆動(Node.js, AnyEvent, etc...)1プロセスで複数の接続を扱うために、処理を細切れにして動かすコールバック地獄になる...
ネイティブスレッド(Java族, C族, etc...)データ競合...マルチスレッドプログラミングはバグを知るのが難しい...forkよりはマシだけれどネイティブスレッドも重いからスレッドプールが必要なのよねえ今のPerl5では使えない(そもそもithreadは……)ithreadはメモリ共有モデルではない
軽量スレッド(Erlang/OTP, Elixer, Go, etc...)もっとも今まで通りに書けるが1プロセスで複数の処理が並行で走るのは変わらないので、バグ潰しが難しい
リアルタイム通信をやりたいリアルタイム通信をやりたいだけでアプリケーション全体だけでアプリケーション全体の言語やプログラミングパラの言語やプログラミングパラダイムを変更するのは理にかダイムを変更するのは理にかなっているかなっているか
わからん!わからん!場合による!!場合による!!自分で考えてくれ!!!自分で考えてくれ!!!
イチから作るか、動いている物に導入するか今の言語で出来るかどうか移行先の言語やパラダイムに精通している人がいるかどうかカネと時間があるかどうか作ったもののパフォーマンスが出るか、検証できるかどうか
別件:アプリケーションと密結別件:アプリケーションと密結合の問題合の問題アプリ更新したいから再起動するぞ!!!!↓プロセスが殺されるから全部一旦切るで~~↓え??????クライアントの再接続が一気に起こります
別の解法 =>サーバプッシュだ別の解法 =>サーバプッシュだけ別にやるけ別にやるNode.jsでSocket.IOしゃべってRedis pub/subでWebAppからつなげるGoで似たようなことをgRPC/SSEでやるActionCableRuby on RailsでWebSocketをしゃべる機能kuiperbelt <-本日はコレの話openfresh/plasma
第1章まとめ第1章まとめ同期的Preforkでは大量の寿命の長い接続をさばくのは困難である現実的にやるには並行プログラミングを導入するか、サーバプッシュだけ別のサーバでやる手法が考えられる
第2章第2章kuiperbeltとはkuiperbeltとは
kuiperbeltとはkuiperbeltとはWebSocketとHTTP/1.1の相互変換プロキシサーバby独立したWebサーバデーモンとして動作するGoで書かれているが使うのにGoの知識は要らないmackee/kuiperbeltREADME.ja.md
機能機能WebSocketの接続を維持する 及び 現実的にやるための仕組みの提供接続認証の方法スケールアウト戦略
思想思想WebSocketをしゃべるサーバを直接インターネットに晒さない。ロードバランサーに入れる他の事例では直接インターネットにさらしているケースが多い大量接続に耐えるWebSocketアプリケーションサーバ構築のコツ - pixiv inside
何故ロードバランサーに入れ何故ロードバランサーに入れる?る?LBに入れることで管理コストを下げるWebAppとWebSocketを疎結合にする
思想思想独自のデータ構造を作らない。HTTPの仕組みをそのままできるだけ使うHTTPがしゃべれるアプリケーションであれば適用可能
接続時の通信の流れ接続時の通信の流れ
試し方試し方から最新版のバイナリを落としてパスを通すGoのコマンドやデーモンはバイナリリリースがあれば自分でビルドせずバイナリリリースを使うのが好ましいのチュートリアルを読むか、 のPlackアプリを読むと良いGitHub ReleasesREADME.ja.md example
認証用のHTTPサーバを作るmy $app = sub {my $env = shift; my $req = Plack::Request->new($env);# some authentication processesmy $endpoint = $req->header("X-Kuiperbelt-Endpoint");my $session_id = generate_uuid();$KVS->set("$user_id:kuiperbelt_session",{ session => $session_id, endpoint => $endpoint });return [200,["Content-Type" => "application/json","X-Kuiperbelt-Session" => $session_id,],['{"message": "Hello WebSocket World!"}'],
認証用のアドレスを設定して起動さっきのを で保存して起動するkuiperbelt用configに立てたサーバのアドレスを設定ファイル( )に書く書いた設定を使ってkuiperbeltを起動する$ plackup --port=5000 app.psgicallback:connect: http://localhost:5000/$ ekbo -config=config.yml
メッセージ送信時の通信の流れ
Perlからメッセージを送るなんでもいいけれどHTTPでAPI叩けば良いです。Bodyの形式は問いません。# $target_user_id 対 $message 送my $furl = Furl->new;my $session =$KVS->get("$target_user_id:kuiperbelt_session");$furl->post($session->{endpoint} . "/send",[ "X-Kuiperbelt-Session" => $session->{session_id} ],$message,);
もともとの動機もともとの動機Perl5でWebSocketとかSSEを喋りたかったが、AnyEventとかMojo使えばPerl5でWebSocket使えるやん?あ、そのせいでイベント駆動プログラミングをしないといけなくなった……DBのトランザクションかけようとしたら、混ざるんじゃないのこれ?
そこで思いついたそこで思いついたWebSocketの接続を維持するだけのサーバを作ればよいのでは?それが普通のHTTP APIの口を持っていて、アプリケーションサーバからAPIを叩いてWebSocketに送るメッセージを送信すればよいのでは?
先人の知恵APNS::AgentAPNS::AgentiOS端末にプッシュ通知(スリープしていても届くと音がなって振動するやつ)をサーバから送るAPIが独自の生TCPかつ、非同期型Net::APNS::Extendedなどの同期型のモジュールもあるが、PreforkのWebAppから送ると当然詰まる
先人の知恵APNS::AgentAPNS::AgentAnyEvent::APNSと言うモジュールもあるが、イベント駆動にしないとAnyEvent::APNSを単体デーモンに切り出したのがAPNS::Agent
仕組み仕組み1. HTTP APIでWebAppからメッセージを受け取る2. AnyEvent::APNSを使ってAppleのサーバにつながっているソケットにメッセージと送り先トークンを書き込む3.プッシュ通知が届くHTTPでiOSのpush通知を送れる未来がやってきた(APNS::Agentの紹介)
偉大なところ偉大なところやることに合わせたアーキテクチャを使い分けれるようにデーモン化して切り出した一つのことをうまくやる 土管に徹するUNIX哲学HTTP APIというどの言語でも持っているようなインターフェイスで触れる現在では というデーモンに思想は引き継がれてAndroidにも通知が送れるようになっていますGunfish
これだ!と思ってWebSocketこれだ!と思ってWebSocketでも同じことをやろうとしたでも同じことをやろうとした
kuiperbeltという名前の由来kuiperbeltという名前の由来もともとはロングポールをやろうと思ってCometって言う別名もあるので、じゃあ彗星の起源ということでエッジワース・カイパーベルトから名前を取った同じような言葉でオールトの雲というのがありoortは短いから良いなあと思ったんですがこの名前のOSSは既にあった
kuiperbeltはWebSocketの接続kuiperbeltはWebSocketの接続を維持するデーモンとしてどを維持するデーモンとしてどのような課題の解決を行ってのような課題の解決を行っているかいるか
一般的なWebSocketの接続を一般的なWebSocketの接続を考える考える1.クライアントから普通のHTTPリクエストが来る2.サーバは繋いで良いクライアントだったらUpgradeする3. WebSocket接続を開始する4.適宜送ったり、受け取ったりする5.切りたいときに切る
課題課題認証の方法誰に送るかの指定方法送る内容の形式切断検知
認証の方法認証の方法kuiperbeltに認証機構を実装するのも考えたが……ユーザDBに接続する? OAuthする?土管に徹したいので認証機構は他所に投げたい一発目は普通のHTTPリクエストだからこれをWebAppにプロキシすればいいのでは?
コールバックを投げるコールバックを投げるクライアントからのHTTPリクエストをそのままバックエンドのWebAppに飛ばす200だったらWebSocket開始認証はWebAppに委ねることでkuiperbeltの実装自体はコンパクトになり堅牢になる
宛先の指定方法宛先の指定方法接続ごとに識別子を指定して欲しい気持ち接続の識別子は認証時のWebAppのレスポンスのヘッダにという名前で送ってもらってそれを使うメッセージを投げるときも というヘッダに入れてもらう
BodyはAppのものBodyはAppのものWebAppがメッセージを投げるときに叩くHTTP APIのリクエストのBodyの内容をそのままWebSocketに書き込んでいるを指定すればバイナリも突っ込めるmsgpackを使っているプロジェクトもあります
BodyはAppのものBodyはAppのものつまり何をどう送るかはWebAppに委ねられている =>土管に徹する思想からなので送信オプションとかはHTTPヘッダを使うようにしている
土管に徹すると何が良いか土管に徹すると何が良いか言語非依存になるPerl専用のデーモンだとユーザが広がりにくい。OSSプロダクトにおいてユーザ数というのは力になる役割が明確になるアプリケーション非依存になる
切断検知切断検知WebSocketはTCPなのでお行儀よく切断すれば接続が切れたことを検知することが出来るアプリケーションによっては切断を知ることは重要なのでクライアントから接続が切れたら設定したコールバックURLをkuiperbeltが叩くように設定できる
が、お行儀よく切断されないことはモバイルの世界だと大いに有り得るので、すぐに切れたことを検知する事はできない
kuiperbeltでの検知kuiperbeltでの検知メッセージを送ろうとして書き込めなかったら切断と認定無通信時間が一定秒数以上続けば切断と認定WebSocketのpingフレームをクライアントが一定間隔で送ってくることが前提の機能
無通信時間による切断検知は使用プロジェクトの要望で追加した機能です
第2章まとめ第2章まとめWebSocketに必要な要素をステートレスHTTPに分解すると普通のWebAppでもWebSocketを扱えるようになる土管に徹することで様々なWebAppでも使えるようになる
第3章第3章本番投入への道本番投入への道
作り始めたときは趣味だった作り始めたときは趣味だった社内でリアルタイムゲームのプロジェクトが立ち上がったときに「ぼくがかんがえたさいきょうのWebSocketアーキテクチャ」を披露し、既に実装はありますと言ったら採用された
趣味プロダクトはそのままで趣味プロダクトはそのままでは本番投入はおそらく出来なは本番投入はおそらく出来ない……い……
そもそもどこのサービスにも投入されていないユースケースが妄想かもしれないProof of Conceptのつもりこういうの考えてみたけれどどう?っていう実装
プロジェクトへの導入をしながら機能を追加していったスケールアウト用の機能ベンチマーク監視API,ログ
スケールアウト(複数台)戦略スケールアウト(複数台)戦略何故複数台構成に出来るようにするか?WebSocket受ける君が1台だと1台落ちるだけでサービス全体が障害に陥る(HA目的)
何故複数台構成に出来るようにするか?1台のスペックが上げられる上限は決まっているまたAWSだとスペックは倍倍に上がっていくのでちょうどいいスペックに設定できないというのもあります
先行事例先行事例PCゲームなどのオンラインゲームそれぞれのインスタンスがPublic IPを持っていてクライアントに接続先IPを指定同じ部屋の人は同じインスタンスに固めるなどが出来るインスタンス落ちたときのことをAppが考えないといけない
先行事例先行事例Node.jsやActionCableなどRedis pub/subを用いる(メッセージキュー)WebSocketの接続を持つサーバが部屋ごとに作られたtopicをsubscribeしておいて、WebApp側が部屋のtopicに対してメッセージをpublishするRedisさえスケールさせれば、そうRedisさえ落ちなければ……
やりたくないことやりたくないことWebAppからつなぐ先を指定させたくない外から繋げられるPublic IPがそれぞれのインスタンスに必要負荷の偏りが生じる
やりたくないことやりたくないこと特定のミドルウェアに依存したくないRedis pub/subを使うとスケール限界がRedisの性能になるインフラ構成でRedisが必須になる
必要なのはどの接続がどのインスタンスどの接続がどのインスタンスにあるかをクライアントにあるかをクライアント(WebApp)が知っておくこと(WebApp)が知っておくこと
KVSのシャーディング方法をKVSのシャーディング方法を参考にする参考にするlibketamaとかtwemproxyとかクライアントがキーから値が入っているホストを特定して取ってくるKVS自体は分散されていることを別に知らなくても良い
WebSocketの場合WebSocketの場合どこにつなぐべきかを接続時に指定すれば同じようなことは出来るが……やりたくないリストに入っている前段にLBを立てる場合はどこに繋ぐかはランダムなので接続識別子からどこに入っているかを導き出すことは出来ないスティッキーセッションを使えばランダムじゃなくすることは出来ますがどこに繋ぐべきかを指定するパターンと同じになる
接続コールバックにアドレス接続コールバックにアドレスを埋め込むを埋め込むcallbackのリクエストにkuiperbeltは以下のヘッダを付けるX-Kuiperbelt-Endpoint: 192.168.100.1:9180
接続コールバックにアドレス接続コールバックにアドレスを埋め込むを埋め込むこれをWebApp側は接続識別子と一緒に入れておいてどこにつながっているかをKVSとかに保存しておく
Q.結局KVS使っているのでQ.結局KVS使っているのでは???は???A.ユーザIDと接続識別子の変A.ユーザIDと接続識別子の変換にKVSとか使うでしょ換にKVSとか使うでしょ
Q.識別子=ユーザIDとかにすQ.識別子=ユーザIDとかにすればKVS必要じゃなくない?ればKVS必要じゃなくない?A. 1人のユーザが複数のWS接A. 1人のユーザが複数のWS接続持つケースがあって続持つケースがあって
これでスケールの上限はこれでスケールの上限はWebApp側の識別子解決の方WebApp側の識別子解決の方法に依存する法に依存する
もしくはネットワークトポロもしくはネットワークトポロジがフルメッシュになっていジがフルメッシュになっているのでそこも性能限界になりるのでそこも性能限界になりうるうる
kuiperbeltインスタンスが持っているSend APIへの接続数はWebApp側インスタンスの数に依存する超大規模環境でWebAppが多いと階層構造にするみたいなのが必要そう?Perlはメモリを食わないので横の台数が少なくて済むので便利
どこがボトルネックになりうどこがボトルネックになりうるかをちゃんと知るのが大切るかをちゃんと知るのが大切
ベンチマークベンチマーク理論上OKでも実際どうなのかというのは実際に動かさないとわからない....実際のアプリケーションで想定される負荷をかけてみるミドルウェアのベンチをとっとけば他のアプリでも採用がすんなりいきやすい
ベンチの条件ベンチの条件クライアントは指定した数だけWebSocketのコネクションを貼る降ってきたメッセージはログに吐くWebApp側は接続中の接続の識別子を取ってきてメッセージを送るここは実アプリの挙動を真似すると良いn人に同じメッセージを送るパターンメッセージサイズetc...
結構古いバージョンの結果なので今とは性能が違う場合があります サーバは2コア2台ですclients event CPU Mem(RSS) msgs/sec1000 1000 13% 68MB 26665000 2000 24% 366MB 53885000 4000 49% 355MB 1091610000 10000 120% 688MB 2666610000 1666 22% 737MB 4466
わかることわかることメモリ使用量はクライアント数に依存するCPUは秒間メッセージ数に依存するここからチューニングポイントが分かる
さらにクライアント数を伸ばしていくと...clients event CPU Mem(RSS) msgs/sec20000 3333 47% 1400MB 866640000 6666 130% 1500MB(4a383f2) 17880
40000 clientsのところでメモリ使用量が減っている?けれどメモリが結構辛くなったのでチューニングを行いました
メッセージを書き込むときに確保するバッファサイズを最適化したuse io.CopyBuffer by @fujiwaraio.Copy makes 32KB buffer automatically, but itis too large for kuiperbelt use case.
これらベンチマークはfujiwaraさんにやっていただきました
監視系の機能も@fujiwaraさんに入れてもらったものが多い
監視系のAPI監視系のAPI時間を返すだけのAPIロードバランサーからのヘルスチェックに必要kuiperbelt固有の監視項目を返す生きてるコネクション数/エラー数はこれを使っている運用と監視は切っても切れない関係にあることがわかりますねmackerel-plugin-kuiperbelt
ログはガンガン出すログはガンガン出すただしパフォーマンスにも影響するのでログレベルで調整できるようにする
ログレベルログレベルPerlだと がよく使われているCRITICAL, WARN, INFO, DEBUGに出力するログを分類するkuiperbeltはDEBUGにガンガンログを出している => WebSocketのデバッグがめんどくさいので開発中に躓いて出したログはだいたい残しているその代わりに運用中にはDEBUGやINFOのログは切っているLog::Minimal
ログうるさすぎ問題ログうるさすぎ問題アクセスログをINFOで出していたけれどログ流量が半端なくて他のログが埋まって機能しなくなった……そもそも前段にnginxやALBがいるときはそっちでログが出せるのでは?導入プロジェクトの人の要望から オプションを追加した
デバッグ時には必要だけれど運用時には必要ないログみたいなのがある
そしてプロダクションへそしてプロダクションへカヤックでゲームをリリースするときは事前に専用のシナリオで負荷試験を行っていますkuiperbelt導入プロジェクトはkuiperbelt含めた負荷試験も行っているそれもクリアして、本番で実際に動いています
これでこれでProduction ReadyProduction Readyと言えると言える
第3章まとめ第3章まとめ本番運用できるミドルウェアにするためには、いくつかの機能追加やベンチマークなど作り始めとは違う視点が必要ミドルウェアは信頼されてなんぼなので、これらは必要な労力だと思う
まとめまとめミドルウェア作りは産みの苦しみもあるけれど、概念作れるし小説の世界観設計に似ている俺がミドルウェアを作るからその上で面白いゲームを作ってくれと日々思っています一回概念を提唱するとそれについてくる人がいて助かるThanks for @fujiwara, @shogo82148, and Users!
提供提供面白法人カヤックでは攻めのゲームづくりを一緒にする仲間を募集しています!!!PerlとかGoとかUnity/C#とか。今やってなくても応募はタダ!!
その他やったことその他やったこと本当にWebSocketをスマホが喋れるか検証レースコンディション解消Docker対応ALBが半死したときにhttp2になっててkuiperbeltも巻き込まれた話
Docker対応Docker対応カヤックでもECSにWebAppを載っけるのを進めていますWebAppをコンテナに押し込んだら、kuiperbeltもコンテナに押し込みたいが、kuiperbeltを複数台構成にしたときは個別のコンテナにつながるホストとポートをkuiperbelt自身が知る必要がある
自分につながるホスト名の取自分につながるホスト名の取得得オプションにホスト名orIPアドレスを書くか、無ければを使うコンテナ内ではコンテナ起動時のネットワーク設定によってはで正しいものが取れない
自分につながるホスト名の取自分につながるホスト名の取得得コンテナ起動時にconfigを書き換えてX-Kuiperbelt-Hostを正しいものにしている以上はECSの場合です。k8sとかの場合はまだ試行錯誤中です。誰か助けてくれ
その他課題その他課題kuiperbelt単体でスケールアウトさせる需要の声ドキュメント日本語ドキュメントはちゃんと書いてあるのだがロゴがほしい社外のユーザ現状社内しかユーザはいない
これからの展望これからの展望ekbo-router(cluster mode)上り対応(需要があれば)WebSocket以外の対応WebRTCが個人的に有望そうWebSocket over HTTP/2
Net::Kuiperbelt, Alien::KuiperbeltHTTP喋れば良いとは言えライブラリがあったほうがみんな使いそうPlack::Middlewareとかで実現しても良いですができるのか?
未実装の未来の話: ekbo-router未来の話: ekbo-router専用デーモンをWebApp側に立ててプロキシする識別子の発行はekbo-routerが行う
未来の話: ekbo-router未来の話: ekbo-routerprefix base routingkuperbeltインスタンスと識別子のprefixを対応させるここは分散KVSが必要......WebAppはどこに接続があるかを気にせずにローカルのekbo-routerに投げれば良い