+stream

インサイド・オンラインゲーム 第2回 バックエンドの実装

by  ringo   2008-08-07 13:20

PDF版はこちらからダウンロード


前回までのあらすじ

前回は、IMPG(インターネット対応マルチプレーゲーム) サービス全体を概説した。 特に IMPG サービスにとって根本的に重要な点として

  • ゲームプレー空間のセキュリティを確保すること
  • ゲームプレー空間のメンテナンスをしていくこと

以上の2点を挙げ、 これら2点を実現していくためには一極集中型のバックエンドサービスが必要 であることを示した。

概論は第1回で終わりにして、今回からは各論に入ろう。 各論のまず最初に、実際のゲームサービスで使われているバックエンドサービスの 考え方や実装例を紹介するとともに、 将来のさらなる性能向上を模索してみたい。

今回の議論では IMPG のバックエンドとして使用されている、 MySQL などデータベースエンジンの話題が中心となる。 そのために SQL やデータベース関連の基本的な用語を多数使うことになるが、 まだまだそれらに馴染みのない読者も多いかもしれない。

SQL やデータベース関連の知識を持っておく事は、 バックエンドの設計にはもちろん必須なのだが、 Web サイトの構築や一般的なプログラミングにおいても非常に有益である。 以下に、データベース関連の入門になりそうな Web サイトをふたつほど紹介する。筆者も時々アクセスするサイトである。 参考にしていただければ幸いである。

RDBMSやデータベースそのものについて
http://www.wakhok.ac.jp/DB/DB.html
SQLプログラミングについて
http://www.ann.hi-ho.ne.jp/hirok/sql/

コラム: スター型 IMPG への意見

前回行なった議論に対して身近な人々から 意見があったので、いくつか紹介しよう。

スター型 IMPG は、アジアにおいてその真価を発揮するのでは?

中国、台湾、韓国などのアジア市場においては、 ソフトウェアのパッケージ販売は違法コピーがあまりに多いため、 市場が大きい割にはパッケージ代金を回収できない。 しかし、スター型 IMPG のように、ゲームサービスの大事な部分をすべて パスワード境界で守られたサーバに配置し、サーバへの接続に対して課金 することにより、コピーによる売上減少を最小限に食いとめることができる (あるゲーム会社社長談)。

実はこの流れは、最近聞かれるようになった「ネットワーク越しのアプリケーション」を使い、 大事なロジックを実行する部分はデータセンターに置いておくモデル とも同調している。そろそろビット列のコピーだけで売りものになる時代は、 終わりつつあるのだろうか(中嶋謙互)。

それでも P2P 型は作られる

P2P 型は、「プログラムが簡単」、「サーバのエンジニアがいらない」、 「設備投資がゼロ」といった特徴があるので、不滅である(あるゲーム開発者談)。

もちろんその通り。しかしこういう問いをしてみるとどうだろう。 「そのゲームにスター型の要素を少しでも追加したら、面白くなるだろうか?」 答えは、ほとんどの場合 Yes である。 あとは、「どれぐらい面白くなるか?」、 「どれぐらいコストがかかるか?」 を比べて、臨界点を探すだけである。 もちろんコストが下がっていっても、 P2P型と同じほどに安くなるわけではない。 しかしスター型の応用をすることでどういう風に面白くなるのか、 いろんなアイデアがもっと出てくれば、ゲームを面白くする方法として スター型に対する積極意見もどんどん出るようになるはずだ(中嶋謙互)。

IMPG バックエンドの仕事

IMPG のバックエンドがこなさなければならない仕事は、 ゲームプレー空間の状態を保存するストレージの仕事ばかりではない。 最新の IMPGサービスでは、 クラスタノードごとに完全に分離できないタイプの サービスは複数種類あるのが普通だ。 ここでは稼働中のゲームの実例として、 ディプスファンタジア(エニックス)の例を紹介する。 ディプスファンタジアのバックエンドでは、 以下のようなサーバ群が常時動作している。 現在でもかなりの種類があるが、 今後ゲーム内容の充実に伴ってますます増えていくことだろう。

ストレージ プレーヤーキャラクターの情報を永続化するために、もちろん必要である
ユーザー・ロック 1個のIDで複数のクラスタノード(ゲームサーバ)にログインできてしまうと、 アイテムの複製などいろいろな問題が生じるために、 統一的なロック制御が必要
ランキング ランキングも、クラスタノードごとにあるよりは、 世界全体に1個に限定されているほうが面白い
オークション オークションの内容も、世界全体で統一のものがあるほうが、 便利で面白い
ギルド ディプスファンタジアには「ギルド」というプレーヤーのグループ化 機能がある。特に世界の中に排他的に制御された拠点を持つグループが 16個存在できるのだが、「全世界で16個」に限定するためにこのバックエンドが 用意された。
インスタント・メッセージ 別のクラスタノードでプレー中のプレーヤーや、 プレーしていないオフラインのプレーヤーにメッセージを送るためには バックエンドが必要
認証 認証に関しては厳密にはバックエンドは必要ないが、 本文で説明している「非同期APIの必要性」により、 バックエンドの一部として用意されている
ログ 全部のクラスタノードが吐きだすログを1個所に固めて取りだしたい。 これを実現するためには通常は syslog デーモンを使うが、 本文で説明する「共通のAPI」の必要性により、 独自のログ収集サーバを用意している。 (標準の syslog は遅すぎるという問題もある)

以上のバックエンドには ディプスファンタジアに特有の部分(オークション、ランキング、ギルド、ログ) と汎用の部分(ストレージ、ロック、インスタントメッセージ、認証)とがある。 汎用の部分については、コミュニティーエンジンから提供している mm-suite という ミドルウェアパッケージに含まれているものが、そのまま使われている。 (よく考えるとログ収集も汎用の機能にしてよさそうだ)。

