2010-04-26

MySQL Proxyでできること、できないこと

はてなブックマーク   livedoor clip

こんにちは。リコーの井上です。今回から私もこのブログを書かせていただくことになりました。よろしくお願いします。

今回はMySQL Proxyについて書きたいと思います。MySQLを利用していると、WebアプリケーションなどのMySQLクライアントからサーバに送られるクエリを解析したい場合があります。あるいは、複数のMySQLサーバに処理を振り分けたいということがあります。

MySQL Proxyはその名の通り、MySQLクライアントとMySQLサーバの間に入って、接続やクエリで取り交わされるデータを参照・加工して、いろいろと役立つことをさせようというものです。動作内容は軽量スクリプト言語Luaにより柔軟に記述できますが、機能的制限もあります。

このエントリではMySQL Proxyとはどういうものか、どういうことができそうなのか、できないのかということを感じ取っていただければと思っています。

必要なものとインストール

MySQL Proxyを一からビルドするにはlibevent, glib, Luaなどの依存ライブラリについてもビルドする必要があります。気軽に試すには、ダウンロードサイトからお使いのOS用のバイナリパッケージをダウンロードするのがおすすめです。バイナリパッケージはtarballで提供されているので、これを/opt以下などに展開して利用します。

Red Hat系ディストリビューション用に、/etc/init.dに置くスクリプトなども別途公開されています。

ここでの動作確認は以下の組み合わせで行いました。

  • CentOS 5.4
  • MySQL Proxy 0.8.0(Red Hat Enterprise Linux用バイナリ)
  • MySQL 5.1.45

簡単なLuaスクリプト

MySQL Proxyの大きな特長は、軽量スクリプト言語Luaを利用して任意の処理を記述できることにあります。逆に、Luaを記述しなければ指定されたMySQLサーバに交互に接続を振り分ける単純なロードバランサとなります。

MySQL Proxyでクライアントから発行されたクエリの内容をファイルに出力するLuaスクリプトは以下のようになります。


 1  function read_query(packet)
 2    local fh
 3    if string.byte(packet) == proxy.COM_QUERY then
 4      fh = io.open("/var/tmp/query.log", "a")
 5      fh:write(string.sub(packet, 2) .. "\n")
 6      fh:close()
 7    end
 8  end

スクリプトをlog.luaという名前で保存し、以下のようにMySQL Proxyを起動します。


% /opt/mysql-proxy/bin/mysql-proxy \
          --proxy-backend-addresses=127.0.0.1:3306 \
          --proxy-lua-script=`pwd`/log.lua

オプション --proxy-backend-addresses で中継先のバックエンドつまりMySQLサーバを、 --proxy-lua-script でLuaスクリプトファイル名を指定します。Luaファイル名はフルパスで指定することに注意してください。

MySQLクライアントを起動してデータベースにSELECTを発行してみましょう。MySQL ProxyはデフォルトでTCPの4040ポートを利用します。なお、接続するDBとSELECTするテーブルは事前に作成しておいてください(ここではhogeとitems)。


% mysql -u root -p -h 127.0.0.1 -P 4040 hoge
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
...(省略)...
mysql> select * from items;
...(省略)...
mysql> exit
Bye
%

この後、 /var/tmp/query.log を開くと以下のような内容が記録されています。


select @@version_comment limit 1
select * from items

1行目はMySQLクライアントがサーバに接続したときに自動的に発行している文です。2行目は発行したSELECT文がそのまま出力されています。exitはクエリではないので、ログは出力されていません。

スクリプトの内容

MySQL ProxyはLua関数により拡張できるポイントをいくつか用意しており、read_queryはその一つです。read_query関数はクライアントからのリクエストがきたときに、そのデータが入ったパケットを引数として呼び出されます。

2行目ではファイルハンドルを格納するローカル変数を定義しています。localキーワードをつけないと変数はグローバルとなり、他のリクエストから呼ばれたLua関数の中でも共通に利用されることになります。なお、関数の外で定義されたローカル変数は、クライアントとの同一コネクションを通して有効です。

3行目のif文では、パケットの最初のバイトを見て、リクエストがクエリなのかどうかを確認しています。MySQL ProxyではCOM_QUERY, COM_QUITといったパケットのタイプを示す定数が定義されています。これをパケットの先頭バイトとマッチすることでパケットを識別することができます。なおstring.byte(str, i, j)はi文字目からj文字目の内部コードを数値として多値で返します。i, jのデフォルト値は1で、文字列の最初1文字ということになります。

4~6行目は実際にファイルを開き、クエリの内容を書き込んでいます。Luaの関数 string.subを使って、関数に渡されたパケットから2バイト目以降を取り出しています。

余談ですが、string.sub() の 「.」 はstringインスタンスのメソッドを呼び出しているように見えますが、Luaはクラスを持ちません。 string は「テーブル」と呼ばれるいわゆる連想配列で、Lua処理系が提供する文字列関数を格納しています。 string.sub() はキー”sub”に対応して格納された関数を呼び出すという意味になります。つまり string["sub"]() と同じ意味です。

MySQL Proxyを使いこなすにはLuaを理解する必要があります。とはいえ、Luaは大変理解しやすい言語なので、Lua 5.1 Reference Manual(日本語訳)などにざっと目を通すだけでかなりのことができるようになるでしょう。

