モバイルソーシャルアプリ特有の仕組み
廣田氏は、ドラコレのサービス提供における大きな問題として、「超高トラフィック」と「モバイルソーシャルアプリ特有の仕組み」を挙げました。後者で苦労した点として強調したのが「5秒問題」と「マイクロバースト問題」です。国内モバイルソーシャルゲーム各社が提供しているモバイルソーシャルアプリのプラットフォームでは、エンドユーザーからのリクエストはモバイルソーシャルゲームのガジェットサーバー(ドラコレの場合はGREE)で認証を得た後にサービス提供者(ASP)のコンテンツサーバー(ドラコレの場合はKONAMI)に届きます。リクエストに対するレスポンスも、ガジェットサーバーを経由してエンドユーザーに届きます。
5秒問題
5秒問題とは、ガジェットサーバーのリクエストに対してコンテンツサーバーは5秒以内にレスポンスを返さなければならないというものです。レスポンスに5秒以上かかったり、エラーレスポンスを返すといったことが規定回数以上行われると、GREEから公開停止にされてしまい、SAPはDeveloper Centerから手動で再公開の設定を行う必要があります。
これは、GREEのガジェットサーバーに待機プロセスが増え続けてしまうことを回避する措置です。アプリケーションのバグや、本当にサービスを継続できない状況であれば公開停止もやむを得ませんが、高トラフィック状態の瞬間的なパフォーマンス低下でも公開停止にされてしまうため、何かしらの対策が必要になります。
そこでKONAMIでは、レスポンスに5秒以上かかりそうな場合にはPHPの処理を中断し、「再度ページを開いてください」といったメッセージを返すことで、最悪の事態を回避することを試みました。もちろん、これはあくまで瞬間的なトラブルを回避するための措置です。
さらにPHPのソースコードにも手を加えました。PHPではset_time_limit()関数を使ってスクリプトの実行時間を制限できます。しかしこの機能は、「PHPのプロセスが処理に費やした時間」だけが計測の対象になり、サーバーとの接続待ちや、ディスクI/O待ちの時間などは計測されません。このため、たとえ実行時間を4秒に設定しても、実際に制限がかかる頃には5秒経過している場合があるのです。これはタイムアウトの実装に setitimer()関数が使われていて、指定できる3種類のタイマーのうち「ITIMER_PROF」を使っているためです。
このタイマーに「ITIMER_REAL」を指定すれば、計測の対象を実時間に変更できますが、PHPは100%非同期シグナルセーフとはいえないため、変更にはリスクも伴います。そのリスクを承知した上でソースコードに変更を加えました。さらに「時間指定の単位を1秒未満の精度にする」「タイムアウト時のHTTP応答を変更可能にする」といった動作の変更、追加も実施したといいます。
マイクロバースト問題
もう一つ、頭を悩ませたのが「マイクロバースト問題」でした。これは前述の「5秒キャンセル」を実装する前のことですが、順調にユーザー数が伸びてトラフィックが増えていく中で、一部のレスポンスが極端に遅くなっていることに気づきました。しかも、遅延が発生するファイルに法則性がなかったのです。その後、遅延が発生しているレスポンスはすべて9秒前後で処理が完了していることに気づき、さらに調査した結果WebサーバーからDBサーバーへのTCP接続で時間がかかっていることが判明しました。パケット解析をした結果、時々SYNパケットをロストしていたのです。
TCP パケットの再送に関してRFCでは以下のように定義されています。
- RTT(Round Trip Time)が確定する前のRTO(Retransmission Time Out)は3秒
- RTOは再送するごとに倍に増やす
SYN送信時にはRTTが確定していないため、RTOは3秒になります。つまり、SYNパケットを2回ロストすると、TCP接続に約9秒かかります。1回だけロストするケースもありましたが、トータルで5秒以内にレスポンスできていたため気づかなかったのです。さらなる調査の結果、このパケットロスはスイッチのキューでの瞬間的なパケットあふれ(マイクロバースト)が原因であることが判明しました。定常的なトラフィックでは十分に余裕があったため、発見までに時間がかかってしまったのです。
この問題は、アプリのすべての通信箇所(DB、memcached、DNSなど)の接続タイムアウト・リトライを調整すれば最適化できるかもしれません。しかし、アプリ側に実装を委ねると、どうしても抜け漏れが発生しますしSYN_SENT状態のソケットが大量に滞留してしまいます。そこで、内側のネットワークに対するRTOを変更することで、この問題を回避したといいます。
具体的には、
- 変更前:初期 RTO = 3 sec / 再送のたびに RTO = RTO × 2
- 変更後:初期 RTO = 1 sec / 再送のたびに RTO 変更せず
としました。初期RTOの変更だけであれば、カーネルソースの定数定義を変更してビルドし直すだけで済みますが、再送のたびにSYNパケットのRTOだけを増やさないようにするとなると、ソースの実体にも手を入れなくてはなりません。
このように、いくつかのオープンソースコードの改造や、ミドルウェアのパフォーマンスチューニングを実施した結果、この半年間で公開停止になることはなくなったといいます。