GovnokodからHighloadへ。 TARANtoolを使用します。 5つのレシピの生産性

私は、プロジェクトのパフォーマンスを向上するための要求で起動ソーシャルゲームのディレクターによって近づかれました。 この段階で、プロトタイププロジェクトが作成され、開始されました。 そして、プロジェクトが機能し、ある種の利益をもたらした開発者に敬意を表さなければなりません。 しかし、プロジェクトが負荷に耐えられなかったため、広告会社を立ち上げることは意味がありませんでした。 MySQLが落ちました(エラーの35%)。

プロジェクトコード...一般に、彼の学歴の低い学生が書いたような印象がありました...そして、これは、別のプログラマーによる部分的なリファクタリングが既に行われているという事実にもかかわらず。 喜んだ唯一のことは、フレームワークが使用されなかったことです。 もちろん、それは常にイエスやモハメッドfleymovy質問ですか? であること、またはではありませんか? UnixまたはWindows? 使用するかしないか 私見、私の意見:フレームワークは典型的なタスクの狭い円のために研ぎ澄まされています。 ソーシャルプロジェクトは通常、典型的なタスクではありません...しかし、一般的に、プロジェクトは私にとって面白そうに思えたので、改善に取り組むことにしました。 このエントリでは、完了することができます...

おそらく、この分野で少なくとも何かを知っている怠zyなWEB開発者だけが、パフォーマンスの向上と高負荷のトピックについて書いていません。 基本的に、新しい何かが、この記事では、あなたは見つけることができません。 高負荷プロジェクトを開発するための主なアイデアは、一連のHighLoad記事で概説されています。 3頭のクジラ。 。 NoSQL tarantoolリポジトリを使用してPHPプロジェクトの生産性を向上させる方法に興味がある場合は、catにようこそ。

原則として、この範囲のタスクに適した別のキー、値のストレージ、およびサーバーロジックの実装は、他のスクリプト言語でも使用できます。

レシピ1.コードを分析

すべてが恐ろしく汚い。 彼らは私の前にこの何百回も書いており、さらに何百もの記事が書かれます...しかし、「私たちは最も賢い」と同じ熊手を着実に踏んでいます。 すべてのWEBプロジェクトの99%のボトルネックがデータベースであると言っても、アメリカを発見することはできません。 そして、これのうちどれを結論付ける必要がありますか?
そうです- クエリの数最小限に抑える必要があります 。 そして、どのようにロジックの過程で5回コードを満たしている場合、それを行うには:

$ユーザ = 新しいユーザー();
$ userData = $ user- > getById $ uid ;

クエリのプロファイリング時に、5つの同一の「選択」を実行したことがわかります。SELECT * FROM users WHERE id = $ uid;
これは非常に簡単に実装されます。内部(プライベート)静的フィールドまたはUserオブジェクトのプロパティを使用します。
クラスユーザー {

プライベート静的$のuserData = NULL;

プライベート 関数 getById_loc $ uid {
データベースにアクセスするためのいくつかのコードを//。
}

パブリック 関数 getById $ uid {
if self :: $ userData return self :: $ userData ;
self :: $ userData = $ this- > getById_loc $ uid ;
自己を 返す :: $ のuserData。
}
}


すぐに目を引くのは2番目です。 これは、2つの方法が近くにある場合です。
$ User- > updateBalance $ sum ;
$ User- > updateRating $ rating ;

これにより、連続して1つのテーブルに対して2つのクエリが実行されます。
UPDATEユーザーのSETバランス=バランス+ $の合計WHERE ID = $のUID。
UPDATEユーザーSET rating = $ rating WHERE id = $ uid;
ただし、頭を少し動かせば、1つのリクエストを作成できます。
ユーザーセットの 更新
バランス=バランス+ $ 和、
評価= $評価
ID = $のUID。

これは2つの方法で行うことができます:別のメソッド$ user-> updateBalanceAndRating($ sum、$ rating)を書くか、すべての場面で彼らが言うように普遍的な何かを実装します:
$ユーザ- > updateFieldAdd( 'バランス '、$合計)。 //フィールドを記憶し、操作を追加-追加、オペランド
$ユーザ- > updateFieldAssign( '評価 '、$定格)。 //フィールド、割り当て操作-割り当て、オペランドを記憶します
$ user- > execUpdate ; //リクエストを作成して実行します


これらの2つの簡単な方法の唯一の導入は10-12 3から5にデータベースクエリの数を削減することが可能となりました。 しかし、正直なところ、まだ既存のコードやリファクタリングリファクタリングがあります。 もちろん、ほとんどのHabrousersは敗者とはほど遠いですが、プロファイリングと分析は常に「神が私たちを助けてくれます」。

