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

Thanks for flagging this SlideShare!

Oops! An error has occurred.

×
Saving this for later? Get the SlideShare app to save on your phone or tablet. Read anywhere, anytime – even offline.
Text the download link to your phone
Standard text messaging rates apply

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

21,124

Published on

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

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

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

Published in: Technology
2 Comments
54 Likes
Statistics
Notes
  • a「18. DB 分割のデメリット ■ 設計でカバーする ・ DB 間を跨いだ JOIN ができない」 ⇒JOINは出来ますが、インデックスが効かないようです。 結果的に、パフォーマンスを下げるのでJOINはしてはいけない ⇒ できないになるかもですが。
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • a
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
No Downloads
Views
Total Views
21,124
On Slideshare
0
From Embeds
0
Number of Embeds
19
Actions
Shares
0
Downloads
101
Comments
2
Likes
54
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. ご清聴ありがとうございました

×