DB Shardingについて2 #17B!

こんにちわ!秘密主義のnekokakです!

十七日目は昨日のDBIx::SkinnyでのDB Shardingについての続きです。

昨日はミドルウェアでShardingして、アプリケーション側では楽をしようと言うお話でした。

しかし、私はミドルウェア屋さんではないので、ミドルウェアの細かいところまで面倒みれません。

面倒みれないので、ミドルウェアに頼らずに、アプリケーション側で何とかしようと考えてしまいます。

そこで今日は私が今まで試したことのあるDB Shardingの方法を紹介してみたいと思います。

関連記事としては、

http://d.hatena.ne.jp/nekokak/20090917/1253119002

こちらを見ていただけると良いかと思います。

この記事で紹介した方法は、考察であって実際に運用で使ったことはありません。

では本題。

今回紹介するshardingの方法は以前WEB+DBの記事で紹介されたMixiさんでつかわれているというshardingの方法を参考にしたやり方です。

sharding管理用のmaster tableを作成します。

CREATE TABLE db_node (
  id     int(10) unsigned NOT NULL AUTO_INCREMENT,
  number int(10) unsigned NOT NULL、
  host   varchar(20) NOT NULL COMMENT 'DB接続先Host名',
  role   enum('master','slave') NOT NULL COMMENT 'nodeの用途',
  status enum('ok','down','wait') NOT NULL COMMENT 'nodeの状態',
  type   varchar(20) NOT NULL COMMENT '何に使うnodeなのかex)message',
  timestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  KEY db_node_idx (type,role,status)
) ENGINE=InnoDB COMMENT='DB接続先マスター情報';

CREATE TABLE db_node_map (
  id     int(10) unsigned NOT NULL AUTO_INCREMENT,
  db_node_id int(10) unsigned NOT NULL,
  type_key varchar(30) NOT NULL,
  timestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  UNIQUE KEY db_node_map (db_node_id,type_key),
  KEY db_node_id (db_node_id)
) ENGINE=InnoDB COMMENT='DB接続先管理テーブル';

db_nodeテーブルには、接続対象とできる、hostの管理をしています。

db_node_mapテーブルでは、db_nodeを任意のキーで割り振るマッピングをしています。

参考レコードとしては

> select id, number, host, role, status, type from db_node;
+----+--------+--------------+--------+--------+---------+
| id | number | host         | role   | status | type    |
+----+--------+-----------------+-----+--------+---------+
|  1 |      1 | 192.168.1.10 | master | ok     | message |
|  2 |      2 | 192.168.1.10 | master | ok     | message |
|  3 |      3 | 192.168.1.10 | master | ok     | message |
|  4 |      4 | 192.168.1.10 | master | ok     | message |
|  5 |      5 | 192.168.1.10 | master | ok     | message |
+----+--------+-----------------+-----+--------+---------+

このようになります。

また、db_node_mapの参考レコードとしては

> select id, db_node_id, type_key from db_node_map;
+----+------------+------------+
| id | db_node_id | type_key   |
+----+------------+------------+
|  1 |          1 | nekokak    |
|  2 |          2 | yappo      |
|  3 |          3 | nekoya     |
|  4 |          4 | walf443    |
|  5 |          5 | kan        |
+----+------------+------------+

このようになります。

db_node_mapは必要に応じて自動でレコードが追加されるようにしますが、

db_nodeは事前にレコードを作成しておく必要があります。

さて、このデータをどのようにDBIx::Skinnyで使うかです。

db_node/db_node_mapのデータを管理利用するmodelを作成します。

このmodelではuserテーブルのnameをキーに使ってどのdb_node/db_node_mapを使うかを決めます。

package Proj::Model::DBShardManager;
use strict;
use warnings;
use Proj::DB;

sub new {
    my $class = shift;
    bless {
        # db_node/db_node_mapの入っているdatabaseに接続する
        db => Proj::DB->new(....),
    }, $class;
}

# db_node_mapに$user_nameに対応するレコードを登録する
sub setup_handler_setting {
    my ($self, $user_name) = @_;
    die "user_name is not defined!" unless $user_name;

    # db_node_mapにすでに登録されている場合はスルー
    return if $self->{db}->single('db_node_map',{type_key => $user_name});

    # 使える全masterノード取得
    my @db_node_ids = map { $_->id } $self->{db}->search_by_sql(
        q{
            SELECT id
            FROM   db_node
            WHERE  type   = 'message'
            AND    role   = 'master'
            AND    status = 'ok'
        }, [], 'db_node'
    );
    die 'undefined db_node!!!' unless @db_node_ids;

    # 既に使用されているノードを取得
    my %list = map { $_->db_node_id => $_->count } $self->{db}->search_by_sql(
        q{
            SELECT db_node_map.db_node_id, count(db_node_map.db_node_id) AS count
            FROM db_node, db_node_map
            WHERE db_node.id   = db_node_map.db_node_id
            AND   db_node.type = 'message'
            GROUP BY db_node_map.db_node_id
            ORDER BY count ASC
        }, [], 'db_node_map'
    );

    my $use_db_node = +{};
    for my $db_node_id (@db_node_ids) {
        next if defined $use_db_node->{count} && ($use_db_node->{count} <= ($list{$db_node_id}||0));
        $use_db_node = +{
            count      => $list{$db_node_id}||0,
            db_node_id => $db_node_id,
        };
    }

    $self->{db}->insert('db_node_map',
        {
            db_node_id => $use_db_node->{db_node_id},
            type_key   => $user_name,
        }
    );
}

my $datasource = +{
    dsn      => '',
    username => 'user',
    password => 'password',
};
sub handler_for {
    my ($self, $user_name) = @_;
    die "user_name is not defined!" unless $user_name;

    my $row = $self->{db}->search_by_sql(
        q{
            SELECT db_node.id, db_node.number, db_node.host, db_node.type
            FROM   db_node, db_node_map
            WHERE  db_node.id = db_node_map.db_node_id
            AND    db_node.type         = 'message'
            AND    db_node.role         = 'master'
            AND    db_node_map.type_key = ?
            LIMIT 1
        }, [$user_name], 'db_node'
    )->first;

    if ($row) {
        $datasource->{dsn} = $row->gen_dsn;
    } else {
        $datasource->{dsn} = $self->setup_handler_setting($user_name)->gen_dsn;
    }

    my $db = Proj::DB->new($datasource);
    # 必要であればset namesを打つ
    $db->do(q{SET NAMES utf8});
    $db;
}

このProj::Model::DBShardManagerをつかうことで、

dbの接続先を変えたDBIx::Skinnyのインスタンスを取得することができるようになります。

use strict;
use warnings;
use Proj::Model::DBShardManager;

my $sm = Proj::Model::DBShardManager->new;
# user_name: nekokakをキーにしたDBIx::Skinnyのインスタンスを取得
my $db = $sm->handler_for('nekokak');
# DBIx::Skinnyのインスタンス取得後は普通のDBIx::Skinnyの操作を行うのみ

このようになります。

Proj::Model::DBShardManagerをもう少し工夫すれば、

slaveへの接続をとりだしたりすることもできますね。

駆け足の説明なので、わかりにくいこともあるかと思いますが

わからないことはどんどんircで質問してみてください。

他にもshardingの方法はあるので、また別の機会にでも。

というかこの方法は特別DBIx::Skinnyに特化しているわけではないので

何かの参考にしていただければと思います。

今日はここまで。

have a nice skinny days!:)