レシピ2.キャッシング

キャッシングとは何ですか、散らばる必要はありません。 Dima Koterovが高負荷プロジェクトの開発に関する記事で書いたように、「できる限りすべてをキャッシュする必要があります。」 このプロジェクトでは、ある種のキャッシングのti病な試みがありました。 しかし、Chernomyrdinによると、すべてが判明しました。彼らは、最も頻繁に使用されるものではなく、最高のものを望みましたが、キャッシュしました。

上記otmechenonoように、データベースの負荷を軽減するためには、リクエストの数を低減する必要があります。 そして、それらを減らす方法は? はい、それは非常に簡単です-たとえば、設定で、データベースから不変データの一部(ユニット、トリビュート、武器のマニュアル)を簡単に削除できます。 構成はXMLであり、任意のXMLエディターでゲームプレイチームの女の子が編集するか、完成した形式(PHP配列)で編集できます-ゲームプレイとコードの開発者が1人の場合。 その場でParsingXMLを実行するのは難しいので、予備的なXSLT変換を直接PHPコードに(メインコードと共に読み込まれる連想配列の形式で)行います。 ただし、XML構成ファイルを変更するたびに、XSLT変換スクリプトまたはコンソールユーティリティを実行する必要があります。 はい、これはキャッシングではありません。これはわずかな改善であり、別のレシピとして割り当てることはできませんが、忘れないでください。

したがって、すべてのディレクトリを構成に詰め込んだので、さらにいくつかのクエリから解放されます。 まあ、何-それは簡単になりましたか?..少なくともレシピ1と2を適用した後、ベースは落ちなくなりました。 まあ、少なくともいくつかの結果...

レシピ3.データ分析

ここであなたは本当にコードを分析し、考えなければなりません...そして、ところで、何かがあります...ユーザーがどのデータを変更し、どのユーザーデータが変更されず、最も頻繁に要求されるのかを知る必要があります。 ここでは、視覚的にコードを実行し、プロジェクトのロジックを理解することが必要です。

私たちのプロジェクトで最も頻繁にリクエストされた情報は、ユーザーのゲームプロフィール、ギフト、賞です。 このデータはすべてNoSQLストレージに配置され、他のすべてのデータ、特に支払いに関連するデータはMySQLに残りました。 NoSQLリポジトリとして、tarantoolが選択されました。

そしてまだ-なぜTARANtoolなのか?

Highload ++ Conference 2011では、Tarantool開発マネージャーのKostya Osipovaに次の質問がありました。
-なぜあなたの名前はそんなに有毒ですか?
-まあ、あなたは名前をラムとツール、つまり プロジェクトを突っ込むためのツール(ツール)として。

