取材

サーバーマシン1台で同時接続者数1万名を実現するにはどうすればいいのかというノウハウと考え方


CEDEC 2012ではドラゴンクエストXは「世界は一つ」を実現するためにどのようなサーバ構成にしているのか?ということで、オンライン作品であるドラクエXを支えるサーバの構成が講演されましたが、ゲームサーバー&ネットワークエンジン「ProudNet」の開発者であるNettention社のCEOであるHyunjik Baeさんは、韓国のオンラインゲームのサーバー開発と利用の経験を通して大規模プレイヤーのためのリアルタイムネットワーク同期技術について講演しました。

サーバーマシン1台でMMO同時接続者数10,000名を実現する方法 | CEDEC 2012 | Computer Entertaintment Developers Conference
http://cedec.cesa.or.jp/2012/program/AB/C12_I0284.html

Hyunjik Bae:
こんにちは。私はゲームエンジン会社Nettentionで代表を務めています。


通訳:
ではこれから韓国語で発表いたします。高性能のサーバ作りのノウハウは多々ありますが、私はその中で一つだけご紹介しようと思います。一つでありますが、深く掘り下げて申し上げたいと思います。まずこの話を理解するためにはネットワークプログラミングの基礎が必要であります。では、その話からいたします。

まず、自己紹介を簡単にいたします。私は子どもの頃から「ダブルドラゴン」「(いかり)」「アウトラン」というゲームが大変好きでした。現在はモバイルソーシャルゲームについついお金を使ってしまっています。またギャルゲーを試みて失敗したこともありました。ストーリー作家に恋愛経験がなかったからです。現在は大変美しい妻と2人の子どもと幸せに暮らしております。またおばあさんたちとゴルフも習っています。ピアノを弾くのも好きですし、旅行も好きなんです。


では本題に入ります。サーバプログラムから見ますと、オンラインゲームには2つの種類があります。ロビーのあるMOオンラインゲーム、そしてワールドが永久的なMMOゲームです。ロビーのあるMOオンラインゲームの代表的なものはFPSゲームで、MMOゲームの代表的なものは「ラグナロクオンライン」です。ロビーのあるMOオンラインゲームはですね、同時接続が多くても作りやすいんです。ひとつの部屋に入るプレイヤーの数が少ないからです。プレイヤーの少ない部屋をたくさん作ればいいので、サーバさえ増やせばいいんです。現在皆さまにご覧いただいているのは「Blitz 1941」というオンラインゲームのロビーです。このゲームは100対100の戦車戦のゲームで、8年前に私が開発したものです。



MOほどまあ簡単に作れます。ワールドひとつを20個のサブワールドに分けて、サブワールドひとつは部屋ひとつです。ひとつの部屋には1000人のプレイヤーが入れます。そうなりますと、ワールドが永久的なMMOほど簡単に作れます。ワールドひとつを20個のサブワールドに分けて、サブワールドひとつは部屋ひとつです。ひとつの部屋には1000人のプレイヤーが入れます。そうなりますと、論理的サーバ1つについて2万人のプレイヤーを処理できます。もしもこのサーバCPUのコアが4つだと仮定してみますと、論理的サーバひとつのために物理的サーバが5つあればすぐ解決できます。ひとつの部屋にプレイヤー1000人が入るのは大変簡単です。シングルスレッドゲームサーバでもできます。このサーバをCPUの個数だけ実行すればいいのです。

次に、このスクリーンショットはMMOソサイエティーゲームの「OZ World」です。


13年前に私が作ったゲームです。ここ数年間CPUのスピードは速くなりませんでしたが、その代わりCPUのコアの数は増えました。しかしプレイヤーの要求は大変高くなっております。同時接続者ひとりひとりに対してのゲームサーバの仕事は増えています。「ディアブロ3」のようにモンスターがいっぱい出るゲームをMMORPGで作ればどうでしょうか。または「三國無双」のようなもののMMORPG版が出てもおもしろいでしょう。このスクリーンショットは私のサーバエンジンでありますProudNetを使ったサンプルのMMORPGで、開発中のものです。このゲームはプレイヤーの数に比べましてモンスターの数が大変たくさん出ます。「ディアブロ3」ですとか「三國無双」のようにですね。ですからつまり一言で言えば大虐殺ゲームです。このようなゲームはプレイヤーの数が増えれば増えるほどサーバの処理量も大変増えます。シングルスレッドサーバではちょっと足りないという状況になってしまいます。同時接続者が多いゲームサーバ自体は作りやすいんです。ネットワークをつなぐだけのサーバですから。しかしクライアントがサーバと1対1でメッセージをやりとりするのもそんなに難しいことではありません。ミスさえしなければいいんです。しかしサーバで1個以上のクライアントにメッセージを送らなければならないという場合はMMOゲームでは必ずこれをしなければなりません。しかしひとつの部屋で処理しなければならない量がCPU1個で不足してしまうと大変難しくなります。この限界を超えてしまいますと大変複雑になってしまいます。

ではその複雑な過程を申し上げますが、その前にいくつか定義をしなければならない用語がありますので、それについてまず申し上げます。後ほど私がたくさん使う表現になると思うんですが、「Lock(X)」です。