ディプスファンタジアでは、これら複数種類のサービスのそれぞれに対して 各ゲームサーバが常時 TCP のセッションを1本保持しておき、 再接続の負荷を軽減している。 この方法は、DBプロセスにアクセスする性能を向上させる 一般的な最適化と同様である。 また、 バックエンドへの接続に TCP を使うと、マシンの能力や資源が不足したときに、 すぐに別のマシンに載せかえができるという利点がある。 実際、ストレージサーバだけは高性能が要求されているので、 専用のマシン上で単体動作させている。 以下に、プロセスと TCP セッションの観点からみた接続図を示す。

プロセスと TCP セッションの観点からみた接続図

図の右側が「フロントエンド世界」であり、 ゲームのロジックを実際に実装している部分、図の左側が、 ストレージなどの仕事を担当する「バックエンド世界」である。 フロントエンド世界とバックエンド世界の間は、Ethernet などの物理 ネットワークで接続されている。 この図では、非常に大量のセッションが多数のプロセス間で 同時に使われていることが見た通り分かる。 セッション数やプロセス数が多いことは そのままメンテナンスの手間にはね返ってくるため、 この複雑さを、性能を保ったままどういうふうに解消するのがよいのか、 現在模索中である(筆者としては、現在のところ 分散オブジェクトの考え方をうまく応用すれば、 性能をそのままにメンテナンス性を良くすることができそうな予感がしている)。

また、これらのバックエンドプログラム群のうち多くは、 データを保持しておくために、さらに下層のプログラムとして MySQL を使っている。 MySQL にアクセスするための libmysqlclient というC言語用のライブラリを用いて、 直接 MySQL にアクセスしている。 MySQL のような DBMS に対して C言語のような低レベルの言語を使って アクセスする例はあまり多くないと思うが、 なぜそのようなデザインになっているのかは、 次節から説明する「dumbなバックエンド」についての議論や、 メンテナンス性と拡張性に関する議論を経ることによって 理解できると思う。

dumb なバックエンド

最近私は「テレコズム」([3])という本を読んだのだが、 その中の「ネットワークは dumb であるべきであり、 インテリジェンスは末端に集中させるべきである。」 という記述に非常に共感できた。 この意味は、インフラはできるだけ単純で賢くないほうが使いやすく、 拡張しやすく、また堅牢になり、複雑なロジックは端末側に実装しておくと 変更しやすくなるという事である。 この論理は通信のコストが十分に低いことを前提としている。

この論理は各種のシステム設計においてかなり重要なアイデアのひとつ になっているようだ。 実際、既存のプロジェクトでも確実に採用されつつある。 たとえば IPv6 の設計においてはできるだけ IP層の動作を単純化しようとする努力がなされているし、 qmail などのインターネットサーバプログラムでも、 「dumbなインフラ」を目指して開発されていることが一目で分かる。

私はIMPGシステムの設計においても 「dumbなインフラ」論を適用することによって、 堅牢で拡張性の高いバックエンドを構築することが可能だろうと考えている。 つまり、複雑で随時バージョンアップしていくようなロジックはすべて フロントエンドのゲームサーバクラスタに置かれ、 バックエンドはあくまでも寡黙に、 データの保存と取り出しだけを行なうようにするのだ。 そうすることで、変更と機能拡張に強いシステムができあがるはずだ。 このデザイン指針の下では、ゲームのロジックのようなインテリジェントな 部分はすべてフロントエンド側に置かれ、 バックエンド側には一切実装されないことになる。

MySQL の必然

「dumbなインフラにすべきだ」という考えに基いて バックエンドサービスを単純化していくと、その極限にあるのは、 単なるリモートファイルシステム(NFSやCIFS、iSCSIなど)である。 わざわざバックエンドのプログラムをごりごりと書いたり、SQLを使わなくても、 リモートファイルシステムをフロントエンドのゲームサーバークラスタから 叩いてやるだけでよいのであれば、簡単なのではないか?

しかし、IMPG のバックエンドとしてはリモートファイルシステムはまだちょっと 足りない部分や、使いにくい部分がある。例えば、 リモートファイルシステムを使うと、 ファイル(データ)全体に対して Atomic にアクセスできないとか、 データの中身に関してちょっとした統計を取りたくてもできないといった問題だ。 これらの問題は、 素のファイルシステムに対してちょっとした wrapper をかぶせてやるだけで 解決する。

この wrapper 役として筆者が現在のところかなりベストな選択肢だと思っているのが、 MySQL データベースである。 MySQL は、単純なことを高速にやることを主眼にデザインされていて、 機能はそれほど多くないものの、非常に高速に動作する。 有償の各種 DBMS と比較しても、シンプルなクエリを処理させる場合は、 圧倒的に高速である。そして何より、特殊な設定をしなくても、 デフォルトの状態でバリバリ高速に動く。

インターネットを見渡しても、PostgreSQL、Oracle、Interbase などと比較して高速に 動作するというベンチマークが多く見つかるし、 コミュニティーエンジンでは IMPG サービスのアクセス特性に基いて Microsoft SQL Server との比較ベンチマークを 行ない、 実際に5~6倍のストレージ性能が実現できることも調べた。

以上のような MySQL の特徴は 「ファイルシステムに wrapper をかぶせるだけ」 という今回の目的に完璧にマッチする。 このような特徴があるので私はほぼすべてのプロジェクトで MySQL を使っている。 さらに現在までのところ、 MySQL がクラッシュしたことは一度もない。 十分に商用利用できる水準に達していると言える。

ストレージに対する要求性能