したがって、NoSQLストレージの選択に影響する要因は次のとおりです。
-サポートとコンサルティングを約束したKostya Osipovプロジェクトのチームリーダーとの個人的な知り合い
-以前のプロジェクトでこのリポジトリを実装した経験。 残念なことに、プロジェクトは:(を離陸しませんでしたが、興味深いものでした。
- 2年近くその前使用後に、新たな機会をtarantoolを探ります
-このNoSQLストレージの高いパフォーマンスと高いデータ可用性。
-データの永続性。ディスクにドロップされた場合、常に発生する可能性のある実際のコピーがあります。
-さて、あまり控えめではないが、私自身はTarantoolのPHP拡張の最初のバージョンの作成者なので、必要に応じて何かにパッチを当てたり、バグを修正したりできます。

しかし、より深刻なことに、このNoSQLデータウェアハウスのユニークな機能が気に入っています。セカンダリキーを使用し、ストアドプロシージャを使用してサーバー側でデータ領域を操作します。

データ分析(続き)

ユーザープロファイル、ユーザーテーブルについて考えます。 可変および可変のデータがあります。 可変データには、バランス、評価、pvpポイント、単位、チュートリアルステップなどが含まれます。
変更不可能なデータには、social_id、ログイン、アバターurl、個人コードなどが含まれます。可変データの中には、多くの場合、可変でほとんど変更されません。 ただし、不揮発性データが頻繁に要求される場合があります。

頻繁に要求されるデータを強調表示します。 それらをtarantoolにキャッシュします。 NoSQLストレージ自体について少し説明します...

TARANtool。 シノプシス

Tarantoolは、上記のように、高性能なキー/バリューNoSQLストレージです。 すべてのデータはRAM内にあり、タプルの形式で表示されるため、それらの抽出速度はredisに劣ったり、わずかに遅い(1000操作あたり6〜7ミリ秒)memcachedです。

それでも、Tarantoolはデータウェアハウスであり、memcacheなどのメモリ内のキャッシュシステムではないことに注意してください。 すべてのデータはRAMにありますが、ファイル(スナップショット0000..01.snap)に常に保存されます(同期システムコールから同期されます)。 従来のmemcachedおよびredisとは異なり、tarantoolには追加機能があります。
-データにセカンダリインデックスをオーバーレイする機能
-インデックスは合成可能
-インデックスのタイプは、HASH、TREE、またはBITSETです。 GEOインデックスの導入が計画されています。
-1つ以上のインデックスの同時サンプリング。
-部分的なデータ抽出(LIMIT / OFFSETのアナログ)。

Tarantoolは、スペースに結合されたデータを操作します。 スペースはMySQLのテーブルに類似しています。 Tarantoolは、デジタルスペース番号(0、1、2、3 ...)を使用します。 近い将来、ネームスペース(MySQLのテーブル名の類似物)を使用する予定です。

各スペースにインデックスを重ねることができます。 インデックスは、数値(int32またはint64)と文字フィールドの両方に重ね合わせることができます。 スペースと同様に、tarantoolはデジタルインデックスの番号付けを定義します。

クライアントとサーバーのタプル間の交換 表MySQLでアナログ回線をKortezh-。 数学では、車列の概念 - 長さnの順序付き有限集合。 タプルの各要素は、 データ要素またはフィールドを表します。 基本的に、タランチュラはフィールドのデータ型を区別しません。 彼にとって、これは一連のバイトです。 しかし、インデックスを使用する場合、つまり このフィールドにインデックスを課す場合、そのタイプはフィールドのタイプと一致する必要があります。 タプルには別の名前があります:tuple。

すべてのインデックスは、人間の知覚であるYAMLという設定に登録されます。 設定の一部の例:
スペース[ 0 ] .enabled = 1
スペース[ 0 ] .index [ 0 ]タイプ = "HASH"
space [ 0 ] .index [ 0 ] .unique = 1
スペース[0] .INDEX [0] [0] .fieldno = 0を .key_field
スペース[ 0 ] .index [ 0 ] .key_field [ 0 ]タイプ = "NUM"
構成では、各スペースのプライマリインデックスとセカンダリインデックスを記述します。 PRIMARY KEYのみで選択する場合、プライマリインデックスのみの説明で十分です(上記の例を参照)。 ユーザー間のレーティングまたはpvpバトルで最適なものを選択する場合、これらのフィールドにセカンダリインデックスを課します。 第2のフィールドがインデックスされてみましょう(fieldno = 1、ゼロから数えて)int32_t - 評価:

スペース[ 0 ] .index [ 1 ]タイプ =「TREE」 //あなたがより少ない操作をサンプリングすることを可能にするタイプ・ツリーを、作ります
space [ 0 ] .index [ 1 ] .unique = 0 //独創性を削除
space [ 0 ] .index [ 1 ] .key_field [ 0 ] .fieldno = 1 //インデックス付きフィールドの番号を示します
スペース[ 0 ] .index [ 1 ] .key_field [ 0 ]type = "NUM" // int32_tと入力

Social Gameプロジェクトがあるため、主キーはsocial_idに対応します。 ほとんどのソーシャルネットワークでは、これは64キーです。 インデックスタイプはHASHになり、データタイプはSTRになります。 理想的にはNUM64が欲しいのですが、残念ながらPHPはlong long型ではうまく機能しません。 ドライバーは、使用されているスペースの主キーのタイプとサイズを認識しません。 現時点では、64ビットキーを使用している場合、32ビット値を使用して検索することはできません。 これは、64ビットのキーとしてパッケージに梱包されなければなりません。 現在、ドライバーは、値が32ビットの範囲を超えている場合にのみこれを行います。 したがって、タイプSTRINGを使用する方が信頼性が高くなります。

メモリ計算

tarantoolはメモリのみのソリューションであるため、使用されるRAMの推定量を計算することが重要です。 計算は次のとおりです。

各タプルの前に、varint型の変数(パック内のperl 'w'のアナログ)と各タプルのヘッダーから12バイトが格納されます。 具体的には、データについては、 プロトコルを調べるか、 Tarantool Data and Protocolの記事を読むことで見つけることができます。

さらに、約15%がアロケーターのデータで占有されています。 たとえば、10個のフィールドがあり、ユーザーデータのサイズが256バイトに収まる場合、1.5Mの場合、およそ次の計算が行われます。
(10 * 1 + 256 + 12)* 1.15 * 1 500 000 = 921150000〜= 440 Mb /日

同様に、メモリ内のすべてのインデックス、保持しています:
-ツリー内の1つのノードには、68バイトのサービス情報が格納されます
-1つのノードについて、56バイトのオーバーヘッド情報がハッシュに保存されます

150万人のユーザーにインデックスを保存するには、合計で80Mbを超えるだけで十分で、1.5Mのプロファイルを保存するのに、わずかに半分ギガバイトが必要になります。 我々は別のキー(タイプTREE)を追加した場合、それはさらに90MのRAMです。

そのような人がいますが、今日の基準では、それほど多くはありません。

レシピ4.フォアグラウンドでMySQLを取り除く

すでに述べたように、ユーザープロファイルデータをtarantoolに転送するとき、現在のコピーをMySQLに保持したいと考えています。 したがって、UPDATEに関連するすべての操作を実行する必要があります。 その結果、キャッシングを行っても、あまり達成できませんでした。 しかし、彼らはまだ主な効果を達成しました:MySQLは落ちなくなりました。 それでは、どうすればスクリプトを数回高速化できますか? これは、MySQLクエリをまったく削除する場合に可能です。 これはどのように可能ですか? データベースへの変更に関する情報を、INSERT / UPDATE操作を実行する他のバックグラウンドスクリプトに転送する必要があります。

この情報はキューを介して送信されます。 リモートタスクの実行を可能にするいくつかの産業用ソリューションがあります。Gaerman、Celery、およびRabbitMQ、ZMQ、redis、その他のキューサーバーを適応させることもできます。 キューサーバがtarantool使用することができた場合でも、なぜ、プロジェクトにいくつかの新しいエンティティを紹介。

タランチュラにはgithub.com/mailru/tntlua/blob/master/queue.luaキューの実装があり、使用することをお勧めします。
ただし、少し簡単に実装されました。 新しいスペースを作成します。

space [ 1 ] .enabled = 1
space [ 1 ] .index [ 0 ]タイプ = 「TREE」
space [ 1 ] .index [ 0 ] .unique = 1
スペース[1] .INDEX [0] [0] .fieldno = 0を .key_field
space [ 1 ] .index [ 0 ] .key_field [ 0 ]タイプ = "NUM"


このスペースでは、次のフィールドを記述します。
-id、自動インクリメントフィールド。 インデックスが必要です。 タイプTREEのプライマリインデックスがスーパーインポーズされます。
-type-操作のタイプ、SQL演算子のパターン番号を決定する数値定数。
-データ-挿入/更新するデータ。

前景スクリプトには次のコードが含まれます。
define 'UPDATE_SPIN_COUNT'1 ;
define 'UPDATE_USER_BALANCE'2 ;
...
$ res = $ tnt- > call 'box.auto_increment' 、array string TBL_QUEUESUPDATE_SPIN_COUNT 、$ spinCount、$ uid )) ;
ストアドプロシージャbox.auto_incrementは組み込みであり、タプルデータを挿入します;主キーの値はmax + 1です。パラメーター:
-データが挿入されるスペースの数
-データ自体
-オプションのフラグパラメータ、デフォルトでは「return new key」に設定
変数の型であるスペースの数(定数TBL_QUEUES )は、 STRING型にキャストする必要があることに注意してください。 このスクリプトは、FIAキューにデータを書き込むluaプロシージャを呼び出します(自動インクリメント番号、実行中のタスクのタイプ、およびデータ自体)。