このXというデータをロックするという意味です。データをロックするというのは他のスレッドが同じデータを同時に読んだり書いたりできないようにするためです。皆様もよくご存知と思いますがクリティカルセクション、ミューテックスがこの役割をいたします。もしもみなさんの上司がですね、みなさんに2つ以上の仕事を同時にやれと指示を出せばどうなりますでしょうか。もちろんサラリーをもらっていますからやらばければなりませんよね。でも普通はひとつをまず終えてから次の仕事を始めると思いますが、上司がすごくいじわるで、その2つを同時進行しろと言えばどうでしょうか。私だったら辞表をたたき出すと思います。しかしバカのようなCPUはこの2つのことをかわりばんこにします。最初の仕事を少しやって、また2番目のことをちょっとやって、というように少しずつやります。こうやって反復して2つの仕事をかわりばんこにやるのを「コンテキストスイッチ(Context switching)」と言います。


ところがこのコンテキストスイッチには大変時間がかかります。CPUは他のスレッドの仕事をするために現在の状態を保存します。そして他のスレッドの仕事をするためのその状態を呼び戻し、また保存する、というこの過程は大変複雑な演算が必要です。ですのでコンテキストスイッチが少ないほどいいというのが私の考えです。

またもうひとつ、CPUがメモリを読んで書くのにも大変時間がかかります。ですのでCPUの中にはキャッシュというものがあります。これはよくご存知だと思います。キャッシュの仕事によって、今さっきまで使っていたメモリのコピーがそのキャッシュの中に臨時に保存されます。キャッシュの中に保存されているメモリはRAMメモリにアクセスするよりもずっと速いんです。ですのでCPUがあるデータにアクセスするためには、それがキャッシュにあるかないかが大変重要です。あればそれをそのまま読んで書くんですが、もしもデータがキャッシュにないと、メモリに行ってそれを持ってこなくてはなりません。ですので、キャッシュにあるメモリとないメモリでは速度の差が100倍くらいです。ゆえにぜひこのキャッシュを最大限使わなければなりません。キャッシュヒットを最大限に高め、キャッシュミスも最大限に避けなければなりません。


もうひとつ、「コンテンション(競合)」という用語を考えてみましょう。先ほど私は2つのスレッドが同じデータを読んで書く前にデータをロックすると申し上げました。2つの同じオーバーセクトでアクセスするにあたって、この2つのスレッドが違うCPUにあるとします。データにアクセスするため、クリティカルセクションにロックするために2つのCPUは戦うんですね、これをコンテンションと言います。コンテンションはCPUのスピードを大幅に下げます。なぜならば2つのCPUがアクセスするためにはCPU間でネットワーキングをしなければならないので、同じデータを違うCPUでアクセスすると大変スピードが落ちてしまうためです。コンテンションは最大限避けねばなりません。これはやはりサーバプログラミングで最も気を使わなくてはならないところです。

例えばデータA、B、C、Dがあります。全部違うデータです。これをもし一度に大きいロックで保護できると仮定をしてみましょう。あるスレッドがAにアクセスし、違うスレッドがBにアクセスします。この2つのデータがこのようになる場合、2つのスレッドは同時に進行できません。そうなりますと時間の浪費になります。性能が大変遅くなるんですね。ですので、右のようにこのロックの領域を分けなくてはなりません。


こうやってResource Granulation、リソース分割と言うのですが、これによって同時に作動できるようにして仕事を早めに終わらせることができます。ここでちょっと注意が必要なのですが、あまり小さく分割してしまいますとロックの回数が多くなって、性能が遅くなり、プログラムも複雑になりますので、プログラマーがミスをするリスクも高くなってしまいます。ということで、簡単ではありますが大変重要な内容をご紹介しました。細かい内容はマルチスレッドに関連する書籍がたくさん出ておりますので、私よりよく説明してくれるだろうと思います。

では、これまで私が経験した問題について申し上げます。私どものサーバエンジン「ProudNet」についてです。この「ProudNet」はスレッドプール基盤のユーザーコールバック機能を既に提供しております。我々のエンジンを使いますと、ゲームの開発者はマルチコアゲームサーバを作ることができましたが、問題がひとつありまして、私どものエンジン自体は内部的にマルチコアを使用しておりませんでした。こういった問題から、同時接続が増えますと、ある一定以上はその性能を上げられないということがありました。これもシングルスレッドで作って、サーバスレッドで解決をすることも可能なんですが、根本的な問題までを解決するためにはひとつのサーバプロセスで全てのCPUを使えるようにしなくてはなりません。


