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

B9bd75cf321e63c5399b99e36c527cff?s=128

elixirfest

June 01, 2019
Tweet

Transcript

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

    任天堂 ネットワークシステム部 渡邉 大洋 わ た な べ た い よ う
  2. 2 所属の紹介 任天堂 ネットワークシステム部 社内情報システム部門 自社ゲーム機向けネットワークサービス/ ゲーム連動サービスの開発・運用 主力言語は Java, Ruby

    必要に応じて技術選定 (今回の事例) 私は こちら
  3. 3 発表者の紹介 渡邉 ⼤洋 キャリア入社、6年目 経歴: Web開発 → 組み込み家電 →

    Web開発(現職) 任天堂でのお仕事 ニンテンドーネットワークID サーバ NPNS サーバ 好きなプログラミング言語 Erlang / Elixir / Ruby
  4. 4 今⽇お話しすること ü NPNSの概要 常時接続部分の ü 技術選定 ü サービス開始までの開発 ü

    サービス開始 ü サービス開始後の開発 ü ふりかえり
  5. 5 今⽇お話ししないこと ü インフラやクラウドの話 (興味がある⽅は AWS Summit Tokyo 2018 の資料を)

  6. 6 NPNSの概要

  7. 7 Nintendo Switch 世界 3474 万台 (2019年3⽉末時点)

  8. 8 NPNS (Nintendo Push Notification Service) ひとことで Switch向けプッシュ通知システム(同カテゴリ: APNS, FCM)

    API + 常時接続 の構成 常時接続=プッシュ通知では必須、同時接続数が激増 採⽤技術 API部分: Ruby on Rails 常時接続部分: Erlang/OTP + ejabberd 開発体制 要件定義・開発・運用まで社内で対応、数人の小規模チーム
  9. 9 NPNSの要件 スケーラビリティ 1億接続に備える 送信対象 プレイ中本体、およびスリープ中の本体 遅延 ~数秒 通知の種類 (次のページで紹介)

    サービス分析 大量のログを出力・収集
  10. 10 通知の種類 個別指定通知 トピック指定通知 (PubSub) ユーザに 見てほしい情報 本体システムに 届けたい情報 みんなに

    配りたい情報
  11. 11 ユーザに 見てほしい情報 • フレンドのオンライン通知 • ゲームサーバのメンテナンス通知 プレイ中のみ 本体システムに 届けたい情報

    個別指定通知 トピック指定通知 (PubSub) みんなに 配りたい情報 通知の種類
  12. 12 個別指定通知 トピック指定通知 (PubSub) みんなに 配りたい情報 通知の種類 ユーザに 見てほしい情報 •

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

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

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

    Consumer API 通知送信者 常時接続 API XMPP HTTP HTTP NPNS
  16. 16 XMPP Cluster XMPP Cluster 通知の流れ(個別&トピック) XMPP Cluster Provider API

    Consumer API 通知送信者 常時接続 API XMPP HTTP NPNS
  17. 17 XMPPクラスタ分割の理由 スケーラビリティ クラスタ追加でシンプルにスケール 負荷試験もクラスタ単位で 安定性 ノード数を抑える 障害の局所化

  18. 18 常時接続部分の 技術選定

  19. 19 XMPP 選定理由 TCPの常時接続は経験が無い いきなりこの規模で大丈夫か? 常時接続プロトコルを扱うOSS/SaaSの活用を検討 XMPPが適切では? 機能: 個別指定とトピック指定、両方の通知に対応 安定性:

    歴史が有り規格の変化が少ない 拡張性: XEP (XMPPの拡張プロトコル) が多数 実績: 大規模な同時接続の事例が豊富
  20. 20 ejabberd 選定理由 評価指標 ライセンス/提供機能/開発の活発度/ 導入実績/サポート 注⽬ポイント: クラスタ化の⽅式 Erlang: 処理系がクラスタ対応

    新規⾔語への対応は︖ 書籍での技術習得 チーム内での勉強会
  21. 21 ejabberdの特徴 XMPP サーバソフトウェア Erlang + C言語拡張(NIF,driver) XMPP Core +

    多数の XEP (XMPP拡張規格) に対応 コンフィグによるモジュラビリティ 機能の選択 ストレージミドルウェアの選択 クラスタ化に対応 フルメッシュ構造 ノード間で通知を直接送信
  22. 22 常時接続部分の サービス開始までの開発

  23. 23 ejabberdクラスタの構造 Redis MySQL ユーザ情報 トピック情報 セッション情報 SQS inner ejabberd

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

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

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

    30万 ⼀⻫ログイン 50万台が30分で接続完了 = 277接続/秒 150接続/秒 ⼀⻫配信 軽負荷で、50万台に30分以内に配信 負荷:⼤ ⼀⻫切断 切断の処理は数分以内に完了 30分 ログ出⼒ 軽負荷、遅延は数分以内 2時間 ejabberdは汎⽤性の⾼い構造 → NPNSの機能や構成に特化して性能改善を
  27. 27 改善の流れ 負荷試験でボトルネックを特定しつつ改善 現象/課題 ⇒ 原因 ⇒ 対策 ⇒ 効果

    のサイクル 「問題はひとつずつ解決していこう」
  28. 28 接続キャパシティ [BEFORE] [現象] ⼤量接続時、Erlang VM のメモリ消費が多い 1接続あたりの消費メモリを減らして、接続数を増やしたい ejabberd は

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

    TLS (OpenSSL) à OpenSSLパラメータ最小化、不要バッファの解放 [効果] メモリ削減 & 接続数増加 1接続あたり 40% に削減 接続可能上限で表すと 30万 → 75万に増加
  30. 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 軽量 プロセス ソケット メッセージ プロセス起動
  31. 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
  32. 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
  33. 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]} プロセス辞書
  34. 34 ⼀⻫切断 [BEFORE] [現象] セッション処理が多い 切断数がそもそも多い (最悪値は、全接続数が一斉に切断) 切断時のセッション操作が多い Redis上ではHASHに保存 HASHを取得してから削除

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

    切断後、いつかは自動で消える [効果] 切断処理の短縮と軽量化 30分以上 → 1分 Redis c2s (なにもしない) 【XMPP切断時のセッション操作フロー】
  36. 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
  37. 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万 ログ処理にかかる時間
  38. 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万ま でにおさえる
  39. 39 ここまでのまとめ 項⽬ 期待する性能 BEFORE AFTER 接続 キャパシティ 最低50万台接続 (r3.largeを想定、約16GiB)

    30万 75万 ⼀⻫ログイン 50万台が30分で接続完了 = 277接続/秒 150接続/秒 300接続/秒 ⼀⻫配信 軽負荷で、50万台に30分以内に配信 負荷:⼤ 負荷:⼩ ⼀⻫切断 切断の処理は数分以内に完了 30分 1分 ログ出⼒ 軽負荷、遅延は数分以内 2時間 30秒 ⼗分な性能を確保できた(と思われる)
  40. 40 サービス開始 =予期せぬ課題との出会い

  41. 41 Redis⾼負荷事件 メトリクスでRedisの負荷が⾼い!? 想定の5倍以上、負荷試験では見たことが無い このペースだと、2日後には Redis の負荷に余裕がなくなる Redisはスケールアップが難しい 原因は︖ おそらく、特定タイミングの切断で、メッセージループが発生

    「一斉切断」の改造が不十分だった?
  42. 42 (メッセージループ) [BEFORE] Redis c2s 1. 自分が最後か? 2. 自IDの全セッションを取得 (HASH)

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

    ループが 発⽣ ↓ ここを⽌めれば 解決か︖ (なにもしない) 6.だれかいないか? → 自分 7. 残通知を転送 ... 4. だれかいないか? → 自分 8.だれかいないか? → 自分 9. 残通知を転送 未処理の通知が残っている場合 Redis c2s 1. 自分が最後か? 2. 自IDの全セッションを取得 (HASH) 3. 自分のセッションだけ削除(HASH) 4. だれかいないか? 5. 残通知を転送 この部分を ⾒落としていた 未処理の通知が残っている場合
  44. 44 Redis⾼負荷事件 早期対応をどのように︖ 修正コードを書いたが、本当にこの問題か断定できない 常時接続サービスではお試しデプロイが難しい 本番環境でそのまま試せたらよいのに…

  45. 45 Redis⾼負荷事件 [解決編] Erlang には hot code deploy 機能がある︕ 無停止で本番環境1台だけに適用(カナリアリリース)

    ただし手作業 (code:soft_purge/1, code:load_file/1) Erlang リモートシェルで接続して更新を実施 手順チェック×3人 → 予行演習×2 → 実施 Redis アクセス回数が激減、修正効果を確認 その後、通常の手順で全クラスタにデプロイ 早期解決︕ ユーザ影響無しで、余裕のあるうちに解決できた
  46. 46 サービス開始後の開発 常時接続部分の

  47. 47 サービス開始後も継続的に改善 すでに機能/性能要件は満たしているが、 メトリクスやアラートなどから改善の余地を探す 「トラブルを防ぐのが私の仕事」

  48. 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利用率
  49. 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利用率
  50. 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
  51. 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
  52. 52 性能改善のまとめ まずは負荷試験 満足行く性能ならそこで終了 ボトルネックを探す メモリ不足 → C拡張に要注意 メッセージキュー詰まり →

    プロセスボトルネック 負荷 (reduction) が高い → profile 実施 (eprof, fprof) 改善策 並列性を上げる (worker作成、負荷を “外側” に移す) 処理量を減らす メッセージキューを積み過ぎない (前段にダムを作る)
  53. 53 Switch 接続数増加への対応 インフラ増強 クラスタ数を増やす クラスタ内のノード数を増やす アプリ改善 性能向上 = 1台あたりの処理能力増

    = 台数を削減可能 台数削減=障害発生数も削減 インフラコストと運用コストを削減
  54. 54 サービス規模 1000万+ 同時接続 約20億 通/⽇ クラスタ停止は一度も発生していない

  55. 55 outer 1ノードあたりの処理 InstanceType: r5.large (CPU 2コア、メモリ 16GiB) 接続数 最大50万、通常は10~20万程度(AZ障害への冗長対応)

    個別指定通知 200通/秒 トピック指定通知 100〜400通/秒 ※CPU負荷に配慮して低めに抑えている
  56. 56 振り返り 常時接続部分の

  57. 57 要件のふりかえり スケーラビリティ 1億接続に備える ... クラスタ分割+性能改善で実現 送信対象 プレイ中本体、およびスリープ中の本体 ... Presenceで送り分け

    遅延 ~数秒(正常時)、~数分(異常時) ... 元々の高い並列性で、混雑時も低遅延 通知の種類 個別通知/トピック通知 ... XMPP の機能で実現 サービス分析 大量のログを出力・収集 ... 専用ログサーバ + ダムで実現
  58. 58 Erlangとejabberdを採⽤してみて 機能 個別指定とトピック指定を短期間で実現できた クラスタ化とその維持が簡単、運用の負担も小さい(ほぼ無い) 性能 NPNSの機能/構成に特化して改造、期待する性能を実現 リモートシェルとトレース機能で、動作理解や状況把握が容易 シングルスレッド性能は高くない →

    重い処理はC言語拡張 安定性 Erlang VMのクラッシュは本番環境では無し
  59. 59 特筆したい点 メモリ効率が⾮常に良い hibernateと多少の効率化で、1接続あたり十数KiB GCで回収したメモリをOSに返却してくれる mmapでの取得/解放と、世代別コピーGCの合わせ技 OSのメモリだけを監視すればよい リモートシェル (remsh) が万能ツール

    本番環境の状況をダイレクトに把握・改変 困ったら remsh、(やろうと思えば)ライブパッチも可能
  60. 60 Erlang/OTP 特徴 安定したネットワークサーバを書きやすい supervisor、軽量プロセス、パターンマッチ、hibernate 文法がシンプルで、暗黙の知識が少ない 動的トレースのおかげで、既存コードの把握がしやすい コード密度が高く、少量の変更で大きな改造ができる 現状と期待 誕生から30年以上たっても活発な開発が続いている

    今後もネットワークサーバ開発の選択肢の一つとなってほしい
  61. 61 ご静聴ありがとうございました

  62. 62

  63. 63 告知 キャリア採⽤募集してます Webエンジニア ネットワークインフラエンジニア ネットワークサービスシステムエンジニア サーバセキュリティエンジニア AWS Summit Osaka

    でも発表有り 『Nintendo Switch Onlineを支えるサーバーシステム開発』 Ruby on Rails で大規模をさばくシステムの開発