カスタマイズできる箇所とその内容

MySQL Proxyが用意している拡張のための関数は以下の7つです。各関数からアクセスできるデータなどについてはスクリプティングのリファレンスマニュアルに詳しく書かれています。

接続に関わる関数

connect_server
クライアントからMySQL Proxyに接続が行われた際、バックエンドとの接続を行う前に呼び出されます。実際に接続および認証を行うサーバを選択できる唯一の場所で、落ちているとマークされているバックエンドを避けるといったことができます。ただしクライアントに関する情報は利用できません。
read_handshake
MySQL Proxyとバックエンドとの接続が行われた後、バックエンドから送り返されたハンドシェイクのための情報をクライアントに転送する前に呼び出されます。クライアントのIPなどの情報が得られる状態になっているので、IPを見て切断するといった制御ができます。
read_auth
ハンドシェイクに続き、クライアントから受けとった認証情報をバックエンドに転送する前に呼び出されます。クライアント側のユーザ名にアクセスできますが、認証が成功するのかどうかはこの時点ではわかりません。
read_auth_result
バックエンドでの認証が終了した後、バックエンドから受けとった認証結果をクライアントに通知する前に呼び出されます。認証の成否がわかるので、認証成功(失敗)後に行いたい処理を記述できます。

認証が終了したコネクションはそのままそのクライアントからのリクエストで利用してもいいですし、バックエンドと結びつけられたコネクションプールに入れて後から利用することもできます。

リクエストに関わる関数

read_query
クライアントから何らかのリクエストが送られたとき、それをバックエンドに転送する前に呼び出されます。「簡単なLuaスクリプト」で利用していたのもこの関数です。SQLを解析して書き換えたり、結果を作って返したりできます。
read_query_result
クエリ結果がバックエンドから返されたとき、それをクライアントに転送する前に呼び出されます。結果を操作したり任意の結果を返すようにすることができます。この関数は結果セットにアクセスしたい旨を前述のread_query()の中で明示的に指定しなければ呼び出されません。

その他の関数

disconnect_client
サーバとクライアントの接続が切断されるときに呼び出されます。実際には切断せずにコネクションをプールするといった処理を記述します。

コネクションプールへのアクセス

少し細かい話になりますが、MySQL Proxyのスクリプトで一番わかりにくいと感じたのがコネクションプールとのやりとりなので、説明を加えておきます。これは proxy.connection.backend_ndx という変数によって制御します。

現在利用している(バックエンドとの)コネクションをコネクションプールに入れるには、 proxy.connection.backend_ndx に0を代入します。 代入した段階でプールへの格納が行われます。

逆にプールにあるコネクションを利用するには、対応するバックエンドの番号 (proxy.global.backends配列の添え字) を proxy.connection.backend_ndx に代入します。 代入と同時に、プールにあるアイドルなコネクションのどれか一つが、現在のコンテキストとなっているクライアントとのコネクションに対応づけられます。

Lua層のこの変数への代入が、Cレベルではコネクションプールを操作する関数呼び出しを起こすような実装となっている、ということを覚えておくと多少理解しやすくなります。

MySQL Proxyでできること、できないこと

MySQL Proxyの用途として、サイトでは以下のようなものが例として挙げられています。

  • 負荷分散
  • フェイルオーバー(冗長化)
  • クエリの解析
  • クエリのフィルタリングや書き換え

Luaでかなり自由なことができそうなのですが、現行のバージョンでは機能にいくつかの制限があり、これらを念頭に置いて使い方を考えなければなりません。

  • Luaスクリプトが呼ばれるのはクライアントがアクションを起こしたときのみです。そのため、クライアントからの指示なしに必要な数のコネクションを作ってプールすることや、バックエンドの生死確認をアクティブに行うといったことはできません。
  • コネクションプールからのコネクションの割り当ては接続してきたユーザの権限に関係なく行われるようです。コネクションの割り当て時にCHANGE_USERコマンドで権限を変更することもできるようなのですが、期待通りの動作を確認できませんでした。そのため、権限の高いユーザと低いユーザが混在するような利用法はうまく動作しません。
  • これはコネクションプールをアプリケーションと独立に持つ場合の宿命かも知れませんが、コネクション固有の状態をうまくサポートできません。プールに入っているコネクションに固有の状態を設定しても、次にそのクライアントに対して同じコネクションが割り当てられる保証はないからです。コネクション固有の状態としては、文字エンコーディングやセッション変数、そしてテンポラリテーブルなどがあります。

R/W Splittingへの適用