みなさんは「アムダールの法則(Amdahl's law)」というものをご存知でしょうか。


このアムダールの法則というのは、プログラムを全体の処理時間の中で、ある部分がマルチコアの並列でやった時にどれくらい性能が上がるのか、という公式なんですが、全体の処理時間の40%を2倍に並列化した場合、性能はどれくらい上がるのでしょうか。この公式通りでしたら25%しか上がりません。この公式だけではちょっと分かりませんよね。ではスライドをご覧いただきましょう。


みなさんよくご存知のIncrediBuildです。大変素晴らしいプログラムで、私どもも使っています。アムダールの法則によりますと、どんなに並列化が上手にできても、全体としての性能向上には限界があるということになりますね。なので、これはアムダールの法則とも言われますけれども、「アムダールの呪い」とも言われております。ここに見られるこのブランクのところがアムダールの呪いの部分なんです。私どものクライアントはもう既に100社を超えました。「魔界村オンライン」にも我々のエンジンが使われています。ただ、不幸なことに、我々のクライアントの一部はサーバマシンにインスタンスが1個だけの構造だったんですね。なので私どもに選択の余地はありませんでした。私どものエンジンのAPIは変更せずに、エンジンの中の並列化を行ったのです。言葉で言うのは簡単なんですが、これは決して簡単ではありませんでした。結局数ヶ月かけてこの問題に取り組み、解決しました。

では、ここからどのように解決したかを説明しましょう。皆様、まあ私もそうなんですけれども、プログラマー、プログラムを作る人というのは、遅いコードというのは絶対作らないですよね。本能的に速いパフォーマンスというのを考えます。しかし、このプログラムのパフォーマンスというのは、相対的なんです。プログラムの中の一部が速ければ反対側は必ず遅い、というふうに言われるわけです。そこで私たちは先にプロファイリングをし、試験、テストコーティングをし、デザインドキュメントをまとめる、そして実装(Implementation)をする、という順番で進めました。


では、まずプロファイリングと分析のところを見てみましょう。プログラムが大きくなればなるほど、また複雑になればなるほど、どこが遅いかを予想するのが難しくなります。それを知るために我々はまずテストプログラムを行いました。たくさんのキャラクターとたくさんのプレイヤーがものすごい動きを見せる、そういうテストプログラムを作ったんです。そこでコードプロファイラを使いました。ちなみに私はGlowCodeとかVisualStudioといったプロファイラをよく使っています。あるいはIntelから出たAmplifier XEというようなソフトもなかなか良いと思いました。で、テストをしましたところ、意外な結果が出ました。WindowsのネットワークのOverlapped I/Oを呼び出すこの関数のところで最も時間がかかっていた、ということが分かったのです。皆様おかしいとお思いでしょうが、私も最初おかしいと思いました。ご存じの通り、Overlapped I/Oはゲームサーバの中の最もパフォーマンスのいいやり方です。IOCPと同じですよね。IOCPで作った。なのになぜこんなに時間がかかるのか、おかしい、と。きっと何かミスがあったんだということで、もう1回テストをして、コード分析もしました。ブロックが遅くなる原因かなと思ったんですけど、そうではありませんでした。なのに、この関数のところで一番時間がかかっている。なぜなのでしょう。みなさんご存知のように、IOCPは同時アクセス数を最も大量に処理できる機能です。しかし、その性能のところでボトルネックがあったというのは驚くべきことなんです。


なぜこういうことが起きたのでしょうか。ユーザーアプリケーションと、そしてオペレーションとCPUメインボードネットワークデバイスの間には非常に複雑なメカニズムがあります。これらの間にはカーネルモードの変換がありますよね。また、様々なイベントハンドラー、そしてポーリングもあって、CPUとバスの中の通信もあり、そしてハードウェアのインタラプトまで、ここに絡んできます。ハードウェアインタラプトというのが何か、みなさんご存知だと思うんですけど、それよりももっと複雑なのがオペレーションと、そしてハードの中で起きていたんです。みなさん、ソケットプログラムはよく使っていますよね。複雑なネットワークプログラミングをハーフシンク・ハーフアシンクのパターンに変えてくれるのがソケットプログラムですよね。しかし、そのためにオペレーションシステムの中の仕事が増えてしまうわけです。ソケット関連の関数を完全に非同期で使うとしても時間がかかるのは避けられないんです。つまり、オペレーションシステムでこの問題は解決できない、ということなんです。Windowsサーバだけじゃないんです。LinuxとかFreeBSDとか、他のOSもみんな共通の問題を抱えています。

とにかく答えは出ました。非同期のネットワーク関数の、つまり非同期のソケット関数の呼び出しそのものを最適化しなくてはいけない、これが答えです。しかし、ここでまた問題があって、我々のエンジンのロジックはもう既に複雑な状態で、後からこれを最適化するのは簡単ではないんです。そこで、最適化の方法を見出すためにテストをし、結果がダメならそれを捨て、テストプログラムを作っては捨てる、ということを繰り返しました。そして最上最高の方法を見つけ、最終のデザインドキュメントにたどり着いたんです。今からそれについて説明しましょう。


パフォーマンスのところは性能でボトルネックが生まれるとどうすればいいでしょうか。まずこのボトルネックをなんとか排除、なくさなくてはいけないのですが、しかしその方法が全くない。じゃあどうすればいいのかというと、なくせないのであれば減らすしかありません。その、どうすれば減らせるのか、ということについて、ひとつの方法を紹介いたします。

ここにAというルーチンがあるとしましょう。これを関数というふうに言ってもいいと思います。AというルーチンはB、C、Dというサブルーチンを持っています。Aは内部のBやC、Dを呼び出したりします。仮にAがここでボトルネックだとしましょう。

もう1回測定してみました。B、C、Dの中で1番処理時間がかかっているのは何か。ここではBだとして、コードも最適化したとしましょう。なのにBは依然として遅い。これ以上もう最適化の方法はない、という時に、では一体どうすればいいでしょうか。

もしBが実行されている間、リソースをロックしなければどうなるでしょうか。Aは並列で実行できない状態です。でもBは並列で実行できます。すると、CPUの数が増えれば増えるほど、Aの実行前にいわゆるスレッドがアイドル状態になる確率は下がりますよね。結果的にAのロックにかかる時間を減らすことができるんです。これは非常に大事なトリックなんです。Aの並列化が不可能だとしても、Aの下のサブルーチンの中のひとつを並列化する、それによってAのスピードを速めるというテクニックです。私はこのテクニックをスクイーズ(Squeeze)とかあるいは絞り出しとかそういう言葉で呼んでいます。


ここではBを並列化してAを絞ったんです。ProudNetの内部の構造はご覧の通りですね、まずはメインがあります。そのメインにつながっているいろんなクライアントの個体があって、クライアント別にP2P pairとかP2P groupとかその他諸々Etc.圧縮情報などなどがぶら下がっています。ProudNetのコードプロファイリングをしてから分かったことがあって、それはほとんどの時間がここでかかっていた、ということです。リモートアクセスに時間がかかっていた。では、どうするか。リモートオブジェクトをスクイーズ、絞り出しの対象にしようということです。

ではスクイーズの過程をご説明しましょう。まずメインをロックします。そして、ここでのポイントは、リモートオブジェクトはアンロック状態なんです。その次、メインをアンロックし、そしてリモートをロックします。リモートだけロックするんです。ただ適当に、いたずらに、どこでも手をつけていい、というわけではありません。これは非常に危険なんですよ。どこを切るのか、この切り口が非常に重要なんです。

この主人公をご存知ですよね。


韓国でもとても有名なんですよ。「スーパードクターK」なんです。この漫画を初めて読んだ時、「北斗の拳」のケンシロウがお医者さんになったのかな、と思ってしまいました。この漫画でKは、ひとりの患者には心臓移植、もうひとりには肺の移植手術という、2つの手術を同時にやるということを行いました。Kは一度に2つを移植すればたくさんの血管を切らなくてもいいということが分かっていたため、男性の心臓を女性に移し、そして脳死状態の患者の肺と心臓を一度に切り出して、それを男性に移したんです。つまり分割しやすいボーダー、境界が見つかったわけなんですね。、私たちもそれと同じで、リモートオブジェクトと他のオブジェクトの間にはたくさんの血管がありました。ここを切ると危険ですので、リモートオブジェクト全体をリソース、分割してはいけない。


じゃあどこを分割するのか。血管が少しだけつながっている部分を探してみると、リモートオブジェクトの中にあったんですね。その中で非同期送受信を処理するメンバー変数とその他を分けたということで、これがずっと作業をやりやすいということが分かりました。


メスを当てる前にもっと考えなくてはいけないこともあります。ロックをあんまりしすぎると性能が落ちるのではないか、という問題があります。


それをどう解決するのかということ。そして2番目には、そのオブジェクトをロックしなくても、そのオブジェクトが消滅しない、ということをどうやって保証するのか。その答えなんですが、まず頻繁にロックしてもいいのかということに関しては、これは大丈夫ではありません。一般的にはよくないです。しかし、いつ危険でいつ大丈夫なのか、その基準があります。この絵をご覧ください。


左側は頻繁にロックしたケースで、一方右側は1回だけロックしたケースです。この左右の差は、左の場合はコンテンションがない、ということ。ほとんどないロックなんです。右側はコンテンションが多いロックです。どっちが時間がかかるかと言うと、通常は右側のほうが長くかかります。右側はコンテンションがたくさん発生しますので、このためにCPUキャッシュシェアリングがたくさん発生しますし、またコンテキストスイッチもたくさん発生します。もちろん必ずしも左側が速いのではありませんが、並列処理の性能を高めるのはやはり左側なんです。

またもうひとつ見てみましょう。


ロックの方法について考えてみますと、これにはブロッキングロックとノンブロッキングロックの2つがあります。ブロッキングロックというのはあるスレッドが多分にリソースを使っている場合、待機状態、アイドルステートになってしまいます。しかしノンブロッキングロックでは、他のスレッドが既にデータを使っている場合、ロックをするために待つのではなく、ロックが失敗したと宣言します。コンテキストスイッチングをしないで次の機械語命令を実行します。つまり、ノンブロッキングロックは失敗することもありますが、スレッドコンテキストスイッチングが発生しないというメリットがあります。私どもは、この先ほど申し上げた技法をいろいろ駆使して並立性の向上はもちろんのこと、CPU全体の処理性能までを改善しました。なぜならば、コンテキストスイッチングとコンテンションを最小化に留めたからです。じゃあオブジェクトが破壊されないことを保証する方法について考えましょう。その前に用語を見てみましょう。「Atomic operations(不可分操作)」という言葉なんですが、この用語はクリティカルセクションを使わなくても簡単な算術、演算でスレッドセーフをする方法です。


これはハードウェアの機能です。ほとんどのCPUではこの機能をサポートしています。例えば足し算ですとか特別な値を持っている際にその値を他の値に替えるコンペア・アンド・スワップということなんですが、特別なことではないように見えますが、高性能のマルチコアプログラミングではこれを大変重要視しています。では見てみましょう。クリティカルセクションで保護されているオブジェクトがあります。これをあるスレッドが既にロックをしていると仮定してみましょう。そのオブジェクトが他のスレッドで破壊されてしまいました。そして他のスレッドがそのオブジェクトにアクセスすればどうなるのかと言うと、サーバクラッシュします。そしてみなさんは真夜中に大急ぎでタクシーで会社に行かなくてはなりません。これをどうすればいいかと言うと、ここを見てください。

Bにアクセスするためには、まずBのアドレスを入手しなければなりません。Bのアドレスを入手するためには、Bにアクセスする前にAにアクセスする必要があります。ですからここにヒントがあるんですね。スレッド1、スレッド2があると見ます。そしてオブジェクトA。オブジェクトAの中には別のオブジェクトBがあります。スレッド1がアクセスするためにはこのBが使用中ということでチェックをします。そしてBをロックして使用するんですね。そして仕事が終わってBのロックを解除します。使用する、しない、の表示なんですが、スレッド2においてBというオブジェクトを破壊する時の条件として使います。既に他のスレッドで使っているという表示があれば、これを消さないんですが、誰も使っていないという表示があればこれを消すことができます。ここで重要なのは、この表示を取り扱う場合、Atomic operationsを使わなくてはならないということです。これまでの説明を連結動作で見てみましょう。


まずメインをロックし、それからメインが使おうとするリモートを探して、そのUSE COUNTをひとつアップします。それからメインを解除します。そしてリモートをロックし、リモートを全部使い終わればこのロックを解除してUSE COUNTをひとつ減らします。このようにリモートをリソース分割してメインをスクイーズするという形で、計画が整理されました。しかし、計画は素晴らしいのですが、エンジンのあちこちを手直ししなければなりません。皆さま、文書の見直しというのは嫌ですよね、私も実は嫌なんですが、この設計ドキュメントをきちんと作ってこそ道を見失う、ということがなくなります。ということで、まずメインからロックします。メインからロックをして、それで終わりなんです。しかし、もしもこのメインの以降にリモートオブジェクトにアクセスしなければならない場合はこうしなければなりません。


ここで重要なのはメインロックの解除をしてリモートをロックするんですが、この過程が必ず入らなければなりません。これをしないと全てのサーバの性能が落ちてしまいます。ではリモートを破壊するルーチンを見てみましょう。


先ほどUSE COUNTで破壊されないということを保証する方法を申し上げたんですが、オブジェクトが破壊されないようにするためには2つの条件が必要です。まず1点目、メインをロックした状態でオブジェクトを破壊しなければなりません。2点目はUSE COUNTが完全に空になった状態でのみ、オブジェクトを破壊しなければなりません。オブジェクトを破壊できなかった場合は、後でこれをしなければなりません。ゲームサーバを追ってみますと、様々な多くのクライアントに同じメッセージを送る場合が多いんですね。ですので、この過程において複数のリモートオブジェクトを巡回しなければなりません。この時にも問題が多く発生します。頻繁なコンテキストスイッチングとコンテンションがたくさん発生するからです。ではこれはどうしましょうか。

みなさんは銀行に行くとまず番号札を取りますよね。これを取って自分の順番になるまでどうしますか。もちろんずっと待っていることもできますが、他の用事をすることも可能です。トイレにも行けますし、携帯電話をいじることもできます。またコーヒーを1杯飲むこともできます。で、自分の順番になれば銀行の用事を済ませればいいのです。プログラムも同じで、もしもロックのために待たなくてはならない場合、性能が落ちますよね。ですのでそのままただ待つのではなく、他のことをすれば性能を向上することができます。リモートオブジェクトをロックできない場合は、それは諦めて、他のリモートオブジェクトのロックを試みます。そして、他の用事をその間に見ればいいんです。さっきロックできなかったオブジェクトはまた改めてロックすればいいんです。こうなりますと、スレッドが待機時間にならず、つまりコンテキストスイッチングが発生しませんので、多数のオブジェクトを順次処理できます。これを私は低変換ループと呼んでおります。


この低変換ループがどのように使われているのか。多数のクライアントに同じメッセージを送信する場合、つまりマルチキャストで使うことができます。ProudNetは5ミリ秒以下時間ごとにパケットをまとめて送るんですけれども、これをコアレッセンスと言います。ネーグルアルゴリズムと同じようなものなんですが、これをマニュアルで制御するんです。これをするのはシリーウィンドウシンドロームをなくすためなんです。またこれには良い、面白い効果も現れまして、先ほど非同期送信関数がありましたが、このコールの回数が減りますので、サーバの性能も少し良くなります。ちょっと難しい内容で、簡単ではないので、みなさん頭の中で想像しながら聞いてください。ついてこられないと眠くなったりします。では、続けます。非同期送信完了のシグナルが出たとしましたとして、どうすればいいでしょうか。

はい、1行目の最初の行の関数、これはIOCPでイベントを待つ関数です。


そしてコンプリーションに関するイベントのハンドリングをすればいいわけです。ポールとかK9を使うときもそうなんですけれども、IOCPは少し違いますよね。プロアクターパターンですし、K9とイベントはリアクターパターンですから。でも目標は一緒です。我々はこのパフォーマンス向上にむけてコンプリーションステータス、GQCSからリモートオブジェクトのアドレスをストレートに得られるようにしました。このルーチンは最も頻繁に呼び出されるものですから、メインをロックしてはダメなんです。もしロックしてしまうとサーバのパフォーマンスが急激に下がってしまいます。ところが、このコードにはエラーがあります。どんなエラーか、どなたか答えていただけますか。どこにエラーがあるでしょうか。ヒントを差し上げますと、このブランクの部分です。このクエスチョンマークのところ、ここに何かが入らないといけません。


では答えを見てみましょう。ここにはリモートをロックし、そしてアンロックするという作業が必要になります。さっき説明しましたね。では今度は受信完了の処理について見てみると、送信の場合と大体同じです。こちらのコードもどこかにエラーがあります。このクエスチョンマークのところに何かが必要なんですが、何でしょうか。ヒントを差し上げますと、この2つがなければサーバが作動して途中でデッドロックが発生してしまいます。

では、答えです。ロックとアンロックの過程が必要になるわけです。メッセージを受信してメインをロックしてから、そしてリモートをロックするという過程が必要なんです。メインをロックして、次にリモートをロックする、という順番が大事で、この2つの過程がなくなるとダメなんです。リモートを先にロックしてしまう、つまり順番を逆にしてしまうと、どうなるか。サーバプログラマーが2番目に嫌う現象は何でしょう。そうです、デッドロックが発生するんです。ですからこの2つの作業が必要で、そうでなければサーバが途中でダウンしてしまうというような現象が起きてしまいます。

さあ、ここまでで難しい話は終わりました。少しお水を飲ませてください。このデザインドキュメントでコーディングを行いました。順調に進み、またパフォーマンス向上のためのいくつかの課題も見つかったんですけれども、それも難なく解決できました。なぜならば我々は既にたくさんのノウハウを持っていたからです。なので、このコーディングの後の作業は全く難しくありませんでした。コーディングをしながらコードプロファイラも時々チェックをしました。サーバとネットワークの仕事をするこのエンジンというのは問題があるんです。つまり、バグが発生すると致命的なんですが、そのバグはなかなか見えないということです。もちろん、みなさんもゲームサーバを作る時はそうなんですけど、我々はサーバエンジンを作る時、十分なテストをしないといけないんです。幸いなことに我社には以前作ってあったテスト用のプログラム、そしてテスト用のハードを持っていたんです。さまざまなユニットテスト用のプログラムも持っていましたし、MMOゲーム用あるいはP2Pカジュアルゲーム用のテストプログラム、あるいはFPSゲーム用のテストプログラムも持っていました。また、たくさんの共有機器、そしてテストコンピュータ、そしてイーサネットカードなどを使いました。1台のコンピュータに5つ以上のイーサネットポートをつないだんです。そして全てのテストケースは同時並列にしました。リグレッションテストもできるようにしました。また様々なシナリオテストもいわゆるオートメーション化して持っていました。そして全てのテストがクリアできるまで、開発を進めたんです。


またストレステストも必要ですよね。我々はテストケースを2つ考えました。ひとつのサーバインスタンスで処理するというのを前提としました。そして全てのプレイヤーは止まらず、ずっと動き続ける、と。サーバは各クライアントからプレイヤーの位置とかスピードとかの情報をもらって、周辺の他のプレイヤーにマルチキャストをします。MMOでもP2Pの通信がミックスされることがありますから、ここではサーバの負荷を下げるという機能がありますが、そういった機能は使いませんでした。なぜならばその機能を使えばサーバにどのくらいの負荷がかかっているか分からないからです。なのでサーバは8コア入りのXeon、CPU2つのコンピュータを使いました。


では、これまでの作業をビフォーアフターで見てみましょう。我々のサーバエンジンを使ったゲームサーバプロセスは、最初は同時アクセス3000でも重い、きつい状況でした。ひとつのCPUを使う比率があまり高くなかったからです。では見てみましょうか。


同時接続者が増えていますが、CPUの使用率は低いです。そこで我々はこの作業をしました。ではアフターの状況を見てみましょう。


同時接続者が増えてきました。ゲームサーバインスタンスひとつです。CPU利用率もここで見られます。MMORPGゲームではプレイヤーが歩き回っています。ここで同時アクセス者がずっと増え、ご覧のように、同時アクセスが1万人を超えました。


でもラックはありません。ひとつのサーバインスタンスです。ご覧のようにCPU利用率は大体半分くらいいってます。ここでおかしなものが見えますか。CPUがひとつだけ非常に利用率が高いんです。なぜかというと、これはカーネルの中でデバイスインタラプトハンドリングをしているんですけれども、それはひとつのCPUしかできないからなんです。このマシンは2つのNUMAノードから構成されています。ですからこのマシンの性能を利用するためにはサーバプロセスは2つ必要ということになります。そうするとこのコンピュータでは同時接続は2万まで可能だということになります。我々は敵の大将を仕留めたんです。でもここで終わりではありません。サブ、副大将が大将になってしまったんですね。つまりこれまでサブだったものが1番目の問題になったんです。そうするとこのサブのリーダーも仕留めないといけません。こういうふうに問題をクリアしていって、サーバの性能を上げていったんです。

ひとつ告白すると、ゲームサーバの開発でサーバに入る最大同時接続者の数、これが何人か、ということは大事ではありません。サーバの数を増やすことでほとんどの場合解決できるからです。しかしやはり我々は最悪の状況も想定する必要があります。オンラインゲームひとつ開発するのにたくさんのお金がかかるというのをみなさんはご存知ですが、せっかく作ったのにゲームサーバが不安定でサービス提供ができない、これこそが避けなくてはいけない状態です。ですからゲームサーバを作る時は最悪のケース、最悪の状態を必ず想定しなくてはいけません。

2点目のポイントは、ゲームサーバのコンピュータ1台が同時接続者を大量に処理できるというのを、逆に表現するとどうなるのか、ということ。つまり、同時接続者を処理するゲームサーバが他の仕事もできるということなんです。どういう意味かと言うと、プログラマーの皆さんがゲームサーバによりたくさんの仕事をさせることができる、プログラムを入れることができるということです。ゲームサーバにたくさんの役割を持たせることのメリットは、例えばゲームのロジックをクライアントじゃなくてサーバの方に移すことができます。そうするとハッカーからこれを守ることが可能です。


じゃあここからサマリー(要約)です。マルチコアスケールアウト、これは非常に難しく、慎重に取りかからないといけません。小さなミスも許されないんです。そしてネットワーク・サーバプログラミングはデバイスのI/Oが非常に関わってくるプログラムで、1対多数ということでインタラクションが多んです。ひとつのオブジェクトがクライアントと1対1ではなくて、いくつかのオブジェクトが絡んでくるという可能性が高いということです。またデバイスのI/Oも多いですよね。ですからマルチコアプログラミングのための様々なツールというものがあります。例えば並列化のライブラリとか、あるいは並列のツールとか。そういうものを使うと、また活用率が下がる、という問題もあります。ただ、サーバの性能を最大化するためにはハードウェアの隅々まで活用し、オペレーションシステムも理解し、それに合わせてゲームサーバを作りこむ必要があって、これによってパフォーマンスを最大化することができます。

では、ここで整理をしてみましょう。皆さまが作っているゲームサーバでも、コンテキストスイッチングは最小限にしなければなりません。そしてコンテンションも減らさなくてはなりません。カーネルAPIの呼び出しもできるだけ減らす必要があります。そしてキャッシュのヒット率、これも考慮しなくてはなりません。ただ、残念なことに、マルチコアプログラミングという分野で、未だ完璧な王道というのはなく、練習するしかないんです。日頃マルチコアプログラミングについてよく勉強しておき、そして皆様が開発の過程でどんな場面に遭遇するかをよく注意深く頭に入れてください。そして一生懸命勉強する。そうすることによって皆さんなりの、皆さんだけの作戦をここで作りこむことができるんです。私の今日のお話が皆様に少しなりともご参考になれれば嬉しいと思います。以上です。ありがとうございました。

質問がございましたらお受けいたします。はい、どうぞ。まずマイクをお持ちしますので、マイクでお話していただければと思います。

質問:
さっき話したことの中でブロードキャストということがあったじゃないですか、ブロードキャストって、僕も一度ソケットサーバを作った経験があります。テストレベルなんですけれども。1万ユーザーに対してブロードキャストするためには、どうやって早めにブロードキャストした方がいいかっていうことを悩んだことがあります。そのタイミングではやっぱり100クライアント単位でスレッドを生成してブロードキャストしたらいいかなと思っ たんですけれども、例えば100人、100オブジェクトのスレッドフルでという形なんですけれども、それで性能から見れば別に悪い方法ではないでしょうか。

通訳:
まず、100オブジェクトのスレッドがデバイスタイムで作動するのか、CPUタイムで作動するのかが重要です。もしも100のスレッドが全てCPUタイムでありますと、とてつもない量のコンテキストスイッチングが発生します。そうなりますとパフォーマンスはめちゃくちゃになるんですね。しかしこの100のスレッドが妥当な理由があるデバイスタイムを持っているとすれば、100のスレッドがあっても問題になりません。ですので100のスレッドでブロードキャストをどのようにすればいいのかについては、ご説明をもう少し聞かなくてはいけないと思うんですが、基本的な指針としましては、この100のスレッドが同時にCPUタイムをしてはいけないということです。また、CPUタイムの原則がありまして、CPUタイムで作動するスレッドの個数はCPUの個数を超えてはいけません。基本的にはですね。しかし普通はCPUを100もつけないですよね。なのでCPUタイムかデバイスタイムかまず確認をしてください。それからCPUタイムでありましたら、スレッドの個数を減らすのが重要であります。スレッドの個数が少ない方がパフォーマンスは上がります。

質問:
サーバに関わらずマルチコアのプログラムの場合は一般的に言えると思うんですけれども、先ほどの処理をスクイーズする場所とそうでない場所に分ける、その分け方がすごいポイントだという話をされたんですけれども、その分け方を見つける時に、何か手法というのはありましたか。

通訳:
そうですね……まずコードプロファイラを使いまして、遅い部分を探すんです。遅く実行される部分を。それから並列性が落ちる部分を探します。インテルの Parallel Studio (パラレル・スタジオ)のようなプログラムの場合は並列性を探す機能がありますよね。私どもはそれを使う方法を当時知らなかったので、Visual StudioのプロファイラとGlowCodeを使いまして、まずはボトルネックが予想されるところを探しました。で、実行時間が長くかかる、そういった部分がどのエリアをロックして、どのエリアをロックしないのか分析しました。その結果、スケッチブック……大きな紙にその図面を描きました。どのルーチンがどこまでロックし、どのルーチンがどこまでアンロックをする、という分析を行ったんですね。それから紙に地図を描いて、どこのスクイーズをしなければいけないのかを見つけたんです。そのスクイーズという技法自体も、本で見たりインターネットで見つけたものではなく、やっている途中で見つけたんです。他でもこういった方法はあるかもしれないんですが、このスクイーズというのは私が開発中に考え出した技法だと申し上げられます。


質問:
ありがとうございます。

司会:
他にご質問される方はいらっしゃいますでしょうか。

質問:
興味深かったです。ProudNetはWindowsのOSの上で動くサーバフレームワークみたいだと思うんですけれども、お話の中にあったように、CPUのコアのうち、ネットワークの割り込みを処理できるコアって、昔は1個だったりとか、制限されているというのは確かに問題がありました。実際に言うとこれはもっと並列度を上げていくとネットワークI/Oを処理できるコアの数が少ないということ自体が多分ボトルネックになってくる可能性があると考えていて、Linuxとかの新しいバージョンだと多分そこらへんの問題も解決したりっていう話もあると思うんですが、WindowsもOSの上でサーバシステムを構築するという面で何かメリットがあるのかとか、あと将来について、やっぱりずっとWindowsのOSを使った方がいいのかとか、そこらへんの技術的な展望があれば教えていただきたいと思います。

通訳:
すばらしいご質問をありがとうございます。Linuxでは既に提供していると、それをチーミングというふうに言っています。ネットワークインターフェースチーミングと言われているテクニックです。これはLinuxに入っています。ひとつのCPUにこれが集まらないように既に作りこんであります。Windowsでは現在、ベータバージョンですけれども、Windowsサーバ2012でこのチーミング技術が盛り込まれるという噂があります。それがどうなるのか、どんな形なのか、私も非常に関心があります。今のバージョンのWindowsではチーミングはサポートしていません。したがってひとつのCPUに使用が集中するという問題があり、それを解決する方法は何か。一つとして、サーバにいくつかのネットワークカードを入れます。そしてWindowsのコントロールパネルの方に入って、デバイスドライバごとに使えるCPUを設定します。それからゲームサーバをマルチホームでネットワーキングさせますとさまざまなCPUに分散処理ができるようにできます。ただこれはちょっとトリックですよね。チーミングくらいの仕事は期待できません。

そしてLinuxのバージョンについてお話ししますと、私どもも現在はLinuxバージョンのものを現在作っています。かつては韓国ではゲームサーバを作る時にほとんどWindowsを使っていました。なぜWindowsだったのか、ということについてはいくつか理由はあるんですけれども、まずはゲームクライアントとのソースコードの共有が簡単だった、そしてスタークラフトをゲームサーバの方で回すことができたということです。しかし今は時代が変わりました。iPhoneを使っている人もいればAndroidのユーザーもたくさんいます。ですから、エスコードでゲームサーバを開発している人もいるんです。エスコードはプリービアスの基盤、ベースです。なのでエスコードで作ったゲームはプリービアスのベースのところでは動きます。またAndroidなどを使う場合もあります。このような理由からこれ以上はもうWindowsにこだわる必要はないということで、Linuxでゲームを作るという人達も増えています。Amazonとかあるいは韓国のLGUクラウドとか、こういったところではLinuxベースでクラウドサービスも提供しています。非常に値段も安くて、Windowsの値段よりも半分なんです。したがってこのメンテナンス費とか維持費を考えてLinuxを使う人もいます。しかし、Windowsを使うのか、Linuxなのか、性能の問題よりも大事なことがあって、それは自分がどのくらい使いこなせるかということです。自分が使いやすい方を選ぶべきだと思います。いくらサーバが良くても安定性が揺らげば話になりません。ですからバグが発生してもいち早くそのバグを制御できるようなツールを、Windowsで自分がやりやすいのか、Linuxの方がやりやすいのか、これによってサーバを選んだ方が賢いと言えるのではないでしょうか。お答えになりましたか。

質問:
ありがとうございました。

司会:
ではお時間になっておりますので、これにて終了とさせていただきたいと思います。ありがとうございました。

この記事のタイトルとURLをコピーする

・関連記事
ドラゴンクエストXは「世界は一つ」を実現するためにどのようなサーバ構成にしているのか? - GIGAZINE

CPUコア40・メモリ128GB・SSD400GB搭載の2Uサーバ「PowerEdge R810」フォトレビュー - GIGAZINE

大量アクセスによるサーバの負荷テストなどが簡単に実行できるフリーソフト「JBlitz Professional」 - GIGAZINE

「アメーバピグのサーバ(仮)」を入手することに成功、じっくりと調べて撮影してみた - GIGAZINE

トラブルをわざと発生させサーバ問題解決能力を鍛える「Trouble-Maker」 - GIGAZINE

in 取材,   ゲーム, Posted by darkhorse_log

You can read the machine translated English article here.