バックエンドに要求される機能はストレージばかりでないことは前に述べたが、 議論の対象としてストレージはわかりやすいので、 今後の議論はストレージに焦点を当てて進める。 ここでは IMPG サービスにおいてストレージにどの程度の性能が要求されるのかを 見ていこう。

いつ保存するのか

ストレージの目的はゲームプレー状態の永続化である。 したがって、前回の「つとむ君、さとし君」の例で説明したように、 あるプレーヤーのゲームプレーが、 クラスタノードの境界を越えて移動するとき(3面サーバから8面サーバに移動 するとき)に1回、保存が必要となる。 クラスタノードから出るときに1回バックエンドに保存し、 別のクラスタノードに入るときに1回ロードする。 実際にはストレージの能力が許すかぎり頻繁に保存するが、 その理由については後述する。

何を保存するのか

クラスタノード間を移動するのは、多くの場合、 1プレーヤーのキャラクターに関係する情報だけである。 1個のキャラクターに関係する情報の量というのはそれほど多くなく、 多めのゲームでも数十Kbytesから100Kbytes程度に収まる。 逆に考えると、クラスタノード間を移動させる必要のある情報の サイズが大きいならばクラスタに分けている意味がない。 そうなってしまうのは、設計が間違っているか、 そもそもゲームの中身がクラスタリングに向いていないということだろう。 ディプスファンタジアの場合は、10Kbytes ~40Kbytes の間のサイズである。 フロントエンドのゲームサーバは、300Kbytes 近くのデータを、 (バックエンドにゲームのデータを保存する際に)自前の圧縮(pack) ルーチンを使って小さくしてから送信している。

どうやって保存するのか

SQL の replace を使って保存し、 select を使ってロードする。 ゲームサーバのプログラミング上の必要により、 非同期API が必要になってくるのだが(後述)、 MySQL は非同期の API を現在は持っていないため、 非同期にするための wrapper を通してアクセスする。

基本的には以上のような感じでストレージへのアクセスが発生するのだが、 実際には、「いつ保存するのか」で示したよりもはるかに頻繁に 保存が行なわれる。その理由はスター型IMPGのサーバならではの特殊な事情 によるものだ。以下に、頻繁な保存が必要である理由を列挙してみた。

  • スター型 IMPG のゲームサーバは、パフォーマンスを最大にするために、 C 言語や C++言語のような低レベルのコンパイル型言語で記述されている。
  • スター型 IMPG の宿命として、頻繁なゲーム内容の更新や、 バグフィックスが期待されているので、毎週のようにゲームサーバのプログラム自体に 手が入れられることになる。よく言われるように C(及びC++)言語で書いたコードには 150行に1個バグが入っているとされるから、 毎週バグが混入していくと考えたほうがよい。
  • このらのバグがサーバのクラッシュを引きおこす確率はかなり高い。
  • ゲームの開発期間は1~2年で、その間インターネットに直接さらされる という経験を経ていない。通常、そのようなプログラム は、数年という年月を経てバグが潰されていくものだが、 ゲームサーバには、そのようなチャンスがない。

このように、IMPG サーバは商業上の制約により、 どうしてもクラッシュをひきおこすバグとの戦いに巻きこまれるのだ。 もちろん、開発チームがどんどんバグを潰すので 数ヶ月もすれば徐々にクラッシュは起きなくなっていくが、 新しい仕様が追加されるごとにバグはかならず混入する。

筆者の経験では、 例えバグが含まれていても、 それが着実に修正され続けていれば、ユーザーはそのゲームを見放さないようだ。 大事なことは、とにかく修正し、更新し続けていくことなのだ。

少し脱線してしまったが、以上の議論から導かれるストレージの動作として、 少なくとも読み込み(キャラクター情報のロード)中心ではなく、 書き込み(セーブ)が中心であるということがわかると思う。 一番書き込みが少なくても、読み込み1に対して書き込みが1の割合で 発生する。実際には、ディプスファンタジアの場合、 書き込み100に対して読み込み1程度のアクセスが発生している。 このようなアクセスパターンは、通常の DBMS が期待している動作とは、 かなり異なるのだ。IMPG におけるバックエンドデータベースは、 この点で特殊な仕事を要求されていると言える。

コラム: クラッシュしないフロントエンドを目指して

処理性能を犠牲にしてクラッシュによる損失を最小化するための方法論として、 本文にあるような頻繁にセーブを繰りかえす方法以外に、 Ruby や Perl のようなインタプリタ言語を使って ゲームのロジックを書き、 サーバにバーチャルマシンを内蔵して実行させるという手が考えられる。 もちろんバーチャルマシンを使ってプログラムを実行する場合は、 その分実行速度が低下する。 どの程度性能が低下するかを、 Ruby、Perl、Java、Cの4言語で比較してみた(使用マシン:AthlonXP 1900+/1GBmem)。

言語Ruby(1.6.4)Perl(5.6.0) Java(kaffe 1.0.6jit)C (gcc 2.96 -O0) C (gcc 2.96 -O2)
CPU時間105.1sec54.7sec 0.44sec0.90sec 0.20sec
C言語(-O2)比525273 2.24.501.00

Ruby言語リスト

N=10000
array = []
(N+1).times { array.push(0) }
for i in 0..N
  for j in 0..N
    array[j]+=i
  end
end

Perl言語リスト

$N=10000;
@array = ( 1 .. $N );
for($i=0;$i<$N;$i++){
    for($j=0;$j<$N;$j++){
        $array[$j]+=$i;
    }
}

Java言語リスト

public class a
{
    public static void main(String args[]) {
        int i,j;
        int N = 10000;
        int array[] = new int[N];
        for(i=0;i<N;i++){
            for(j=0;j<N;j++){
                array[j]+=i;
            }
        }
    }
}

