土曜日 4 25, 2009

こんにちは。今日はMyISAMの内部動作をトレースする方法をご紹介します。

MyISAMはデバッグ目的に内部動作をログに書き出す機能を持っています。これを使う機会はあまり無いと思いますが、環境の動作分析やパフォーマンスチューニングの参考情報にはなると思います。

[myisam-logの出力方法]
my.cnfもしくはmy.iniファイルの[mysqld]セクションに以下のように設定します。

[mysqld]
log-isam=c:/temp/mysql_isam.log

c:/temp/mysql_isam.logの部分はmyisam動作トレースログを出力したいファイルパスに書き換えて設定を行ってください。設定後、MySQLサーバを再起動します。

早速mysql_isam.logを見てみましょう。myisamlogはバイナリ形式で出力されているので、MySQLに付属している専用の閲覧ツールで内容を確認してみます。

[myisamlogの使用例]

C:\MySQLEntServer5.1\bin>myisamlog -i -vvv c:\temp\mysql_isam.log

-vvvを指定すると最も詳細な情報を出力することができます。-v, -vv, -vvvの三段階で出力レベルを調節できます。

[出力例]

C:\MySQLEntServer5.1\bin>myisamlog -i -vvv c:\temp\mysql_isam.log
        0: 5180 ./mysql/host: open -> 12
       27: 5180 ./mysql/host: extra(13) -> 0
       37: 5180 ./mysql/host: extra(12) -> 0
       47: 5180 ./mysql/host: extra(5) -> 0
       57: 5180 ./mysql/user: open -> 14
       84: 5180 ./mysql/user: extra(13) -> 0
       94: 5180 ./mysql/user: extra(12) -> 0
      104: 5180 ./mysql/user: extra(5) -> 0
      114: 5180 ./mysql/db: open -> 16
      139: 5180 ./mysql/db: extra(13) -> 0
      149: 5180 ./mysql/db: extra(12) -> 0
      159: 5180 ./mysql/db: extra(5) -> 0
      169: 5180 ./mysql/host: extra(3) -> 0
      179: 5180 ./mysql/host: extra(4) -> 0
      189: 5180 ./mysql/user: extra(3) -> 0
      199: 5180 ./mysql/user: extra(4) -> 0
      209: 5180 ./mysql/db: extra(3) -> 0
      219: 5180 ./mysql/db: extra(4) -> 0
      229: 5180 ./mysql/time_zone_leap_second: open -> 18
      273: 5180 ./mysql/time_zone_leap_second: extra(13) -> 0
      283: 5180 ./mysql/time_zone_leap_second: extra(12) -> 0
      293: 5180 ./mysql/time_zone_leap_second: extra(5) -> 0
      303: 5180 ./mysql/time_zone_name: open -> 20
      340: 5180 ./mysql/time_zone_name: extra(13) -> 0
      350: 5180 ./mysql/time_zone_name: extra(12) -> 0
      360: 5180 ./mysql/time_zone_name: extra(5) -> 0
      370: 5180 ./mysql/time_zone: open -> 22
      402: 5180 ./mysql/time_zone: extra(13) -> 0
      412: 5180 ./mysql/time_zone: extra(12) -> 0
      422: 5180 ./mysql/time_zone: extra(5) -> 0
      432: 5180 ./mysql/time_zone_transition_type: open -> 24
      480: 5180 ./mysql/time_zone_transition_type: extra(13) -> 0
      490: 5180 ./mysql/time_zone_transition_type: extra(12) -> 0
      500: 5180 ./mysql/time_zone_transition_type: extra(5) -> 0
      510: 5180 ./mysql/time_zone_transition: open -> 26
      553: 5180 ./mysql/time_zone_transition: extra(13) -> 0
      563: 5180 ./mysql/time_zone_transition: extra(12) -> 0
      573: 5180 ./mysql/time_zone_transition: extra(5) -> 0
      583: 5180 ./mysql/time_zone_transition: close -> 0
      592: 5180 ./mysql/time_zone_transition_type: close -> 0
      601: 5180 ./mysql/time_zone: close -> 0
      610: 5180 ./mysql/time_zone_name: close -> 0
      619: 5180 ./mysql/time_zone_leap_second: close -> 0
      628: 5180 ./mysql/tables_priv: open -> 18
      662: 5180 ./mysql/tables_priv: extra(13) -> 0
      672: 5180 ./mysql/tables_priv: extra(12) -> 0
      682: 5180 ./mysql/tables_priv: extra(5) -> 0
      692: 5180 ./mysql/columns_priv: open -> 20
      727: 5180 ./mysql/columns_priv: extra(13) -> 0
      737: 5180 ./mysql/columns_priv: extra(12) -> 0
      747: 5180 ./mysql/columns_priv: extra(5) -> 0
      757: 5180 ./mysql/procs_priv: open -> 22
      790: 5180 ./mysql/procs_priv: extra(13) -> 0
      800: 5180 ./mysql/procs_priv: extra(12) -> 0
      810: 5180 ./mysql/procs_priv: extra(5) -> 0
      820: 5180 ./mysql/servers: open -> 24
      850: 5180 ./mysql/servers: extra(13) -> 0
      860: 5180 ./mysql/servers: extra(12) -> 0
      870: 5180 ./mysql/servers: extra(5) -> 0
      880: 5180 ./mysql/servers: extra(3) -> 0
      890: 5180 ./mysql/servers: extra(4) -> 0
      900: 5180 ./mysql/func: open -> 26
      927: 5180 ./mysql/func: extra(13) -> 0
      937: 5180 ./mysql/func: extra(12) -> 0
      947: 5180 ./mysql/func: extra(5) -> 0
      957: 5180 ./mysql/func: extra(3) -> 0
      967: 5180 ./mysql/func: extra(4) -> 0
      977: 5180 ./mysql/func: close -> 0
      986: 5180 ./mysql/event: open -> 26
     1014: 5180 ./mysql/event: extra(13) -> 0
     1024: 5180 ./mysql/event: extra(12) -> 0
     1034: 5180 ./mysql/event: extra(5) -> 0
     1044: 5180 ./mysql/event: extra(4) -> 0

Commands   Used count    Errors   Recover errors
open               14         0                0
close               6         0                0
extra              53         0                0
Total              73         0                0

まだ何もテーブル操作を行っていないので、ユーザテーブルは一覧に出て来ていないのがわかります。以下にログの見方を説明します。

isamlogのファイルポジション プロセス番号 ファイル名: 操作ごとのログ

「操作ごとのログ」には例えば以下のようなものがあります。
open -> ファイルオープン数
extra(操作関数番号) -> リターンコード
write at ファイル中の行位置, length=0 -> 行自体に使用したバイト数
update at ファイル中の行位置, length=0 -> 行自体に使用したバイト数
openは、MYDファイルのオープン時、extraはSELECT、updateはUPDATE、writeはINSERT、deleteはDELETE文発行時にログに出力されます。

ここで、試しにtest.testというテーブルに対してアクセスをしてみたいと思います。mysqlクライアントでselectをしてみます。

mysql> use test;
Database changed
mysql> select * from test;
Empty set (0.00 sec)

ログの方はどうなっているでしょうか。

<中略>
      967: 5180 ./mysql/func: extra(4) -> 0
      977: 5180 ./mysql/func: close -> 0
      986: 5180 ./mysql/event: open -> 26
     1014: 5180 ./mysql/event: extra(13) -> 0
     1024: 5180 ./mysql/event: extra(12) -> 0
     1034: 5180 ./mysql/event: extra(5) -> 0
     1044: 5180 ./mysql/event: extra(4) -> 0
     1054: 5180 ./test/test: open -> 28
     1080: 5180 ./test/test: extra(13) -> 0
     1090: 5180 ./test/test: extra(12) -> 0
     1100: 5180 ./test/test: extra(5) -> 0

Commands   Used count    Errors   Recover errors
open               15         0                0
close               6         0                0
extra              56         0                0
Total              77         0                0

testテーブルにアクセスされたという情報がログの最後の部分に出ています。括弧中はselect実行時に使用されたメソッドの種類になります。クエリキャッシュが効いている場合などログにselect操作の結果が出てこないこともあります。

テストのため、テーブルにデータを一行挿入してみます。

mysql> insert into test values(1);

[myisamlog]

     1321:  576 ./test/test: write at 0, length=0 -> 5
     1347:  576 ./test/test: extra(4) -> 0

最初の一行なので、データが0の場所から5バイト書き込まれています。さらにもう一行挿入してみます。

     3384:  576 ./test/test: write at 7, length=0 -> 5
     3410:  576 ./test/test: extra(4) -> 0

今度は7バイト目から5バイト書き込まれました。ちなみにtestの構造は以下のようになっています。Row_formatはFixedとなっています。

