• Like
ソーシャルゲーム案件におけるDB分割のPHP実装
Upcoming SlideShare
Loading in...5
×

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

  • 17,536 views
Uploaded on

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

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

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

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
  • a「18. DB 分割のデメリット ■ 設計でカバーする ・ DB 間を跨いだ JOIN ができない」 ⇒JOINは出来ますが、インデックスが効かないようです。 結果的に、パフォーマンスを下げるのでJOINはしてはいけない ⇒ できないになるかもですが。
    Are you sure you want to
    Your message goes here
  • a
    Are you sure you want to
    Your message goes here
No Downloads

Views

Total Views
17,536
On Slideshare
0
From Embeds
0
Number of Embeds
15

Actions

Shares
Downloads
80
Comments
2
Likes
38

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. 1 ソーシャルゲーム案件における DB 分割の PHP 実装 ~とにかく分割ですよ。 10 回じゃ足りない。 20 回くらい分割。~ 株式会社インフィニットループ 佐々木 亨基
  • 2. 自己紹介 ・佐々木 亨基 ・ゆきこ yukicon ・ Twitter:@yukiconEx ・株式会社インフィニットループ所属 ・札幌 MySQL 勉強会代表 ・ PHP 歴は 4 年くらい ・現在仕事では PHP オンリー ・いい加減な人間なので PHP の緩さは好き
  • 3. インフィニットループについて ・北海道札幌市にあるシステム開発会社  約 90 名(契約スタッフ・アルバイト含む)で活動中  社長も含め、ほぼ全員がエンジニア ・主な開発実績(主にサーバサイドを担当)  ブラウザ三国志 (2009)  英雄クエスト (2010)   Lord of Knights(2012)  フォトバトラー (2012)   Vim 検定 (2012)   PHP 検定 (2013)  その他いろいろ
  • 4. お題目 ■ はじめに ・ DB 分割とは ・どうして DB 分割なんかするの? ・ユーザ単位による水平分割 ・ DB 分割のデメリット
  • 5. お題目 ■ 実装のお話 ・クラス設計 ・使用例 ・エイリアス ・水平分割 ・水平分割された DB への問い合わせ ・トランザクション ・トランザクションの開始 ・コミット ・まとめ
  • 6. DB 分割とは 分割していない DB A テーブル B テーブル C テーブル D テーブル E テーブル F テーブル
  • 7. DB 分割とは 分割した DB A テーブル B テーブル E テーブル F テーブル E テーブル F テーブル E テーブル F テーブル … 垂直分割 ( テーブル単位での分割 ) 水平分割 ( 同テーブルの ID 単位による分割 ) C テーブル D テーブル
  • 8. DB 分割とは 更にそれぞれがマスタスレーブ構成を取る Master Master … 垂直分割 ( テーブル単位での分割 ) 水平分割 ( 同テーブルの ID 単位による分割 ) Slave Slave Slave Master Slave Slave Slave Slave Slave Slave Master Slave Slave Slave Master Slave Slave Slave
  • 9. どうして DB 分割なんかするの? ■ 要件 数万~十数万の同時接続数に耐えられるシステム ■ 案件の特徴 ソーシャルゲーム案件の規模は予測が難しい 突然跳ねる事もあり、正確な規模が見積もれない
  • 10. どうして DB 分割なんかするの? ・同時接続数万という要求は高い ・さらに予想を上回る可能性もある ・しかしその実さっぱり流行らない可能性もある ・スモールスタート可能 ・かつ困った時にサーバ追加で解決できる構成 つまり スケールアウト可能なシステム構成
  • 11. どうして DB 分割なんかするの? Web サーバは単純なサーバ追加で対応可能 しかし DB は簡単にはいかない ・・・ ×nWeb Web Web Web DB × ?
  • 12. どうして DB 分割なんかするの? DB のスケールアウトと言えばマスタスレーブ構成 Master Slave Slave Slave ・・・ ×n SELECT UPDATE SELECT SELECT
  • 13. どうして DB 分割なんかするの? しかしマスタスレーブ構成のマスタサーバは 1 台 マスタサーバの更新性能がネックとなり いずれ限界が来る Master Slave Slave Slave ・・・ ×n SELECT UPDATE SELECT SELECT
  • 14. どうして DB 分割なんかするの? マスタ 1 台で数万の同時接続数を捌くのは不可能 要件的にマスタスレーブ構成では破綻する
  • 15. どうして DB 分割なんかするの? ではどうする? ・ Fusion-io のような超高速ストレージを使う?  →用意できるとは限らない  →導入しても解決しなかった場合に詰む ・ MySQL Cluster を使う?  →まだ枯れていない技術という印象  →制約も多く、導入は怖い
  • 16. どうして DB 分割なんかするの? マスタスレーブのセットを増やそう !        |    \   __   /     _ (m) _ピコーン        | ミ |     /  ` ´   \       ('A`)      ノヽノヽ        くく
  • 17. ユーザ単位による DB 分割 ユーザ数が増えたなら UserDB を追加 Global が苦しくなったら更に垂直分割をする事で スケールアウト可能 User1 User2 User3 … 水平分割された UserDB ユーザに紐付くデータを一定のルールで振り分けて格納 Global GlobalDB ユーザに紐付かない共通のデータを格納
  • 18. DB 分割のデメリット ■ 設計でカバーする ・ DB 間を跨いだ JOIN ができない → 冗長なデータの持ち方をしてしまう → マスタデータは全 DB に持つなどして対策 ・水平分割すればするほどパフォーマンスが下がる  ※とにかく分割とかいうタイトルになってますが   あんまり分割しちゃダメです、最低限にしましょう
  • 19. DB 分割のデメリット ■ できるだけライブラリ側で吸収する ・水平分割された DB の串刺し検索が大変 → ユーザ一覧を持ってくるのですら一苦労 ・複数 DB のトランザクション管理が煩雑
  • 20. ここからは実装のお話
  • 21. クラス設計 3 つのクラスから成っている ・ DatabaseAccess  全 DB 、トランザクションを統括するクラス ・ DatabaseAccessNode  単一 DB にアクセスするクラス  マスタスレーブの切り替えも行う  分割なしならこのクラスのみで完結 ・ DatabaseAccessMultiNode  水平分割された DB にまとめてアクセスするクラス
  • 22. クラス設計 図にすると Master Master … DatabaseAccess Slave Slave Slave Master Slave Slave Slave Slave Slave Slave Master Slave Slave Slave Master Slave Slave Slave DatabaseAccessNode DatabaseAccessMultiNode
  • 23. 使用例 // 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');
  • 24. 使用例 // 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'); なんか難しい!
  • 25. エイリアス エイリアスをつくって抽象化 class DatabaseAccess { function gb() { // global の DatabaseAccessNode オブジェクトを返す return $this->getDBAN('global'); } function user($user_id) { // user の DatabaseAccessNode オブジェクトを返す return $this->getDBAN('user', $user_id); } : :
  • 26. エイリアス // 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'); だいぶすっきり
  • 27. エイリアス // 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 用のエイリアスも用意すると便利
  • 28. 水平分割 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
  • 29. 水平分割 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;
  • 30. 水平分割 分割ルール ・テーブルによる管理 必ずテーブルを参照する必要がある server_id をキャッシュするなどの工夫が必要 登録数の少ないサーバに振り分け 各サーバに重みをつけて振り分け 既存データを意図通りに再配置 など柔軟な対応が可能 $server_id = getServerId('id_partition_tbl', $id);
  • 31. 水平分割 分割ルール ・剰余やハッシュによる振り分け 均等にバランシングされる サーバ追加時に既存データの再配置が必要 $server_id = ($id % $server_cnt) + 1;
  • 32. 水平分割 分割ルール ・範囲による振り分け ある程度意図を持ってバランシングできる 既存データに手をいれずサーバ追加が可能 ただし小回りは効かない foreach ($range_arr as $range) { if ($range['min'] <= $id and $id <= $range_info['max']) { $server_id = $range['server_id']; break; } }
  • 33. 水平分割された 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');
  • 34. 水平分割された 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); }
  • 35. 水平分割された 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; : :
  • 36. 水平分割された 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);
  • 37. 水平分割された 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);
  • 38. 水平分割された 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 されてしまう
  • 39. 水平分割された DB への問い合わせ __call による実装は、あくまでも全 DB に同じ クエリを投げているだけ レスポンスも型によって機械的に対応している INSERT のようにそれでは問題がある場合は、 専用メソッドを立てて対応する
  • 40. トランザクション トランザクションは DB 単位でかかるため 管理に気を使わなくてはいけない // 対象のユーザ DB にトランザクション開始 $dba->beginTransactionToUser($user_id); // この更新はグローバル DB への更新のため // トランザクションの対象とならない $dba->gb()->update(); XA トランザクション… うっ…頭が… ( 分離レベルが SERIALIZABLE に限られる、  挙動が怪しいという事で、ミドルウェアに頼らず  アプリによる実装としました )
  • 41. トランザクションの開始 複数 DB へのトランザクション開始方法は 2 通り ・最初にまとめて開始してしまう ・必要になった時点で開始する トランザクションが必要な事がわかりきっている場合は最 初にまとめてしまう方が管理が楽かつ簡単 どれか 1 つでもトランザクションがかかっていれば他の DB も更新処理時に自動でトランザクション状態となるオート モードも用意したが、管理できなくなる懸念があったため 使っていない
  • 42. トランザクションの開始 ■ 最初にまとめて開始してしまう場合 トランザクションは短い方が良い コネクションのコストによって無用にトランザクションが 長くならないようにマスタサーバへのコネクションを行 なってからまとめてトランザクションを開始する // 対象の DB をマスタに接続 $dba->gb()->useMaster(); $dba->user($user_id)->useMaster(); // マスタに接続した DB を一斉にトランザクション開始 $dba->myBeginTransactionToConnectionMaster(); Global User コネクション BEGIN コネクション BEGIN Global User コネクション コネクション BEGIN BEGIN
  • 43. トランザクションの開始 ■ 必要になった時点で開始する場合 ある 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); }
  • 44. コミット 各 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
  • 45. コミット コミットは必ず処理の最後にまとめて行うようにする 処理の順番によるデータ不整合に気を配る必要が無くなり コーディングの難易度も下がる // グローバル DB をアップデート $dba->gb()->update(); // ユーザ DB をアップデート $dba->user($user_id)->update(); // まとめてコミット $dba->allCommit(); Global User UPDATE UPDATE COMMIT COMMIT
  • 46. コミット まとめてコミットと言っても順番にコミットするだけ いわゆる 2 相コミットではないため、一部がコミットされ てしまうと全体のロールバックは不可能 やはり途中でエラーとなった場合はデータ不整合となる $commited_arr = array(); foreach ($dban_arr as $dban) { $dban->commit(); $commited_arr[] = $dban->database_name; }
  • 47. コミット 途中でコミットがエラーとなった場合は、どの 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))); } }
  • 48. コミット ログを頼りに手で対応する事になってしまうが 実際データ不整合はほとんど起こらず 1 年で 1 回や 2 回という低い頻度のため ログによる対応で問題となった事はない
  • 49. まとめ ・エイリアスを用意 ・複数 DB を束ねて管理するクラスを用意 抽象化は重要 抽象化する事で経験の浅いエンジニアでも扱える ・分割ルールは設計段階で破綻の無いように決めておく 必要であれば独自のルールによる振り分けを実装する ・トランザクションの管理もできるだけ簡単にする ・特にコミットはデータ不整合の起こりやすいポイント ・ 2 相コミットではない ・一部コミットされると全体ロールバックは不可 ・データ不整合はコミット失敗のログを残す事で対応する DB 分割意外となんとかなる でも気をつけるところはちゃんと気をつけないとダメ
  • 50. 求人募集 インフィニットループでは、エンジニアを募集しています ・社長も含めほぼ全員がプログラマで技術者に優しい環境 ・勤務地は北海道札幌市、他社出向などは無し ・おいしい食べ物、自然いっぱい、花粉少ない、涼しい ・短い通勤時間、徒歩や自転車で通勤が可能 ・ U ターン、 I ターン大歓迎 ・ PHP 開発エンジニア ・スマホ開発エンジニア ・ MySQL エンジニア ・インフラエンジニア
  • 51. ご清聴ありがとうございました