Archive for the ‘ソーシャルゲーム開発’ Category.

ゲーム業界はオワコン

そろそろゲーム業界から卒業すべき時がきたでしょうか?

私が携わっていたのは、AppStoreセールスランキングで上位の有名アプリでしたので、負荷は半端ないものでした。
ところが今携わっている案件で納品されてきたプログラムは質が低すぎて、これで実績のあるゲーム会社が作ったの?と目を疑うレベルです。
モバゲーアプリの開発実績もある会社なので、このレベルの低さは理解できなかったのですが、どうやらモバゲーは想像以上にオワコンらしいです。

私がモバゲーアプリを開発したのは、モバゲーAPIがサードパーティに提供された時の初期参入アプリです。
API公開時に参入した数社だけが当時の地獄を経験したのでしょうかね?
分間50万アクセスとか当たり前のようにくるのが、ソーシャルゲームという戦場だったんですがねぇ?
どうも話を聞いていると、今はモバゲーもゴミみたいな作り方で、まったく問題なく動くようです。
memcached使いました!ドヤァみたいな程度で動くみたいです。
なので、質の低いサーバーサイドエンジニアが量産されてしまっているのはあるんだと思います。

スマホアプリもセールスランキングトップのアプリ以外は全然負荷も高くないみたいです。
どの程度の順位までが負荷が高いのかは分かりませんが、少なくとも100位以内に入ってないようなのはゴミみたいなプログラムで動くみたいです。
ゴミで良いのなら、ゲーム業界なんていても良い事ありませんよねぇ。

トランザクションを理解する

さて、昨日書いた補足説明をしたいと思います。

まずレイドボスの場合は、倒したかどうか?という判断が必要になります。
読んでいる数値が保証されていなければどうなるか?
通常は討伐報酬が発生しますが、報酬の2重付与が発生してしまいます。
悲しい事に、ここまで書かないと何が悪いの?と理解できない人間が多いのがゲーム業界のサーバーエンジニアです。

ですから、プランナーがサーバーエンジニアをただのweb屋レベルのペチパーと勘違いするのも仕方ないと思います。
本当は数が少ないだけで、最も知識が必要で高いレベルが要求されるのがサーバーエンジニアです。
アプリ側は人間が余っていますが、マトモなサーバーエンジニア・インフラエンジニアは数が少ないのでどこの会社も人材を求めている所でしょう。

で、次に減らしてから増やすという話も説明しないと理解できない人が大勢いるでしょう。
そもそもA処理が終わってB処理が終わらない。
このシチュエーションが起こるケースは
1.トランザクションを利用していない。
2.B処理が別の保存先(例えばキャッシュ)
という事が考えられます。

もう1つ考えられる最もアホな発想についてまずは潰しておきます。
3.トランザクション中にコミットして片方が切り捨てられるという発想です。
はい、WALとか知らないんだったら勉強しましょうね。
insert文やupdate文を実行した時にそのデータはどこにいくとお考えですか?
メモリ上を更新するだけです。
そしてコミット処理を受け付けた時にmysqlが何をしているか知っていますか?
REDO(トランザクション)ログをシーケンシャルに書き出してメモリ上のデータが仮で無くなるだけです。
(設定されていればbinlogも)
binlogがredoログだと思っている人が多いですが、binlogはbinlogです。
多分レプリケーションの関係で後から付けた仕組みなんじゃないですか?>binlog
ib_logfile0とib_logfile1というのがmysqlのトランザクションログです。
チェックポイントに到達するとログファイルを切り替えて、実際にディスクに書き込むのでそれまではオンメモリなんですよ。
ですのでmysqlのリカバリのメカニズムは、ディスクに書き込まれていないデータはトランザクションログから復旧を試みます。
コミットに途中で?失敗する=トランザクションログの書き込みに途中で失敗する=mysqlサーバーのディスク逝ってるレベル。
2行目のクエリ書き込みタイミングでディスク逝く確率はゼロじゃないけど、このパターンの障害はファイルも救い出せないレベルじゃねーの?って事。
仕組みを知らずにケース3を想像してた人は、せいぜい意地になって超低確率のレアケースを想定してなさいってこった。

次にケース1
トランザクションを利用していない。>アホか、氏ね。