mysql> show create table test;
+-------+-------------------------------------
| Table | Create Table
+-------+-------------------------------------
| test  | CREATE TABLE `test` (
  `a` int(11) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8 |
+-------+-------------------------------------
1 row in set (0.00 sec)

int型のaという名前のカラムが1つだけ定義されています。int型はストレージを4バイト使用し、行ヘッダーはこのテーブルの場合1バイト使用します。ヘッダー1バイト + int型ストレージ4バイト=5バイトが 「length=0 -> 5」の意味するところです。さらにMyISAMの場合、最小行バイト数はグローバルサーバ変数myisam_data_pointer_sizeに指定された値+1(削除リンク分)に設定されます。デフォルト値は6ですので、6+1=7バイトが最小バイト数になります。myisam_data_pointer_sizeはCREATE TABLE時に使用されるので、もしこの値を変更する場合はCREATE TABLE前である必要があります。

この操作を行っている部分のソースは以下になります。
mi_create.c

  if (!(options & (HA_OPTION_COMPRESS_RECORD | HA_OPTION_PACK_RECORD)))
  {
    if (reclength <= pointer)
      reclength=pointer+1;        /* reserve place for delete link */
  }

実際にストレージに書き込まれるのは、ヘッダー1バイト + int型ストレージ4バイト+最小7バイトに足りない分を00値でパッディングする2バイト = 7バイトとなります。これはmyisamlogからも観測することができます。何行もデータを挿入してみます。

mysql> delete from test;
Query OK, 4 rows affected (0.00 sec)

mysql> insert into test values(1);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test values(2);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test values(3);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test values(4);
Query OK, 1 row affected (0.00 sec)

mysql> insert into test values(5);
Query OK, 1 row affected (0.00 sec)

myisamlogは以下です。

     8679:  576 ./test/test: re-open -> 0

     8688:  576 ./test/test: write at 0, length=0 -> 5
     8714:  576 ./test/test: extra(4) -> 0
     8724:  576 ./test/test: write at 7, length=0 -> 5
     8750:  576 ./test/test: extra(4) -> 0
     8760:  576 ./test/test: write at 14, length=0 -> 5
     8786:  576 ./test/test: extra(4) -> 0
     8796:  576 ./test/test: write at 21, length=0 -> 5
     8822:  576 ./test/test: extra(4) -> 0
     8832:  576 ./test/test: write at 28, length=0 -> 5
     8858:  576 ./test/test: extra(4) -> 0

書き込みは5バイトで、実際は7バイトずつストレージに書かれていることがわかります。またmyisamlogを見れば、データが実際にどの位置に挿入されたかがわかります。MyISAMでは行の削除はレコードヘッダに削除フラグが立てられるだけというのは以前書きましたが、実際にmyisamlogで挙動を確認することが可能です。試しにaの値が3(テーブルの中ごろに入っている)を削除してから新しいレコードをインサートしてみましょう。

mysql> delete  from test where a = 3;
Query OK, 1 row affected (0.00 sec)

mysql> insert into test values(6);
Query OK, 1 row affected (0.00 sec)

myisamlogには以下が出力されています。

    8952:  576 ./test/test: delete at 14 -> 0
    8969:  576 ./test/test: extra(4) -> 0
    8979:  576 ./test/test: write at 14, length=0 -> 5
    9005:  576 ./test/test: extra(4) -> 0

一度削除された部分に対してインサートが行われているのが確認できます。

[オマケ]
myisamlogユーティリティには、出力されたmyisamログからMyISAMデータファイル(MYD)を再構築する機能もあります(使うかどうかはまた別問題として)。以下の手順で行います。

[ログの最初から最後までを再適用して、テーブルデータをリカバリする場合]

最初に、データを全部消してみます。

mysql> delete from test;
Query OK, 15 rows affected (0.02 sec)

バッファ内容をディスクにFlushします。

mysql> flush tables;
Query OK, 0 rows affected (0.00 sec)

以下のコマンドで、myisamlogからリカバリを行います。

C:\MySQLEntData\data>c:\mysqlentserver5.1\bin\myisamlog -i -r -vvv c:\temp2\mysql_isam.log ./test/test

バッファをクリアします。

mysql> flush tables;
Query OK, 0 rows affected (0.00 sec)

テーブルを見ると、ログからリプレイされデータが復活しているのがわかります。

mysql> select * from test;
+------+
| a    |
+------+
|    1 |
|    2 |
|    6 |
|    4 |
|    5 |
+------+
5 rows in set (0.00 sec)

-oオプションを使うと、特定Log番号以降のログのみをリプレイに使うこともできます。(deleteもログに記録されているので削除される前のログをどこかに退避しないと、再び全行消えます 。。)他にもいくつかオプションがあるので試して見るのも趣がありますね。まあ、公式にはバイナリログを使うべきだと思いますのであくまで情報収集やデバッグ目的で行う方が良いと思います。

ちなみに、myisamlog -?とやると簡単な使用方法がプリントされるのですが、By Montyっていうあたり、ウケました。。。

C:\MySQLEntServer5.1\bin>myisamlog -?
myisamlog  Ver 1.4 for Win32 at ia32
By Monty, for your professional use

水曜日 4 15, 2009

こんにちは。今回はオープンソースのメッセージキューであるOpen Message Queue(OpenMQ)のHAストアにMySQL Clusterを使う方法について書いてみたいと思います。(Connector/J設定の参考にもなるかと思います)

OpenMQはオープンソースアプリケーションサーバGlassFishのサブプロジェクトとして進められているMQを開発するプロジェクトです。エンタープライズシステム・コンポーネントであるOpenESBや、GlassFish Enterprise Serverなどのコアモジュールとして使用されている大変重要なソフトウエアとなっています。疎結合化システムを構築するためには無くてはならない存在で、システムによっては非常に高い可用性を求められる部分です。Myはうす(まい・はうす改め)ではOpenMQについての色々なトピックを書いて来ましたがいよいよ本命の登場です。

OpenMQのHAクラスタについてはこちらで説明していますのでよろしければご覧ください。

以下のトピックでは、OpenMQのクラスタでSunのHADBを使用する手順を書いています。
HADBを使用したHAクラスタ構成その1
HADBを使用したHAクラスタ構成その2

[4/17日Update]
奇しくも、4/16日にOpenMQクラスタについてのWebcastがリリースされました。brokerとproducer,consumer,永続ストレージとメッセージデリバリの関係がすごくわかりやすいです。Cluster構成にした場合の動きなどが特に参考になりますね(動画です!!)http://www.sun.com/offers/details/message_queue_ha_clusters.html

MySQL Clusterを使用した設定を行う場合も、HADBの時と流れは一緒ですので参考にしてください。
アーキテクチャ図を一枚引用します。

MQ4.1HAクラスタ・アーキテクチャとなっていますが、基本的にMQ4.3HAクラスタは4.1のものと同一のアーキテクチャとなっています。複数のMQのブローカがクラスタを構成し、同一のシェアドHAストアを利用します。少しおさらいしてみたいと思います。MySQL Clusterとの設定方法だけを見たい方は飛ばしてください。

[ホームブローカ]

MQのクライアント(JMSクライアントなど)が最初に通信を行ったブローカのことを「ホームブローカ」と呼びます。JMSクライアントのロードバランス機能などにより各クライアントのホームブローカはバラバラになります。OpenMQのHAクラスタでは従来の「コンベンショナルクラスタ」と同様に、どのブローカからもメッセージを取り出すことができる仕組みとなっています。(コンベンショナルでは、永続化メッセージは各ブローカがローカルに持っています)

例えばbroker1に対してsendしたメッセージはbroker2からも同様にreceiveすることが可能です。ただしOpenMQのHAクラスタでは単純に同一のデータ・セッション・コネクションを全brokerが共有する形にはなっていません。ホームブローカでないブローカ経由でメッセージがリクエストされた場合、ブローカがHAストアから他のブローカが管理しているメッセージを探して、クライアントに返す形となります。
#JMSのBrowserでキューの中を覗いた場合ホームブローカ以外にはメッセージがないように見えます。これは覗いた時点ではメッセージがそのブローカに所属していないためです。

[ホームブローカがダウンした場合]

ホームブローカがダウンした場合、上記のような構造となっているのでそのままでは他のブローカからのメッセージリクエストに応えられません。broker間では事前定義された間隔でお互いにハートビートを確認しています。ハートビートチェックがタイムアウトしそのブローカがダウンしていると判断された場合には、ブローカが管理している資源(セッション、トランザクション、メッセージ)のフェイルオーバが発生します。ダウンしたブローカが持っていた資源をHAストアから引継ぎ先にテイクオーバします。ブローカリストを持ったクライアント内でブローカへの接続も自動でフェイルオーバしますので、クライアントはMQのダウンを関知する必要がありません。

[全体としてHAにするための考慮点]

HAクラスタ・アーキテクチャ図の黄色枠内については、各モジュールがHA化されている必要があります。さもなければそこが単一障害点(Single Point of Failure)となってしまいます。冗長化するポイントは以下です。

・OpenMQブローカインスタンス
・HAストア

ブローカインスタンスについては、ブローカインスタンスを複数並行に並べることで解決可能です。ブローカインスタンスを複数並べ、全体として1つの仮想インスタンスとして取り扱うことをMQクラスタと呼んでいます。HAストアについてはMySQLを使用する場合、単体のインスタンスではMySQLサーバプロセスがダウンした場合そこが単一障害点になります。今回はMySQL Clusterを使用します。(単体のMySQLインスタンスも使用可能です)

[HAストアに使用するMySQL Clusterの考慮点]

・ 複数のデータノードを起動
・ 複数のSQLノードを起動
・ 各ブローカからSQLノードに分散/フェイルオーバするように設定

今回MySQL Clusterは説明のため最小構成で設定を行います。(本番適用時には要件に応じて適宜構成設計を行ってください)今回使用する環境のndb_mgmでの構成リストを以下に載せます。
使用するOpenMQのバージョンはここからダウンロードできる4.3を使用しました。

-- NDB Cluster -- Management Client --
ndb_mgm> show
Connected to Management Server at: 192.168.70.128:1186
Cluster Configuration
---------------------
[ndbd(NDB)]     2 node(s)
id=2    @192.168.70.128  (mysql-5.1.30 ndb-6.3.20, Nodegroup: 0, Master)
id=3    @192.168.70.128  (mysql-5.1.30 ndb-6.3.20, Nodegroup: 0)

[ndb_mgmd(MGM)] 1 node(s)
id=1    @192.168.70.128  (mysql-5.1.30 ndb-6.3.20)

[mysqld(API)]   2 node(s)
id=4    @192.168.70.128  (mysql-5.1.30 ndb-6.3.20)
id=5    @192.168.70.128  (mysql-5.1.30 ndb-6.3.20)

[OpenMQ Cluster with MySQL Cluster設定方法]

1.MySQL Clusterを起動し、mysqlクライアントでSQLノードのどちらかに接続してopenmqデータベースを作成します。

Server version: 5.1.30-ndb-6.3.20-cluster-gpl MySQL Cluster Server (GPL)

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> create database openmq character set utf8;
Query OK, 1 row affected (0.23 sec)

MQで大きなサイズのメッセージを扱う予定の場合、MySQLの以下のパラメータを設定しておきます。最大サイズによって値を調整してください。

config.ini
[tcp default]
SendBufferMemory=2M
ReceiveBufferMemory=1M

この値を設定しておかないと、JMSクライアントの仕様によってはNDB-APIエラー(SEND BUFFERS OVERLOADED IN NDB KERNEL)となり送信できないことがあります(ストリームバッファを多めに取っている場合など)。デフォルト値はそれぞれ256k,64kです。

my.cnf
[mysqld]
max_allowed_packet = 16M

なお、何も指定していないとデフォルト1Mとなります(最大指定可能は1GB,それ以上の数字を設定しても1GBになる)。それ以上大きなメッセージを保存しようとすると以下のようなエラーとなってしまいます。これより大きなサイズのメッセージを扱いたい場合はByteMessageを使って複数のメッセージに分割する方法もあります。

[15/4/2009:23:01:46 JST] エラー [B2011]: 16-192.168.11.9(f8:22:94:e5:d3:14)-3584-1239778905890 からの JMS メッセージの保存に失敗しました:
com.sun.messaging.jmq.jmsserver.util.BrokerException: [B4004]: メッセージ 16-192.168.11.9(f8:22:94:e5:d3:14)-3584-1239778905890 を維持できませんでした
        at com.sun.messaging.jmq.jmsserver.persist.jdbc.MessageDAOImpl.insert(MessageDAOImpl.java:416)
        at com.sun.messaging.jmq.jmsserver.persist.jdbc.MessageDAOImpl.insert(MessageDAOImpl.java:340)
        at com.sun.messaging.jmq.jmsserver.persist.jdbc.JDBCStore.storeMessage(JDBCStore.java:239)
        at com.sun.messaging.jmq.jmsserver.persist.jdbc.JDBCStore.storeMessage(JDBCStore.java:201)
        at com.sun.messaging.jmq.jmsserver.core.PacketReference.store(PacketReference.java:1177)
        at com.sun.messaging.jmq.jmsserver.core.Queue.routeNewMessage(Queue.java:542)
        at com.sun.messaging.jmq.jmsserver.data.handlers.DataHandler.routeMessage(DataHandler.java:462)
        at com.sun.messaging.jmq.jmsserver.data.handlers.DataHandler.handle(DataHandler.java:244)
        at com.sun.messaging.jmq.jmsserver.data.handlers.DataHandler.handle(DataHandler.java:97)
        at com.sun.messaging.jmq.jmsserver.data.PacketRouter.handleMessage(PacketRouter.java:181)
        at com.sun.messaging.jmq.jmsserver.service.imq.IMQIPConnection.readData(IMQIPConnection.java:1489)
        at com.sun.messaging.jmq.jmsserver.service.imq.IMQIPConnection.process(IMQIPConnection.java:644)
        at com.sun.messaging.jmq.jmsserver.service.imq.OperationRunnable.process(OperationRunnable.java:170)
        at com.sun.messaging.jmq.jmsserver.util.pool.BasicRunnable.run(BasicRunnable.java:493)
        at java.lang.Thread.run(Thread.java:619)
Caused by: java.sql.SQLException: [INSERT INTO MQMSG41Cmqhamysqlcluster ( ID, MESSAGE_SIZE, STORE_SESSION_ID, DESTINATION_ID, TRANSACTION_ID, CREATED_TS, MESSAGE) VALUES ( ?, ?, ?, ?, ?, ?, ? )]: Packet for query is too large (2713149 > 1048576). You can change this value on the server by setting the max_allowed_packet' variable.
        at com.sun.messaging.jmq.jmsserver.persist.jdbc.DBManager.wrapSQLException(DBManager.java:863)
        at com.sun.messaging.jmq.jmsserver.persist.jdbc.MessageDAOImpl.insert(MessageDAOImpl.java:411)
        ... 14 more



また、OpenMQ自体にも取り扱えるメッセージの最大バイト数の設定がありますのでそれらの値も調整します。-1は無制限を意味します。OpenMQ側で制限をかけたい場合はこの値を正の数値に設定します。maxBytesPerMsgとmaxTotalMsgBytesのデフォルトは10MB、imq.message.max_sizeは70MBとなっています。
imq.autocreate.destination.maxBytesPerMsg=-1
imq.autocreate.destination.maxTotalMsgBytes=-1
imq.message.max_size=-1
※autocreate宛先の場合、一度作成されてしまうと上の値の設定を行っても反映しないので注意してください。その場合は一度宛先を再作成すると新しい値が反映します。

2.MySQL Connector/J(MySQL用JDBCドライバ)をここからダウンロードします。ダウンロードしたzipファイルを解凍すると、mysql-connector-java-5.0.8-bin.jarという名前のJARが出てきますので、これをMQ_HOME\lib\extフォルダにコピーしておきます。
※OpenMQではConnector/J 5.1ではなく、Connector/J 5.0を使用します。5.1をHAストアとして利用すると以下のようになってしまい、ブローカを起動することが出来ません。

com.sun.messaging.jmq.jmsserver.util.BrokerException: [B4221]: ストアからすべての HA クラスタ情報を読み込めませんでした
        at com.sun.messaging.jmq.jmsserver.persist.jdbc.BrokerDAOImpl.getAllBrokerInfos(BrokerDAOImpl.java:1077)
        at com.sun.messaging.jmq.jmsserver.persist.jdbc.JDBCStore.getAllBrokerInfos(JDBCStore.java:2764)
        at com.sun.messaging.jmq.jmsserver.persist.Store.getAllBrokerInfos(Store.java:1212)
        at com.sun.messaging.jmq.jmsserver.cluster.ha.HAClusterManagerImpl$HAMap.updateHAMapForState(HAClusterManagerImpl.java:1498)
        at com.sun.messaging.jmq.jmsserver.cluster.ha.HAClusterManagerImpl$HAMap.updateHAMap(HAClusterManagerImpl.java:1485)
        at com.sun.messaging.jmq.jmsserver.cluster.ha.HAClusterManagerImpl.getKnownBrokers(HAClusterManagerImpl.java:486)
        at com.sun.messaging.jmq.jmsserver.cluster.ha.HAClusterManagerImpl.getConfigBrokers(HAClusterManagerImpl.java:511)
        at com.sun.messaging.jmq.jmsserver.service.HAMonitorService.<init>(HAMonitorService.java:437)
        at com.sun.messaging.jmq.jmsserver.Broker._start(Broker.java:869)

これは以下のConnector/Jの問題のためです。(これが解決すれば利用可能だと思います)
http://bugs.mysql.com/bug.php?id=41484

3.broker1とbroker2を以下のコマンドでスタートし、初期設定ファイルを生成させます。

imqbrokerd -name broker1 -port 7676

imqbrokerd -name broker2 -port 7677 

4. MQ_VARHOME/instances/以下のbroker1ディレクトリに、broker2ディレクトリが生成されています。その直下にpropsというディレクトリがありそこにインスタンス用コンフィグファイルが入っています。
これを以下のように編集します。

broker1のconfig.properties

imq.instanceconfig.version=300
imq.brokerid=broker1
imq.cluster.ha=true
imq.cluster.clusterid=mqhamysqlcluster
imq.persist.store=jdbc
imq.persist.jdbc.dbVendor=mysql
imq.persist.jdbc.mysql.property.url=jdbc:mysql://192.168.70.128:3308,192.168.70.128:3307/openmq
imq.persist.jdbc.mysql.user=root
imq.persist.jdbc.mysql.needpassword=false
imq.persist.jdbc.mysql.property.autoReconnect=true
imq.persist.jdbc.mysql.property.failOverReadOnly=false
imq.persist.jdbc.mysql.property.autoReconnectForPools=true
imq.persist.jdbc.mysql.property.maxReconnects=1
imq.persist.jdbc.mysql.property.roundRobinLoadBalance=true
imq.persist.jdbc.mysql.tableoption=ENGINE=NDBCLUSTER
imq.persist.jdbc.connection.limit=15
imq.autocreate.destination.maxBytesPerMsg=-1
imq.autocreate.destination.maxTotalMsgBytes=-1
imq.message.max_size=-1

broker2のconfig.properties

imq.instanceconfig.version=300
imq.brokerid=broker2
imq.cluster.ha=true
imq.cluster.clusterid=mqhamysqlcluster
imq.persist.store=jdbc
imq.persist.jdbc.dbVendor=mysql
imq.persist.jdbc.mysql.property.url=jdbc:mysql://192.168.70.128:3307,192.168.70.128:3308/openmq
imq.persist.jdbc.mysql.user=root
imq.persist.jdbc.mysql.needpassword=false
imq.persist.jdbc.mysql.property.autoReconnect=true
imq.persist.jdbc.mysql.property.failOverReadOnly=false
imq.persist.jdbc.mysql.property.autoReconnectForPools=true
imq.persist.jdbc.mysql.property.maxReconnects=1
imq.persist.jdbc.mysql.property.roundRobinLoadBalance=true
imq.persist.jdbc.mysql.tableoption=ENGINE=NDBCLUSTER
imq.persist.jdbc.connection.limit=15
imq.autocreate.destination.maxBytesPerMsg=-1
imq.autocreate.destination.maxTotalMsgBytes=-1
imq.message.max_size=-1

重要なプロパティについて補足説明したいと思います。

imq.brokerid=broker2
ブローカごとにユニークにする必要があります。
ブローカ名と同じに設定すると良いでしょう。

imq.cluster.ha=true
imq.cluster.clusterid=mqhamysqlcluster
OpenMQをHAクラスタで稼動させるために必須となります。同じクラスタ内に入るブローカは全て同じ値を指定する必要があります。

imq.persist.jdbc.mysql.property.url (Connector/J属性:url)
urlでは接続対象のホスト:ポート番号をカンマ区切りで書きます。
Connector/Jの仕様ではurlに?prop=value&prop=valueという書式が可能ですが、ブローカ設定では使用できないので注意してください(無視されます)。その代わりに、imq.persist.jdbc.mysql.property.+プロパティ名=値と書けばその値を反映することが出来ます。Connector/JをAPサーバなどから使う場合はプロパティ名=値のみを設定すればOKです。

imq.persist.jdbc.mysql.property.autoReconnect=true (Connector/J属性:autoReconnect)
imq.persist.jdbc.mysql.property.failOverReadOnly=false (Connector/J属性:failOverReadOnly)
フェイルオーバのための設定です。特にfailOverReadOnly=falseを忘れるとフェイルオーバ後のMySQLサーバに対して一切更新SQLが使えなくなりますので注意が必要です。

imq.persist.jdbc.mysql.property.autoReconnectForPools=true (Connector/J属性:autoReconnectForPools)
接続プールを使用している場合に追加すべきプロパティです。これがtrueとなっているとプールから取得した接続を使用しSQLを実行する時に、その接続が前回SQLクエリ実行に失敗している場合接続テスト(内部PING)が行われます。pingが返ってこない場合、接続が内部的に更新(リコネクト)されます。

imq.persist.jdbc.mysql.property.maxReconnects=1 (Connector/J属性:maxReconnects)
ホストリストの次のホストにFailOverする前に試行する再接続回数です。

imq.persist.jdbc.mysql.property.roundRobinLoadBalance=true (Connector/J属性:roundRobinLoadBalance)
ホストリストに対してラウンドロビン方式でアクセスを行います。

imq.persist.jdbc.mysql.tableoption=ENGINE=NDBCLUSTER
OpenMQのHAストアテーブルをNDBCLUSTERストレージエンジンでCREATEします。
MySQL ClusterをHAストアに設定する場合にMUSTな定義です。

imq.persist.jdbc.connection.limit=15
OpenMQでは、MySQLにアクセスを行うために内部的に次のクラスを使用しています。
・com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource
ブローカ起動時にここで設定した数の接続が作成され、接続プールに保持されます。
デフォルトは5です。mysqlクライアントのshow processlistコマンドでブローカからいくつの接続が張られているかを確認可能です。

mysql> show processlist;
+-----+-------------+-------------------+--------+---------+------+-----------------------------------+------------------+
| Id  | User        | Host              | db     | Command | Time | State                             | Info             |
+-----+-------------+-------------------+--------+---------+------+-----------------------------------+------------------+
|   1 | system user |                   |        | Daemon  |    0 | Waiting for event from ndbcluster | NULL             |
| 243 | root        | localhost         | NULL   | Query   |    0 | NULL                              | show processlist |
| 412 | root        | 192.168.70.1:3255 | openmq | Sleep   |   54 |                                   | NULL             |
| 423 | root        | 192.168.70.1:3312 | openmq | Sleep   |   64 |                                   | NULL             |
| 424 | root        | 192.168.70.1:3316 | openmq | Sleep   |   59 |                                   | NULL             |
| 425 | root        | 192.168.70.1:3320 | openmq | Sleep   |   54 |                                   | NULL             |
| 426 | root        | 192.168.70.1:3324 | openmq | Sleep   |   48 |                                   | NULL             |
| 427 | root        | 192.168.70.1:3328 | openmq | Sleep   |   43 |                                   | NULL             |
| 428 | root        | 192.168.70.1:3334 | openmq | Sleep   |   38 |                                   | NULL             |
| 429 | root        | 192.168.70.1:3339 | openmq | Sleep   |   32 |                                   | NULL             |
| 430 | root        | 192.168.70.1:3343 | openmq | Sleep   |   27 |                                   | NULL             |
| 431 | root        | 192.168.70.1:3348 | openmq | Sleep   |   22 |                                   | NULL             |
| 432 | root        | 192.168.70.1:3352 | openmq | Sleep   |   16 |                                   | NULL             |
| 433 | root        | 192.168.70.1:3361 | openmq | Sleep   |   11 |                                   | NULL             |
| 434 | root        | 192.168.70.1:3366 | openmq | Sleep   |    6 |                                   | NULL             |
| 435 | root        | 192.168.70.1:3374 | openmq | Sleep   |    0 |                                   | NULL             |
+-----+-------------+-------------------+--------+---------+------+-----------------------------------+------------------+
limit - 1の数が用意されます。

5. broker1とbroker2を停止します。初期ユーザIDとパスワードはadmin/adminとなります。

C:\OpenMQ\mq\bin>imqcmd shutdown bkr -b localhost:7676
ユーザ名: admin
パスワード: admin
指定されたブローカの停止中:

---------------------
ホスト          プライマリポート
---------------------
localhost    7676

このブローカを停止してもよろしいですか ? (y/n)[n] y

localhost:7676 でのブローカの停止を待機しています...
ブローカの停止に成功しました。


C:\OpenMQ\mq\bin>imqcmd shutdown bkr -b localhost:7677
ユーザ名: admin
パスワード: admin
指定されたブローカの停止中:

---------------------
ホスト          プライマリポート
---------------------
localhost    7677

このブローカを停止してもよろしいですか ? (y/n)[n] y

localhost:7677 でのブローカの停止を待機しています...
ブローカの停止に成功しました。

6. broker1とbroker2を起動します。

C:\OpenMQ\mq\bin>imqbrokerd -name broker1 -port 7676
[14/4/2009:21:26:03 JST]
==================================================================
Open Message Queue 4.3
Sun Microsystems, Inc.
バージョン:  4.3  (Build 7-g)
コンパイル:  Fri 11/07/2008

Copyright (c) 2007 Sun Microsystems, Inc.  All rights reserved.  Use is
subject to license terms.
==================================================================
Java ランタイム: 1.6.0_12 Sun Microsystems Inc. C:\jdk1.6.0_12\jre
[14/4/2009:21:26:03 JST]    IMQ_HOME=C:\OpenMQ\mq
[14/4/2009:21:26:03 JST] IMQ_VARHOME=C:\OpenMQ\var\mq
[14/4/2009:21:26:03 JST] Windows XP 5.1 x86 PSQ96040 (2 cpu) takemura
[14/4/2009:21:26:03 JST] Java ヒープサイズ: 最大=189376k、現在=18176k
[14/4/2009:21:26:03 JST] 引数: -name broker1 -port 7676
[14/4/2009:21:26:03 JST] [B1060]: 持続データの読み込み中...
[14/4/2009:21:26:05 JST] プラグイン持続ストアの使用:
        version=410
        brokerid=broker1
        database connection url=jdbc:mysql://192.168.70.128:3307,192.168.70.128:3308/openmq
        database user=root
[14/4/2009:21:26:05 JST] 警告 [B4239]: データベーステーブル MQVER41Cmqhamysqlcluster から持続ストアバージョンを読み込めませんでした:
java.sql.SQLException: [SELECT STORE_VERSION FROM MQVER41Cmqhamysqlcluster]: Table 'openmq.MQVER41Cmqhamysqlcluster' doesn't exist
[14/4/2009:21:26:05 JST] [B1114]: 持続的ストアが新規に作成されます...
[14/4/2009:21:26:05 JST] データベーステーブル MQVER41Cmqhamysqlcluster を作成しています...
[14/4/2009:21:26:06 JST] データベーステーブル MQBKR41Cmqhamysqlcluster を作成しています...
[14/4/2009:21:26:07 JST] データベーステーブル MQSES41Cmqhamysqlcluster を作成しています...
[14/4/2009:21:26:08 JST] データベーステーブル MQPROP41Cmqhamysqlcluster を作成しています...
[14/4/2009:21:26:10 JST] データベーステーブル MQCREC41Cmqhamysqlcluster を作成しています...
[14/4/2009:21:26:11 JST] データベーステーブル MQCON41Cmqhamysqlcluster を作成しています...
[14/4/2009:21:26:13 JST] データベーステーブル MQCONSTATE41Cmqhamysqlcluster を作成しています...
[14/4/2009:21:26:14 JST] データベーステーブルのインデックス MQCONSTATE41CmqhamysqlclusterIDX1 を作成しています...
[14/4/2009:21:26:16 JST] データベーステーブル MQDST41Cmqhamysqlcluster を作成しています...
[14/4/2009:21:26:18 JST] データベーステーブルのインデックス MQDST41CmqhamysqlclusterIDX1 を作成しています...
[14/4/2009:21:26:19 JST] データベーステーブル MQMSG41Cmqhamysqlcluster を作成しています...
[14/4/2009:21:26:21 JST] データベーステーブルのインデックス MQMSG41CmqhamysqlclusterIDX1 を作成しています...
[14/4/2009:21:26:22 JST] データベーステーブル MQTXN41Cmqhamysqlcluster を作成しています...
[14/4/2009:21:26:25 JST] データベーステーブルのインデックス MQTXN41CmqhamysqlclusterIDX1 を作成しています...
[14/4/2009:21:26:29 JST] [B1039]: ブローカ "broker1@PSQ96040:7676" の準備ができました。
(broker2の起動ログは省略します)
初回起動時に関連するテーブルが存在しなければ自動的に作成されます。

MySQLテーブルは以下のようになります。

mysql> show table status;
+-------------------------------+------------+---------
| Name                          | Engine     | Version
+-------------------------------+------------+---------
| MQBKR41Cmqhamysqlcluster      | ndbcluster |      10
| MQCON41Cmqhamysqlcluster      | ndbcluster |      10
| MQCONSTATE41Cmqhamysqlcluster | ndbcluster |      10
| MQCREC41Cmqhamysqlcluster     | ndbcluster |      10
| MQDST41Cmqhamysqlcluster      | ndbcluster |      10
| MQMSG41Cmqhamysqlcluster      | ndbcluster |      10
| MQPROP41Cmqhamysqlcluster     | ndbcluster |      10
| MQSES41Cmqhamysqlcluster      | ndbcluster |      10
| MQTXN41Cmqhamysqlcluster      | ndbcluster |      10
| MQVER41Cmqhamysqlcluster      | ndbcluster |      10
+-------------------------------+------------+---------

7. Broker起動状態の確認 (imqcmd list bkr)

C:\OpenMQ\mq\bin>imqcmd list bkr
ユーザ名: admin
パスワード: admin
次のブローカが所属するクラスタ内のすべてのブローカをリストします:

---------------------
ホスト          プライマリポート
---------------------
localhost    7676

クラスタ ID       mqhamysqlcluster
クラスタは高可用性です   true

--------------------------------------------------------------------------
                                                    ブローカの ID     前回からの時間
ブローカ ID       アドレス           状態       ストア内のメッセージ   継承を実行しています   状態のタイムスタンプ
--------------------------------------------------------------------------
broker2   PSQ96040:7677   OPERATING   0                         5 minutes
broker1   PSQ96040:7676   OPERATING   0                         5 minutes

クラスタ内のブローカのリストに成功しました。

8.送受信のテスト
QK2もしくはQBrowserV2などでメッセージの送受信テストを行います。
Myはうすオリジナルツールの使い方は以下をご覧ください。
送受信のテスト
QBrowserV2(QK2の後継ツール)

QBrowserV2を使ってテストしてみます。接続先のポート番号を変更する場合は.batファイルの最後の部分を変更してください。
新規メッセージ→新規メッセージ作成を選択します。

宛先名をTESTにし、Message bodyに「broker1へ送ったメッセージ」と入力し送信ボタンを押します。

broker1(localhost:7676)にメッセージが入っていることが確認できます。

もう1つQBrowserV2を起動してbroker2(localhost:7677)に接続します。QUEUE名にTESTと入力してBrowseボタンを押しても何も出てこないことを確認します。[宛先コマンド]→[全宛先情報リスト]またはimqcmd list dst -b localhost:7677 -t q -n TESTコマンドを実行してキューの中を見てみます。

0になっていることが確認できます。ここで、broker1を強制シャットダウンし情報をbroker2に引き継がせてみたいと思います。broker1をシャットダウンするとすぐにbroker2がダウンを検知します。

[15/4/2009:22:23:56 JST] [B1214]: ブローカ broker1 が応答しません。継承を試みています
[15/4/2009:22:23:56 JST] brokerStatusChanged broker1:
        BrokerStatus[LINK_DOWN:IN_DOUBT:UP]
        BrokerStatus[LINK_DOWN:NOT_INDOUBT:DOWN]
        null
[15/4/2009:22:23:56 JST] [B1183]: Unsuspect broker1
[15/4/2009:22:23:56 JST] [B1184]: Unsuspected /192.168.11.9:7676 [brokerID=broker1, brokerSession=8722663960158640128] (seq#=0, ts=1239774656921, i
[15/4/2009:22:23:56 JST] [B1186]: broker1 へのハートビートを停止します
[15/4/2009:22:23:56 JST] [B1182]: ハートビートエンドポイント /192.168.11.9:7676 [brokerID=broker1, brokerSession=8722663960158640128] (seq#=0, ts=1
[15/4/2009:22:23:56 JST] [B1216]: ブローカ broker1 のストアの継承を試みています
[15/4/2009:22:23:56 JST] [B1175]: このブローカはブローカ [brokerID=broker1, storeSession=8722663960158640128] を継承しようとしています
[15/4/2009:22:23:56 JST] [B1168]: 障害の発生したブローカ broker1 の継承ロックが取得されました (タイムスタンプ: 1239774656921)
[15/4/2009:22:23:56 JST] state = FAILOVER_PENDING broker1
[15/4/2009:22:23:56 JST] state = FAILOVER_STARTED broker1
[15/4/2009:22:23:57 JST] ブローカ broker1 から 0 個のローカル送信先を継承中です: []
[15/4/2009:22:23:57 JST] ブローカ broker1 から 1 個のメッセージを継承中です
[15/4/2009:22:23:57 JST] ブローカ broker1 から 0 個のトランザクションを継承中です
[15/4/2009:22:23:57 JST] ブローカ broker1 から 0 個のリモートトランザクションを継承中です
[15/4/2009:22:23:57 JST] ブローカ broker1 から 3 個のストアセッションを継承中です: [7125574942207197952, 8722663960158640128, 2586509467726251264]
[15/4/2009:22:23:57 JST] state = FAILOVER_COMPLETE broker1
[15/4/2009:22:23:57 JST] [B1217]: ブローカ broker1 の継承が成功しました。そのブローカが所有するデータと状態情報を処理しています
[15/4/2009:22:23:57 JST] [B1218]: ブローカ broker1 からの 0 トランザクションを処理しています
[15/4/2009:22:23:57 JST] [B1297]: ブローカ broker1 から 0 個のリモートトランザクションを処理しています
[15/4/2009:22:23:57 JST] monitoring 0 transactions
[15/4/2009:22:23:57 JST] [B1283]: 継承後に 0 のトランザクションを処理しています
[15/4/2009:22:23:57 JST] [B1221]: ブローカ broker1 からの 1 メッセージを処理しています
[15/4/2009:22:23:57 JST] Loading message 23-192.168.11.9(ff:e4:53:af:d7:4f)-2513-1239774044828 on Q:TEST
[15/4/2009:22:23:57 JST] attempting to set Message Bytes Limit to 10485760K for destination TEST [キュー]
[15/4/2009:22:23:57 JST] attempting to set Message Count Limit to 100000 for destination TEST [キュー]
[15/4/2009:22:23:57 JST] attempting to set Message Size Limit to 10240K for destination TEST [キュー]
[15/4/2009:22:23:57 JST] Loading Stored destination TEST [キュー] connectionUID=null
[15/4/2009:22:23:57 JST] [B1151]: 1 メッセージとともに送信先 TEST [キュー] を読み込み
[15/4/2009:22:23:57 JST] [B1152]: 送信先 TEST [キュー] の読み込み完了
[15/4/2009:22:23:57 JST] [B1196]: 送信先 TEST [キュー] で 1 メッセージを処理します
[15/4/2009:22:23:57 JST]  Message 23-192.168.11.9(ff:e4:53:af:d7:4f)-2513-1239774044828 has 1 consumers
[15/4/2009:22:23:57 JST] loadTakeoverMsgs: lookup 23-192.168.11.9(ff:e4:53:af:d7:4f)-2513-1239774044828:[consumer:0, type=NONE] found tid=null
[15/4/2009:22:23:57 JST] non-transacted: Routing Message 23-192.168.11.9(ff:e4:53:af:d7:4f)-2513-1239774044828 to 1 consumers:[consumer:0, type=NON
[15/4/2009:22:23:57 JST] [B1223]: broker1 の継承処理が完了しました
[15/4/2009:22:23:57 JST] [B1177]: ブローカ [brokerID=broker1, storeSession=8722663960158640128] のブロードキャスト継承が完了しました...
[15/4/2009:22:23:57 JST] Processing G_TAKEOVER_COMPLETE(41)

broker2のTESTキューを見てみるときちんとメッセージが継承されて来ているのがわかります。

上記はあくまでメッセージの継承動作の説明のためのものです。broker1をわざわざdownさせてbroker2へ継承させずともJMSクライアントは普通にbroker1のメッセージをbroker2からレシーブすることが可能です。以下のようなプログラムを作成して試してみます。プログラムを実行する前にbroker1(localhost:7676)に対してメッセージを送っておきます。(broker1,broker2共に生きている状態です)

com.sun.messaging.ConnectionFactory conFactory = new com.sun.messaging.ConnectionFactory();
conFactory.setProperty(ConnectionConfiguration.imqAddressList, "localhost:7677");

javax.jms.QueueConnection tcon = conFactory.createQueueConnection("admin","admin");
tsession = tcon.createQueueSession(false,QueueSession.AUTO_ACKNOWLEDGE);
tcon.start();

javax.jms.Queue queue = tsession.createQueue("TEST");
MessageConsumer receiver = tsession.createConsumer(queue);
TextMessage tmessage = (TextMessage)receiver.receive();

receiver.close();
tsession.close();
tcon.close();

[結果]
broker2へ接続してもbroker1のメッセージが取得できました。

broker1に送ったメッセージですよ。

9. MySQL ClusterのSQLノードがダウンした場合
SQLノードは2重化してありますので、MQの稼動に問題は生じません。(Connector/Jのフェイルオーバ機能で活動中のSQLノードに自動でコネクトされます。フェイルオーバ中にアクセスしてきたJMSクライアントには例外が返されます。これはアプリ側で処理する必要があります。設定によってフェイルオーバ完了までに時間の差が生じますので必要に応じて調整を行ってください。以下のメッセージがブローカログに出てくればSQLノードのフェイルオーバが完了しています。

jdbc:mysql://192.168.70.128:3308,192.168.70.128:3307/openmq へのデータベース接続が再確立されました。

MySQL ClusterやOpen MQに関するご相談はSUN-PSまでお寄せください。設計、構築、アプリ開発、技術支援などに既に多数のお客様よりご好評いただいております。MySQLメニューについてはこちらをご覧ください。

土曜日 4 11, 2009

こんにちは。今回はMyISAMでのCOUNT()関数について書いてみたいと思います。

COUNT()は皆さんご存知のように、テーブル中の行数を数える関数です。
MyISAMではテーブル全件を数えるCOUNT(*)関数の実行が非常に速いというのが巷の噂ですが、実際のところはどうなのでしょうか?ちょっと見てみたいと思います。

まずは比較のため、innodbのテーブルでカウントしてみたいと思います。
少しは件数の入ったテーブルが良いので、MySQLのサンプルとして提供されている
WorldデータベースのCityテーブルを使ってみました。

[InnoDB結果]

全件

mysql> alter table city engine=innodb;
Query OK, 4079 rows affected (0.27 sec)
Records: 4079  Duplicates: 0  Warnings: 0

mysql> flush tables;
Query OK, 0 rows affected (0.00 sec)

mysql> select count(*) from city;
+----------+
| count(*) |
+----------+
|     4079 |
+----------+
1 row in set (0.02 sec)

mysql> show profile;
+--------------------+----------+
| Status             | Duration |
+--------------------+----------+
| starting           | 0.000106 |
| Opening tables     | 0.001007 |
| System lock        | 0.000009 |
| Table lock         | 0.000016 |
| init               | 0.000024 |
| optimizing         | 0.000010 |
| statistics         | 0.000018 |
| preparing          | 0.000016 |
| executing          | 0.000012 |
| Sending data       | 0.005298 |
| end                | 0.000022 |
| query end          | 0.000009 |
| freeing items      | 0.000336 |
| logging slow query | 0.000009 |
| cleaning up        | 0.000009 |
+--------------------+----------+

WHERE句付き

mysql> flush tables;
Query OK, 0 rows affected (0.00 sec)

mysql> select count(*) from city where name = 'tokyo'
+----------+
| count(*) |
+----------+
|        1 |
+----------+
1 row in set (0.00 sec)

mysql> show profile;
+--------------------+----------+
| Status             | Duration |
+--------------------+----------+
| starting           | 0.000127 |
| Opening tables     | 0.001020 |
| System lock        | 0.000009 |
| Table lock         | 0.000017 |
| init               | 0.000052 |
| optimizing         | 0.000022 |
| statistics         | 0.000021 |
| preparing          | 0.000022 |
| executing          | 0.000011 |
| Sending data       | 0.006663 |
| end                | 0.000009 |
| query end          | 0.000007 |
| freeing items      | 0.000061 |
| logging slow query | 0.000007 |
| cleaning up        | 0.000008 |
+--------------------+----------+

続けて、MyISAMテーブルでの結果です。

[MyISAM結果]
全件

mysql> flush tables;
Query OK, 0 rows affected (0.00 sec)

mysql> select count(*) from city;
+----------+
| count(*) |
+----------+
|     4079 |
+----------+
1 row in set (0.00 sec)

mysql> show profile;
+--------------------+----------+
| Status             | Duration |
+--------------------+----------+
| starting           | 0.000106 |
| Opening tables     | 0.002326 |
| System lock        | 0.000011 |
| Table lock         | 0.000016 |
| init               | 0.000023 |
| optimizing         | 0.000012 |
| executing          | 0.000021 |
| end                | 0.000010 |
| query end          | 0.000008 |
| freeing items      | 0.000046 |
| logging slow query | 0.000007 |
| cleaning up        | 0.000007 |
+--------------------+----------+


WHERE句付き

mysql> flush tables;
Query OK, 0 rows affected (0.05 sec)

mysql> select count(*) from city where name = 'tokyo';
+----------+
| count(*) |
+----------+
|        1 |
+----------+
1 row in set (0.02 sec)

mysql> show profile;
+--------------------+----------+
| Status             | Duration |
+--------------------+----------+
| starting           | 0.000131 |
| Opening tables     | 0.002439 |
| System lock        | 0.000010 |
| Table lock         | 0.000017 |
| init               | 0.000051 |
| optimizing         | 0.000021 |
| statistics         | 0.000022 |
| preparing          | 0.000024 |
| executing          | 0.000011 |
| Sending data       | 0.003237 |
| end                | 0.000011 |
| query end          | 0.000009 |
| freeing items      | 0.000094 |
| logging slow query | 0.000009 |
| cleaning up        | 0.000012 |
+--------------------+----------+

まだまだ行数が少なかったようで、体感レスポンスタイムはあまり変わりませんでした。
しかし、プロファイリングしてみると中での動きは全然違うことが分かります。

・結果を出すためのステップがMyISAM-全件ではかなり省略されている
・MyISAM-全件では、特にSending dataのステップがない

以上のことから皆さんも「MyISAMの全件カウントは何か特別なことをしているな~」と感じられるのではないでしょうか。

実はMyISAMでは、テーブル中に現在何件の行があるかの情報をインデックスファイル(MYIファイル)に持っています。そして、MyISAMのテーブルで全件のCOUNT()が来たらMySQLサーバはデータファイルを一切読まず、計算などすることもなくインデックスファイルにある行数を返す仕様になっています。MyISAMテーブルに対するINSERTやDELETEが行われるたびに、この行数カウントが増減するようになっています。なお、InnoDBは構造上このようなフィールドを持つことができないため、件数カウントに時間がかかる仕様となっています(MVCC等のためです。詳しい理由は別の機会に・・・)

もうちょっと細かく見てみましょう。MYIファイルの中身はおおよそ以下のようになっています。

HEADER
  ステート情報
  ベース情報(主にデータ長定義)
  キー定義情報
 ..
  レコード情報
  ..
KEY
  キーセグメント情報(キー長さ、ポインタ情報)
  ..

さらに、[ステート情報]の部分を細かく見てみると以下のようになっています。

ヘッダ:24バイト(1-24)
オープンカウント : 2バイト(25-26)
ステート変更フラグ:1バイト(27)
ソートキー(不使用):1バイト(28)
レコード数:8バイト(29-36)
削除レコード数:8バイト(37-43)
<以下略>

MyISAMでは全件のCOUNT()関数が来るとMYIファイルの[現在の行数]の部分からデータを読みこんで結果を返します。ちょっと実験してみましょう。
MYIファイルの件数をみてCOUNT()関数が結果を返すならば、そこを変えるとテーブルデータはそのままでも結果件数は変わるはずです(不整合となりますが・・・)

何もしないと4079行という結果が返ってきます。

mysql> select count(*) from city;
+----------+
| count(*) |
+----------+
|     4079 |
+----------+
1 row in set (0.01 sec)

ここで、MYIファイルをバイナリエディタでちょっといじってみます。編集するのは、city.MYIという名前のファイルです。下のスクリーンショットのちょうどカーソルがあたっている部分が該当する部分となります。現在値は0FEF(4079)なので、これを適当な数字に変更してみます。

[変更後]
150FEFに変更しました。

さて、COUNT(*)の結果はどうなっているでしょうか。

mysql> select count(*) from city;
+----------+
| count(*) |
+----------+
|  1380335 |
+----------+
1 row in set (0.00 sec)

1380335(150FEF)に変更されています。
今回は実験目的で実施しましたが、通常バイナリを直接編集する機会はあまりないと思います。
もしも、今回のようにMYI(インデックス)の内容とMYD(データ)に差異が出てしまった場合は以下のようにして調査、修復することが可能です。

mysql> check table city;
+------------+-------+----------+----------------------------+
| Table      | Op    | Msg_type | Msg_text                   |
+------------+-------+----------+----------------------------+
| world.city | check | error    | Found 4079 keys of 1380335 |
| world.city | check | error    | Corrupt                    |
+------------+-------+----------+----------------------------+
2 rows in set (0.00 sec)

上記のようにCHECK TABLE文(オンライン用)または、myisamchkユーティリティ(オフライン用)を使ってファイルの状態を検査します。今回はMYIの件数が間違っているので、きちんと1380335ではなく実数は4079であると警告されています。

このように問題を発見した場合、REPAIR TABLE文(オンライン用)もしくはmyisamchkユーティリティ(オフライン用)で修復しましょう。

mysql> repair table city;
+------------+--------+----------+---------------------------------------------+
| Table      | Op     | Msg_type | Msg_text                                    |
+------------+--------+----------+---------------------------------------------+
| world.city | repair | warning  | Number of rows changed from 1380335 to 4079 |
| world.city | repair | status   | OK                                          |
+------------+--------+----------+---------------------------------------------+
2 rows in set (0.03 sec)

mysql> select count(*) from city;
+----------+
| count(*) |
+----------+
|     4079 |
+----------+
1 row in set (0.00 sec)

これで、もとの行数に戻りました。MYIファイルが消失してしまった場合でもMYDからREPAIR TABLEコマンドでMYIが復活できますので便利です(消失したときはUSE_FRMオプションをつけてください)。

注:DISTINCTやWHEREを使うとMYIから件数を持って来なくなります(それはそうですね)。

日曜日 4 05, 2009

こんにちは。今回はMyISAMストレージエンジンの並列インサート機能(Concurrent Insert)についてちょっと書いてみたいと思います。

MyISAMは皆さんご存知のように、テーブルレベルのロックのみで行レベルでのロックがサポートされていません。MySQLではSELECTコマンドの実行時に暗黙的リードロック(Shared Read lock)がかかるようになっており、一般的にはこのロックが対象のテーブルに存在している場合には、書き込み処理(INSERT,UPDATE,DELETE,ALTER TABLEなど)がロック待ちとなります。SELECTの実行が完了してからINSERTなどが実行される形になります。

MyISAMはテーブルロックなので、SELECTの対象としている行以外への更新も同様に待たされてしまい、同時処理性能に影響を与えているであろうことは容易に想像できると思います。

MyISAMには、コンカレントインサート(Concurrent Insert)と呼ばれる機能があります。コンカレントインサートとは、他のコネクション(スレッド)がSELECT文を発行もしくはLOCK文を実行している時であってもコンカレントインサート処理が対象のテーブルに対して書き込みロックを獲得できる(=Shared Read lockがかかっている状態のテーブルにINSERTできる)機能です。なおSELECTのShared Read lock要求は既にターゲットのテーブルにコンカレントインサートが書き込みロックをかけていても通るようになっています。

要するに、1つまたは複数のSELECTと1つのINSERTを同時実行できるようにする機能です。

コンカレントインサートが使用可能になるLOCKコマンドは以下となります。
LOCK TABLE テーブル名 READ LOCAL;
(LOCALをつけないとコンカレントインサートは利用不可になります。)

[設定方法]

グローバルサーバ変数:concurrent_insertを設定します。

0 : コンカレントインサートを使用しません。(使用できる状況でも使用しない)
1 : 対象のMyISAMテーブルに「穴」がない場合に限り(最後尾にインサートできる場合)、コンカレントインサートを実施します。
2 : 対象のMyISAMテーブルに「穴」があっても、レコードを強制的に最後尾にインサートし、コンカレントインサートを実施します。

ここで言う「穴」とは、ストレージのレコードシーケンスの途中に「穴」がある場合のことを言います。
MyISAMでは、一度DELETEしたデータは物理的には消去されず削除フラグを立てるだけとなっています。INSERTのみで作られたテーブルデータには「穴」はありませんが一度最後尾以外の行をDELETEすると削除フラグが立ちそこが「穴」となります。MyISAMは次回インサート時になるべくその「穴」の場所にデータを挿入しようとします。

concurrent_insert=2の場合は、対象のテーブルに既にリードロックがかけられていた場合のみ「穴」ではなく最後尾へデータを挿入します(穴はそのまま)。テーブルに何もロックがない場合は「穴」の場所にデータが挿入されます。concurrent_insert=1に比べて領域の使用効率が悪くはなります。なお、「穴」は次のコマンドで物理的になくすことが出来ます。

mysql> optimize table test.locktest;
+---------------+----------+----------+----------+
| Table         | Op       | Msg_type | Msg_text |
+---------------+----------+----------+----------+
| test.locktest | optimize | status   | OK       |
+---------------+----------+----------+----------+
1 row in set (0.00 sec)

[設定コマンド例]
set global concurrent_insert=0;
set global concurrent_insert=1;
set global concurrent_insert=2;
※デフォルトは1となっています。

グローバルサーバ変数なので、次回コネクション作成時から有効になります。
または、my.cnfなどの設定ファイルに書いておくと良いでしょう。

こう書いてみると常時1か2に設定したくなるような機能ですが実際のところはどうなのでしょうか。
ソース分析後に手元の環境で簡単なベンチマークをしてみました。

まずは、sysbenchでのテスト結果です。

コンカレントインサート有の場合(強制使用)
concurrent_insert=2;

OLTP test statistics:
    queries performed:
        read:                            1400
        write:                           500
        other:                           0
        total:                           1900
    transactions:                        100    (473.84 per sec.)
    deadlocks:                           0      (0.00 per sec.)
    read/write requests:                 1900   (9002.99 per sec.)
    other operations:                    0      (0.00 per sec.)

Test execution summary:
    total time:                          0.2110s
    total number of events:              100
    total time taken by event execution: 9.3607
    per-request statistics:
         min:                                 35.74ms
         avg:                                 93.61ms
         max:                                155.12ms
         approx.  95 percentile:             140.13ms

Threads fairness:
    events (avg/stddev):           1.0000/0.40
    execution time (avg/stddev):   0.0936/0.04

コンカレントインサート無しの場合
concurrent_insert=0;

OLTP test statistics:
    queries performed:
        read:                            1400
        write:                           500
        other:                           0
        total:                           1900
    transactions:                        100    (532.79 per sec.)
    deadlocks:                           0      (0.00 per sec.)
    read/write requests:                 1900   (10122.93 per sec.)
    other operations:                    0      (0.00 per sec.)

Test execution summary:
    total time:                          0.1877s
    total number of events:              100
    total time taken by event execution: 7.4302
    per-request statistics:
         min:                                 34.88ms
         avg:                                 74.30ms
         max:                                152.66ms
         approx.  95 percentile:             128.75ms

Threads fairness:
    events (avg/stddev):           1.0000/0.76
    execution time (avg/stddev):   0.0743/0.05

コンカレントインサート無しの方が若干ですがスループットが良いという結果となりました。
今回たまたまというわけではなく、ほぼ毎回このような結果となります。
実行時間の短いSELECTとINSERTは比較的効率的に処理されているためと推測されます。
逆にコンカレントインサートのオーバヘッドが目立つ形になったといったところでしょうか。

(sysbenchでは、INSERT,DELETE,UPDATEがミックスされてしまうので、INSERT以外の文が発生させるロックが多少結果に影響を与えると考えられます。念のためにmysqlslapでSELECTとINSERTのみのベンチを実施し結果は同様になるのを確認しました。詳細はここでは割愛します)

MyISAMの処理フローから逆算すると、コンカレントインサート機能が真価を発揮するのは実行時間の長いSELECT文が頻繁に流れているような状況下と仮定することができます。

そのような状況をシミュレートしてベンチマークテストを実施してみました。
今回は複雑なシナリオなので、JMeterを使ってみました。(SuperSmackなどでも良いと思います)

以下のスレッドを同時実行して性能を見るシナリオです。

・SELECT文(短い)とINSERT文をたくさん実行するスレッド(複数)
・長いSELECT文(ランダムで最大約1秒リードロックを保持する)を何度も実行するスレッド(1つ)

こんな感じです。

実行時間の長いSELECTはそれだけ長くリードロックを保持するので、コンカレントではないインサートは待ちとなってしまいます。

結果を見てみましょう。

concurrent_insert=2;
コンカレントインサート強制使用

テーブルロックの状況

mysql> show status like 'Table%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Table_locks_immediate | 1185  |
| Table_locks_waited    | 20    |
+-----------------------+-------+
2 rows in set (0.00 sec)

コンカレントインサートを使用しない
concurrent_insert=0;

テーブルロックの状況

mysql> show status like 'Table%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Table_locks_immediate | 865   |
| Table_locks_waited    | 390   |
+-----------------------+-------+
2 rows in set (0.01 sec)

コンカレントインサート使用時はINSERTのスループットは113.2、不使用時は66.6となっています。(SELECT REQ/SELECT REQ2はINSERTと同じスレッドなのでINSERTのスループットに引っ張られます)
※並列スレッドを100程度に絞り込んでいるのと、スレッド間での処理のずれを作り出すためにランダムでの待ち時間を入れているため絶対的なスループットは少なめとなっています。

長いSELECTが稼動していると、コンカレントインサートなしでは(当然ですが)ロック待ちになるケースが増えています。このロック待ちがスループットを落とす原因となっています。逆にコンカレントインサートを使っているとほとんどロック待ちが出ないのが観測されました。

結論としては、よく現在のMySQLの使われ方を分析した上でコンカレントインサート機能の使用/不使用を決定した方が良いでしょう。FITするケースにはパフォーマンスチューニングの一手段として使っても良いと思います。場合によってはOFFにすることも有効だと考えられます。

金曜日 3 20, 2009

今日は、最近導入が増えてきたMySQLのレプリケーション関連の記事を書いてみようと思います。
ご存知のようにMySQL5.1では以下の2種類+1のレプリケーションフォーマットを使用することが可能です。
・Statement-based-replication (SBR)
・Row-based-replication (RBR) ※5.1.5新機能
・Mixed ※5.1.8新機能

マスターからスレイブに対してどのようなフォーマットのバイナリログを送信するかの設定です。
SBRはSQLステートメントベースのイベントがログに書き込まれます。対してRBRは変更対象行自体をログに書き込んでスレイブに送り込む形式となります。

Non-Deterministic(実行するごとに異なる結果が出る)な関数はステートメントベースでは使用不能(Unsafe)であるというのが一般論だと思いますが、MySQL5.1では一部例外があります。Non-Deterministicなものについては全部RBRで対応すれば良いと思われるかもしれませんが、諸般の事情でSBRでやりたいケースはあると思います。

以下2つの関数について取り上げてみたいと思います。

・RAND()
・NOW()

RAND関数は、0以上1より小さい乱数を生成する関数です。実行結果はNon-Deterministicであり、実行ごとに結果が変化します。ステートメントベースでバイナリログにRAND()を書いてしまうと、スレーブにこのSQL文が送られリプレイされた時にマスターでの結果とは違う数字を生成してしまうと危惧される方もおられるのではないでしょうか。

実はMySQLではバイナリログ部分でRAND関数を扱うための特別なロジックを搭載しており、SBRでもRBRでもマスターの実行結果とスレイブの実行結果が同じになるように設計されています。また、バイナリログを使用したロールフォワードでも結果が同じになります。

ステートメントベースレプリケーションで、RAND()を使ってみます。

MASTER:[3306]> set binlog_format='STATEMENT';
Query OK, 0 rows affected (0.00 sec)
MASTER:[3306]> insert into test values(rand());
Query OK, 1 row affected (0.09 sec)

マスターでの結果

MASTER:[3306]> select * from test;
+------------------+
| a                |
+------------------+
| 0.34047383287966 |
+------------------+
1 row in set (0.00 sec)

スレイブでの結果
SLAVE:[3307]> select * from test;
+------------------+
| a                |
+------------------+
| 0.34047383287966 |
+------------------+
1 row in set (0.00 sec)

全く同じ結果ですね。結論を先に書いてしまうと、RAND()関数はSBRで安全です。

ソースの該当部分を見てみると理由が分かります。RAND()が呼び出された時に、シード値をスレッドクラスに保存しておくようになっています。

    if (!thd->rand_used)
    {
      thd->rand_used= 1;
      thd->rand_saved_seed1= thd->rand.seed1;
      thd->rand_saved_seed2= thd->rand.seed2;
    }
    rand= &thd->rand;

バイナリログの書き込みタイミングで、先ほど保存しておいたシード値を本体SQLに先立って書き込んでいます。

    if (thd->rand_used)
        {
       Rand_log_event e(thd,thd->rand_saved_seed1,thd->rand_saved_seed2);
          if (e.write(file))
            goto err;
        }

バイナリログに2つのシード値が保存されているのが確認できます。

# at 951
#090319 21:48:45 server id 1  end_log_pos 986   Rand
SET @@RAND_SEED1=934874093, @@RAND_SEED2=782184184/*!*/;
# at 986
#090319 21:48:45 server id 1  end_log_pos 1080  Query   thread_id=3     exec_time=0
SET TIMESTAMP=1237466925/*!*/;
insert into test values(rand())
/*!*/;
# at 1080
#090319 21:48:45 server id 1  end_log_pos 1107  Xid = 36
COMMIT/*!*/;
DELIMITER ;
# End of log file

スレイブがバイナリログ(リレーログ)からログイベントを読み込み、適用する時に保存されていたSEED1とSEED2がスレッド変数に再格納されます。スレイブはこのシードを使って乱数を生成するので結果が同じになります。

NOW()関数も同様となります。NOW()は現在のdate/timeを返す関数ですが、同じくNon-Deterministicなのでマスターとスレイブ間で差異が出そうな気がします。しかしRAND()同様に特別な処理が入っているのでSBRで安全な関数となっています。

ソースの該当箇所は以下となります。

  /*
    Timestamp on the master(for debugging and replication of
    NOW()/TIMESTAMP).  It is important for queries and LOAD DATA
    INFILE. This is set at the event's creation time, except for Query
    and Load (et al.) events where this is set at the query's
    execution time, which guarantees good replication (otherwise, we
    could have a query and its event with different timestamps).
  */
  time_t when;

Log_event::Log_event(THD* thd_arg, uint16 flags_arg, bool using_trans)
  :log_pos(0), temp_buf(0), exec_time(0), flags(flags_arg), thd(thd_arg)
{
  server_id=    thd->server_id;
  when=        thd->start_time;
  cache_stmt=    using_trans;
}

クエリとLoadステートメントの場合は、ステートメントの実行時のタイムスタンプが入ります。
ここで保存されたタイムスタンプはバイナリログにSET TIMESTAMP=xxxxxxという形で書き込まれます。

SET TIMESTAMP=1237087230/*!*/;
SET @@session.time_zone='SYSTEM'/*!*/;
insert into test values(now())

以下のSQLで実験してみましょう。(マスターとスレイブでNOW()の実行タイミングに差が出るように、スレイブのIO_THREADを一時停止しました)

MASTER:[3306]> set binlog_format='STATEMENT';
Query OK, 0 rows affected (0.00 sec)

MASTER:[3306]> insert into test values(now());
Query OK, 1 row affected (0.08 sec)

マスター結果

MASTER:[3306]> select * from test;
+---------------------+
| a                   |
+---------------------+
| 2009-03-19 22:31:14 |
+---------------------+
1 row in set (0.00 sec)

スレイブ結果

SLAVE:[3307]> select * from test;
+---------------------+
| a                   |
+---------------------+
| 2009-03-19 22:31:14 |
+---------------------+
1 row in set (0.00 sec)

同じ結果がテーブルに格納されました。

[注意点]
SBRでは、RAND()やNOW()と違って以下の関数は安全ではないので注意が必要です。
LOAD_FILE()
UUID()
UUID_SHORT()
SYSDATE()
FOUND_ROWS()
ROW_COUNT()
USER()
CURRENT_USER()

これらを処理するための特別なロジックが実装されていないためですが、
レプリケーションフォーマットをMIXEDに設定することで、いくつかの関数は安全に実行することができるようになります。MIXEDモードではこれらの関数を実行するときにレプリケーションフォーマット内部的にRBRへ変更します。(MySQL5.1.8からの機能)

UUID()
UUID_SHORT()
SYSDATE() 注1
FOUND_ROWS() 注2
ROW_COUNT() 注2
USER() 注2
CURRENT_USER() 注2

注1: NOW()のエイリアスとするオプションも使用可能
注2: MySQL 5.1.24から

関数以外にも、例えばSELECTをORDER BY句なしでLIMITと一緒に使った場合などにもMIXEDに指定していれば自動でRBRに変換してくれます。

噂のUUID()で実験してみましょう。

MASTER:[3306]> set binlog_format='MIXED';
Query OK, 0 rows affected (0.00 sec)

MASTER:[3306]> insert into uuidtest values(uuid());
Query OK, 1 row affected (0.09 sec)

マスター結果

MASTER:[3306]> select * from uuidtest;
+--------------------------------------+
| a                                    |
+--------------------------------------+
| ab19b40b-1457-11de-8726-c70ad9252c6d |
+--------------------------------------+
1 row in set (0.00 sec)

スレイブ結果

SLAVE:[3307]> select * from uuidtest;
+--------------------------------------+
| a                                    |
+--------------------------------------+
| ab19b40b-1457-11de-8726-c70ad9252c6d |
+--------------------------------------+
1 row in set (0.02 sec)

結果が同じになりました。
ついでなので、変換する部分のソースをチェックしてみます。
例えばUUID()は、以下のように作成時にSBRではUnSafeであるとマークしています。

Item*
Create_func_uuid::create(THD *thd)
{
  thd->lex->set_stmt_unsafe();
  thd->lex->safe_to_cache_query= 0;
  return new (thd->mem_root) Item_func_uuid();
}

UnSafeであるとマークされたステートメントは、後でログフォーマットチェックロジックの中で、
MIXED設定の場合にRBRに変換されます。

if (thd->lex->is_stmt_unsafe() || 
   (flags_all_set & HA_BINLOG_STMT_CAPABLE) == 0)
{
    thd->set_current_stmt_binlog_row_based_if_mixed();
}

また、5.1.20からは「安全ではない」関数が使われるシチュエーションが発生した場合
(BINLOG_FORMAT='STATEMENT'とされているなど)、ログに警告が表示されるようになりました。

090319 16:35:35 [Warning] Statement is not safe to log in statement format. Stat
ement: insert into uuidtest values(uuid())

[オマケ]
MySQLのバージョンによる、binlog_formatのデフォルトは以下です。
5.1.5  - 5.1.7        STATEMENT
5.1.8  - 5.1.11      STATEMENT
5.1.12 - 5.1.28     MIXED
5.1.29 -              STATEMENT

土曜日 2 21, 2009

DB自体の話が最近多くなってきたので、アプリ層のことも少し書いてみようと思います。
今回はプログラミング言語RubyからMySQLにアクセスするベーシックなプログラムを作ってみようと思います。最低限の要素が入っているようなものを作成したいと思います。

1. Rubyのインストール
今回はお手軽版ということで、以下を使ってみました。
One-Click Ruby Installer 186-26 Final Release
ダウンロードはこちらからできます

Rubyの既成バイナリは色々な種類があるのですが、その中でも1クリックでインストール出来てしまうと評判のものを今回使っています。ウィザード付のバイナリです。

インストール先にc:\rubyを指定しました。

2.MySQL/Rubyのインストール
Rubyのインストールが完了したら、MySQL-gemをインストールします。
以下のコマンドをc:\ruby\binから発行します。

gem install mysql -v 2.7.3

C:\ruby\bin>gem install mysql -v 2.7.3
Select which gem to install for your platform (i386-mswin32)
 1. mysql 2.7.3 (mswin32)
 2. Skip this gem
 3. Cancel installation
> 1
Successfully installed mysql-2.7.3-mswin32
Installing ri documentation for mysql-2.7.3-mswin32...
Installing RDoc documentation for mysql-2.7.3-mswin32...
While generating documentation for mysql-2.7.3-mswin32
... MESSAGE:   Unhandled special: Special: type=17, text="<!--
... RDOC args: --op C:/ruby/lib/ruby/gems/1.8/doc/mysql-2.7.3-
(continuing with the rest of the installation)

One-Click Ruby Installerはウィザードだけではなく、色々なWindows用バイナリも既にインストールされています。
gem listで、ローカルgemを確認すると良いと思います。

3. MySQLのbinフォルダを環境変数PATHに通しておきます。
別にインストールしてあるMySQLのbinをパスに通しておきます。c:\mysql5.1\binなどになります。

4. MySQLのテーブルを作っておきます。
MySQLサーバが起動していなければ起動します。
rubyという名前のデータベースをutf8キャラクタセット指定で作成します。mysqlクライアントプログラムを使用します。

C:\MySQLComm5.1.31\bin>mysql -uroot -p
create database ruby character set utf8
use ruby

次に以下のようなitemテーブルを作成します。
CREATE TABLE item (
  id int NOT NULL,
  name varchar(100) DEFAULT NULL,
  price decimal(10,0) DEFAULT NULL,
  times timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (id));

テーブルには適当にデータを入力しておきます。今回はこんな感じに入れてみました。

*************************** 1. row ***************************
   id: 1
 name: 南イタリア産ブラッドオレンジ
price: 350
times: 2009-02-20 23:00:54
*************************** 2. row ***************************
   id: 2
 name: 奥久慈完熟りんご
price: 250
times: 2009-02-20 23:01:18
*************************** 3. row ***************************
   id: 3
 name: 沖縄産有機パイナップル
price: 500
times: 2009-02-20 23:01:38
3 rows in set (0.00 sec)

5. 適当なフォルダにmysql_app.rbという名前のテキストファイルを作成します。

6. mysql_app.rbファイルを編集します。

require "mysql"                                                 
 
dbname = "ruby"              

m = Mysql.init()
m.options(Mysql::SET_CHARSET_NAME, "cp932")
m.real_connect("localhost", "root", "adminadmin", dbname)
 
result = m.query("SELECT name,price FROM item")
while (row = result.fetch_row) != nil do            
    puts row.at(0) + "は" +  row.at(1) + "円ですよ"
end

m.close    

今回はデータに日本語が含まれているので、Windows上のコマンドラインで表示させるためAPIのキャラクタセットをcp932(Windows-SJS)に指定しています。C APIのmysql_optionsと同じ機能です(名前は違いますが)。
あとは、コネクトしてクエリを投げた後にリザルトセットをプリントしてあげます。

5.コマンドラインでrubyアプリを実行してみます。

C:\ruby\bin>ruby c:\rubydev\mysql_app.rb

結果

C:\ruby\bin>ruby c:\rubydev\mysql_app.rb
南イタリア産ブラッドオレンジは350円ですよ
奥久慈完熟りんごは250円ですよ
沖縄産有機パイナップルは500円ですよ

Rubyはインタプリタなのでめんどくさいコンパイルが必要ありません。
Textエディタでプログラムを変更したらすぐに実行できます(あたりまえですが・・・)

そのうちJRuby on Rails/MySQLも書きたい思います。

金曜日 1 23, 2009

今回は、postgresqlからMySQLへの移行手順を書いてみたいと思います。
今回もMySQLマイグレーションツールキットを使用します。

全体の流れは、Oracleからの移行で説明したのと同様になります。

1.ツールを起動する前に、以下のフォルダにpostgresqlのjdbc-jarをコピーしておいてください。
postgresのJDBCドライバはhttp://jdbc.postgresql.org/download.htmlからダウンロードできます。

私の環境では以下をコピーしました。
C:\Program Files\MySQL\MySQL Tools for 5.0\java\lib
へpostgresql-8.3-604.jdbc3.jarをコピー

2.ツールを起動します。

3.移行元データベース(Source Database)の画面で以下のように設定します。
Database System : Generic jdbc
Class Name : org.postgresql.Driver
Connection String : jdbc:postgresql://ホスト名:ポート番号/データベース名
例: jdbc:postgresql://eslab177:5432/testdb
Username : postgres

4.移行先データベースの設定を行います。
今回はOracle移行の時と同じインスタンスを使います。

5.サーバへ接続処理されます。
接続が成功すると、Oracleの時同様にExecution completed successfullyと出力されます。

6.移行元スキーマの選択を行います。
Oracleの時と同様にスキーマ(データベース)の選択を行います。
※3の設定でデータベース名にtemplate1やpostgresを指定しているとスキーマは選択できますが、それ以降のオブジェクトが空になります

7.リバースエンジニアリングが実行され、メタデータが移行元DBから抽出されます。

8.移行するオブジェクトを選択します。
これもOracleの時と同様に移行対象と、それ以外とを指定します。

これ以降の流れはOracle移行の時と全く同一になります。移行のベースとするには恰好のツールですね。

ただし、現在Migration Toolkitでは明示的なpostgresqlサポートがないのでGeneric JDBCを使う形になっています。データタイプの移行などは自動成功する割合がOracle移行の時よりも少ないのが事実です。一度ツールで変換してから内容を変更していくか、Migration ToolkitのMigrationGeneric.javaに望ましいpostgresql→mysqlのデータマップ定義を埋め込んであげれば良いと思います。

金曜日 12 05, 2008

前回にて、OracleサンプルテーブルからMySQLへの移行は完了しました。
今回は、気になる結果をチェックしてみたいと思います。

scott.emp表の移行結果

BEFORE : ORACLE DB

SQL> select dbms_metadata.get_ddl('TABLE','EMP','SCOTT') from dual;

DBMS_METADATA.GET_DDL('TABLE','EMP','SCOTT')
--------------------------------------------------------------------------------

  CREATE TABLE "SCOTT"."EMP"
   (    "EMPNO" NUMBER(4,0),
        "ENAME" VARCHAR2(10),
        "JOB" VARCHAR2(9),
        "MGR" NUMBER(4,0),
        "HIREDATE" DATE,
        "SAL" NUMBER(7,2),
        "COMM" NUMBER(7,2),
        "DEPTNO" NUMBER(2,0),
         CONSTRAINT "PK_EMP" PRIMARY KEY ("EMPNO")
  USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS"  ENABLE,
         CONSTRAINT "FK_DEPTNO" FOREIGN KEY ("DEPTNO")
          REFERENCES "SCOTT"."DEPT" ("DEPTNO") ENABLE
   ) PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS"

AFTER : MySQL DB

mysql> show create table emp;
+-------+---------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------+
| Table | Create Table
                                                                            |
+-------+---------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------+
| emp   | CREATE TABLE `emp` (
  `empno` int(4) NOT NULL,
  `ename` varchar(10) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  `job` varchar(9) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  `mgr` int(4) DEFAULT NULL,
  `hiredate` datetime DEFAULT NULL,
  `sal` decimal(7,2) DEFAULT NULL,
  `comm` decimal(7,2) DEFAULT NULL,
  `deptno` int(2) DEFAULT NULL,
  PRIMARY KEY (`empno`),
  KEY `fk_deptno` (`deptno`),
  CONSTRAINT `fk_deptno` FOREIGN KEY (`deptno`) REFERENCES `dept` (`deptno`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+-------+---------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------+
1 row in set (0.14 sec)

上手に移行されているような印象ですね。単純な移行ではなくてちょっと工夫した?移行をしてる感じです。
例えば:

Oracle  : NUMBER(4,0),
MySQL : int(4) NOT NULL,

なんかです。(ただしMySQLではint(4)とやってもストレージの節約にはなりませんが・・)あと、このテーブルに付加されている主キーと、外部キーはきちんとリバースエンジニアリングされて来ています。

Oracle

CONSTRAINT "PK_EMP" PRIMARY KEY ("EMPNO")
CONSTRAINT "FK_DEPTNO" FOREIGN KEY ("DEPTNO")
REFERENCES "SCOTT"."DEPT" ("DEPTNO") ENABLE

MySQL

PRIMARY KEY (`empno`),
  KEY `fk_deptno` (`deptno`),
  CONSTRAINT `fk_deptno` FOREIGN KEY (`deptno`) REFERENCES `dept` (`deptno`) ON DELETE NO ACTION ON UPDATE NO ACTION

次にEMP表のデータをチェックしてみます。きちんと移行されているでしょうか?左が移行されたMySQLデータ、右がオリジナルのOracleデータです。(クリックすると拡大します)

EMP表に関しては完璧ですね。若干表示形式が異なっていますが、これは私が使ったOracleのSQL*Plusの設定(SET~)とNLSの設定がmysqlの表示形式と若干異なっているためです。(完全に同じような表示にすることも設定次第で可能です)

他の表についても参考までに載せておきます。

scott.bonus表の移行結果

BEFORE : ORACLE DB

SQL> select dbms_metadata.get_ddl('TABLE','BONUS','SCOTT') from dual;

DBMS_METADATA.GET_DDL('TABLE','BONUS','SCOTT')
--------------------------------------------------------------------------------

CREATE TABLE "SCOTT"."BONUS"
( "ENAME" VARCHAR2(10),
"JOB" VARCHAR2(9),
"SAL" NUMBER,
"COMM" NUMBER
) PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
TABLESPACE "USERS"

AFTER : MySQL DB

mysql> show create table bonus;
+-------+------------------------------------------------------------------
| Table | Create Table
+-------+------------------------------------------------------------------
| bonus | CREATE TABLE `bonus` (
`ename` varchar(10) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`job` varchar(9) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`sal` decimal(22,0) DEFAULT NULL,
`comm` decimal(22,0) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+-------+------------------------------------------------------------------
1 row in set (0.00 sec)

scott.dept表の移行結果

BEFORE : ORACLE DB

SQL> select dbms_metadata.get_ddl('TABLE','DEPT','SCOTT') from dual;

DBMS_METADATA.GET_DDL('TABLE','DEPT','SCOTT')
--------------------------------------------------------------------------------

CREATE TABLE "SCOTT"."DEPT"
( "DEPTNO" NUMBER(2,0),
"DNAME" VARCHAR2(14),
"LOC" VARCHAR2(13),
CONSTRAINT "PK_DEPT" PRIMARY KEY ("DEPTNO")
USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
TABLESPACE "USERS" ENABLE
) PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
TABLESPACE "USERS"

AFTER : MySQL DB

mysql> show create table dept;
+-------+-----------------------------------------------------------------
| Table | Create Table
+-------+-----------------------------------------------------------------
| dept | CREATE TABLE `dept` (
`deptno` int(2) NOT NULL,
`dname` varchar(14) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`loc` varchar(13) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
PRIMARY KEY (`deptno`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+-------+-----------------------------------------------------------------
1 row in set (0.00 sec)

scott.salgrade表の移行結果

BEFORE : ORACLE DB

SQL> select dbms_metadata.get_ddl('TABLE','SALGRADE','SCOTT') from dual;

DBMS_METADATA.GET_DDL('TABLE','SALGRADE','SCOTT')
--------------------------------------------------------------------------------

CREATE TABLE "SCOTT"."SALGRADE"
( "GRADE" NUMBER,
"LOSAL" NUMBER,
"HISAL" NUMBER
) PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
TABLESPACE "USERS"

AFTER : MySQL DB

mysql> show create table salgrade;
+----------+-------------------------------------
| Table | Create Table
+----------+-------------------------------------
| salgrade | CREATE TABLE `salgrade` (
`grade` decimal(22,0) DEFAULT NULL,
`losal` decimal(22,0) DEFAULT NULL,
`hisal` decimal(22,0) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+----------+-------------------------------------
1 row in set (0.00 sec)

全体としては、よく移行できていますね。今回はサンプルテーブルということでシンプルな構成のためですが・・・。(マイグレーション結果は警告なしでした)他にOracleを使用している方は色々な機能を使用していると思いますが(マテリアライズドビュー、シーケンス、シノニム、他)そういったものはやはり手動またはツールのカスタマイズ/作成での移行が必要な場合もあります。あと、テーブル名とカラム名は小文字で出てきてしまうので大文字にしたい場合はもう一工夫必要ですね(※1)。複数ステートメントを含むプロシージャもDELIMITER文を追加するなどマニュアルでの対応は必要です。

※1 Migration Tool Kit 1.1.14の実装ではMaxDB, Oracleからの移行ではテーブル名等のIdentifierは小文字に変換するようになっています。ここを変更したり結果をSQLとして出力してそのSQLに手を加えることでオリジナルCaseでの移行は可能です。MySQLではデフォルト設定でWindows機上のテーブル名の大文字小文字はステートメント中で区別されません。ただしUNIX機でもストレージエンジンにInnoDBを使用する場合はテーブル名は小文字のままが推奨です(またはシステム変数lower_case_table_names=1をセットしてMySQLサーバを起動後create文を発行する)。カラム名は大文字小文字の区別はありません

このツールはあくまでも移行のための手段です。MySQLマイグレーションツールキットの良い点は移行処理中にたくさんの、人間が介入する機会が与えられているところだと思います。つまりどのように使うかは使用する側次第です。(例えば、NUMBER(4,0)ならSMALLINTでも可能、、、とか)しかも移行処理全体をスクリプト化できるので、移行プログラムを作成するよりも非常に少ない工数にて完全カスタマイズされた移行を実現できる可能性を持っていると思います。

さらに、MySQLマイグレーションツールキットの移行主要ロジックはJavaで記述されています。各種DBからの移行部分とリバースエンジニアリングの部分について、既に用意されている基底クラスをextendしてそれらのモジュールを作成できるようになっているので(かつオープンソースなので)、デフォルトのモジュールでは実装されていない部分の移行や、顧客要件に応じたカスタマイズされた移行を実装することが可能です。

Sunプロフェッショナルサービスでは、MySQLベンダーとしてMySQLの移行サービスを提供しています。移行の要件に基づく移行計画/設計、移行環境の構築、移行の実施、テストなどを行うアセスメントサービスとなります。MySQLマイグレーションツールキットのカスタマイズ対応や顧客専用移行ツール作成も可能です(あくまで移行手段の中の一つとしてですが、、、目的は移行そのものです)。是非ご利用をご検討ください。

木曜日 12 04, 2008

前回はMySQLマイグレーションツールキットで移行元DBと移行先DBへの接続設定までを行いました。
今回は残りの以下の部分について説明します。

5.移行対象のスキーマを選択(グラフィカルに表示される)
6.移行元DBから情報をリバースエンジニアリング(抽出)
7.移行対象オブジェクトの選択(テーブル、View、ストアドルーチンその他)
8.移行元⇔移行先間のオブジェクトマッピング指示(自動または手動)
9.SQL(DDL: CREATE TABLE文)生成
10.エラーリポート(自動移行判定できなかったデータタイプなどが表示され、必要ならば手動でSQLを修正する)
11.オンラインでデータベースオブジェクトを移行先で生成/生成SQL文をファイルに出力
12.データのバルク転送(オンラインで転送/データロードSQL生成)
13.最終結果の表示、リポートファイルへの出力

それではまた例によって個別に見てみましょう。

5.移行対象のスキーマを選択(グラフィカルに表示される)

移行対象にするスキーマの一覧が移行元DBから抽出されてグラフィカルに表示されています。
今回はEMP表を含むSCOTTスキーマを選択してみます。

選択して、Next>ボタンを押すとリバースエンジニアリングが開始され、データベースからメタデータが抽出されます
(スキーマ配下のテーブルのカラム情報、インデックス情報、制約情報、関連するストアドルーチン、View情報が取得されます)

6.移行元DBから情報をリバースエンジニアリング(抽出)

ここで、Execution completed successfully.と出力されれば処理OKです。

7.移行対象オブジェクトの選択(テーブル、View、ストアドルーチンその他)

次に移行対象とするオブジェクトの選択を行います。左側の箱に入っているのが今回移行対象になるもの、右側の箱に入っているのが今回の移行対象外になるオブジェクトです。
Viewやストアドプロシージャが存在する場合、ここで移行対象として選択します。Detailed selection >>を押します。
今回は既に自動でSCOTTスキーマ配下のテーブルである以下の4つの表が移行対象の方の箱に表示されています。

・SCOTT.BONUS
・SCOTT.DEPT
・SCOTT.EMP
・SCOTT.SALGRADE

8.移行元⇔移行先間のオブジェクトマッピング指示(自動または手動)

オブジェクトマッピング処理ではまずOracleスキーマとOracleテーブルの移行方式(migration method)を選択します。
ここでは、Oracle DefaultとGenericがあるのでOracle Defaultを選択しましょう。GenericはGeneric JDBCなどで接続した場合に特定のデータベースの特性を考慮しないで一般的な移行を行う場合に選択します。(Generic JDBCではGenericしかもちろん選択できません)

ParameterはキャラクターセットやCollation規則を設定します。入力するデータにしたがってMultilanguageにするのかLatin1にするのかを選択しましょう。
ここではマルチバイト文字が格納される可能性を前提としてMultilanguageを選択してみます。

9.SQL(DDL: CREATE TABLE文)生成

next>ボタンを押すと、移行処理が実行され、DDLが生成されます。

Execution completed successfully.と表示されれば出力成功です。

10.エラーリポート(自動移行判定できなかったデータタイプなどが表示され、必要ならば手動でSQLを修正する)

next>ボタンを押すと、移行されたオブジェクトについての移行結果画面が表示されます。
右上のFileterドロップダウンボックスから、Show All Objects with Messagesを選択して、テーブルの各カラムごとに移行がうまく行ったかを確認します。

Advanced>>ボタンを押して、移行されたDDLを確認することが出来ます。

SQL Create Script of Selected Objectテキストエリアが表示されている状態でテーブルをクリックすると、SQLが表示されます。DECIMALの桁数などはオブジェクトツリーでは見えないので、確認した方が良いと思います。

また、この画面で適宜出力予定のSQLを変更することが出来ます。直接SQLを編集して、Apply Changesボタンを押します。
(望ましくないデータ型に変換されそうになっている場合、手動で変更してください)

それから、Migrated Objectのツリーのカラム名をシングルクリックすることでも、カラム名だけは変更可能です。(データ型は変更しないので注意)

11.オンラインでデータベースオブジェクトを移行先で生成/生成SQL文をファイルに出力
next>を押すとスキーマ作成方法選択画面になります。

今すぐにオンラインで対象データベースにスキーマとテーブルを作成するのか、とりあえずSQL文を生成しておくのかを選択できます。今回はオンラインで作成してみます。

次に結果が表示されます。FilterをShow All Objects with Messageにしてテーブルやカラムが正常に作成されているかを確認します。

12.データのバルク転送(オンラインで転送/データロードSQL生成)

次に、データ転送についての設定を行います。
今すぐにオンラインで対象データベースにデータを一括転送するのか、とりあえずINSERT-SQL文を生成しておくのかを選択できます。今回はオンラインで作成してみます。

※生成されるInsert文には、最初に外部キー制約を無効にして一気にデータをロードできるようなものが書かれます。

13.最終結果の表示、リポートファイルへの出力

最後にサマリが表示されて完了です。リポートをファイルに出力することも可能です。
また、マイグレーション全体をLuaスクリプトとして出力することも可能です。

これを使えば、GUIにインタラクティブに各設定するスタイルではなく、スクリプトとして移行処理を実行させることが可能です。中身をカスタマイズすれば、より細かい動きをマイグレーションプロセスに追加することも可能です。(特定のデータの変換処理を入れ込むなど)

次回は、移行されたEMP表について検分してみたいと思います。
(今回は手動で何も変更しない完全自動移行です。どのように移行されたかをBefore/Afterで見てみたいと思います)

水曜日 12 03, 2008

前回はMySQLのマイグレーションツールキットの概要を説明しました。
今回は、マイグレーションツールキットを使用した具体的な移行方法について書いてみたいと思います。

MySQLマイグレーションツールキットでは、移行元DBと移行先DBに直接接続して移行を実施するダイレクト移行モードと、移行エージェントを目的のサーバにインストールしてそこから情報を収集させるエージェントベース移行モードがあります。

ただし、version1.1.14現在では、エージェントベース移行モードは選択できません(見えてますが・・・)、開発者の言うところによると、この機能はまだ未実装なので使えるようになっていないとのこと。将来的にはマイグレーションツールキットに入るというよりは、MySQL Workbenchに入るかもしれないという情報もあります(未確定)。

エージェントベース移行モードの目的としては、マイグレーションツールキットがサポートしていないOSにエージェントだけ入れてから、ツールキット⇔エージェント間で通信させて、エージェント⇔目的のDBがローカルにやり取りできるようにするものとのことです。ですので、ダイレクトモードを使用する場合は、ソースDBか、ターゲットDBのどちらかのマシンにツールキットをローカルにインストールするのがお勧めとドキュメントされています(どちらか片方だけでもDBに対してローカルアクセスとなる=ネットワーク高負荷を避ける)。もちろん両方ともSolaris上にDBがある場合は全部リモートにせざるを得ないのが現状ですけど・・・

MySQLマイグレーションツールキットのバイナリが提供されているのは現在のところ、以下となります。

・Windows 2000, XP, Vista, 2003
・Linux(グラフィカルデスクトップを導入済であること、Kernel Version 2.4または2.6)
・Mac OS X 10.4以上

今回は、ダイレクト移行モードを使って説明します。

1.移行元DBへの接続設定
2.移行先DBへの接続設定
3.移行元DBへ接続しスキーマ情報を取得
4.移行先DBへの接続テスト

それでは、個別に見てみましょう。
1.移行元DBへの接続設定

まず移行元(ソースDB)への接続情報を入力します。Oracleからの移行ですので、以下の情報を入力します。リモートサーバで稼動するOracle10gに接続します。

Database System : Oracle Database Server
Driver : Oracle Thin JDBC Driver using SID

※Oracle Thin DriverのJARファイルの場所をマイグレーションツールキットで指定する必要があります。
Thin Driverがロードされていない状態ですと、JARの場所を指定するように促す画面が表示されるのでダウンロード+場所指定を行ってください。ojdbc14.jarをダウンロード、指定すればOKです。

※ツールの起動時に、×Initialized Java loaderエラーになり起動できないケースがあります。

この問題は以下の対策で解決します。
・ツールキットの起動をコマンドラインから行い、-debugオプションを付ける

C:\MySQLMigrationToolkit5.0>MySQLMigrationTool.exe -debug




SID : ORA1 (オラクルDBでデータベースを作成したときのSIDを指定。Oracle Netで使用できるようにオラクル側でconfigしてください)
Hostname : IPアドレスまたは、ホスト名(名前解決できる必要があります)。通常はIPで十分でしょう。
Port : 1521 (Oracleリスナのポート番号を指定します)
Username : sys as sysdba
Password : sysのパスワード
入力したら[next]ボタンを押します。

または、

JDBCを使い慣れた方でしたら、その下のAdvanced>>ボタンを押します。Advanced Settingsウィンドウが開いてJDBC URLを直接入力できる画面が現れます。ここにJDBC URLをダイレクトに入力してもOKです。

例:jdbc:oracle:thin:sys as sysdba/oracle@10.14.3.88:1521:ORA1
※通常フィールドにも値を入れないとnext>ボタンが有効化しないのでJDBC-URLを使う場合は適当な値を入力してください

2.移行先DBへの接続設定

移行先のデータベースへの接続情報を入力します。
今回はローカルサーバで稼動しているMySQLを移行対象とします。

Hostname : localhost または 127.0.0.1などのループバックIP
username : root
password : ユーザパスワード

こちらもさきほどのOracleの場合と同じくJDBCを使用しているので、Advanced >>ボタンからJDBC URL入力フィールドを表示させて直接入力する手段もあります。

入力したら[next]ボタンを押します。

3.移行元DBへ接続しスキーマ情報を取得
4.移行先DBへの接続テスト

が自動的に実行されます。Advanced>>ボタンを押すとログが表示されます。
右上にイルカが泳ぐのを楽しく鑑賞しましょう。
最後に、Execution completed successfullyと出れば4までの処理が完了です。

※ここで、OracleのSIDが間違っていたり、OracleのサーバIPが間違っていたりするとエラーとなります。
Advanced >>ボタンを押してログを確認してください。以下の例��は間違ったIP(Pingも通らないような)を指定しているた���、「接続を確立できません」と出ています。OracleのTNSListenerが応答しなかったり、サービス名やSIDがインスタンスに登録されていなかったりした場合も同様にエラーとなりますので、このログをチェックして対処してください。

次回は移行対象のスキーマを選択するところから説明したいと思います。

火曜日 12 02, 2008

今回は、OSSつながりということで、MySQLのマイグレーションツールキットについて書いてみようと思います。
MySQLは言わずと知れた実用度の高いOSS-RDBMSです。
MySQLでは無償にて他データベースからスキーマとデータ移行をヘルプするツールが提供されています。
それが今回移行に使用したMySQLマイグレーションツールキットです。

今回から数回に分けてOracle10g→MySQLの移行手順を書いてみようかと思います。
まず第一回目ということで、OracleおなじみのEMP表をMySQLへ移行してみたいと思います。
EMP表などのSCOTTスキーマ配下のサンプルテーブルはデータ型が非常に簡潔なのですが、外部キー制約など基本的な機能が定義されているのでMySQLにどんな感じで移行されるのかを見るには持ってこいの素材であると思います。

EMP表の移行後は、Oracle独自のデータタイプがどのように移行されるか(または自動移行できない部分についてどのように手動マッピングできるか)について見ていこうかと思います。

MySQLマイグレーションツールキットは、Version1.1.14現在以下のソースデータベースからの移行をサポートしています。

・Oracle
・Microsoft SQL Server
・Microsoft Access
・Sybase
・MySQL
・MaxDB
・Generic JDBC

Generic JDBCは、JDBCドライバが提供されているDBに対してJDBC-APIにてメタデータの抽出、データの移行を行う機能です。JDBCドライバさえ提供されていれば移行に必要なテンプレート的SQLを生成してくれるので、広範囲なDBMSからの移行作業の工数を少なく出来るのではないでしょうか。

以下にMySQLマイグレーションツールキットを使用して他DBより移行する場合の大まかな流れを説明します。
全てグラフィカルに出来るので非常にやりやすいです。(とっつきやすいツールですね)

1.移行元DBへの接続設定
2.移行先DBへの接続設定
3.移行元DBへ接続しスキーマ情報を取得
4.移行先DBへの接続テスト
5.移行対象のスキーマを選択(グラフィカルに表示される)
6.移行元DBから情報をリバースエンジニアリング(抽出)
7.移行対象オブジェクトの選択(テーブル、View、ストアドルーチンその他)
8.移行元⇔移行先間のオブジェクトマッピング指示(自動または手動)
9.SQL(DDL: CREATE TABLE文)生成
10.エラーリポート(自動移行判定できなかったデータタイプなどが表示され、必要ならば手動でSQLを修正する)
11.オンラインでデータベースオブジェクトを移行先で生成/生成SQL文をファイルに出力
12.データのバルク転送(オンラインで転送/データロードSQL生成)
13.最終結果の表示、リポートファイルへの出力

ツールで対応できない部分についても、編集可能なSQLを生成してくれるのであとはレビュー・修正を行う形で補えます。

次回はOracleのEMP表(SCOTTスキーマ)をMySQLに移行するための具体的な手順を書いてみたいと思います。

This blog copyright 2009 by naokitakemura