C言語リスト

#define N 10000
int main()
{
    int i,j;
    int array[N];

    for(i=0;i<N;i++){
        for(j=0;j<N;j++){
            array[j]+=i;
        }
    }
}

本命の Ruby や Perl は、それぞれ C言語の 500倍以上、250倍以上という 壮絶な遅さである(C言語の -O2は、 アセンブリレベルでちゃんと配列加算している事を確認した)。 もちろん、ここでプログラムしたロジックは、 高級言語の良さをまったく生かすことのできない、 2重ループを使って配列の各要素に加算するという単純なものである。 それに、C言語の場合全部の命令がCPUの命令キャッシュに載ってしまうので 現実のプログラムよりも高速になっているし、 Java の場合はJIT(Just In Time compiler) の効果がいちばんあらわれやすい プログラム例となっている。

しかし、現在 C言語で実装されているゲームのフロントエンドサーバのロジックを ざっと見てみると90%以上の部分が整数の四則演算と条件分岐によって 占められていて、のこりの10%の部分が文字列の演算となっている。 さらに、ゲームサーバの実行時間を計測すると、 I/Oのためにシステムコールを呼びだしている時間は全体の数%に満たない。 従って、おそらくここで調べた速度差が、そのままとまではいかないまでも、 相当な程度でサーバの動作に影響を与えるだろう。 ここではプログラム例は省略するが、 より現実的な(大きな)プログラムを使って比較をしてみたところ、 C言語と Ruby では 10~20倍程度の差しか出なかった。そして Ruby と Perl と Java の差はぐっと縮まった。

Ruby のようなスクリプト言語でIMPGのサーバを書くことは夢に終わるのだろうか? 私はまだあきらめてはいない。サーバのクラッシュを防ぐことができる以外にも、 開発効率の向上という大きな利点を手に入れることができるからだ。 私は特にメンテナンスのしやすさとコードの再利用、 C言語やUNIXとの親和性の点から Ruby に注目しているが、 いろいろな工夫を積んでいくことで、IMPGのサーバのロジックを Ruby で書く ことができるようになると信じている。 以下に、今後私が実行してみようと思っている工夫を挙げる。

ロジックを2つのレイヤに分けて開発する
内側のループは下層のレイヤとしてC言語のRuby拡張ライブラリとして できるだけ作っておき、それらの API を Ruby 部分から使う。
ハッシュや動的なデータ構造、文字列を使って、ロジックそのものを変 更する
スクリプト言語が得意とするデータ構造を駆使して、 ロジックそのものを変更していくことで、 実質の性能差を縮めることができそうだ。
バーチャルマシンを高速にする
C言語よりも100倍遅いと採用できないが、10倍遅いだけなら、 採用できるかもしれない。IMPG サーバに特化した、 何らかの別なRuby処理系の可能性はゼロではない。

IMPG サーバの開発効率に関しては、このほかにもあらゆる部分に未開拓部分が 残っている。今後の連載でそのあたりにも触れていきたい。

レプリケーションが使えない!

MySQL など、DBMS の性能をチューンするにはどうしたらいいか という問いを、データベースのエンジニアに投げかけると、 10人中10人が、「レプリケーションを使って、サーバを複数台に分けてみた?」という反応をする。 CPUやネットワークの負荷が高いのであれば、マシンを複数台使ったり、 ネットワークを分けたりして高速化することができるはずだというのは 素直な発想だ。

ここでひとつ確認しておかなければならない事は、 レプリケーションの問題は、 基本的に読み込み(検索)の性能を上げることしかできないということしかできない点だ。 検索エンジンやディレクトリサービス、掲示板などの読み込み中心の Web でのサービスでは、レプリケーションを使って読み込み性能を激しく チューンできるのだが、書き込みが多い場合は、性能が思ったほどは向上しないのだ。 これは MySQL リファレンスマニュアルの Replication の項にも明記されてい るのだが、これはなぜだろうか?

通常、データベースのレプリケーション機能は、1個のマスターサーバに 対して複数個のスレーブサーバから接続し、マスターサーバに対して更新され た情報を随時スレーブにコピーする。 そして、スレーブで検索するときに、 マスターからコピーされてきた情報を使う(図)。 このようにして検索の性能を上げるためには、 通常はスレーブサーバを別のマシンで立ちあげて Ethernet などのデバイス を使って接続する。したがって、Ethernetが 100Mbps の場合は、 100 Mbps 以上の速度でマスターに対して更新すると マスターからスレーブに対してコピーする速度が物理的に不足することは 直観的に分かるし、さらに考えれば、コピーするためのトラフィックが 1の書き込みに対して2発生し、100Mbps の3分の1しか書き込みができない ことが分かる(並列ではなく直列につなぐと2分の1にまで改善するが)。

従って、この例のシステムにおける書き込み速度の上限は、 「1台のマシンにつなぐことができる物理層の最高速度の3分の1」と等しくなる。 構造からも明らかな通り、更新の最大性能は1個のデータベースだけを使う場合 と比べると一切向上しないか、低下している。

次の考えとして、データベースの設定を多少工夫して、 それぞれのサーバに対して更新要求ができるようにするという案がある。 つまり、それぞれのサーバをマスターでもありスレーブでもあるように設定して、 それぞれが他の2つのサーバに更新をコピーするようにしておく(図)。

このようにしても、実は全体の更新速度の合計の最大値は前回とまったく変わらず、 1台のマシンに接続できる物理層の速度で決まってしまう。 この理由は例えばA、B、C のそれぞれに 33Mbps で書き込みをする場合、 Cに対しては、A、B のサーバからも 66Mbps の更新通知が到着することになり、 合計で 100Mbps の書き込み要求を処理する必要が生じる状態を考えると分かるだろう。 この限界は、更新してすぐにコピーする方式をやめて、 必要になってから取り出す方式にしたとしても変化しない。

