Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ソーシャルゲーム案件におけるDB分割のPHP実装

 ソーシャルゲーム案件におけるDB分割のPHP実装

ソーシャルゲーム案件におけるDB分割のPHP実装
~とにかく分割ですよ。10回じゃ足りない。20回くらい分割。~
株式会社インフィニットループ 佐々木 亨基

2013/7/15にPHPMatsuri2013内で発表された講演のスライド

Infiniteloop

July 12, 2023
Tweet

More Decks by Infiniteloop

Other Decks in Programming

Transcript

  1. 自己紹介 ・佐々木 亨基 ・ゆきこ yukicon ・ Twitter:@yukiconEx ・株式会社インフィニットループ所属 ・札幌 MySQL

    勉強会代表 ・ PHP 歴は 4 年くらい ・現在仕事では PHP オンリー ・いい加減な人間なので PHP の緩さは好き
  2. DB 分割とは 分割した DB A テーブル B テーブル E テーブル

    F テーブル E テーブル F テーブル E テーブル F テーブル … 垂直分割 ( テーブル単位での分割 ) 水平分割 ( 同テーブルの ID 単位による分割 ) C テーブル D テーブル
  3. DB 分割とは 更にそれぞれがマスタスレーブ構成を取る Master Master … 垂直分割 ( テーブル単位での分割 )

    水平分割 ( 同テーブルの ID 単位による分割 ) Slave Slave Slave Master Slave Slave Slave Slave Slave Slave Master Slave Slave Slave Master Slave Slave Slave
  4. どうして DB 分割なんかするの? マスタスレーブのセットを増やそう !        |    \  

    __   /     _ (m) _ピコーン        | ミ |     /  ` ´   \       ('A`)      ノヽノヽ        くく
  5. ユーザ単位による DB 分割 ユーザ数が増えたなら UserDB を追加 Global が苦しくなったら更に垂直分割をする事で スケールアウト可能 User1

    User2 User3 … 水平分割された UserDB ユーザに紐付くデータを一定のルールで振り分けて格納 Global GlobalDB ユーザに紐付かない共通のデータを格納
  6. DB 分割のデメリット ▪ 設計でカバーする ・ DB 間を跨いだ JOIN ができない →

    冗長なデータの持ち方をしてしまう → マスタデータは全 DB に持つなどして対策 ・水平分割すればするほどパフォーマンスが下がる  ※とにかく分割とかいうタイトルになってますが   あんまり分割しちゃダメです、最低限にしましょう
  7. クラス設計 3 つのクラスから成っている ・ DatabaseAccess  全 DB 、トランザクションを統括するクラス ・ DatabaseAccessNode

     単一 DB にアクセスするクラス  マスタスレーブの切り替えも行う  分割なしならこのクラスのみで完結 ・ DatabaseAccessMultiNode  水平分割された DB にまとめてアクセスするクラス
  8. クラス設計 図にすると Master Master … DatabaseAccess Slave Slave Slave Master

    Slave Slave Slave Slave Slave Slave Master Slave Slave Slave Master Slave Slave Slave DatabaseAccessNode DatabaseAccessMultiNode
  9. 使用例 // singleton なインスタンスを取得 $dba = DatabaseAccess::getInstance(); // グローバル DB

    から SELECT $dba->getDBAN('global')->select('user_id_tbl'); // 対象ユーザ ID のデータがある DB から SELECT $dba->getDBAN('user', $user_id)->select('user_tbl'); // 複数の対象ユーザ DB から SELECT $dban_arr = $dba->getDBANArr('user', $user_id_arr); $dbam = new DatabaseAccessMultiNode($dban_arr); $dbam->select('user_tbl');
  10. 使用例 // singleton なインスタンスを取得 $dba = DatabaseAccess::getInstance(); // グローバル DB

    から SELECT $dba->getDBAN('global')->select('user_id_tbl'); // 対象ユーザ ID のデータがある DB から SELECT $dba->getDBAN('user', $user_id)->select('user_tbl'); // 複数の対象ユーザ DB から SELECT $dban_arr = $dba->getDBANArr('user', $user_id_arr); $dbam = new DatabaseAccessMultiNode($dban_arr); $dbam->select('user_tbl'); なんか難しい!
  11. エイリアス エイリアスをつくって抽象化 class DatabaseAccess { function gb() { // global

    の DatabaseAccessNode オブジェクトを返す return $this->getDBAN('global'); } function user($user_id) { // user の DatabaseAccessNode オブジェクトを返す return $this->getDBAN('user', $user_id); } : :
  12. エイリアス // singleton なインスタンスを取得 $dba = DatabaseAccess::getInstance(); // グローバル DB

    から SELECT $dba->gb()->select('user_id_tbl'); // 対象ユーザ ID のデータがある DB から SELECT $dba->user($user_id)->select('user_tbl'); // 複数のユーザ DB から SELECT $dba->user_multi($user_id_arr)->select('user_tbl'); だいぶすっきり
  13. エイリアス // singleton なインスタンスを取得 $dba = DatabaseAccess::getInstance(); // グローバル DB

    から SELECT $dba->gb()->select('user_id_tbl'); // 対象ユーザ ID のデータがある DB から SELECT $dba->user($user_id)->select('user_tbl'); // 複数のユーザ DB から SELECT $dba->user_multi($user_id_arr)->select('user_tbl'); // 全ユーザ DB から SELECT $dba->user_all()->select('user_tbl'); 全ユーザ DB 用のエイリアスも用意すると便利
  14. 水平分割 ID とサーバ ID をマッピングするテーブルを グローバル DB に作成して管理 CREATE TABLE

    `id_partition_tbl` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `server_id` tinyint(4) unsigned NOT NULL, PRIMARY KEY (`id`), KEY `server_id` (`id`,`server_id`) ) id server_id 1000 1 1001 2 1002 3
  15. 水平分割 ID の発行 ( 仮に server_id を 1 で指定 )

    INSERT INTO id_partition_tbl (server_id) VALUE (1); SELECT LAST_INSERT_ID();  ↓ 分割ルールに従ってサーバの割り当て  ↓ テーブルへの登録 UPDATE id_partition_tbl SET server_id = 3 WHERE id = 1000;
  16. 水平分割された DB への問い合わせ DatabaseAccessMultiNode クラスにより実現 複数 DB に同じ SQL を投げ、結果をマージ

    使用者は複数 DB への問い合わせである事を意識 せず、単一 DB を扱うのと同様に記述する事がで きる // 単一 DB への問い合わせ $dba->user($user_id)->select('user_tbl'); $dba->user($user_id)->update('user_tbl'); // 複数 DB への問い合わせ $dba->user_multi($user_id_arr)->select('user_tbl'); $dba->user_multi($user_id_arr)->update('user_tbl');
  17. 水平分割された DB への問い合わせ __call と call_user_func_array によって実装 DatabaseAccessNode クラスに単一 DB

    へ問い合 わせる処理を追加すると、 DatabaseAccessMultiNode クラスを経由して複 数 DB に問い合わせもする事ができる class DatabaseAccessMultiNode { function __call($func_name, $args = array()) { // 各 DB に対して実行 foreach ($this->dban_arr as $key => $dban) { $tmp_data_arr[] = call_user_func_array(array($dban, $func_name), $args); }
  18. 水平分割された DB への問い合わせ レスポンスは型によって処理を振り分ける $tmp_data = reset($tmp_data_arr); if (is_numeric($tmp_data)) {

    // 数値の場合は和を返す $sum = 0; foreach ($tmp_data_arr as $tmp_data) { $sum += $tmp_data; } return $sum; } else if (is_array($tmp_data)) { // 配列の場合はマージして返す $data = array(); foreach ($tmp_data_arr as $tmp_data) { $data = array_merge($data, $tmp_data); } return $data; : :
  19. 水平分割された DB への問い合わせ user_id をキーにした場合 UPDATE は対象レコードが存在しないため問題無いが User1 (1000-1999) User2

    (2000-2999) User3 (3000-3999) UPDATE t SET a = a + 100 WHERE user_id IN (1000, 2000, 3000); UPDATE t SET a = a + 100 WHERE user_id IN (1000, 2000, 3000); UPDATE t SET a = a + 100 WHERE user_id IN (1000, 2000, 3000);
  20. 水平分割された DB への問い合わせ INSERT は気をつける必要がある User1 (1000-1999) User2 (2000-2999) User3

    (3000-3999) INSERT INTO t (user_id, a) VALUES (1000, 0), (2000, 0), (3000, 0); INSERT INTO t (user_id, a) VALUES (1000, 0), (2000, 0), (3000, 0); INSERT INTO t (user_id, a) VALUES (1000, 0), (2000, 0), (3000, 0);
  21. 水平分割された DB への問い合わせ INSERT は気をつける必要がある User1 (1000-1999) User2 (2000-2999) User3

    (3000-3999) INSERT INTO t (user_id, a) VALUES (1000, 0), (2000, 0), (3000, 0); INSERT INTO t (user_id, a) VALUES (1000, 0), (2000, 0), (3000, 0); INSERT INTO t (user_id, a) VALUES (1000, 0), (2000, 0), (3000, 0); 不要なレコードまで INSERT されてしまう
  22. トランザクション トランザクションは DB 単位でかかるため 管理に気を使わなくてはいけない // 対象のユーザ DB にトランザクション開始 $dba->beginTransactionToUser($user_id);

    // この更新はグローバル DB への更新のため // トランザクションの対象とならない $dba->gb()->update(); XA トランザクション… うっ…頭が… ( 分離レベルが SERIALIZABLE に限られる、  挙動が怪しいという事で、ミドルウェアに頼らず  アプリによる実装としました )
  23. トランザクションの開始 複数 DB へのトランザクション開始方法は 2 通り ・最初にまとめて開始してしまう ・必要になった時点で開始する トランザクションが必要な事がわかりきっている場合は最 初にまとめてしまう方が管理が楽かつ簡単

    どれか 1 つでもトランザクションがかかっていれば他の DB も更新処理時に自動でトランザクション状態となるオート モードも用意したが、管理できなくなる懸念があったため 使っていない
  24. トランザクションの開始 ▪ 最初にまとめて開始してしまう場合 トランザクションは短い方が良い コネクションのコストによって無用にトランザクションが 長くならないようにマスタサーバへのコネクションを行 なってからまとめてトランザクションを開始する // 対象の DB

    をマスタに接続 $dba->gb()->useMaster(); $dba->user($user_id)->useMaster(); // マスタに接続した DB を一斉にトランザクション開始 $dba->myBeginTransactionToConnectionMaster(); Global User コネクション BEGIN コネクション BEGIN Global User コネクション コネクション BEGIN BEGIN
  25. トランザクションの開始 ▪ 必要になった時点で開始する場合 ある DB に対してトランザクションをかけるが、 ある DB に対しては 10

    回に 1 回くらいしかトランザクショ ンが必要が無い処理の場合、必要になった時にトランザク ションを開始する 別々のユーザ ID が対象になった際に両方のユーザ ID が同 じ DB に所属している場合など、既にトランザクションが開 始されている事もある // 対象のユーザ DB にトランザクション開始 $dba->beginTransactionToUser($user_id); // たまにしかここに来ないので、ここでトランザクション開始 // 既にトランザクション開始されているならスルーする If ($dba->user($other_user_id)->isTransaction()) { // 同じ DB の場合はここにはこない $dba->beginTransactionToUser($other_user_id); }
  26. コミット 各 DB に対してバラバラのタイミングでコミットを行うとつ くりが複雑になり、データ不整合となるバグを引き起こす 可能性が高くなる 悪い例 // グローバル DB

    をアップデート $dba->gb()->update(); // グローバル DB をコミット $dba->commitToGlobal(); // ユーザ DB をアップデート $dba->user($user_id)->update(); // ユーザ DB をコミット $dba->commitToUser($user_id); ここでエラーが起こるとユーザ DB のみ更新されず データ不整合状態となる Global User UPDATE COMMIT UPDATE COMMIT
  27. コミット 途中でコミットがエラーとなった場合は、どの DB がコミッ トされ、どの DB がコミットされていないのかをログに残す } catch (Exception

    $e) { if (0 < count($commited_arr)) { // 1 度以上コミットしたということはデータ不整合 $uncommited_arr = array(); foreach ($dban_arr as $dban) { if ($dban->isTransaction()) { // トランザクション中なら配列に含める $uncommited_arr[] = $dban->database_name; } } // エラーとなった DB 情報のログを残す logging(sprintf('commit error commited[%s] uncommited[%s]', implode(',', $commited_arr), implode(',', $uncommited_arr))); } }
  28. まとめ ・エイリアスを用意 ・複数 DB を束ねて管理するクラスを用意 抽象化は重要 抽象化する事で経験の浅いエンジニアでも扱える ・分割ルールは設計段階で破綻の無いように決めておく 必要であれば独自のルールによる振り分けを実装する ・トランザクションの管理もできるだけ簡単にする

    ・特にコミットはデータ不整合の起こりやすいポイント ・ 2 相コミットではない ・一部コミットされると全体ロールバックは不可 ・データ不整合はコミット失敗のログを残す事で対応する DB 分割意外となんとかなる でも気をつけるところはちゃんと気をつけないとダメ