MySQL Proxyの利用法として、更新系と参照系のクエリをそれぞれマスタとスレーブに振り分けて負荷分散する、いわゆるR/W Splittingがあります。これが完璧に動作すれば、アプリ側での制御が不要になるため開発が楽になります。MySQL Proxyのパッケージに含まれるサンプルスクリプトの中にも rw-splitting.lua としてコンセプト実証のコードが入っています。(0.8.0に付属のものはいくつかバグがあってそのままでは動作しません。例: Bug #39629)

実現方法は、認証済みのコネクションをマスタ・スレーブそれぞれに対して一定数プールしておき、クエリがきた段階で書き込みならマスタへの、読み込みならスレーブへのコネクションを使うというものです。LAST_INSERT_ID()やトランザクションに対する考慮もされています。

しかし、前節に書いたような制限があるため、バグを修正して使う場合でもアプリ側で以下のような注意が必要になります。

  • 文字エンコーディングなどコネクション固有になる設定はバックエンド側で一括して設定する
  • MySQL Proxy起動初期にはコネクションプールに書き込みのためのコネクションが不足することがあるため、エラー時に再接続するようアプリ側で制御する

R/W SplittingについてはMySQL Proxy RW Splittingに制限を含めた記載があります。

まとめ

以上、MySQL Proxyの機能やその制限について書いてみました。開発においてクエリを解析したり、特定の条件で書き換えたりということには利用できるレベルになっていると思います。ただ、MySQLサーバ群の手前に配置してすべてのアプリからの読み書きを分散、という使い方はまだ荷が重いようです。今後の開発に期待したいところです。

2009-11-30

MySQLパーティショニングについて(その2:性能検証編)

はてなブックマーク   livedoor clip

こんにちは、濱田です。

前回から時間が経ってしまいましたが、今回は「性能検証編」ということで、パーティションドテーブルに対して実際にデータを挿入・参照することでパーティショニングの性能面を検証してみようと思います。

性能検証環境

使用したマシンのスペックは以下の通りです。

OS
CentOS 5.3 32bit (on Windows XP Pro SP3 32bit via VMware Server 2.0.0)
CPU
Core2 Duo E8300 2.83GHz (VMには1CPUを割り当て)
Memory
3.25GB (VMには512MBを割り当て)

MySQL のバージョンおよび設定は以下の通りです。なお、MySQL サーバおよびクライアントは同一マシン上で動作させました。

MySQL
5.1.35-community (設定は my-medium.cnf をそのまま使用)

また、パーティションドテーブルのスキーマは以下の通りです。

CREATE TABLE logs (
    id INT NOT NULL AUTO_INCREMENT,
    client_name VARCHAR(255) NOT NULL,
    log_data VARCHAR(1024) NOT NULL,
    logged_at DATETIME NOT NULL,
    PRIMARY KEY(id, logged_at),
    INDEX(client_name)
) ENGINE=MyISAM
PARTITION BY RANGE( TO_DAYS(logged_at) ) (
    PARTITION p200901 VALUES LESS THAN ( TO_DAYS('2009-02-01') ),
    PARTITION p200902 VALUES LESS THAN ( TO_DAYS('2009-03-01') ),
    PARTITION p200903 VALUES LESS THAN ( TO_DAYS('2009-04-01') ),
    PARTITION p200904 VALUES LESS THAN ( TO_DAYS('2009-05-01') ),
    PARTITION p200905 VALUES LESS THAN ( TO_DAYS('2009-06-01') ),
    PARTITION p200906 VALUES LESS THAN ( TO_DAYS('2009-07-01') ),
    PARTITION p200907 VALUES LESS THAN ( TO_DAYS('2009-08-01') ),
    PARTITION p200908 VALUES LESS THAN ( TO_DAYS('2009-09-01') ),
    PARTITION p200909 VALUES LESS THAN ( TO_DAYS('2009-10-01') ),
    PARTITION p200910 VALUES LESS THAN ( TO_DAYS('2009-11-01') ),
    PARTITION p200911 VALUES LESS THAN ( TO_DAYS('2009-12-01') ),
    PARTITION p200912 VALUES LESS THAN ( TO_DAYS('2010-01-01') ),
    PARTITION pmax VALUES LESS THAN MAXVALUE
);

データ挿入の性能検証

検証内容

データ挿入の性能検証には下記 INSERT 文を1万回連続で発行し、それに要した時間を測定しました。INSERT 文の「?」の部分には ‘client_1′ ~ ‘client_1000′ がそれぞれ10回ずつ入ります。(実際には上記の処理を行なう Ruby スクリプトを作成し、その実行時間を time コマンドで測定しました)

INSERT INTO logs (client_name, log_data, logged_at) \
VALUES (?, 'test log data', '2009-11-01');

logs テーブルの事前条件として、以下の2パターンで測定しました。

条件1
1件もレコードが格納されていない状態
条件2
1~10月の各月に1000クライアント×1000件分のログデータ(計1000万件)が格納されている状態

  • logged_at : 10通り (’2009-01-01′ ~ ‘2009-10-01′)
  • client_name : 1000通り (’client_1′ ~ ‘client_1000′)
  • 上記組み合わせをそれぞれ1000件ずつ

検証結果

結果は図1のようになりました。

result_insert_myisam.png
図1 : データ挿入の性能検証結果 (MyISAM)

条件1を見ると、パーティションあり/なしで処理時間が殆ど変わっていません。これはデータ挿入時にパーティション振り分け処理のオーバーヘッドが殆どないことを示しています。今回振り分け処理に用いたパーティショニング表現は TO_DAYS(logged_at) であり、この程度の処理ではパーティション振り分けのオーバーヘッドを全く気にしなくてよいことが分かります。

逆に言えば、パーティショニング表現に時間のかかる処理を用いてしまうとそこがボトルネックになってしまいます。それが気になる場合は、以下のように BENCHMARK() 関数を用いて事前にパーティショニング表現の処理時間を確認しておくとよいと思います。

mysql> SELECT BENCHMARK(10000, TO_DAYS('20091101') );
+---------------------------------------+
| benchmark(10000, TO_DAYS('20091101')) |
+---------------------------------------+
|                                     0 |
+---------------------------------------+
1 row in set (0.00 sec)

また条件1、2での処理時間を比較すると、パーティションなしの場合は処理時間が増加しているのに対し、パーティションありの場合は処理時間が殆ど変わっていないことが分かります。これはインデックスデータがパーティション毎に独立して作成されることを示しています。今回のスキーマで作成されるインデックスは (id, logged_at) の複合インデックスと client_name ですが、このうち client_name はユニークなカラムではありません。そのため、事前に格納されているレコード数が多いほどインデックスデータ(BTREE 構造)の更新処理に時間がかかり、故にデータ挿入処理も遅くなります。

パーティションなしの場合に事前に0件と1000万件格納されている場合とで挿入時間に差が出ているのはそのためです。しかし、パーティションありの場合、条件2において p200911 (以降)には1件もレコードが格納されていない状態であるため、logged_at が ‘2009-11-01′ (以降)のデータを挿入する場合はテーブルが空の場合と同じ処理時間になるのです。つまり、ログ情報のような日毎に蓄積されるデータを格納する場合、月別にパーティションを設定しておくことで、レコード数増加に伴い累積的に遅くなるデータ挿入処理の影響を一ヶ月分に抑えられることが分かります。

なお、ストレージエンジンを InnoDB にして同じ検証を行なってみましたが、MyISAM の場合と同様の結果になりました。(図2)

result_insert_innodb.png
図2 : データ挿入の性能検証結果 (InnoDB)

データ参照の性能検証

検証内容

データ参照の性能検証には、MySQL クライアントから下記2パターンの SELECT 文を1回発行し、それに要した時間を測定しました。

条件1
client_name 指定なしで11月分のログデータ参照する

SELECT * FROM logs WHERE logged_at >= '2009-11-01' \
AND logged_at < '2009-12-01';
条件2
client_name 指定ありで11月分のログデータ参照する

SELECT * FROM logs WHERE logged_at >= '2009-11-01' \
AND logged_at < '2009-12-01' AND client_name = 'client_1';

logs テーブルの事前条件は、データ挿入の性能検証(条件2)を行なったときの状態としました。つまり、1~10月の各月に1000クライアント×1000件分のログデータが、11月に1000クライアント×10件分のログデータが格納されている状態(計1001万件)です。

検証結果

結果は図3のようになりました。

result_select_myisam.png
図3 : データ参照の性能検証結果 (MyISAM)

条件1を見ると、パーティションありの方が処理時間が劇的に速いことが分かります。これはパーティションなしでは全レコード(1001万件)を参照しているのに対し、パーティションありでは p200911 のレコード(1万件)のみを参照しているためです。これはオプティマイザが SELECT 文中のクエリ logged_at >= ‘2009-11-01′ AND logged_at < ‘2009-12-01′ から判断した結果です。理論的には検索件数が1001万から1万に減るので処理時間も1/1000になって欲しいところですが、そうなっていないのはオプティマイザの判断処理等によるオーバーヘッドの影響かもしれません。

なお、SELECT 文の前に EXPLAIN PARTITIONS を付けることで、参照時にアクセスするパーティションを調べることができます。(これを見るとなぜか p200912 にもアクセスしていることが分かります。なぜそうなるのかは分かりませんが…)

mysql> EXPLAIN PARTITIONS SELECT * FROM logs
    -> WHERE logged_at >= '2009-11-01'
    -> AND logged_at < '2009-12-01'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: logs
   partitions: p200911,p200912
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 10010000
        Extra: Using where

また、条件2を見ると、インデックス併用時にもパーティションありの方が処理時間が劇的に速いことが分かります。この場合、まずオプティマイザが参照に必要なパーティションを判断し、そこからさらに該当するパーティションのインデックス情報を使って参照するデータ数を絞り込んでいるようです。ここまでいくと、データ参照のコストがほぼゼロになりました。

なお、ストレージエンジンを InnoDB にして同じ検証を行なってみましたが、こちらも MyISAM の場合と同様の結果になりました。(図4)

result_select_innodb.png
図4 : データ参照の性能検証結果 (InnoDB)

まとめ

本エントリでは、パーティションドテーブルに対して実際にデータを挿入・参照することでパーティショニングの性能面を検証しました。その結果、今回検証した条件ではデータ挿入・参照のいずれにおいてもパーティショニングしない場合に比べて性能が向上することが分かりました。

このように、パーティショニングは上手に利用すればいいことずくめとなる機能です。本エントリがパーティショニング活用に役立てれば幸いです。

2009-10-21

MySQLパーティショニングについて(その1:基本知識編)

はてなブックマーク   livedoor clip

初めまして、リコーの濱田です。このたび私も本ブログを担当することになりました。今後ともよろしくお願いいたします。

本エントリではデータベースに関する技術トピックとして、MySQL 5.1 から導入された機能であるパーティショニングについて書こうと思います。少し長くなりそうなので、「基本知識編」「性能検証編」の2回に分けて書くことにします。

今回は「基本知識編」として、パーティショニングの概要と基本的な使い方について紹介します。

パーティショニングの概要

パーティショニングとは、事前に設定されたルールに従ってデータをパーティションと呼ばれる部分的なテーブルに分割する仕組みです。

データ挿入時には、設定ルールに従ってデータが該当するパーティションに自動的に振り分けられます。データ参照時には、オプティマイザがクエリから必要なパーティションを判断し、該当するパーティションのみにアクセスします。これらは MySQL の内部で行なわれるため、データの操作においてパーティショニングを意識する必要はありません。
partitioning
パーティショニングを利用することで、以下の利点が得られます。

  • 大量のデータを処理することによる性能上のボトルネックの発生を抑えられる
  • テーブルサイズに上限がある場合(MyISAMなど)でも、その上限を超える量のデータを格納できる

パーティションドテーブルの作成

それでは実際にパーティションドテーブル(パーティショニングされたテーブル)を作成してみます。パーティショニングにはいくつかの種類がありますが、ここでは RANGE パーティショニングについて説明します。

パーティションドテーブルの作成は簡単で、通常のテーブル作成文の後にパーティション設定ルールの記述を追加するだけです。以下の例は、複数クライアントのログ情報を月別に分けて格納するパーティションドテーブルの作成例です。

CREATE TABLE logs (
    id INT NOT NULL AUTO_INCREMENT,
    client_name VARCHAR(32) NOT NULL,
    log_data VARCHAR(1024) NOT NULL,
    logged_at DATETIME NOT NULL,
    PRIMARY KEY(id, logged_at),
    INDEX(client_name)
) ENGINE=MyISAM
PARTITION BY RANGE( TO_DAYS(logged_at) ) (
    PARTITION p200910 VALUES LESS THAN ( TO_DAYS('2009-11-01') ),
    PARTITION p200911 VALUES LESS THAN ( TO_DAYS('2009-12-01') ),
    PARTITION p200912 VALUES LESS THAN ( TO_DAYS('2010-01-01') ),
    PARTITION pmax VALUES LESS THAN MAXVALUE
);

PARTITION BY RANGE 以下がパーティション設定ルールです。上記の例では p200910、p200911、p200912、pmax という名前の4つのパーティションが作成され、TO_DAYS(logged_at) の値を基にデータが各パーティションに振り分けられる設定になっています。例えば、logged_at が ‘2009-11-01′ より小さいデータは p200910 に格納されるといった具合です。ここで、振り分けに利用される表現(上記例では TO_DAYS(logged_at) )を「パーティショニング表現」と呼びます。

パーティション設定ルールの最後の行の “VALUES LESS THAN MAXVALUE” は「キャッチオール」節と呼ばれるものです。MAXVALUE は表現可能な最大整数値を表すので、logged_at が ‘2010-01-01′ 以上となるデータは全て pmax に格納されます。これが無いと、logged_at が ‘2010-01-01′ 以上となるデータを格納する場合にエラーが起こります。

なお、パーティションドテーブルの作成に際してはいくつかの制約があります。以下に代表的なものを挙げておきます。

  • パーティショニング表現に用いられるカラムはテーブル内に存在するプライマリキー(またはユニークキー)の一部でなければならない
    • プライマリキー(またはユニークキー)が存在しない場合は例外
  • パーティション表現は連続した整数値をとるものでなければならない
    • 日付データを利用する場合、TO_DAYS() 関数などを使って整数値に直す必要がある
  • 作成できるパーティションの上限は1テーブルにつき1024個

パーティションの追加

既存のパーティションドテーブルから更にパーティションを追加する方法は2つあります。

  • ADD PARTITION を使って新規パーティションを追加する方法
  • REORGANIZE PARTITION を使って既存のパーティションを再分割する方法

ただし、既存のパーティションドテーブルに「キャッチオール」節が存在する場合は後者の方法しか選択肢がありません。

以下にそれぞれの方法について説明します。

ADD PARTITION を使う方法

既存のパーティションドテーブルにパーティションを新規追加するには以下のようにします。

ALTER TABLE logs ADD PARTITION (
    PARTITION p201001 VALUES LESS THAN ( TO_DAYS('2010-02-01') )
);

注意点として、RANGE パーティショニングされているテーブルにおいて、ADD PARTITION は既存パーティションの「後」にしかパーティション追加ができません(「前」や「間」はNGです)。そのため、「キャッチオール」節(MAXVALUE)が既に存在する場合は(MAXVALUE より後の値は存在し得ないため)、この方法が使えません。

REORGANIZE PARTITION を使う方法

既存のパーティションを再分割するには以下のようにします。この例では、pmax を p201001 と、(新しい) pmax に再分割しています。

ALTER TABLE logs REORGANIZE PARTITION pmax INTO (
    PARTITION p201001 VALUES LESS THAN ( TO_DAYS('2010-02-01') ),
    PARTITION pmax VALUES LESS THAN MAXVALUE
);

REORGANIZE PARTITION を行なうと、既存のデータが再分割後のパーティションに適切に振り分けられます。例えば上記の例では、旧 pmax に格納されていたデータのうち logged_at が ‘2009-02-01′ より小さいものは p201001 に、’2009-02-01′ 以上のものは 新 pmax に格納されます。この再振り分け処理により、既存データが大量にある場合は REORGANIZE PARTITION の実行にそれなりの時間がかかります。

REORGANIZE PARTITION は、既存パーティションの結合にも利用できます。例えば、p200910 と p200911 を結合したい場合は以下のようにします。

ALTER TABLE logs REORGANIZE PARTITION p200910, p200911 INTO (
    PARTITION p200910_11 VALUES LESS THAN ( TO_DAYS('2009-12-01') )
);

さらに、既存の複数パーティションを新しい複数パーティションに再構成することもできます。

ALTER TABLE logs REORGANIZE PARTITION p200910, p200911, p200911, pmax INTO (
    PARTITION p200909_10 VALUES LESS THAN ( TO_DAYS('2009-11-01') ),
    PARTITION p200911_12 VALUES LESS THAN ( TO_DAYS('2010-01-01') ),
    PARTITION pmax VALUES LESS THAN MAXVALUE
);

パーティションの削除

パーティションドテーブルから特定のパーティションを削除するには、以下のようにしてパーティションごと DROP します。

ALTER TABLE logs DROP PARTITION p200912;

上記の例では、パーティション p200912 および p200912 に格納されているデータが全て削除されます。また、上記例の実行前に p200910、p200911、p200912、pmax のパーティションがあった場合、p200912 が削除されたことで logged_at が ‘2009-12-01′ 以上となるデータは全て pmax に格納されるようになります。

パーティションの削除は内部的にテーブルの DROP とほぼ同じであるため、(ストレージエンジンが MyISAM や InnoDB の場合であれば)格納されているデータ件数によらず一瞬で処理が完了します。

パーティショニングの解除

パーティションドテーブルからパーティションを取り除き、通常のテーブルにするには以下のようにします。

ALTER TABLE logs REMOVE PARTITIONING;

DROP PARTITION とは異なり、各パーティションに格納されているデータは削除されません。各パーティションのデータを一つのテーブルにマージするため、既存データが大量にある場合は REMOVE PARTITIONING の実行にそれなりの時間がかかります。

まとめ

本エントリでは、MySQL 5.1 から導入された機能であるパーティショニングについて概要を説明し、RANGE パーティショニングによるパーティションドテーブルの作成、パーティションの追加・削除、パーティショニングの解除方法について具体例を交えて説明しました。RANGE パーティショニング以外のパーティショニング方法などパーティショニングについてより深く知りたい方は、MySQL 5.1 リファレンスマニュアル :: 15 パーティショニング を一読されると良いと思います。

次回のエントリでは、パーティションドテーブルに対して実際にデータを挿入・参照することでパーティショニングの性能面を検証してみようと思います。お楽しみに!

2008-07-22

MogileFS のために MySQL の NDB Cluster を動かす

はてなブックマーク   livedoor clip

お久しぶりです、日野原です。引き続き MogileFS の話題です。

と言っても前回の続きで開発環境上での話ではなく、サーバに MogileFS を入れていこうと言う話になります。

そこで MogileFS についてちゃんと調べ始めると、「Learning MogileFS」という資料の「mysql database cluster」のところになんと「その名の通りNDB Cluster推奨」と書いてあるではありませんか。

というわけで今回は MySQL の NDB Cluster を 3 台の CentOS 5.2 のマシンにセットアップします。NDB Cluster 自体の説明はThink ITの記事などいろいろとあるので、参考にしてください。
インストールにはrpm 形式で配布されているバイナリを使います。(CentOS 5.2 なので Red Hat Enterprise Linux 5 用の RPM をダウンロードします。記事中ではダウンロードの部分は省略します。)

配布されているページに行くとバージョン番号が MySQL Cluster 6.2.15 となっていて面食らいますが、管理ツールの出力によると MySQL 自体は 5.1.23 で、その上に ndb の 6.2.15 が載っているようです。

今回の構成では、クラスタの 3 種類のノードは以下のように構成します。

3台のマシンのホスト名は srv[1-3] 、IPは 192.168.10.[1-3] とします。そして srv1 上で管理ノード(ndb_mgmd)、srv2 と srv3 上で SQLノード(mysqld)とデータノード(ndbd)を動かします。

この構成だと管理ノードが単一故障点になってしまっていますが、ここを複数構成にするための説明はまだ見つかっていないので今後の課題とさせてください。

それでは実際にインストール作業を始めていきます。
ちなみに、今回のサーバはちゃんと識者に CPAN が使えるようにしてもらってあるので、perl のモジュールのインストールは yum ではなく CPAN から行います。

まずは管理ノードです。
管理ノードに必要なのは以下の3つです。

  • perl の Class::MethodMaker モジュール
  • MySQL-Cluster-gpl-management-6.2.15-0.rhel5.i386.rpm
  • MySQL-Cluster-gpl-tools-6.2.15-0.rhel5.i386.rpm

これらを srv1 にインストールします。

% sudo cpan -i Class::MethodMaker
% sudo rpm -ivh MySQL-Cluster-gpl-management-6.2.15-0.rhel5.i386.rpm
% sudo rpm -ivh MySQL-Cluster-gpl-tools-6.2.15-0.rhel5.i386.rpm

次に設定ファイルを作成します。今回は設定ファイルは /var/lib/mysql-cluster/config.ini という名前で配置します。また、管理ノードは mysql というユーザで起動しますので、ユーザを作成しておいてください。

% sudo /usr/sbin/useradd mysql -d /var/lib/mysql-cluster
% cd /var/lib
% sudo mkdir mysql-cluster
% sudo chown mysql:mysql mysql-cluster
% cd mysql-cluster
% sudo vim config.ini
% cat config.ini
[NDBD DEFAULT]
NoOfReplicas = 2
DataDir = /var/lib/mysql-cluster
ServerPort = 63132[MGM]
Id = 1
HostName = 192.168.10.1
DataDir = /var/lib/mysql-cluster

[NDBD]
Id = 11
HostName = 192.168.10.2

[NDBD]
Id = 12
HostName = 192.168.10.3

[MYSQLD]
Id = 21
HostName = 192.168.10.2

[MYSQLD]
Id = 22
HostName = 192.168.10.3

ここで、NDBD DEFAULT セクションの最後の ServerPort がはまりどころです。
オフィシャルのドキュメントを見ると「旧式」となっており、「デフォルトのポートは同じコンピュータ上の 2 つのノードが同じポート番号を受信しないようにダイナミックに割り当てられているため、通常このパラメータの値を指定する必要はありません。」とあるのですが、これを指定しておかないとファイアーウォールに阻まれて通信がうまくいきません。ひょっとしたら将来のバージョンでは不要になるかもしれませんが、今回インストールしたバージョンでは必要です。
具体的には、データノードから管理ノードに一見ちゃんと接続されているように見えているのに接続されておらず、SQLノードが管理ノードに接続できないという現象が起こります。
私はこれで2日つぶしました。

そのほかのオプションについてはオフィシャルのドキュメントを見てください。基本的な設定例等を見ればわかると思います。

設定ファイルができたら管理ノードにアクセスするためのポートを開けます。

% sudo /usr/sbin/lokkit

デフォルトのポートの 1186 は CentOS 5.2 では /etc/services に書いてあるので、カスタマイズのボタンから「その他のポート」に「mysql-cluster:tcp」を追加します。

そして、管理ノードを起動します。

% sudo -u mysql /usr/sbin/ndb_mgmd -f /var/lib/mysql-cluster/config.ini

起動したら、管理クライアントを起動して show コマンドで状況を確認してみましょう。

% ndb_mgm
-- NDB Cluster -- Management Client --
ndb_mgm> show
Connected to Management Server at: localhost:1186
Cluster Configuration
---------------------
[ndbd(NDB)]     2 node(s)
id=11 (not connected, accepting connect from 192.168.10.2)
id=12 (not connected, accepting connect from 192.168.10.3)

[ndb_mgmd(MGM)] 1 node(s)
id=1    @192.168.10.1  (mysql-5.1.23 ndb-6.2.15)

[mysqld(API)]   2 node(s)
id=21 (not connected, accepting connect from 192.168.10.2)
id=22 (not connected, accepting connect from 192.168.10.3)

これで管理ノードの準備はOKです。

次にデータノードとSQLノードの設定をしますが、これは srv2 と srv3 の両方に対して実行します。以下では srv2 に対して実行しているものとして解説しますが、srv3 に対しても実行してください。

まず、最低限必要なのは以下の4つです。

  • perl の DBI モジュール
  • MySQL-Cluster-gpl-client-6.2.15-0.rhel5.i386.rpm
  • MySQL-Cluster-gpl-storage-6.2.15-0.rhel5.i386.rpm
  • MySQL-Cluster-gpl-server-6.2.15-0.rhel5.i386.rpm

これらを順にインストールします。

% sudo cpan -i DBI
% sudo rpm -ivh MySQL-Cluster-gpl-client-6.2.15-0.rhel5.i386.rpm
% sudo rpm -ivh MySQL-Cluster-gpl-storage-6.2.15-0.rhel5.i386.rpm
% sudo rpm -ivh MySQL-Cluster-gpl-server-6.2.15-0.rhel5.i386.rpm
% mysqladmin -u root password rootpass

(mysql のルートのパスワードは適切に設定してください)

次に設定ファイルを作成します。ひな形はrpmで提供されているので、これを /etc/ 以下にコピーして、NDB Clusterに必要な設定を追加します。

% sudo cp /usr/share/doc/MySQL-Cluster-gpl-server-6.2.15/my-large.cnf /etc/my.cnf
% sudo vim /etc/my.cnf

追加するのは以下の内容です。
[mysqld]セクション内に以下の2行

ndbcluster
ndb-connectstring = 192.168.10.1

ファイル末尾に [mysql_cluster] セクションを追加して1行。

ndb-connectstring = 192.168.10.1

そしてファイアーウォールの設定をします。

% sudo /usr/sbin/lokkit

で、受信を許可する「その他のポート」に「63132:tcp mysql:tcp」と書きます。

また、これだけだと mysql が管理ノードにアクセスしに行くときに selinux に引っかかってエラーになるのですが、そのエラーをわざと一度起こして許可ルールを作るためのログを吐かせます。
もし、/var/log/audit というディレクトリが存在しない場合、audit をインストールしておいてください。

% sudo yum install audit
% sudo /etc/init.d/auditd start% sudo /etc/init.d/mysql start
% sudo grep denied /var/log/audit/audit.log

これで、

type=AVC msg=audit(1216111039.402:59893): avc:  denied { name_con
nect } for  pid=29878 comm="mysqld" dest=32960 scontext=user_u:sy
stem_r:mysqld_t:s0 tcontext=system_u:object_r:port_t:s0 tclass=tc
p_socket

というような行が見つかればOKです。(適当に改行してありますが、本当は一行です。)
mysql はこのまま起動に失敗してプロセスが残ったままになってしまうので、ちゃんとプロセスを kill しておいてください。
次にこのログから許可ルールを作成します。

% sudo audit2allow -a -M mysqlcluster

すると mysqlcluster.pp というファイルが生成されるのでこれをselinuxに取り込ませます。

% sudo /usr/sbin/semodule -i mysqlcluster.pp

ただ、これだとmysqlがネットワークにアクセスするのを全て許可してしまうので、宛先やポートを指定して許可したい場合には適切な te ファイルを作成して設定してください。
ちなみに、srv2 でこの pp ファイルを作れば、srv3 ではログを吐かせるあたりは省略して pp ファイルをコピーして読みこませるだけでOKです。

ここまで来たらあとはデータノードとSQLノードを起動するだけです。
まずデータノードを起動します。

% sudo -u mysql /usr/sbin/ndbd

この時点で srv1 の ndb_mgm から確認すると以下のようになっているでしょう。

ndb_mgm> show
Cluster Configuration
---------------------
[ndbd(NDB)]     2 node(s)
id=11   @192.168.10.2  (mysql-5.1.23 ndb-6.2.15, starting, Nodegroup: 0, Master)
id=12   @192.168.10.3  (mysql-5.1.23 ndb-6.2.15, starting, Nodegroup: 0)

[ndb_mgmd(MGM)] 1 node(s)
id=1   @192.168.10.1  (mysql-5.1.23 ndb-6.2.15)

[mysqld(API)]   2 node(s)
id=21 (not connected, accepting connect from 192.168.10.2)
id=22 (not connected, accepting connect from 192.168.10.3)

ここでndbdのどちらにもMasterの表示がない場合、外からndbdへのアクセスに失敗している可能性があります。srv1 の config.ini の [NDBD DEFAULT] セクションで SeverPort が 63132 に設定された上で、srv2 と srv3 でポートが空いているかどうか確認してください。

次にsrv2 とsrv3 でmysqlを起動します。

% sudo /etc/init.d/mysql start

そして srv1 で確認します。

ndb_mgm> show
Cluster Configuration
---------------------
[ndbd(NDB)]     2 node(s)
id=11   @192.168.10.2  (mysql-5.1.23 ndb-6.2.15, starting, Nodegroup: 0, Master)
id=12   @192.168.10.3  (mysql-5.1.23 ndb-6.2.15, starting, Nodegroup: 0)

[ndb_mgmd(MGM)] 1 node(s)
id=1   @192.168.10.1  (mysql-5.1.23 ndb-6.2.15)

[mysqld(API)]   2 node(s)
id=21   @192.168.10.2  (mysql-5.1.23 ndb-6.2.15)
id=22   @192.168.10.3  (mysql-5.1.23 ndb-6.2.15)

この画面がちゃんと表示されれば、インストールは成功です。

それでは srv2 と srv3 でデータが同期されることを確認しましょう。

まずは srv2 で。

% mysql -u root -p
mysql> create database cluster_test;
Query OK, 1 row affected (0.24 sec)

そして srv3 で。

% mysql -u root -p
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| cluster_test       |
| mysql              |
| ndb_12_fs          |
+--------------------+
4 rows in set (0.00 sec)

を!?

では srv3 で

mysql> use cluster_test
Database changed
mysql> create table foo(bar int(10) primary key, name varchar(255))
    -> engine=ndbcluster;
Query OK, 0 rows affected (0.82 sec)

そして srv2 で

mysql> use cluster_test
Database changed
mysql> show tables;
+------------------------+
| Tables_in_cluster_test |
+------------------------+
| foo                    |
+------------------------+
1 row in set (0.00 sec)

をを!?

では srv2 で

mysql> insert into foo values(1, 'ggrecus');
Query OK, 1 row affected (0.01 sec)

そして srv3 で

mysql> select * from foo;
+-----+---------+
| bar | name    |
+-----+---------+
|   1 | ggrecus |
+-----+---------+
mysql> drop database cluster_test;
Query OK, 2 rows affected (0.82 sec)
1 row in set (0.02 sec)

うむ。
そして srv2 で

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| ndb_11_fs          |
+--------------------+
3 rows in set (0.00 sec)

素敵ですね。

ちなみにテーブルを作るときに engine=ndbcluster を入れ忘れると同期されないので寂しいです。

さて、これでやっと MogileFS をインストールする準備ができました。

長かったですね。当初の目的を忘れるところでしたよ…