このようにレプリケーションを使って、 「同じ情報の内容をもつ複数のデータベースサーバ」を構築しようとする努力は、 どうやら問題を解決しないということがわかった。

ディプスファンタジアの場合は、更新速度は現在 100Mbps でぎりぎり足りているが、 中国や韓国など、サイトへのアクセスが1桁も2桁も多い環境に この仕組みのまま持っていくと問題が生じることは目に見えている。 これはギガビット Ethernet にしたり、速いマシンに入れ換えるだけで 解決する問題ではない。何か、まったく別の解決策が必要とされているのだ。

分散型ストレージを模索する

前節の議論により、 バックエンドの更新性能を向上させるためには 何らかの分散化が必要で、分散されたそれぞれの部分(ノード)が同じ情報を 持たないようにすることが必要であることがわかった。 この要求を実現する方式は筆者が考えるだけでも複数あるが、 それぞれ、規模拡張性(スケーラビリティ)やメンテナンス性の点で、 トレードオフの関係にある(図)。

規模拡張性が高いほど、メンテナンス性がわるくなるの図

本稿では次節から、 このトレードオフの両極端の間にある4種類を順次紹介していくが、 その4種類について、最初にまとめておこう。 筆者はいまのところこの4方式のいい名前を思いついていないのだが、 何かいい名前はないだろうか。

1. いたって普通の方式
データもインデックスも、1個のデータベースに集中させる方式。 何の工夫もしない実装の例である。
2. インデックスだけ集中させるタイプ
キャラクターデータの本体と、それを見つけるためのインデックス を分けてしまう。 保存したいデータの本体はUNIXのファイルとして保存し、 MySQL には、そのデータが「どこにあるか」だけを保管しておく。 現在のディプスファンタジアの方式がこれである。
3. ユニークキーで分散させるタイプ
各フロントエンドが、 バックエンドの複数のサーバに対して総あたり的に接続し、 クエリーをするときに使うユニークキーをもとに 何らかの固定的な方法でどのサーバに対してクエリーを発行するかを 決める。メンテナンス性がかなり悪いが、 更新性能は事実上無限にできる。 アクセスの多い Web サイトでも使われている方式である。
4. ユニークキーで分散させるタイプで中継付き
これは2番目の方法の応用で、多少メンテナンス性を良くすることができる。 最大規模のサイトを運用するためには、 おそらく3番目の方法ではメンテナンス不能に陥りそうなので この方法を考えてみたが、実際に3番目の方法で無理になるような サイトは地球上には存在しないかもしれない(10Gbyte/sec の書き込みとか)。
5. P2P 的な方式(おまけ)
圧倒的に強力なストレージ能力を得る作戦というのは 世界を見渡すと色々あるのだが、 ここ数年では P2P的なネットワークを使ったやり方で何とか ならないかと思ってチャレンジしているプロジェクトがいくつかある。 P2P 方式はレスポンスが IMPG で要求されているよりも遅いことが多く、 すぐには採用できそうにないが、 何かおもしろい方法論が見つかるかもしれない。

次節以降では、以上のそれぞれの方式について、 どの程度の保存能力を実現できるのか、その反面どういった欠点を持っている のかを見ていこう。

いたって普通のタイプ

激しくチューニングしたモデルを紹介する前に、 何の工夫もせずに作るとどうなるのかを見ておくのは良いことだ。 ここでは、本当に何も工夫をしない場合どのようなデザインになるのかを見て みよう。この例の場合は、複数のフロントエンドゲームサーバから、 たったひとつの MySQL サーバに対してクエリが発行される。

ここでフロントエンドのゲームサーバが、 「あるひとかたまりのデータ」をデータベースに保存したいとする。 特に工夫をしないとすれば、 以下のような構造のテーブルを作ることになるだろう (MySQLの例。分かりやすくするために、テーブルの構造は実際よりも単純化している)。

mysql> create table savedata ( uid varchar(32) not null primary key, data mediumblob );
mysql> describe savedata;
+----------+---------------------+------+-----+---------+----------------+
| Field    | Type                | Null | Key | Default | Extra          |
+----------+---------------------+------+-----+---------+----------------+
| uid      | varchar(32) binary  |      | PRI |         |                |
| data     | mediumblob          | YES  |     | NULL    |                |
+----------+---------------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

uid はユーザーIDに対応していて、1ユーザーあたり1個のデータを保存できる。 実際のゲームでは、1人のユーザーが複数のゲーム状態を保存できることが 多いが、今回の例では議論を単純にするために省略している。 保存したいゲームのデータ自体は、 data フィールドに格納される。 例えば、1人分のデータを保存したい場合は、以下のような SQL 文を発行す ることになる(データ部分は適当。パックされたバイナリと思ってほしい)。

mysql> insert into savedata values ( "ringo@example.net", "*@3#@a@d9asd9f3#" );

データを取りだすときは、以下のようにする。

mysql> select data from savedata where uid="ringo@example.net";

これ以上単純にできないぐらい単純な方法だが、 MySQL を使うと相当なパフォーマンスを期待できる。 保存したいデータの量が1回あたり10Kbytesだとすると、 秒間500回程度保存することが可能である。 ただし、以下の問題により、更新性能は頭打ちとなる。

