メニュー

【MySQL】Spiderストレージエンジンによる水平分散(シャーディング)

どうも、開発2部サーバー担当の山本です。

皆さんDBの水平分散は行ってますか?

最近ではサービス開始時点から水平分散しているタイトルも割と多いのではないでしょうか。ユーザーやデータが増えるのはサービスが好調な証ではありますが、いざ対応しようと思うと大変ですよね。

今回はMySQLのストレージエンジンであるSpiderを使った水平分散(以下シャーディング)について紹介したいと思います。

Spiderとは

Spiderは2009年にver1.0がリリースされた国産のMySQLストレージエンジンで、MySQL以外にもMariaDBとOracleに対応しています。

MySQLにはInnodbを代表にMyISAM、MEMORYなど様々な特徴を持ったストレージエンジンが用意されていますが、Spiderの特徴はストレージエンジンがシャーディングに対応していることです。

ここ最近は皆さんAWSのAuroraを使っているのか、Spiderの国内利用実績はあまり見かけませんが、世界最大のゲーム会社であるテンセント社(Tencent)では、オンラインゲームの基盤として396のSpiderノードと2800のデータノードで100TBのデータを扱っているとか!?とんでもない規模感で使われています。

 

DBのシャーディングとは

DBのシャーディングとは特定のデータ項目の剰余や範囲などのルールにより1つのテーブルのデータを複数のDBに分散して保持することです。

ゲームサーバーではuser_id:1は1台目のDBサーバー、user_id:2は2台目のDBサーバーといった具合にユーザーのデータを複数のDBに分散して保持することがよくあります。

シャーディングのイメージ

 

シャーディングのメリットとデメリットとして、以下のようなものがあります。

シャーディングのメリット

拡張性が高い

シャーディングのメリットは何よりもこれに尽きます。

ゲームサーバーでは更新によるマスターDBのI/O負荷がボトルネックになることが多々あります。参照の負荷であればスレーブの台数を増やすことで軽減できますが、更新の負荷はそうもいきません。

DBのストレージを高速ディスク(SSDやioDriveのような)に変えたり、DBを垂直分割(機能単位などで1つのDBにあるテーブル群を複数のDBに分ける)する対策が有効ですが、いずれも変更には大掛かりな実装変更や移行が発生し、アクティブユーザーの増加により頭打ちが訪れます。

DBをユーザーのIDでシャーディングしている場合、DBを増やすことで更新負荷を複数のサーバーに分散することができるため、アクティブユーザーの増減によるDB性能の拡張性(縮退も)が高いです。

 

ALTER TABLEが速い

サービスを長く運営していると悩みの種になるのが、DBの項目追加や削除といったテーブル定義の変更(ALTER TABLE)です。

データ量の多いテーブルでは項目を1つ追加するのに数時間かかることもざらです。これはALTER TABLEでは内部的に以下の処理を行っているためです。

  1. 変更後の定義でテーブルの新規作成(CREATE TABLE)
  2. 変更前のテーブルから変更後のテーブルへデータの全件登録(INSERT)
  3. 変更前のテーブルと変更後のテーブルの名前の入れ替え(RENAME)
  4. 変更前のテーブルの削除(DROP TABLE)

上記の2の処理時間はデータ量に依存し、データや制約が多いテーブルほど時間がかかります。

n台のDBでシャーディングしている場合は単純計算で1台当たりのデータ量が1/nとなり、複数台のDBで平行してALTERを実行することで処理時間が1/nになることを期待できます。(レプリケーション完了までの時間を考えると1/2n)

 

シャーディングのデメリット

オートインクリメント

シャーディング環境ではオートインクリメント値の採番が面倒です。

というのは、分散する各サーバーで別々にオートインクリメントでIDを採番した場合、サーバー間でIDの重複が発生するためです。

これを回避するために、別に用意した分散しないDBでオートインクリメント値の採番だけを行い、採番した値で分散する各サーバーにデータを登録するといった対策が必要となります。

 

サーバーを跨ぐクエリ

サーバーを跨いだ結合(JOIN)、ソート(ORDER BY)、集計(GROUP BY)を発行する際に、複数のDBを串刺しで横断検索して得た結果をプログラムでごにょごにょする必要があり、シャーディングしている環境では実装が複雑になりがちで、処理速度も不安です。

