Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Erlang/OTP と ejabberd を活用した Nintendo Switch(TM)向け プッシュ通知システム 「NPNS」の 開発事例

Erlang/OTP と ejabberd を活用した Nintendo Switch(TM)向け プッシュ通知システム 「NPNS」の 開発事例

任天堂 ネットワークシステム部
わたなべ たいよう
渡邉 大洋

私たちは、家庭用ゲーム機 Nintendo Switch (TM) 向けに、プッシュ通知のシステム「Nintendo Push Notification Service (NPNS)」を開発・運用しています。
NPNS には常に1000万台超のデバイスが接続していますが、日々安定してさまざまな通知を送り続けています。

NPNS の全体像およびインフラ面の構成については別の機会にお話ししたことがありますが、今回の Erlang and Elixir Festでは、特に NPNS の常時接続部分の基盤技術として採用している Erlang/OTP、およびその上で動作する OSS である ejabberd に重点を置いて説明します。

具体的には、NPNS に求められる要件に対して、
・Erlang/OTP および ejabberd を選定するに至った理由
・事前の負荷試験/障害試験を受けて行った NPNS ならではのカスタマイズ
・実運用上で直面した課題とその分析・対策の内容
などをお話しします。
NPNS で対応してきた課題の多くはWebシステム開発において普遍的なものですのでErlang/OTP になじみが無い方にも参考になる内容があると考えております。

なお NPNS は現在、企画・開発・運用をすべて社内のメンバーで行っていますので、任天堂のネットワークサービス開発における考え方や実際の開発の雰囲気の一端を皆様に感じとっていただける機会になれば幸いです。

elixirfest

June 01, 2019
Tweet

More Decks by elixirfest

Other Decks in Technology