データフラグメントの問題
データの実体の長さが伸びたり縮んだりする場合は、 MySQL のデータファイル中でデータがバラバラに保存されてしまう。 このため、1個のデータをアクセスするだけで複数回のディスクアクセスが 必要となり、パフォーマンスが劇的に低下してしまう。 これを回避するには、optimize table という処理をする必要があり、 その処理をしている間はデータベースを利用できない。 この制限を回避するためには、データ長を冗長に持って拡張に耐えられる ようにするなどの工夫が必要だが、大きなディスクの無駄が発生してしまう。 通常 IMPG のゲームデータは何らかの方法でpack(詰め込み)して保存するが、 そういう方法だとデータの長さが頻繁に変化してしまうからである。
マシン性能の問題
1個のMySQLサーバープロセスがすべてのクエリを処理する必要が あるため、早い段階でマシンのCPUやネットワークの性能が 足りなくなる。 この問題に対処するには、SMPマシンを使ったり、 ネットワーク設備をギガビット Ethernet にするなどの投資が必要となる。

マシン性能やネットワークをチューンして限界性能を高めていっても、 たかだか毎秒1000回保存程度の更新性能までしか実現できない。

性能の限界は低い反面、メンテナンス性は最高だ。 1個のデータベースだけを管理すればよく、 テーブルの全体にアクセスするための SQL も、いたって普通に書くことができる。 3000人~5000人同時ログイン程度の小規模なIMPGサイトの場合は、 ベストの選択肢になるだろう。

インデックスだけ集中させるタイプ

基本的に「いたって普通のタイプ」と同様のテーブルを作るのだが、 データ部分に、データの実体ではなく、 「データがどこにあるのか」というメタ情報を単一のテーブルに保存するようにする。 データの中身は UNIX のファイルやリモートファイルシステムを使って、 適宜保存する(図)。

保存する場合: ファイルに保存、その場所をMySQLに保存
読みだす場合: MySQLにアクセス、ファイルに保存

このようにデータの場所とインデックスの場所を分離することによって 高速化できる理由は3点ある。

ファイル保存を分散化できる
「データのある場所」は MySQL が動作しているホスト(マシン)と同じ ホスト上にある必要はない。これによって、 総スピンドル数(合計のアクセス可能なハードディスク台数) を向上させたり、MySQL サーバが走っているホストのネットワーク負荷を 大幅に軽減することが可能。
MySQL のテーブルに保存されるデータの長さを固定化できる
適当に工夫をして、ファイルのアドレスは常に100文字など 限定することによって、実際に MySQL のテーブルに書き込まれる データの長さを固定化できる。 これは optimize tableを不要にするためメンテナンス性が向上する。
データが大きい場合にはさらなる高速化ができる
たとえデータの実体とMySQLのインデックスが同じホスト上にあっても、 データが大きい場合は、UNIXファイルに直接アクセスして大きなデータを 保存するほうが、スループットが高い場合がある。 コミュニティーエンジンでの実験では、50Kbytes~100Kbytes付近でこの状態になる マシンが多かった。そのサイズよりも小さいと、MySQL テーブルに データの中身を保存するほうが高速だった。

ただし、以下のような構造上の欠点も存在する。

1レコードに対するアクセスが atomic でなくなる
atomic とは、その処理をしている間に割り込みが入らないということで ある。割り込みとは、ディスクがいっぱいだとか、 マシンが落ちるとか、例外的な現象が起きて処理が中断される ことである。 例えばデータを保存する場合、 ファイルを保存してからMySQLにアクセスするまでの間に クライアント(フロントエンド)がクラッシュするなど、 1個の I/O 動作の途中で異常終了してデータに整合性が取れなくなる場合が ありえる。 この問題を回避するためのロジックは完璧には組めないが、 現実に問題のない水準まで持っていくことは可能である。
データの中身に対する検索がSQLからは行いにくい
データの中身に対して SQL から文字列検索をかける場合に、 データがテーブル内に存在しないので、そのままではデータにアクセスできない ということである。ストアドプロシージャを作るなどして対処する必要がある。 実際の IMPG サーバにおいては、データはフロントエンドによって pack されて保存されるので、SQL を直接使って検索をする可能性はほとんど ない。従って、現実的な問題とはなりにくい。 ただし、Webサイトとの連動など別の環境からのデータベースアクセスが併用 される場合は、この限りではない。
結局はインデックス作りを単一のMySQL で実行している
データの実体を保存する場所は分散しているが、インデックスを作るのは やはり単体の MySQL サーバなので、ここにボトルネックが発生し得る。

以上のように、インデックスだけ集中させるタイプは、 データベースアクセスを分散化せずに、ボトルネックとなりがちな データ保存の部分だけを分散させることで性能の向上を計ろうとする。 気になる限界能力は、5千人~2万人程度の同時接続数をこなす IMPG サイト向け として丁度よい水準になるだろう。

ディプスファンタジアの場合はデータファイルのサイズが30Kbytes~50Kbytesと、 さほど大きくないのだが、optimize table が不要になるという メンテナンス性の理由によりこの方式を採用している。

実はメタデータをMySQLテーブルに持つ方式は、 MySQL リファレンスマニュアルの「最適化に関するその他の助言」 で画像の保存に関して言われている方式の応用である。 実際に、このメカニズムを使って、ディスクアクセスを向上させようとする ストレージ製品(日本チボリシステムズのSANergyなど)も存在するほど典型的な考えかたである。 コミュニティーエンジンがリリースしている mm-suite の現在のバージョンでは、 この方式をアプリケーションから「いたって普通のタイプ」と同様の API で 透過的に使えるインターフェイスを実装している。

ユニークキーで分散させるタイプ

さて、5万~10万同時接続、さらにもっと大量のアクセスをさばきたい IMPG サイトの場合は、これまでに説明してきた方式ではまったく能力が不足する。 つまり、「インデックス作成」の処理自体を分散する必要が生じるのだ。 メンテナンス性は悪くなるが、必要な最大性能を得るには分散させる方法を 考えなければならない。幸い、そのような巨大プロジェクトでは、 人力によるメンテナンスや、運用上の工夫を行なう経済的余裕は期待できる。

