Slide 1

Slide 1 text

1 Erlang/OTP と ejabberd を活⽤した Nintendo Switch(TM)向け プッシュ通知システム 「NPNS」の 開発事例 任天堂 ネットワークシステム部 渡邉 大洋 わ た な べ た い よ う

Slide 2

Slide 2 text

2 所属の紹介 任天堂 ネットワークシステム部 社内情報システム部門 自社ゲーム機向けネットワークサービス/ ゲーム連動サービスの開発・運用 主力言語は Java, Ruby 必要に応じて技術選定 (今回の事例) 私は こちら

Slide 3

Slide 3 text

3 発表者の紹介 渡邉 ⼤洋 キャリア入社、6年目 経歴: Web開発 → 組み込み家電 → Web開発(現職) 任天堂でのお仕事 ニンテンドーネットワークID サーバ NPNS サーバ 好きなプログラミング言語 Erlang / Elixir / Ruby

Slide 4

Slide 4 text

4 今⽇お話しすること ü NPNSの概要 常時接続部分の ü 技術選定 ü サービス開始までの開発 ü サービス開始 ü サービス開始後の開発 ü ふりかえり

Slide 5

Slide 5 text

5 今⽇お話ししないこと ü インフラやクラウドの話 (興味がある⽅は AWS Summit Tokyo 2018 の資料を)

Slide 6

Slide 6 text

6 NPNSの概要

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

8 NPNS (Nintendo Push Notification Service) ひとことで Switch向けプッシュ通知システム(同カテゴリ: APNS, FCM) API + 常時接続 の構成 常時接続=プッシュ通知では必須、同時接続数が激増 採⽤技術 API部分: Ruby on Rails 常時接続部分: Erlang/OTP + ejabberd 開発体制 要件定義・開発・運用まで社内で対応、数人の小規模チーム

Slide 9

Slide 9 text

9 NPNSの要件 スケーラビリティ 1億接続に備える 送信対象 プレイ中本体、およびスリープ中の本体 遅延 ~数秒 通知の種類 (次のページで紹介) サービス分析 大量のログを出力・収集

Slide 10

Slide 10 text

10 通知の種類 個別指定通知 トピック指定通知 (PubSub) ユーザに 見てほしい情報 本体システムに 届けたい情報 みんなに 配りたい情報

Slide 11

Slide 11 text

11 ユーザに 見てほしい情報 • フレンドのオンライン通知 • ゲームサーバのメンテナンス通知 プレイ中のみ 本体システムに 届けたい情報 個別指定通知 トピック指定通知 (PubSub) みんなに 配りたい情報 通知の種類

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

15 XMPP Cluster XMPP Cluster 全体の構成 XMPP Cluster Provider API Consumer API 通知送信者 常時接続 API XMPP HTTP HTTP NPNS

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

17 XMPPクラスタ分割の理由 スケーラビリティ クラスタ追加でシンプルにスケール 負荷試験もクラスタ単位で 安定性 ノード数を抑える 障害の局所化

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

19 XMPP 選定理由 TCPの常時接続は経験が無い いきなりこの規模で大丈夫か? 常時接続プロトコルを扱うOSS/SaaSの活用を検討 XMPPが適切では? 機能: 個別指定とトピック指定、両方の通知に対応 安定性: 歴史が有り規格の変化が少ない 拡張性: XEP (XMPPの拡張プロトコル) が多数 実績: 大規模な同時接続の事例が豊富

Slide 20

Slide 20 text

20 ejabberd 選定理由 評価指標 ライセンス/提供機能/開発の活発度/ 導入実績/サポート 注⽬ポイント: クラスタ化の⽅式 Erlang: 処理系がクラスタ対応 新規⾔語への対応は︖ 書籍での技術習得 チーム内での勉強会

Slide 21

Slide 21 text

21 ejabberdの特徴 XMPP サーバソフトウェア Erlang + C言語拡張(NIF,driver) XMPP Core + 多数の XEP (XMPP拡張規格) に対応 コンフィグによるモジュラビリティ 機能の選択 ストレージミドルウェアの選択 クラスタ化に対応 フルメッシュ構造 ノード間で通知を直接送信

Slide 22

Slide 22 text

22 常時接続部分の サービス開始までの開発

Slide 23

Slide 23 text

23 ejabberdクラスタの構造 Redis MySQL ユーザ情報 トピック情報 セッション情報 SQS inner ejabberd inner ejabberd outer ejabberd outer ejabberd outer ejabberd outer ejabberd クラスタの内側/外側でノードの役割を分けて、効率化を狙う 内側 inner ejabberd 通知をejabberdクラスタに⼊れる役割 CPU バウンド (通知量に⽐例) 外側 outer ejabberd 通知をSwitchに渡す役割 メモリバウンド(接続数に⽐例)

Slide 24

Slide 24 text

24 機能 ejabberd の機能を使って、短期間で実現 使った機能 個別指定通知: Message トピック指定通知: PubSub (XEP-0060) 一時保存: Offline Message (XEP-0013) 稼働状況で送り分ける: Presence (使っていない機能) フレンドグラフ: Roster クラスタ間の転送: Server-to-Server (XEP-0288)