次に、別のリモートマシン上でも実行できるバックグラウンドスクリプトがキューからデータを取得し、SQLを実行します。

define 'UPDATE_SPIN_COUNT'1 ;
define 'UPDATE_USER_BALANCE'2 ;
...
$ res = $ this- > callProc 'my_pop' 、array string TBL_QUEUES ;

/ *
空の場合は以下を返します:
配列 2 {
[ "count" ] => int 0
[ "tuples_list" ] =>配列 0 { }
}

* /
もし($ RES [ 'count'という !])のリターン;

$ tuple = $ res [ 'tuples_list' ] [ 0 ] ;
switch $ tuple [ 1 ] {
ケースUPDATE_SPIN_COUNT:
$ sql = "UPDATE users SET spinCount = {$ tuple [2]} WHERE uid = {$ tuple [3]}" ;
休憩 ;

ケースUPDATE_USER_BALANCE
$ sql = "UPDATE users SET money = money + {$ tuple [2]} WHERE uid = {$ tuple [3]}" ;
休憩 ;

デフォルト:
新しい例外(「unknowタスクの種類を投げます 」);
休憩 ;
}

の$ this - > ExecSqlの($ sqlを) ;


その結果、フロントエンドスクリプトは高速のタランチュラでのみ動作し、バックグラウンドスクリプトはデーモンとしてハングするか、クラウンで実行され、WEBサーバーのリソースを無駄にすることなくMySQLのデータを別のサーバーに保存します。 その結果、生産性で30%以上勝つことができます。 バックグラウンドスクリプトのテーマは別の記事に値します。