インデックス作成を分散するにはどうしたらいいだろうか? そのためには、 またまたフロントエンドのクラスタリングの項目で説明した基本事項である、 「情報アクセスの局所性とバースト性」を最大限に活用することになる。 今回利用するのは、あるユーザーにアクセスされたデータは、 次も同じユーザーによってアクセスされる確率が高い(ほぼ100%)という事実である。 IMPG で通常使われるユーザーIDは、ユーザーごとに保存されているデータに アクセスするためのユニークキーとして使うことができるのだ。 そのため、ユーザーID空間をいくつかに分割して、 ユーザーID空間ごとにバックエンドを担当させることで、 そのまま性能向上を計ることができるのだ。 以下の図では、 3つのバックエンドと4つのフロントエンドが接続されている状態を表している。

3つのバックエンドと4つのフロントエンドが接続されている状態

この図において各フロントエンドは、総当たり方式でバックエンドに接続し、 どのバックエンドに対してどのユーザを割り振るかについて、 ユーザーIDに含まれる情報を用いて静的な方法で決定する。 たとえば、ユーザーIDをメールアドレスのような ユーザーID@ドメイン名 のような形式で表現することにして、ドメイン名のところを使って 静的に分配する。この分配の方法が静的に決まっていることによって、 分配の際に何か中央集権的な資源(MySQLサーバなど)にアクセスする 必要がなくなるのだ。

また、ユーザーがどのフロントエンドにアクセスしてくるか分からないので、 必ず接続は総当たりでなければならない。 またパフォーマンスの観点から、接続は常にアクティブでなければならない。

この方式だと、フロントエンドにわずかの処理能力を要求するだけで バックエンドの側が持つ能力が事実上無制限になる。 バックエンドの能力が足りなくなるごとにバックエンドホストの数を増やして、 フロントエンドの設定を変更して新しいバックエンドに接続するようにするだけだ。 以上から分かるように、この方式の利点はその拡張性にある。 その反面以下に列挙するような、これまでにないメンテナンス性の悪さが発生する。 問題は、メンテナンス性の悪さが、「総当たり」の呪いによって、 ホストの数のかけ算に比例して増大する事である。

バックエンドの数を減らしたい場合に困る
ユーザーに関する情報はバックエンドに強く結びつけられているので、 バックエンドノードを削除するときには、 そのバックエンドノードが管理していたユーザを別のノードに移動させる とともに、「分配のための静的な方法」を、 減ったバックエンドに応じて更新する必要がある。 そのためには全部のフロントエンドを停止させる必要がある。 これを自動化するためには、専用のツールが必要になってくるだろう。
バックエンドやフロントエンドの数が多い場合、運用が面倒
巨大なサイトの場合は、バックエンドが10個~20個、 フロントエンドが50個~100個を超える数になる。 こうなると、総当たりで接続するのでコネクションの数は1000を超え、 数千に達してしまう。実際の運用では、このすべてのコネクションを 正常に保つ必要があり、 すべてを監視するために専用のツールが必要になるだろう。
データの全体をなめるような検索ができない
例えば、キャラクターが何個保存されているのかを数えるだけでも、 分散されたバックエンドのすべてに対してアクセスし、 別々に SQL のクエリを発行する必要が生じる。 データベースに自由にアクセスするためには、 特殊な専用ツールを作る必要がある。

無制限の拡張性を得るためには、このままではかなりの手間を強いられる。 どうやら実際にメンテナンスするためには、 さまざまな専用ツールを作成することは必須のようだ。 そもそも「かけ算」方式で手間がかかるようになるという性質を何とかできない ものだろうか? 何とか足し算か、それに近いところまで持っていけたらいいのだが。

ユニークキーで分散させるタイプその2(中継つき)

前節で考察したユニークキーによる分散方式では、 ノード数に対して「かけ算」方式でメンテナンスの手間が増えて いくことが示唆された。 そこで、何とか工夫をして、「足し算」方式にできないかを考えてみる。

ここでは「真ん中を絞る」という作戦がよさそうだ。 たとえば、世界の各国語を相互に翻訳するソフトウェアをデザインしたいとする。 特に工夫をしないと、 「日本語→中国語」、「英語→ドイツ語」、「日本語→ドイツ語」…… という感じで、言語の数の2乗(かけ算)に比例した個数の翻訳エンジンが 必要になってしまう。しかし、どの言語も一旦エスペラント語に翻訳してから 目的の言語に翻訳するという風に2段階の翻訳をすることで、 翻訳エンジンの必要個数を、言語数の1乗に比例させることができる。 この工夫をする場合に問題になってくるのは、エスペラント語の性能である。 エスペラント語の表現能力が十分でない場合、情報が欠損してしまい、 翻訳の品質が低いものになってしまうからだ。 この場合のエスペラント語は、「中間言語」あるいは 「中継のための言語」と呼べるだろう。 実際の自然言語ではこういう方法論で翻訳をするのは途方もなく難しい だろうが、データベースへのアクセス方法となるとかなり応用しやすい。

総当たりと真ん中絞りの絵

バックエンドへのアクセスにおいても、同様のことが言える。 十分に性能の高い「中継」が実装できるならば、以下のような中継を用いた デザインをすることで、ぐっとメンテナンス性を良くすることができそうだ。 もちろん、翻訳ソフトウェアの場合と同じく、 中央に置かれる中継の性能が良いことが絶対条件になってくる。

brokerの数がバックエンドよりも少ないことが条件