次にケース2
片方だけ更新しちゃう>アホか、氏ね
キャッシュだったとしても、ロールバック出来る形で実装していないのは設計が間違ってるだけ
キャッシュだったとしたら、キャッシュを消せば次回はDBから読み込んでキャッシュ化する作りになってなかったらアホ
ロールバックするの仕組みを実装するのが面倒だったら、DBへのコミット失敗したらキャッシュ消せ
つまり減らして増やすじゃなくて、DBコミット→キャッシュコミットという順番なら正しい
キャッシュコミットに失敗する=キャッシュサーバーがフェイルオーバーするから、DBから読み直して動くので全く問題なし
問題があるなら設計に問題がある、消えて困るデータをキャッシュするな

唯一筋が通りそうなのは、キャッシュじゃなくて
ケース4.垂直分割とかDBが分かれているケース
XA-TRANSACTION使えアホと言いたい所だが、Transactionマネージャーを自作するのが面倒くさいので自力Commitループしてるパターン。
このパターンだったら片方だけコミットされるというのはあり得るから、最終的にはエラーログからデータは戻すんだけど
それまでの間の問題にならないように減らす→増やすの順番でコミットせーよ!ってのは有りかもしれませんね。

ペチパー酷過ぎ

ソシャゲーというかスマホアプリ
今のクラサバ型ネイティブアプリを支えているのは、サーバーエンジニアの一部のマトモな人間だとひしひしと感じる出来事があった。
これは久しぶりにソシャゲ系の記事も書いてみようかと思うに足る衝撃である。

基本的にゲーム系で育った人間はトランザクションを理解していない人間が多いみたいである。
同じプログラムが並行で動いたら、これじゃデータおかしくなりますよね?っていうのが理解出来ていない。
これは、ソシャゲの勉強会?の資料で減らして→増やすとか意味不明な事を書いて撒き散らしてる人達も同じなんだろうと思われる。
トランザクションを使ってないか?使い方を間違えてるから、このような意味不明な事を言うのだろう。
ゲーム業界の低レベルな人間だと、一見なるほどと思ってしまうような内容ですしね。

ここら辺は業務系で育った人間の方が優秀である。
数字が狂ったら業務に支障が出て始末書物の世界なので、並行動作への教育は十分にされている。
ゲーム系のスライド等を見ても一部のマトモな人間はちゃんとread lockについて言及していますし、こういった人間が支えているんですね。
気持ちは分かるんですけど、ゲーム業界の低レベルな人間がよくやってしまうのは
更新処理の部分だけでしかbeginTransactionとcommitを考えれない。
今読んでいるデータの値が動作中に保証されている必要があるのか?まで考えが及ばない人が多いようです。

簡単に例を上げるとレイドボスのHP
現在HPを読んでHPからダメージ減算という処理があったとします。
ほぼ同時に処理が動作した時に、現在HPが同じ値を読む事があるという当たり前の事を考えられないんですよねぇ。
Aさん:ボスHP:1000、攻撃100、残りHP900
Bさん:ボスHP:1000、攻撃200、残りHP800
現在読んだデータを信用できない作り方にするのであれば、
update文では、boss_hp = boss_hp – 100
のような更新をする事でカバー出来る事があります。
レイドボスの場合は読んだデータを信用するパターンじゃないと困るケースでしょうけど。

phpフレームワークとの付き合い方

フレームワークを使うならメモリ消費について諦めなければいけない点かもしれません。

よくあるフレームワークで考えると、indexというメインコントローラがアクションクラスのインスタンスを作成します。
このメモリが解放されるタイミングは、どこになるでしょうか?
恐ろしい事に(ほぼ)全ての処理が終わってindexコントローラーからインスタンスが解放されるタイミングです。

なので、アクションクラスではprotectedやpublicのメンバ変数(プロパティ)はなるべく使用しない方が良いです。
インスタンスが解放される=メインコントローラーの最後までメモリ解放されません。

アクションメソッドの中でマスターデータなどを取得した場合もunsetしない限りはスコープから外れるまで解放されません。
スコープから外れる=アクション終了=(ほぼ)全ての処理が終わっている

オブジェクトを多用するのは、zend_class_entryに設計図が登録されるので、クラス数だけメモリを消費します。
クラスを一杯作成するのはメモリ消費には繋がります。
ですがstaticメソッドを持ったクラスを作るのは別に問題だとは思いません。
これはfunctionを作ってもfunctionが登録されるのと同じだからです。