上記のようなサーバーを跨ぐ処理は設計時点で極力減らす、シャーディングしないDBに保持するなどの配慮が必要です。

 

運用コスト

ゲームサーバーのDBはマスター、スレーブの複数台で構成するのが一般的です。

シャーディングする場合はマスター、スレーブをセットで必要となり、例えばマスター1台、スレーブ2台、ユーザーデータをn台のサーバーにシャーディングする場合、n×3台のサーバーが必要です。

サーバーの台数が増えるとインフラ費用が上がるのはもちろんですが、障害点が増えるため障害発生の確率が上がる、パッチ適用などサーバー保守作業の手間が増えるなどのデメリットも発生します。

 

トランザクション管理

複数のDBに跨って更新クエリを発行する際に、一方の更新クエリが正常にコミットされ、もう一方の更新クエリがエラーとなった場合にデータの不整合が発生します。commitのタイミングをなるべく近くすることで、ほとんどの場合は回避が可能ですが、commitでエラーが発生した場合は回避できません。

障害時に備えて万全を期すならXAトランザクションによる二相コミットのような特別な配慮が必要となります。

 

Spiderによるシャーディング

SpiderストレージエンジンはFederatedストレージエンジンに構成がよく似ており、ローカルのSpiderノードとリモートの複数のデータノードで構成します。

SpiderストレージエンジンのSpiderノードは予め決められたルールにより各データノードにデータを振り分け、データノードはInnodbなどのストレージエンジンで実データを格納します。

データノードは普通のInnoDBとなんら変わらないので、Spiderノードを経由しなくとも直接参照、更新することが可能です。(データノードを直接更新する場合、データノードを跨いだ制約チェックではエラーになりませんが、データ不整合が発生するので注意)

他にもSpiderはHA機能を備えており、データノードの自動フェールオーバーにも対応していますが、HA機能についてはここでは取り扱いません。

Spiderによるシャーディング

 

デメリットの緩和

Spiderは先ほど挙げたいくつかのシャーディングのデメリットを緩和します。

オートインクリメント

Spiderノード経由でデータを登録すると、Spiderノードによりオートインクリメント値が採番されるため、特別な採番の仕組みを設ける必要がありません。

 

サーバーを跨ぐクエリ

Spiderはデータノードを跨いだ結合、ソート、集計に対応しているため、プログラムで特別な考慮なく、サーバーを跨ぐクエリを発行できます。

ただし、データノードが多いほど処理時間は長くなるので、いずれにせよデータノードを跨ぐクエリを極力控える設計や1か所にデータをまとめる構成が重要となります。

 

トランザクション管理

SpiderはXAトランザクションに対応しており、データノードを跨いだ更新クエリでデータの一貫性を保つことができます。(データノード側でもXAトランザクションの設定が必要なはずです。)

 

Spiderを使ってみる

実際にSpiderストレージエンジンを用いてシャーディングを試してみます。

 

環境

  • CentOS 7.1
  • MariaDB 10.1

※MaraDBの場合、標準でSpiderがバンドルされているので試しやすいです。

 

構築

MariaDBのインストール

# vi /etc/yum.repos.d/mariadb.repo