まずフロントエンドは、中継(brokerと呼ぶ)に対して1本だけ接続を確立し、 バックエンドに対するすべてのクエリーを broker に対して発行する。 その際に、そのクエリがどのユーザーから発生したものなのかを、 ユーザーIDの形で含めておく。 クエリを受信した broker は、ユーザーIDのドメイン名の部分を見て、 静的な分配方法でどのバックエンドにクエリを投げるかを決める。 したがって broker とバックエンドの間は総あたりで接続をしておく必要があるが、 バックエンドに対して相対的に broker の query/sec が高い場合は、 broker の台数をバックエンドに対してかなり減らすことができるのだ。

コミュニティーエンジンで行なった実験では、 どうやらこの中継方式はかなり有効であることがわかった。 その理由は、 やはり broker はバックエンドに対して相対的に極めて高速だからである。 broker はストレージにも一切アクセスせず、静的な方法に基づいてクエリ の振り分けをするだけだから、ほぼ純粋に I/O だけをすることになるのだ。 broker に対して 100Mbps のネットワークを満杯にするほどアクセスしても、 CPUの使用率は数%程度だった。 通常、バックエンドに対して 100Mbps を一杯にするほどアクセスする ようなデザインには絶対にしないので、おそらくバックエンド5つに対して broker を一つという感じの対応関係にできるはずである。 broker に対してギガビットのネットワークを使って接続すれば、 もっと相対的な性能差を大きくできる。

中継付き方式には欠点がないわけではない。 ひとつだけ不利な点がやはり存在する。 それは設備が余分に必要な事だ。 バックエンドの台数の5分の1程度の台数の broker がどうしても必要になる上、 合計のトラフィックは1段階中継が追加されることで増えるため、 通信インフラ(LAN設備)も必要だ。これらの投資は全体から見るとわずかだが、 このコストアップが、3番目の方式(中継なし)にくらべてメンテナンス性が よくなることにくらべてどの程度大きいかを今後見極めなければならない。

このようなメンテナンス性の向上に関しては、 最近のHPC(High Performance Computing)方面での成果が興味深い。 HPC用メンテナンスツールなどがうまく流用できるならば、 もしかすると broker がないほうが有利だという結論が出る可能性もある。

P2P的方式

P2P 的なストレージネットワークは基本的に読み込み性能を最大化させる 目的に向いている。 ネットワークやメモリ、ディスクなどの資源を冗長化することで得られるのは、 主に静的なコンテンツの読み込みのレスポンス速度と保存容量 (検索対象となる情報の大きさ)のより良いトレードオフポイントにすぎない。

現在実装が進んでいる Oceanstore、Freenet、Mojo Nation など WAN を使った保存や検索のエンジンは、 まだまだセキュリティの問題やレスポンスの問題を抱えている。しかし、 それらのプロトコルを既存の集中型サービスとうまく融合することができれば、 低コストなストレージを IMPG サービスに使える水準で作ることができる 可能性が出てくる。

さらに一歩進んだ夢としては P2P的ネットワークを「IMPG のゲーム自体を 実行するエンジン」として使うアイディアがある。 この夢は、サーバに対する投資をゼロ付近に近づけられるという点で、 プロトコル野郎たちをずっと魅了し続けてきたのだ。 将来、常時接続ブロードバンドネットワークが十分に普及することがあれば、 この野望も現実味を帯びてくるかもしれない(SCE の Cell と DNAS か?)。

バックエンドの性能について: まとめ

今回はバックエンドに要求される性能、限界の性能について、既存の方法、 実験中の方法、今後の研究が面白くなりそうな部分などを紹介した。 専門的な用語をたくさん使わざるを得なかったが、 IMPG におけるバックエンドをどのタイプにするか選択するときは、 性能とメンテナンス性のトレードオフやコンテンツの中身をよく吟味 しなければならないことを、分かっていただけたと思う。 また、 クラスタリングを使ってアプリケーションの性能を向上させるときに ぶつかる一般的な問題に関しても、多少垣間見ることができたと思う。

現在コミュニティーエンジンではディプスファンタジアなど 日本で稼働しているオンラインゲームをアジア展開する場合の 負荷に耐えさせるために、バックエンドに対して改良をしようとしているが、 おそらく3番目か、4番目に紹介した「ユニークキーによる分散」方式を 採用して mm-suite パッケージに追加することになるだろう。

次回予告 : フロントエンドの実装

連載第3回は、IMPG のフロントエンドであるゲームのロジックサーバの実装や、 フロントエンドにとって大切な TCP の性能チューンなどについて、 議論を進めていこう。 もちろん、その議論をするときには、 今回の議論で紹介したいろいろな考えかたが役にたつことは言うまでもない。

Resource

RDBMSやデータベースそのものについて
http://www.wakhok.ac.jp/DB/DB.html
SQLプログラミングについて
http://www.ann.hi-ho.ne.jp/hirok/sql/
テレゴズム - ブロードバンド革命のビジョン
George Gilder著/葛西重夫訳/2800円(税別)/ISBN4797318392/ソフトバンクパブリッシング/2001年11月
原著は「TELECOSM:How Infinite Bandwidth will Revolutionize Our World」(ISBN-0684809303)
The OceanStore Project
http://oceanstore.cs.berkeley.edu/
The Endeavour Expedition: Charting the Fluid Information Utility
http://endeavour.cs.berkeley.edu/
the free network project
http://freenetproject.org/cgi-bin/twiki/view/Main/WebHome
Mojo Nation
http://sourceforge.net/projects/mojonation/

2002年4月 LinuxJapan初出
インサイド・オンラインゲーム 第1回 はこちら

Add comment

You can add a comment by filling out the form below. Plain text formatting.

(Required)
(Required)
(Required)