Slide 25

Slide 25 text

25 性能 求める性能 [キャパシティ] 1ノードに大量につなぎたい 接続キャパシティ → 50万~100万ぐらい [スループット] ワーストケースでもシステムが破綻しない 一斉ログイン → 30分くらい 一斉配信 → 軽負荷で 一斉切断 → すぐ再接続できるように [遅延] 大量のログを低遅延で書きたい ログ出力 → 他の処理に影響を与えないように

Slide 26

Slide 26 text

26 負荷試験結果 項⽬ 期待する性能 当初の測定値 接続 キャパシティ 最低50万台接続 (r3.largeを想定、メモリ 15.25GiB) 30万 ⼀⻫ログイン 50万台が30分で接続完了 = 277接続/秒 150接続/秒 ⼀⻫配信 軽負荷で、50万台に30分以内に配信 負荷:⼤ ⼀⻫切断 切断の処理は数分以内に完了 30分 ログ出⼒ 軽負荷、遅延は数分以内 2時間 ejabberdは汎⽤性の⾼い構造 → NPNSの機能や構成に特化して性能改善を

Slide 27

Slide 27 text

27 改善の流れ 負荷試験でボトルネックを特定しつつ改善 現象/課題 ⇒ 原因 ⇒ 対策 ⇒ 効果 のサイクル 「問題はひとつずつ解決していこう」

Slide 28

Slide 28 text

28 接続キャパシティ [BEFORE] [現象] ⼤量接続時、Erlang VM のメモリ消費が多い 1接続あたりの消費メモリを減らして、接続数を増やしたい ejabberd は hibernate を使っているが、それでもまだ多い hibernate = プロセスのメモリを切り詰める ≒ 布団圧縮袋 system 領域が processes 領域の6倍程度大きい 調査: instrument:memory_status(types) で内訳を集計 [原因] C拡張の中で確保しているメモリだった XMLパーザ (expat) TLS (OpenSSL)

Slide 29

Slide 29 text

29 接続キャパシティ [AFTER] [対策] C拡張部分の使い⽅を分析&改造 XMLパーザ (expat) à hibernate 前に一旦使用終了して解放 TLS (OpenSSL) à OpenSSLパラメータ最小化、不要バッファの解放 [効果] メモリ削減 & 接続数増加 1接続あたり 40% に削減 接続可能上限で表すと 30万 → 75万に増加

Slide 30

Slide 30 text

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 軽量 プロセス ソケット メッセージ プロセス起動

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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]} プロセス辞書

Slide 34

Slide 34 text

34 ⼀⻫切断 [BEFORE] [現象] セッション処理が多い 切断数がそもそも多い (最悪値は、全接続数が一斉に切断) 切断時のセッション操作が多い Redis上ではHASHに保存 HASHを取得してから削除 調査: Erlangトレース [原因] XMPPはマルチログイン 同一IDで複数デバイスから接続可能 NPNSは 1 ID = 1 セッションなのに… Redis c2s 1. 自分が最後か? 2. 自IDの全セッションを取得 (HASH) 3. 自分のセッションだけ削除 (HASH) 【XMPP切断時のセッション操作フロー】

Slide 35

Slide 35 text

35 ⼀⻫切断 [AFTER] [対策] 切断時は「なにもしない」 シングルログイン構造に変更 他のデバイスを気にしなくてよい Redis上ではTEXTに セッションにEXPIREを設定 ログイン中は定期的にRefresh 切断後、いつかは自動で消える [効果] 切断処理の短縮と軽量化 30分以上 → 1分 Redis c2s (なにもしない) 【XMPP切断時のセッション操作フロー】

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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万 ログ処理にかかる時間

Slide 38

Slide 38 text

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万ま でにおさえる

Slide 39

Slide 39 text

39 ここまでのまとめ 項⽬ 期待する性能 BEFORE AFTER 接続 キャパシティ 最低50万台接続 (r3.largeを想定、約16GiB) 30万 75万 ⼀⻫ログイン 50万台が30分で接続完了 = 277接続/秒 150接続/秒 300接続/秒 ⼀⻫配信 軽負荷で、50万台に30分以内に配信 負荷:⼤ 負荷:⼩ ⼀⻫切断 切断の処理は数分以内に完了 30分 1分 ログ出⼒ 軽負荷、遅延は数分以内 2時間 30秒 ⼗分な性能を確保できた(と思われる)

Slide 40

Slide 40 text

40 サービス開始 =予期せぬ課題との出会い

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

43 (メッセージループ) [BEFORE] [AFTER] Redis c2s 5. 残通知を転送 ここで メッセージ ループが 発⽣ ↓ ここを⽌めれば 解決か︖ (なにもしない) 6.だれかいないか? → 自分 7. 残通知を転送 ... 4. だれかいないか? → 自分 8.だれかいないか? → 自分 9. 残通知を転送 未処理の通知が残っている場合 Redis c2s 1. 自分が最後か? 2. 自IDの全セッションを取得 (HASH) 3. 自分のセッションだけ削除(HASH) 4. だれかいないか? 5. 残通知を転送 この部分を ⾒落としていた 未処理の通知が残っている場合