ただし、これだけではありません。 luaプロシージャmy_popを開始するには、初期化する必要があります。 これを行うには、次のコードをinit.luaファイルに配置し、work_dirまたはscript_dirに配置する必要があります。

関数 my_pop spaceno
spaceno = tonumber spaceno
local min_tuple = box.space [ spaceno ] .index [ 0 ] .idx: min
ローカル 最小 = 0
min_tuple〜 = nilの 場合
最小 =ボックス。 アンパック 'i' 、min_tuple [ 0 ]
他に
帰る
終わり

ローカル ret = box.select spaceno、 0min
box.delete spaceno、 min

戻る

終わり


work_dirの値は、tarantool.confで指定されます。

レシピ5.アクティブに再生されているプロファイルのみをキャッシュする

既に以前に実装したように、すべてのプロファイルはtarantoolに保存され、バックグラウンドスクリプトによって行われたすべての変更はMySQLに記録されます。 CAP定理に従って、少し遅れて関連データが常にあります。

しかし、プロジェクトが150万人だけでなく300万人または500万人のユーザーを獲得した場合はどうでしょうか。 ユーザーがログインし、ゲームは好きではなかった-左。 しかし、タランチュラのデータは残り、メモリを占有し、使用されません...したがって、より効率的に使用し、より高速なデータ検索のために、常にプレイしているユーザーのみを保存するのが理にかなっています。

つまり、プレイしていないユーザー、つまり 私たちは、例えば、一週間以上、運用キャッシュから削除することができ、遊びに来ませんでした。 データベースに実際のコピーがあるため、いつでも操作可能なキャッシュに復元できます。 オンラインキャッシュを使用するクラスのコードは、キャッシュの標準タイプに従って構築されます。

クラス User DbModelを拡張します{

パブリック 関数 getByUid($の UID){

$ result = this- > getFromCache $ uid ;

(もし!IS_NULL($結果) ){
return $ result ;
}

$ result = $ this- > execSQL "SELECT * FROM users WHERE uid = $ uid " ;
$ this- > setToCache $ result ;

return $ result ;
}
....
}


それをきれいにするいくつかの方法があります:
-スクリプトを使用してデータベースからすべての「期限切れ」レコードのリストを選択し、タランチュラでそれらを削除します
-タランチュラにクリーニングブローカーを設定しました(彼は自分でやらなかった) github.com/mailru/tarantool/wiki/Brokers-ru
-すべての「期限切れ」レコードを削除し、クラウンで呼び出しを実行するために、luaでストアドプロシージャを記述します。

すべてのデータ(タプル)がディスクからRAMにレイズされるのではなく、最も要求の多いデータのみがレイズされるように、開発チームからの新しいタイプのストレージを楽しみにしています。 おおよそ、Mongo DBのように。 その後、レシピ5は自動的に消えます。

結論の代わりに

上記はすべて、ソーシャルプロジェクトが実装されている任意の言語(PHP、Perl、Python、Ruby、Java)で実装できます。
NoSQLデータストアとして運用キャッシュは、次の品種の任意のキー/値ストアに近づくことができます。
-memcached、永続性はなく、キューの実装に頭を悩ませる必要がありますが、これはAPPEND操作を使用して解決できます
- MemBase値ではなく、非常に優れた開発と、そのクリエイターによってサポートされなくなっているように見えました
- memcacheDb&memcacheQの束
-大根、基本的にこの機能を実装するすべてがあります
- TokyoTyrant、KyotoTyrant - 基本的にキューのlua手順で実施することができます
-LevelDb Daemon、マンバ人のまともな開発。 小さなフィニッシュとキューはすでにポケットに入っています。
-コメントで何かを提供する

さて、結論として、ナシや少しPRについて少し。
悪魔、具体的には可能なタスク、相互作用、実装の微妙さについては、「DevConf 2013」「 ソーシャルゲームのPHP悪魔 」でお話しする予定です。 また、完了したプロジェクトの生産性を向上させるいくつかの機能を強調しています。 興味深い場合は、タランチュラのトピックに触れます(このため投票を使用しました)。では、DevConf 2013でお会いしましょう。

PS。 すでに夜の3時間目、achapotkiは可能です...プライベートでpliiiz-私はすぐに修正します。 事前に感謝します。

Source: https://habr.com/ru/post/J113298/


All Articles