どうせ数msで終わるのだから、頑張ってメモリ解放しなくても良いというアプローチこそ
lightweight languageに相応しい気がしますので、あまり神経質にならなくて良い気がします。
アクションクラスのメンバ変数に大きなマスターデータを持たせるとかは、やめた方が良いかもしれませんけども。

久しぶりに連日技術的なネタを投稿して充実していましたが、明日ソードアートオンラインのゲームが出るので若干更新ペースが落ちるかもしれませんw

php extensionのススメ3(メモリ消費)

同時アクセス数が多いという事はweb(ap)サーバーのメモリ消費に注意しなくてはいけません。
webサーバーレイヤーではnginxが有利だと言われる部分はアプリレイヤーでも考慮する必要があります。

phalconフレームワークとCakePHPで単純にHelloWorld的なプログラムを実行してみます。
テンプレートでmemory_get_usage()をechoするだけのプログラムです。

phalconフレームワークは400Kくらいです。
phalcon

cakephpは10倍の4Mくらいです。
cakephp

DB接続すらせずに、Actionも空っぽでテンプレートでecho memory_get_usage()しているだけのプログラムで10倍のメモリ消費となります。
phalconフレームワークは早いだけではなくメモリ消費という最重要項目に関しても優秀です。
これは、先日説明したphpのzvalを思い出して下さい。
c言語のネイティブレイヤーでスタック変数を定義するのと、phpレイヤーで変数を定義するのとではメモリ消費が違います。
同じ処理をするにしても、ネイティブ側で例えばint型(私のPCは32bitなので4バイト)で良い値があるとします。
phpレイヤーでしか定義出来ない場合は、まず$aというポインタ変数として(私のPCは32ビットなので)4バイト
ポインタの参照先の実態として、zval構造体+共用体のメモリを消費します。
ネイティブで済ませる部分が多ければ多いほど、zvalの呪縛から解放されメモリ消費を抑える事も出来ると言うことです。

Actionやテンンプレートが空っぽでこれなのですから、CakePHPをそのまま使ってしまうと1アクセス当たり平均10MBは覚悟しておいた方が良いかもしれません。
対象サーバーでの同時アクセス数を100に設定したと仮定すると軽く見て1GBは覚悟しておくべき数値かと思われます。
1000アクセスだとすると10GBですね。
ゲーム開発の場合はマスターデータをKVSから全件取得したりもしますので、多めに考えておいた方が良さげです。
あくまでアプリレイヤーのメモリ消費ですので、webサーバーは別途メモリを消費します。
そしてAPCにキャッシュさせたデータもメモリを消費します。
APCのコンパイルキャッシュもメモリを消費しますので、プログラムの本数分消費します。

その他サーバー監視プロセスやログ収集プロセスなど最低限動いているプロセスもメモリを消費します。
これから考えてもApacheとnginxの項でも書いた、C10Kという単位は普通に馬鹿かと言いたくなる単位なのは、ご理解いただけるかと思いますw

配列(ハッシュ)よりオブジェクトのメモリ消費が少ないという嘘?

前回のエントリーの続きです。

参考サイト①に記載されている内容は別に間違いではありません。
前提条件としてオブジェクトに有利な形になる形の説明をしているだけですね。
参考サイト①の参考リンクにある参考サイト②の説明をされているだけです。

まず参考サイト①にも記載されていますが、PHPの言語仕様の説明をします。
確かPHP5から変わったハズなんですけど

①は値のコピーでは無く、$aの実態への参照となります。
①のタイミングでコピーしてしまうとメモリの消費が激しいので、値に変化がない間はメモリを節約する為に言語仕様が変わりました。
なので初めて値が変わった②のタイミングで新しいzvalがメモリに確保されます。
xdebugをインストールすると簡単に確認できます。

①は$aしかないので$aの実態の参照カウントは1です。
②は$bからも参照されたので$aの実態の参照カウントは2になりました。
③は$aの実態を参照しているので②と同じ結果になります。
④は$bは値変更により新しい実態を持ったので参照カウントが$aの1つに戻りました。
⑤は$bの新しい実態なので参照カウントは$bの1つです

という基本的な言語仕様を前提条件として理解して下さい。

昨日説明したzend_class_entryというのがクラスの設計図だと思います。