# MariaDB 10.0 CentOS repository list - created 2014-04-02 07:21 UTC
# http://mariadb.org/mariadb/repositories/
[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/10.1/centos7-amd64
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1
enabled=0

# yum install --enablerepo=mariadb MariaDB-client MariaDB-devel MariaDB-server MariaDB-shared

 

MariaDBの多重起動設定

今回の検証ではmysqld_multiを使って1台のマシンで複数のDBを立ち上げています。

# vi /etc/my.cnf.d/server.cnf

[mysqld]
character-set-server = utf8

## mysqld_multiの設定、UNIXドメインソケットでの接続
# vi /etc/my.cnf

[mysqld_multi]
mysqld     = /usr/bin/mysqld_safe
mysqladmin = /usr/bin/mysqladmin
log        = /var/lib/mysql/multi.log

[mysqld1]
port = 3306
datadir  = /var/lib/mysql1
pid-file = /var/lib/mysql1/mysql.pid
socket   = /var/lib/mysql1/mysql.sock

[mysqld2]
port = 3307
datadir  = /var/lib/mysql2
pid-file = /var/lib/mysql2/mysql.pid
socket   = /var/lib/mysql2/mysql.sock

[mysqld3]
port = 3308
datadir  = /var/lib/mysql3
pid-file = /var/lib/mysql3/mysql.pid
socket   = /var/lib/mysql3/mysql.sock

# mysql_install_db --datadir=/var/lib/mysql1 --user=mysql
# mysql_install_db --datadir=/var/lib/mysql2 --user=mysql
# mysql_install_db --datadir=/var/lib/mysql3 --user=mysql
# chown -R mysql:mysql /var/lib/mysql1 /var/lib/mysql2 /var/lib/mysql3

# mysqld_multi start

 

Spiderの下準備

mysql1をspiderノード、mysql2とmysql3をデータノートとして、Spiderストレージエンジンの準備とユーザー、サーバーの定義を行います。

# mysql -uroot --socket=/var/lib/mysql1/mysql.sock
MariaDB [(none)]> source /usr/share/mysql/install_spider.sql
MariaDB [(none)]> SHOW ENGINES;
+--------------------+---------+--------------------------------------------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                                                          | Transactions | XA   | Savepoints |
+--------------------+---------+--------------------------------------------------------------------------------------------------+--------------+------+------------+
| SPIDER             | YES     | Spider storage engine                                                                            | YES          | YES  | NO         |
| CSV                | YES     | CSV storage engine                                                                               | NO           | NO   | NO         |
| MRG_MyISAM         | YES     | Collection of identical MyISAM tables                                                            | NO           | NO   | NO         |
| MyISAM             | YES     | MyISAM storage engine                                                                            | NO           | NO   | NO         |
| SEQUENCE           | YES     | Generated tables filled with sequential values                                                   | YES          | NO   | YES        |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables                                        | NO           | NO   | NO         |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                                                               | NO           | NO   | NO         |
| Aria               | YES     | Crash-safe tables with MyISAM heritage                                                           | NO           | NO   | NO         |
| InnoDB             | DEFAULT | Percona-XtraDB, Supports transactions, row-level locking, foreign keys and encryption for tables | YES          | YES  | YES        |
+--------------------+---------+--------------------------------------------------------------------------------------------------+--------------+------+------------+
9 rows in set (0.00 sec)

MariaDB [(none)]> CREATE SERVER mysqld2 FOREIGN DATA WRAPPER mysql OPTIONS (USER 'spider', PASSWORD 'spider', HOST '127.0.0.1', PORT 3307);
Query OK, 0 rows affected (0.00 sec)

MariaDB [(none)]> CREATE SERVER mysqld3 FOREIGN DATA WRAPPER mysql OPTIONS (USER 'spider', PASSWORD 'spider', HOST '127.0.0.1', PORT 3308);
Query OK, 0 rows affected (0.00 sec)

MariaDB [(none)]> SELECT * FROM mysql.servers;
+-------------+-----------+----+----------+----------+------+--------+---------+-------+
| Server_name | Host      | Db | Username | Password | Port | Socket | Wrapper | Owner |
+-------------+-----------+----+----------+----------+------+--------+---------+-------+
| mysqld2     | 127.0.0.1 |    | spider   | spider   | 3307 |        | mysql   |       |
| mysqld3     | 127.0.0.1 |    | spider   | spider   | 3308 |        | mysql   |       |
+-------------+-----------+----+----------+----------+------+--------+---------+-------+
2 rows in set (0.00 sec)

# mysql -uroot --socket=/var/lib/mysql1/mysql.sock -e "CREATE DATABASE example_db; GRANT ALL PRIVILEGES ON *.* TO 'spider'@'localhost' IDENTIFIED BY 'spider'; FLUSH PRIVILEGES;"
# mysql -uroot --socket=/var/lib/mysql2/mysql.sock -e "CREATE DATABASE example_db; GRANT ALL PRIVILEGES ON *.* TO 'spider'@'localhost' IDENTIFIED BY 'spider'; FLUSH PRIVILEGES;"
# mysql -uroot --socket=/var/lib/mysql3/mysql.sock -e "CREATE DATABASE example_db; GRANT ALL PRIVILEGES ON *.* TO 'spider'@'localhost' IDENTIFIED BY 'spider'; FLUSH PRIVILEGES;"

 

シャーディングの構築

前の図と同じUSERテーブルをuser_idのハッシュによりmysqld2とmysqld3の2つのサーバーにシャーディングします。

パーティショニングと同様の構文でシャーディングするキーをパーティションのキーに、格納先のデータノードをパーティションのコメントに記述します。

# mysql -uroot --socket=/var/lib/mysql1/mysql.sock example_db
MariaDB [(none)]> CREATE TABLE `USER` (
  `user_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `profile` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `level` int(10) unsigned DEFAULT NULL,
  `exp` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE = SPIDER DEFAULT CHARSET=utf8
PARTITION BY HASH(user_id) (
  PARTITION p1 comment 'server "mysqld2", table "USER"',
  PARTITION p2 comment 'server "mysqld3", table "USER"'
);
Query OK, 0 rows affected (0.01 sec)

# mysql -uroot --socket=/var/lib/mysql2/mysql.sock example_db
MariaDB [(none)]> CREATE TABLE `USER` (
  `user_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `profile` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `level` int(10) unsigned DEFAULT NULL,
  `exp` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE = InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.01 sec)

# mysql -uroot --socket=/var/lib/mysql3/mysql.sock example_db
MariaDB [(none)]> CREATE TABLE `USER` (
  `user_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `profile` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `level` int(10) unsigned DEFAULT NULL,
  `exp` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE = InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.01 sec)

 

動作確認

データの登録

Spiderノードに接続し、10件のデータを登録します。

user_idは明示的に指定せず、オートインクリメントに任せています。

# mysql -uroot --socket=/var/lib/mysql1/mysql.sock example_db
MariaDB [example_db]> INSERT INTO USER(name, profile, level, exp) VALUES 
('NAME01', 'PROF01', '1', '101')
,('NAME02', 'PROF02', '2', '102')
,('NAME03', 'PROF03', '3', '103')
,('NAME04', 'PROF04', '4', '104')
,('NAME05', 'PROF05', '5', '105')
,('NAME06', 'PROF06', '6', '106')
,('NAME07', 'PROF07', '7', '107')
,('NAME08', 'PROF08', '8', '108')
,('NAME09', 'PROF09', '9', '109')
,('NAME10', 'PROF10', '10', '110');
Query OK, 10 rows affected (0.10 sec)
Records: 10  Duplicates: 0  Warnings: 0

 

Spiderノードからのデータ取得

Spiderノードに接続してデータを取得します。

データの格納場所がバラバラなため、ソート順を指定しない場合にuser_id順に並びませんでした。

user_idが自動的に採番されていることから、Spiderがオートインクリメントに対応していることが分かります。

また、ORDER BY、COUNT、SUMなど、データノードを跨いだソート、集計関数がシャーディングを意識することなく利用出来ています。

# mysql -uroot --socket=/var/lib/mysql1/mysql.sock example_db
MariaDB [example_db]> SELECT * FROM USER;
+---------+--------+---------+-------+------+
| user_id | name   | profile | level | exp  |
+---------+--------+---------+-------+------+
|       2 | NAME02 | PROF02  |     2 |  102 |
|       4 | NAME04 | PROF04  |     4 |  104 |
|       6 | NAME06 | PROF06  |     6 |  106 |
|       8 | NAME08 | PROF08  |     8 |  108 |
|      10 | NAME10 | PROF10  |    10 |  110 |
|       1 | NAME01 | PROF01  |     1 |  101 |
|       3 | NAME03 | PROF03  |     3 |  103 |
|       5 | NAME05 | PROF05  |     5 |  105 |
|       7 | NAME07 | PROF07  |     7 |  107 |
|       9 | NAME09 | PROF09  |     9 |  109 |
+---------+--------+---------+-------+------+
10 rows in set (0.00 sec)

MariaDB [example_db]> SELECT * FROM USER ORDER BY user_id;
+---------+--------+---------+-------+------+
| user_id | name   | profile | level | exp  |
+---------+--------+---------+-------+------+
|       1 | NAME01 | PROF01  |     1 |  101 |
|       2 | NAME02 | PROF02  |     2 |  102 |
|       3 | NAME03 | PROF03  |     3 |  103 |
|       4 | NAME04 | PROF04  |     4 |  104 |
|       5 | NAME05 | PROF05  |     5 |  105 |
|       6 | NAME06 | PROF06  |     6 |  106 |
|       7 | NAME07 | PROF07  |     7 |  107 |
|       8 | NAME08 | PROF08  |     8 |  108 |
|       9 | NAME09 | PROF09  |     9 |  109 |
|      10 | NAME10 | PROF10  |    10 |  110 |
+---------+--------+---------+-------+------+
10 rows in set (0.00 sec)

MariaDB [example_db]> SELECT COUNT(*) FROM USER;
+----------+
| COUNT(*) |
+----------+
|       10 |
+----------+
1 row in set (0.00 sec)

MariaDB [example_db]> SELECT SUM(level) FROM USER;
+------------+
| SUM(level) |
+------------+
|         55 |
+------------+
1 row in set (0.01 sec)

 

データノードからのデータ取得

次に実際のデータ格納場所であるデータノードに接続してデータを取得します。

2つのデータノードに登録したデータが分散されていることが分かります。

# mysql -uroot --socket=/var/lib/mysql2/mysql.sock example_db
MariaDB [example_db]> SELECT * FROM USER;
+---------+--------+---------+-------+------+
| user_id | name   | profile | level | exp  |
+---------+--------+---------+-------+------+
|       2 | NAME02 | PROF02  |     2 |  102 |
|       4 | NAME04 | PROF04  |     4 |  104 |
|       6 | NAME06 | PROF06  |     6 |  106 |
|       8 | NAME08 | PROF08  |     8 |  108 |
|      10 | NAME10 | PROF10  |    10 |  110 |
+---------+--------+---------+-------+------+
5 rows in set (0.00 sec)

MariaDB [example_db]> SELECT COUNT(*) FROM USER;
+----------+
| COUNT(*) |
+----------+
|        5 |
+----------+
1 row in set (0.00 sec)

MariaDB [example_db]> SELECT SUM(level) FROM USER;
+------------+
| SUM(level) |
+------------+
|         30 |
+------------+
1 row in set (0.00 sec)

# mysql -uroot --socket=/var/lib/mysql3/mysql.sock example_db
MariaDB [example_db]> SELECT * FROM USER;
+---------+--------+---------+-------+------+
| user_id | name   | profile | level | exp  |
+---------+--------+---------+-------+------+
|       1 | NAME01 | PROF01  |     1 |  101 |
|       3 | NAME03 | PROF03  |     3 |  103 |
|       5 | NAME05 | PROF05  |     5 |  105 |
|       7 | NAME07 | PROF07  |     7 |  107 |
|       9 | NAME09 | PROF09  |     9 |  109 |
+---------+--------+---------+-------+------+
5 rows in set (0.00 sec)

MariaDB [example_db]> SELECT COUNT(*) FROM USER;
+----------+
| COUNT(*) |
+----------+
|        5 |
+----------+
1 row in set (0.00 sec)

MariaDB [example_db]> SELECT SUM(level) FROM USER;
+------------+
| SUM(level) |
+------------+
|         25 |
+------------+
1 row in set (0.00 sec)

 

まとめ

Spiderストレージエンジンを利用したシャーディングを紹介しましたが、いかがでしたでしょうか?

シャーディングは規模の小さなサービスでは面倒なだけで恩恵はありませんが、ユーザー数の多いサービスでは必須となる構成で、その手段はプログラム(ライブラリ)、ミドルウェア、クラウドサービスなど多種多様です。その1つのアプローチとして参考にしてもらえれば幸いです。

(Spiderを紹介しつつ、オーツーでは自社フレームワークで水平分散に対応してます。)

 

おまけ

Spiderは水平分散に対応したストレージエンジンですが、同じ作者からVP(Vartical Partitioning)という垂直分割に対応したストレージエンジンも提供されています。Spiderと組み合わせて水平分散と垂直分割が同時にできるようなので興味のある方はお試し下さい。

この記事をシェアする

  • twitter
  • facebook
  • Google+
  • B!はてブ
  • pocket
トップへ戻る

CONTACT

ゲーム開発、Webサイト制作に関するご相談等ございましたら、お気軽にお問い合わせください。

ゲーム開発に関する
お問い合わせはこちら

Webサイト制作に関する
お問い合わせはこちら