Slide 44

Slide 44 text

44 Redis⾼負荷事件 早期対応をどのように︖ 修正コードを書いたが、本当にこの問題か断定できない 常時接続サービスではお試しデプロイが難しい 本番環境でそのまま試せたらよいのに…

Slide 45

Slide 45 text

45 Redis⾼負荷事件 [解決編] Erlang には hot code deploy 機能がある︕ 無停止で本番環境1台だけに適用(カナリアリリース) ただし手作業 (code:soft_purge/1, code:load_file/1) Erlang リモートシェルで接続して更新を実施 手順チェック×3人 → 予行演習×2 → 実施 Redis アクセス回数が激減、修正効果を確認 その後、通常の手順で全クラスタにデプロイ 早期解決︕ ユーザ影響無しで、余裕のあるうちに解決できた

Slide 46

Slide 46 text

46 サービス開始後の開発 常時接続部分の

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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利用率

Slide 49

Slide 49 text

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利用率

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

52 性能改善のまとめ まずは負荷試験 満足行く性能ならそこで終了 ボトルネックを探す メモリ不足 → C拡張に要注意 メッセージキュー詰まり → プロセスボトルネック 負荷 (reduction) が高い → profile 実施 (eprof, fprof) 改善策 並列性を上げる (worker作成、負荷を “外側” に移す) 処理量を減らす メッセージキューを積み過ぎない (前段にダムを作る)

Slide 53

Slide 53 text

53 Switch 接続数増加への対応 インフラ増強 クラスタ数を増やす クラスタ内のノード数を増やす アプリ改善 性能向上 = 1台あたりの処理能力増 = 台数を削減可能 台数削減=障害発生数も削減 インフラコストと運用コストを削減

Slide 54

Slide 54 text

54 サービス規模 1000万+ 同時接続 約20億 通/⽇ クラスタ停止は一度も発生していない

Slide 55

Slide 55 text

55 outer 1ノードあたりの処理 InstanceType: r5.large (CPU 2コア、メモリ 16GiB) 接続数 最大50万、通常は10~20万程度(AZ障害への冗長対応) 個別指定通知 200通/秒 トピック指定通知 100〜400通/秒 ※CPU負荷に配慮して低めに抑えている

Slide 56

Slide 56 text

56 振り返り 常時接続部分の

Slide 57

Slide 57 text

57 要件のふりかえり スケーラビリティ 1億接続に備える ... クラスタ分割+性能改善で実現 送信対象 プレイ中本体、およびスリープ中の本体 ... Presenceで送り分け 遅延 ~数秒(正常時)、~数分(異常時) ... 元々の高い並列性で、混雑時も低遅延 通知の種類 個別通知/トピック通知 ... XMPP の機能で実現 サービス分析 大量のログを出力・収集 ... 専用ログサーバ + ダムで実現

Slide 58

Slide 58 text

58 Erlangとejabberdを採⽤してみて 機能 個別指定とトピック指定を短期間で実現できた クラスタ化とその維持が簡単、運用の負担も小さい(ほぼ無い) 性能 NPNSの機能/構成に特化して改造、期待する性能を実現 リモートシェルとトレース機能で、動作理解や状況把握が容易 シングルスレッド性能は高くない → 重い処理はC言語拡張 安定性 Erlang VMのクラッシュは本番環境では無し

Slide 59

Slide 59 text

59 特筆したい点 メモリ効率が⾮常に良い hibernateと多少の効率化で、1接続あたり十数KiB GCで回収したメモリをOSに返却してくれる mmapでの取得/解放と、世代別コピーGCの合わせ技 OSのメモリだけを監視すればよい リモートシェル (remsh) が万能ツール 本番環境の状況をダイレクトに把握・改変 困ったら remsh、(やろうと思えば)ライブパッチも可能

Slide 60

Slide 60 text

60 Erlang/OTP 特徴 安定したネットワークサーバを書きやすい supervisor、軽量プロセス、パターンマッチ、hibernate 文法がシンプルで、暗黙の知識が少ない 動的トレースのおかげで、既存コードの把握がしやすい コード密度が高く、少量の変更で大きな改造ができる 現状と期待 誕生から30年以上たっても活発な開発が続いている 今後もネットワークサーバ開発の選択肢の一つとなってほしい

Slide 61

Slide 61 text

61 ご静聴ありがとうございました

Slide 62

Slide 62 text

62

Slide 63

Slide 63 text

63 告知 キャリア採⽤募集してます Webエンジニア ネットワークインフラエンジニア ネットワークサービスシステムエンジニア サーバセキュリティエンジニア AWS Summit Osaka でも発表有り 『Nintendo Switch Onlineを支えるサーバーシステム開発』 Ruby on Rails で大規模をさばくシステムの開発