この先はphpのユーザークラスをコンパイルした後の世界なので、ソースコードもよく分かりません。
public $member0 = 0;
例えば上記のようなプロパティに初期値が設定されている時は、
①0を表すzval
②default_properties_tableが①を参照
③new Classした時にnew_class.member0はdefault_properties_table[member0]を参照
という動きになるので参考サイト①のように参照カウントが3から始まるのだと思います。

この動きと配列を比較するのであればこういう形で初期化するのが平等な計測になります。

結果は当然配列の方が早くてメモリ消費も少ないです。

メモリ消費が少ないのは、昨日説明した階層を思い出して下さい。
内部的にはzval->obj->HashTableでありプロパティの実値部分のHashTable部分のメモリ消費が同じならobjの部分が配列よりも多くなります。
参考サイト②は間違いと言っている訳ではありません、あえて配列が不利になる動きで比較をしているという事です。
逆に言うと私があえて配列が有利になるように初期化しただけですw
同条件なら配列の方が早くて軽量!
これはphpの内部データの持ち方から間違いありませんから、私にとっては予想通りの結果になったつー事ですね。

如何だろうか?
ネットには単純に鵜呑みにしてしまうと嘘になってしまう情報がゴロゴロ転がっているのである。
真実はいつもひとつ!嘘じゃない情報も嘘になる事がある。

オブジェクトより配列(ハッシュ)の方が早い理由

このエントリーは別に配列を使っての高速化を推奨する訳ではありません。
オブジェクトなんて使うより配列の方が早いからオブジェクトと配列の選択肢がある時の参考にして貰えればという感じですね。
例えば散々例に上げているPDOも結果行の形式をオブジェクト・配列から選択するパラメーターがありますよね。

これを説明するに当たって、とても素晴らしいサイトを見つけました。
2009年と古い内容ですが、図解してくれているのでとても分かりやすいです。
参考サイト:http://d.hatena.ne.jp/yokkuns/20090614/1244994082

所謂何でも型というのはc言語の共用体の拡張だというのは、どの言語でも同じだと思います。
私が業務系でVBをやり始めた頃はC言語出身者もゴロゴロ転がってましたので、VBのVariant型は共用体拡張だと聞いていたので想像はついていました。
phpの何でも型の正体?呼び名?はzvalと言います。

zval_val共用体とzval構造体

参考サイトの図を合わせて見ながら読んで貰うと分かりやすいと思います。
zval構造体の中にtypeという項目があります。
これが何でも型だけど内部的にstring型だとか持ってる奴ですね。
図の右に飛ぶと共用体に移ります。
共用体っつーのは同じメモリ領域を複数の型で共用できるそのまんまの奴なんですが、c言語を知らない人は分かりやすい図入りのサイトを探して下さい。
大事なのは、配列はHashtableで、オブジェクトはzend_object_valueで型が違う=データの持ち方が違うという事です。

脱線してzval構造体のrefcount_gcがガベージコレクタの説明によく出てくる参照カウンタつー奴ですね。
GCはこのヒープ変数がどこからも参照されていない時(=0)に解放する仕組みです。
面白いのでついでに覚えておくと自己満足できます!
オブジェクト側にもrefcountとか出てくるので、GC側のソースも何かあれば見てみたいと思います。

zend_object_value構造体

ここからの説明の仕方に迷うというか文章力を試されてしまいますね。
まずは、表題通りの何故オブジェクトの方が遅いのか?について説明したいと思います。
参考サイトで図解してくれていますので私が簡単に書くのと合わせて見て下さい。
ハッシュの場合は
zval->value->ht->zval->value
オブジェクトの場合は
zval->value->obj->(Hashtable)get_properties->zval->value
単純に矢印が多い=目的の値に辿り着くまでの経路が多いと思って下さい。
参考サイトにも書かれているように、最終的にオブジェクトのプロパティは内部的にはハッシュとして扱われるんです。

で、オブジェクトはクラスの型を管理する情報と実際にヒープメモリに展開したインスタンスの情報を管理してるぽいですね。
やたらとceという変数が出てきますが、これがzend_class_entryという奴でクラスの設計図の情報ぽいですね。
さっきのzend_object_valueのhandleとhandlersというのがインスタンス側の管理情報で、グローバル変数objects_storeで管理してるぽいです。