Transcript

  1. 1 Erlang/OTP と ejabberd を活⽤した Nintendo Switch(TM)向け プッシュ通知システム 「NPNS」の 開発事例

    任天堂 ネットワークシステム部 渡邉 大洋 わ た な べ た い よ う
  2. 3 発表者の紹介 渡邉 ⼤洋 キャリア入社、6年目 経歴: Web開発 → 組み込み家電 →

    Web開発(現職) 任天堂でのお仕事 ニンテンドーネットワークID サーバ NPNS サーバ 好きなプログラミング言語 Erlang / Elixir / Ruby
  3. 8 NPNS (Nintendo Push Notification Service) ひとことで Switch向けプッシュ通知システム(同カテゴリ: APNS, FCM)

    API + 常時接続 の構成 常時接続=プッシュ通知では必須、同時接続数が激増 採⽤技術 API部分: Ruby on Rails 常時接続部分: Erlang/OTP + ejabberd 開発体制 要件定義・開発・運用まで社内で対応、数人の小規模チーム
  4. 12 個別指定通知 トピック指定通知 (PubSub) みんなに 配りたい情報 通知の種類 ユーザに 見てほしい情報 •

    フレンドのオンライン通知 • ゲームサーバのメンテナンス通知 プレイ中のみ 本体システムに 届けたい情報 • PC版Nintendo eShopで購入したソフトのDL開始 • 『Nintendo みまもり Switch』アプリでの設定変更 接続中は常に (一時保存も)
  5. 13 通知の種類 個別指定通知 トピック指定通知 (PubSub) ユーザに 見てほしい情報 • フレンドのオンライン通知 •

    ゲームサーバのメンテナンス通知 プレイ中のみ 本体システムに 届けたい情報 • PC版Nintendo eShopで購入したソフトのDL開始 • 『Nintendo みまもり Switch』アプリでの設定変更 接続中は常に (一時保存も) みんなに 配りたい情報 • 本体更新/パッチ配信のおしらせ • ゲームニュース 接続中は常に + 毎ログイン時 (Retain方式)
  6. 14 通知の種類 個別指定通知 トピック指定通知 (PubSub) ユーザに 見てほしい情報 • フレンドのオンライン通知 •

    ゲームサーバのメンテナンス通知 プレイ中のみ 本体システムに 届けたい情報 • PC版Nintendo eShopで購入したソフトのDL開始 • 『Nintendo みまもり Switch』アプリでの設定変更 接続中は常に (一時保存も) みんなに 配りたい情報 • 本体更新/パッチ配信のおしらせ • ゲームニュース 接続中は常に + 毎ログイン時 (Retain方式)
  7. 15 XMPP Cluster XMPP Cluster 全体の構成 XMPP Cluster Provider API

    Consumer API 通知送信者 常時接続 API XMPP HTTP HTTP NPNS
  8. 21 ejabberdの特徴 XMPP サーバソフトウェア Erlang + C言語拡張(NIF,driver) XMPP Core +

    多数の XEP (XMPP拡張規格) に対応 コンフィグによるモジュラビリティ 機能の選択 ストレージミドルウェアの選択 クラスタ化に対応 フルメッシュ構造 ノード間で通知を直接送信
  9. 23 ejabberdクラスタの構造 Redis MySQL ユーザ情報 トピック情報 セッション情報 SQS inner ejabberd

    inner ejabberd outer ejabberd outer ejabberd outer ejabberd outer ejabberd クラスタの内側/外側でノードの役割を分けて、効率化を狙う 内側 inner ejabberd 通知をejabberdクラスタに⼊れる役割 CPU バウンド (通知量に⽐例) 外側 outer ejabberd 通知をSwitchに渡す役割 メモリバウンド(接続数に⽐例)
  10. 24 機能 ejabberd の機能を使って、短期間で実現 使った機能 個別指定通知: Message トピック指定通知: PubSub (XEP-0060)

    一時保存: Offline Message (XEP-0013) 稼働状況で送り分ける: Presence (使っていない機能) フレンドグラフ: Roster クラスタ間の転送: Server-to-Server (XEP-0288)
  11. 25 性能 求める性能 [キャパシティ] 1ノードに大量につなぎたい 接続キャパシティ → 50万~100万ぐらい [スループット] ワーストケースでもシステムが破綻しない

    一斉ログイン → 30分くらい 一斉配信 → 軽負荷で 一斉切断 → すぐ再接続できるように [遅延] 大量のログを低遅延で書きたい ログ出力 → 他の処理に影響を与えないように
  12. 26 負荷試験結果 項⽬ 期待する性能 当初の測定値 接続 キャパシティ 最低50万台接続 (r3.largeを想定、メモリ 15.25GiB)

    30万 ⼀⻫ログイン 50万台が30分で接続完了 = 277接続/秒 150接続/秒 ⼀⻫配信 軽負荷で、50万台に30分以内に配信 負荷:⼤ ⼀⻫切断 切断の処理は数分以内に完了 30分 ログ出⼒ 軽負荷、遅延は数分以内 2時間 ejabberdは汎⽤性の⾼い構造 → NPNSの機能や構成に特化して性能改善を
  13. 28 接続キャパシティ [BEFORE] [現象] ⼤量接続時、Erlang VM のメモリ消費が多い 1接続あたりの消費メモリを減らして、接続数を増やしたい ejabberd は

    hibernate を使っているが、それでもまだ多い hibernate = プロセスのメモリを切り詰める ≒ 布団圧縮袋 system 領域が processes 領域の6倍程度大きい 調査: instrument:memory_status(types) で内訳を集計 [原因] C拡張の中で確保しているメモリだった XMLパーザ (expat) TLS (OpenSSL)
  14. 29 接続キャパシティ [AFTER] [対策] C拡張部分の使い⽅を分析&改造 XMLパーザ (expat) à hibernate 前に一旦使用終了して解放

    TLS (OpenSSL) à OpenSSLパラメータ最小化、不要バッファの解放 [効果] メモリ削減 & 接続数増加 1接続あたり 40% に削減 接続可能上限で表すと 30万 → 75万に増加
  15. 30 ⼀⻫ログイン [BEFORE] [現象] 処理詰まりが発⽣ (赤矢印の箇所) 1. ソケットの accept 2.

    トピックアイテムの配信 調査: etop (Erlang top) [原因] プロセスボトルネック 1つのプロセスが大量の リクエストを受ける構造 大量アクセスに不向き おう listen 中 listener receiver accept 後 c2s conn pool Redis MySQL conn pool process topic配信 (クライアントの数だけ生成) conn pool process conn pool outer ejabberd 軽量 プロセス ソケット メッセージ プロセス起動
  16. 31 ⼀⻫ログイン [AFTER] [対策] workerを都度⽣成 1. ソケットの accept à launch

    worker 2. トピックアイテムの配信 à topic配信 worker [効果] 詰まり解消 ログインレートも向上 150 → 300 接続/秒 listen 中 listener receiver accept 後 c2s conn pool Redis MySQL (クライアントの数だけ生成) launch worker topic配信 worker conn pool process conn pool process conn pool outer ejabberd
  17. 32 ⼀⻫配信 [BEFORE] [現象] 配信時に⼤量通信&⾼負荷 通信① MySQL → inner (購読者一覧)×1

    通信② Redis → inner (セッション情報)×購読者 通信③ inner → outer (通知)×接続済みの購読者 調査: メトリクスとErlangトレース [原因] 個別配信と同じ送り⽅ inner 側で送信先を選別 inner ejabberd inner ejabberd outer ejabberd outer ejabberd outer ejabberd outer ejabberd Redis MySQL ユーザ情報 セッション情報 ① ② ③ SQS
  18. 33 ⼀⻫配信 [AFTER] [対策] outer 側で選別 XMPPログイン時に、 c2sプロセス辞書に「購読情報」保存 配信時、inneràouterにばらまき その後outer内のプロセスを全走査

    (processes/0, process_info/2) 通信① inner→outer (通知)×outer数 [効果] 負荷/通信量減 スケール性も向上 (outer増加で配信レートも増加) inner ejabberd inner ejabberd outer ejabberd outer ejabberd outer ejabberd outer ejabberd Redis MySQL ユーザ情報 セッション情報 ① c2s process {jid, user1} {sub,[1,2,3]} c2s process {jid, user42} {sub,[2,3,5]} プロセス辞書
  19. 34 ⼀⻫切断 [BEFORE] [現象] セッション処理が多い 切断数がそもそも多い (最悪値は、全接続数が一斉に切断) 切断時のセッション操作が多い Redis上ではHASHに保存 HASHを取得してから削除

    調査: Erlangトレース [原因] XMPPはマルチログイン 同一IDで複数デバイスから接続可能 NPNSは 1 ID = 1 セッションなのに… Redis c2s 1. 自分が最後か? 2. 自IDの全セッションを取得 (HASH) 3. 自分のセッションだけ削除 (HASH) 【XMPP切断時のセッション操作フロー】
  20. 35 ⼀⻫切断 [AFTER] [対策] 切断時は「なにもしない」 シングルログイン構造に変更 他のデバイスを気にしなくてよい Redis上ではTEXTに セッションにEXPIREを設定 ログイン中は定期的にRefresh

    切断後、いつかは自動で消える [効果] 切断処理の短縮と軽量化 30分以上 → 1分 Redis c2s (なにもしない) 【XMPP切断時のセッション操作フロー】
  21. 36 ログ出⼒ [BEFORE] [課題] メトリクス⽤にログを⼤量に出⼒したい データ間引きはしたくない/一時的な遅延は許容する 通常の logger とは別系統にしたい [対策]

    専⽤ログサーバプロセスを作成 ログのBINARY文字列を受け取り、ファイルに書き込むだけ OTPのgen_serverビヘイビアを使用 file:open/2 で raw, delayed_write を使う c2s `` log_server log file ログBINARY を cast file:write/2 c2s `` c2s message queue
  22. 37 ログ出⼒ [BEFORE-2] [現象] 処理時間が⾮線形に増加 1万ログ → 1秒未満 50万ログ →

    2時間! その間、1コアを100%使用 調査: トレース、プロファイル [原因] reduction処理が重い︖ erlang:bump_reductions/1→37% gen_server:try_dispatch/3→62% log_serverがファイル出力をや めると、処理遅延は起きない 00:00 00:15 00:30 00:45 01:00 01:15 01:30 01:45 02:00 1万 5万 10万 15万 20万 25万 30万 35万 40万 45万 50万 ログ処理にかかる時間
  23. 38 ログ出⼒ [AFTER] [対策] ダムをつくる ダムはほぼ何もしない 後段のメッセージキュー 長を監視、放流を制御 process_info/2 を利用

    それ以外はsleep [効果] 処理時間を⼤幅に短縮 50万ログの処理時間が 2時間 → 30秒 他に良い方法あればアドバイスを! c2s ダム log file ログBINARY を cast file:write/2 log_server ログBINARY を cast c2 c2s message queue message queue ここは数⼗万たまっ ても遅くならない ここは最⼤1万ま でにおさえる
  24. 39 ここまでのまとめ 項⽬ 期待する性能 BEFORE AFTER 接続 キャパシティ 最低50万台接続 (r3.largeを想定、約16GiB)

    30万 75万 ⼀⻫ログイン 50万台が30分で接続完了 = 277接続/秒 150接続/秒 300接続/秒 ⼀⻫配信 軽負荷で、50万台に30分以内に配信 負荷:⼤ 負荷:⼩ ⼀⻫切断 切断の処理は数分以内に完了 30分 1分 ログ出⼒ 軽負荷、遅延は数分以内 2時間 30秒 ⼗分な性能を確保できた(と思われる)
  25. 42 (メッセージループ) [BEFORE] Redis c2s 1. 自分が最後か? 2. 自IDの全セッションを取得 (HASH)

    3. 自分のセッションだけ削除(HASH) 4. だれかいないか? 5. 残通知を転送 この部分を ⾒落としていた 未処理の通知が残っている場合
  26. 43 (メッセージループ) [BEFORE] [AFTER] Redis c2s 5. 残通知を転送 ここで メッセージ

    ループが 発⽣ ↓ ここを⽌めれば 解決か︖ (なにもしない) 6.だれかいないか? → 自分 7. 残通知を転送 ... 4. だれかいないか? → 自分 8.だれかいないか? → 自分 9. 残通知を転送 未処理の通知が残っている場合 Redis c2s 1. 自分が最後か? 2. 自IDの全セッションを取得 (HASH) 3. 自分のセッションだけ削除(HASH) 4. だれかいないか? 5. 残通知を転送 この部分を ⾒落としていた 未処理の通知が残っている場合
  27. 45 Redis⾼負荷事件 [解決編] Erlang には hot code deploy 機能がある︕ 無停止で本番環境1台だけに適用(カナリアリリース)

    ただし手作業 (code:soft_purge/1, code:load_file/1) Erlang リモートシェルで接続して更新を実施 手順チェック×3人 → 予行演習×2 → 実施 Redis アクセス回数が激減、修正効果を確認 その後、通常の手順で全クラスタにデプロイ 早期解決︕ ユーザ影響無しで、余裕のあるうちに解決できた
  28. 48 トピック配信の負荷 [BEFORE] [現象]トピック配信時の outer ejabberd の負荷が 処理のわりに⾼い(ように思える) 全力で送るとCPUがMAXにはりついてしまうので、シーケン シャルに送信し、合間に

    timer:sleep/1 を入れている sleepの値とCPU利用率が相関しない 例: 20ms と 50ms でほぼ同じ負荷 不要にCPUパワーを使っているのでは? 調査: Erlang関連の情報収集 0% 5% 10% 15% 20% 25% 30% 35% 40% 45% 50% 0 10 20 30 40 50 トピック配信時の負荷 sleep 値 [ms] CPU利用率
  29. 49 トピック配信の負荷 [AFTER] [原因] VMスケジューラのビジーウェイト状態 「負荷の低いスケジューラスレッドがすぐスリープ状態にならないように、 Erlangスケジューラの扱うスレッドはしばらくビジーウェイト状態にな ります」 -- “Erlang

    in Anger” 日本語版, 5.1.2 CPU, p.37 ※この日本語訳は、昨年の Erlang & Elixir Fest を契機にプロジェクト 化されたとのこと。ありがとうございます。 [対策] VM パラメータを設定 +sbwt medium à very_short [効果] outer ejabberd の負荷減少 CPU使用率が 30% → 21% ↓ medium: 30% ↑ very_short: 21% 時刻 CPU利用率
  30. 50 セッションアクセス削減 [BEFORE] [課題] ログイン時のセッション 問合せを最適化したい 必須アクセスは2回のはず 既存セッション問合せ 新規セッション書き込み 実際は数回~十数回程度発生

    調査: Erlangトレース [原因] topic配信worker workerから通知を送るたびに セッションを問い合わせていた listen 中 listener receiver accept 後 c2s conn pool Redis MySQL (クライアントの数だけ生成) launch worker topic配信 worker outer ejabberd conn pool process conn pool process conn pool
  31. 51 セッションアクセス削減 [AFTER] [対策] pidを直接指定して送信 送信先の pid (プロセスID)を workerプロセス辞書に保存 プロセス辞書を見て、

    セッションを引かずにすぐ送信 [効果] 最適化完了 Redisコマンド発行数: 1/2に Redis CPU利用率: 4/5に listen 中 listener receiver accept 後 c2s conn pool Redis MySQL (クライアントの数だけ生成) launch worker topic配信 worker {c2s_pid, <0.x.y>} プロセス辞書 outer ejabberd conn pool process conn pool process conn pool
  32. 52 性能改善のまとめ まずは負荷試験 満足行く性能ならそこで終了 ボトルネックを探す メモリ不足 → C拡張に要注意 メッセージキュー詰まり →

    プロセスボトルネック 負荷 (reduction) が高い → profile 実施 (eprof, fprof) 改善策 並列性を上げる (worker作成、負荷を “外側” に移す) 処理量を減らす メッセージキューを積み過ぎない (前段にダムを作る)
  33. 55 outer 1ノードあたりの処理 InstanceType: r5.large (CPU 2コア、メモリ 16GiB) 接続数 最大50万、通常は10~20万程度(AZ障害への冗長対応)

    個別指定通知 200通/秒 トピック指定通知 100〜400通/秒 ※CPU負荷に配慮して低めに抑えている
  34. 57 要件のふりかえり スケーラビリティ 1億接続に備える ... クラスタ分割+性能改善で実現 送信対象 プレイ中本体、およびスリープ中の本体 ... Presenceで送り分け

    遅延 ~数秒(正常時)、~数分(異常時) ... 元々の高い並列性で、混雑時も低遅延 通知の種類 個別通知/トピック通知 ... XMPP の機能で実現 サービス分析 大量のログを出力・収集 ... 専用ログサーバ + ダムで実現
  35. 62