その中のバケットという中にオブジェクトを詰め込んでいってるポジションがhandle的な感じぽいです。

糞長すぎて混乱してしまいますねw
実際に使用されている部分を抜粋するとこんな感じです。
EG(objects_store).object_buckets[handle].bucket.obj;

さらに最終的にこのバケットに入ってる_store_bucket->obj->objectがこれになるんだと思うんですが、ちょっとそろそろ眠たいのでソース見て確証するのは後日で・・・。

なので、さっき書いた
zval->value->obj->(Hashtable)get_properties->zval->value
を詳細に書くと
properties_hash = zval->value->obj->handlers->get_properties(EG(objects_store).object_buckets[zval->value->obj->handle].bucket.obj);
多分こんな感じの糞長い動きでようやくプロパティ配列が取れる感じだと思います。

さて配列の方が早い理由はご理解頂けたでしょうか?
分かり難い人は

より

の方が早いのと同じだと思って下さい。
$hoge->0->1->$iより1->$iの方が目的の場所に辿り着く経路が短い=早いです。

次に、参考サイトに書かれているオブジェクトのメモリ消費量について書きたいと思いますが、やっぱり長くなったのでエントリーを分けたいと思います。
to be continued

クエリ発行回数の削減(SELECT)

MySQLのプロシージャは複数結果セットに対応しています。
これを利用しない手はありません。
昨日、関連テーブル一括削除のエントリーで1:nの親子関係テーブルを複数JOINするとねずみ算式に結果セットの行数が膨らむ話をしました。
そのようなケースでも利用できますし、JOIN出来ない関係ないデータを1クエリで同時に取得するという用途にも利用出来ます。

1:nの子テーブルがあるパターンです。
取得するカラム数も結果セット毎に違います。

スクリプトを準備します。
PDOには複数結果セットの為のPDOStatement::nextRowsetというメソッドが準備されています。

3回クエリを発行するのと同じ結果が1クエリで取得出来ます。

比較の為に普通に3回SELECTするテストスクリプトも用意します。

今回はループじゃなくてabでベンチマークを取ってみます。
ネットワークを介するデメリットが対象なのでDBサーバーとWEBサーバーはちゃんと別のVMになっています。
私のPCはシングルコアの糞PCにLubuntu入れて延命してる環境なので負荷は軽めにしています。

秒間38.35リクエストが64.57リクエストまで跳ね上がっています。
ネットワークを介するデメリットは想像以上ではないですか?
スレーブから取得するデータであれば、IF文等の制御ロジックを含むプロシージャも許容範囲となるかもしれません。

クエリ発行回数の削減(関連データ一括削除)3

手動で削除するテクニックを記載しましたが、MySQLに任せる事で1クエリで済ませる事も可能です。

DELETE CASCADE指定をしておけば親テーブルが削除された時に自動的に削除されます。
私は使用するなとの指示がありましたので使用していません。

使用して問題なければ、外部キー制約を付けるのが一番簡単で手っ取り早いですね。
MySQLが提供している機能なのですから性能面でも問題ないと思うんですけどねぇ。
「mysql 外部制約 問題」とかでググると比較的新しい日付でも問題点?が出てくるので、使わない方が良いという判断が下されたんだという気がします。

これに関しては要調査の課題としてメモしときたいと思います。

クエリ発行回数の削減(関連データ一括削除)2

先程のエントリーは注意点を書きたかったので書き記した物です。
これこそプロシージャ使えよ!って話ですねw

何も考えずに関連テーブルのdeleteを記述していけば良いだけです。
勿論hoge_idはindexが張られていますよね?
先程のように技術者のスキルによる心配をしなくても良い上に1クエリのみで削除出来ます。
プロシージャの場合は、内部で発行されたクエリがbinログに落ちるようです。
無理にJOINした場合、削除クエリが重くなってレプリ遅延を起こす可能性もありますしプロシージャ化はメリットしかありませんよね?
IF文とか何らかのプログラム制御させるならマスターDBに若干負荷をかけるデメリットがあるかもしれませんが、単純クエリの発行だけですからね。
tcpレイヤーでの処理が減る分プロシージャの方が有利な気もしますね。

この関連データはhoge_idで絞れる内容ですので、DELETEで大量のデータが対象となる事は想定していません。
大量データの削除は後日別エントリーで記述したいと思います。