デベロッパー
デベロッパー
分散DBMS「Mnesia」の並列処理
はじめに
Mnesiaは、Erlangという言語をベースとした並列プログラミング用のオープンソース開発環境であるErlang OTPに付属している高機能のデータベース管理システム(DBMS)です。Mnesiaは真の分散DBMSなので、世界中の何千ものノード間でデータを分散し、複製し、断片化することも朝飯前です。ユーザーがしなければならないのは、Mnesiaデータベースの分散先となるさまざまなErlangノードを実行することだけです。Mnesiaという名前になった経緯は、少々眉唾ではありますが、もともとは「Amnesia(健忘症)」という名前だったものを、エリクソンの重役が「データベースに『物忘れ』を連想させる名前を付けるのはいかがなものか」と発言したことから、エンジニアが「A」の文字を取って「Mnesia」とし、「すべてを記憶するもの」の意味を持たせたと言われています。Mnesiaは、Erlangで実装されたあらゆるプログラムにフォールトトレランス機能を提供します。また、ユーザーは直接Erlangを使ってMnesiaデータベースとやり取りできます。Mnesiaでは、Erlangがデータベース言語になるのです。
この記事では、Mnesiaの重要な機能をいくつか紹介します。この記事のダウンロード可能なサンプル内のtest.erlファイルには、数行のコードをまとめた簡単な関数がいくつか記述されています。これは、主としてErlangシェルで使用します。
やむを得ない場合を除き、Erlang構文のすべての側面や基本概念について説明することはしません。そのような情報は、オンラインマニュアルで簡単に見つけることができます。アトム、変数、タプル、リストには特に注意してください。私のDevXの記事「Writing Parallel Programs with Erlang」にも、役立つ背景情報が書かれています。この記事では、Erlangの並列処理に関する側面について説明しています。
ノード:Erlangの心臓部
ノードという概念は、Erlangの心臓部です。Erlangシェルを起動するときに、名前とcookieと呼ばれる秘密のワードを指定すれば、そのErlangシェルをノードにできます。いくつかの例で、Erlangの動作を見ていきましょう。私のLAN上には3台のDebian Linuxマシン(charlie、delta、nemo)が接続されています。これらのマシンには、以下のコマンドを実行してErlangをインストールしてあります。apt-get install erlang
erl -sname first -setcookie AXHMTYGVJDNJIFGYADNJ erl -sname second -setcookie AXHMTYGVJDNJIFGYADNJ
(first@charlie)1> net_adm:ping(second@delta). pong
(first@charlie)2> nodes(). [second@delta]
Mnesiaスキーマ
サンプルのtest.erlファイルには、test:start_schema(NodesList)
とtest:start_tables
という2つの関数が記述されています。これらは1回だけ実行する必要があります。これらの関数によって、Mnesiaスキーマを作成し、テーブルを定義し、定義したテーブルに初期データを設定します。最初にしなければならない操作はMnesiaスキーマの作成です。この操作は、接続されているノード上でMnesiaエンジンを起動する前に行う必要があります。Mnesiaスキーマとは、ファイルシステム上の特殊なテーブルとローカルディレクトリです。このディレクトリはファイルを格納することができ、ノードごとに一意でなければなりません。デフォルトでは、Mnesiaスキーマは各データベースにつき1回、現在のディレクトリの下に「Mnesia.node_name@host」という名前で作成されます。ただし、
erl
コマンドのオプション-mnesia dir [database_dir]
を使用して、デフォルトを無効にできます。関数
mnesia:create_schema(NodesList)
は、既に接続されているErlangノードのリストを引数として取ります。接続されているノードのいずれかから以下を実行すると、Mnesiaのdir
はローカルノードにのみ作成されます。mnesia:create_schema([local_node_name@localhost]).
mnesia:create_schema([local_node_name@localhost, remote_node_name1@host1, remote_node_name2@host2]).
これを実践するため、3つのノードにtest.erlファイルを配布し、以下のコマンドでコンパイルします。
c(test.erl).
mnesia:create_schema([first@charlie,second@delta]).
mnesia:start()
を実行します。Mnesiaテーブル
データベーステーブルの定義も1回の操作だけで行います。この操作はスキーマのコピーがあるノードのいずれかで実行します。mnesia:create_table
関数を使用して、テーブルの構造を設計したり、重要なテーブル属性を多数設定できます。次の例では、table1およびtable2を作成し、データを設定しています。(first@charlie)3> test:start_tables(). =INFO REPORT==== 4-Jan-2010::16:22:27 === application: mnesia exited: stopped type: temporary stopped
mnesia:create_table
がどのように使用されるかを確認してください。mnesia:create_table(table1, [ {type, set}, {attributes, record_info(fields, table1)}, {disc_copies, [first@charlie, second@delta]} ])
{key, value}
のリストを引数として取ります。この場合は、table1という名前の「set」テーブルを作成します。このテーブルの列は、ファイルの最初の方にあるrecord命令で以下のように定義されています。-record(table1, {table1_id, name, color, number}).
disc_copies
キーに注目してください。このキーは、テーブルのハードディスクコピーを作成する場所を指定しています。これは、Mnesiaデータベースの冗長性をテーブルレベルで調整できるということを意味します。この場合は、table1のコピーをノードfirst@charlieとsecond@deltaのハードディスク上に作成します。次のセクションでは、この関数がどのように動作するかを説明します。Mnesiaノードをテストする
second@deltaノードで以下を実行します。(second@delta)5> test:select(). [{table1,1,"record1","brown",1724}, {table1,2,"record2","orange",2367}, {table1,3,"record3","red",7834}]
(first@charlie)6> test:insert(). ok (first@charlie)7> test:select(). [{table1,1,"record1","brown",1724}, {table1,2,"record2","orange",2367}, {table1,3,"record3","red",7834}, {table1,4,"record4","orange",8888}]
mnesia:start()
でMnesiaエンジンを起動します。次に、table1のレコードを抽出します。(second@delta)2> test:select(). [{table1,1,"record1","brown",1724}, {table1,2,"record2","orange",2367}, {table1,3,"record3","red",7834}, {table1,4,"record4","orange",8888}]
ram_copies
キーまたはdisc_only_copies
キーがテーブルに対して持つ意味は推測できるでしょう。テーブルのRAMコピーをノード上に保持することは、パフォーマンス面で優れたやり方です。しかし、この方法では当然、データを永続的に保持することはできません。ここで理解すべき重要なポイントは、Mnesiaの独創的なテーブルミラーリング機能です。Mnesiaノードを追加する
私のLAN上にある第3のマシンはnemoです。以下のコマンドでErlangノード「third@nemo」を起動してから、そのノード上でMnesiaを起動します。erl -sname third -setcookie AXHMTYGVJDNJIFGYADNJ
(first@charlie)15> mnesia:change_config(extra_db_nodes,[third@nemo]).
(third@nemo)4> mnesia:system_info(). ... running db nodes = [first@charlie,second@delta,third@nemo] ... remote = [table1] ram_copies = [schema] ...
テーブルコピーを追加する
もちろん、以下のコマンドを使用して、third@nemoのスキーマテーブルの性質をその場で変更することもできます。(first@charlie)16> mnesia:add_table_copy(schema, third@nemo, disc_copies).
ここまでの説明でお分かりのとおり、ノードを起動し、ノード上にMnesiaエンジンを作成し、サービスをまったく停止せず完全に透過的にノードを追加することがごく簡単にできます。パフォーマンスを向上させ、高度な冗長性を実現するために、各ノードはスキーマとその他のテーブルの複製をそれぞれ独自に持つことができます。
トランザクション
Mnesiaが真の分散DBMSである(スキーマのいずれかのコピーに対して操作を実行すると、他の複製とフラグメントすべてに自動的に伝播される)ことは明らかですが、トランザクションデータベースとも言えるのでしょうか? 一連の操作をトランザクション、すなわち原子性、首尾一貫性、独立性、耐久性(ACID)という特質を持つ作業単位にまとめ、その作業単位でMnesiaデータベースに対して操作を実行することはできるのでしょうか? 答えはイエスです。このセクションの残りの部分では、トランザクションを実行する方法について説明します。データベースに対して「すべてか無か」の操作を実行しなければならない場合を考えてみましょう。例えば、以下の関数
atomic_op
に示すように、既存のレコード(#1)を削除して、新規レコード(#5)を挿入したいとします。atomic_op() -> Row = #table1{table1_id=5, name="record5", color="black", number=4598}, mnesia:delete({table1,1}), mnesia:write(Row).
atomic_op
関数をmnesia:transaction
関数に引き渡すことだけです。op() -> F = fun atomic_op/0, {atomic, Val} = mnesia:transaction(F), Val.
atomic_op
を変数F
にバインドし、それを引数としてmnesia:transaction
に渡しています。これで、削除と挿入という2つの操作の原子性と独立性だけでなく、首尾一貫性と耐久性も保証されます。mnesia:transaction
関数は、ロックの設定と解放など、トランザクションに影響を与える並列処理の問題をすべて管理します。ユーザーがこの種の問題を管理する必要はありません。Mnesiaは、ロックの取得に成功しなかったトランザクションを保留し、既に取得していたロックをすべて解放させることによって、デッドロックも回避します。画面上への出力(io:format
)など副次的な悪影響をもたらすコードがトランザクションに含まれている場合は、トランザクションが成功するまで多数のメッセージが標準出力に繰り返し表示されることがあります。ダーティな操作
より高速にデータにアクセスしなければならない場合のために、Mnesiaには、トランザクションのオーバーヘッドなしにテーブルを操作する「ダーティな」関数が用意されています。この関数を使用するとパフォーマンスが高まり、Mnesiaは一種のリアルタイムDBMSになります。ただし、トランザクションの原子性と独立性は著しく損なわれます。サンプルのtest.erlファイルには、以下の関数が記述されています。
op
:レコード#1を削除し、レコード#5を挿入するトランザクション関数reverse_op
:レコード#5を削除し、レコード#1を挿入するトランザクション関数mop
:opとreverse_opを指定の回数実行するdirty_op
、dirty_reverse_op
、dirty_mop
:上記の関数のダーティバージョン
Counter = 10000
にしてtest:mop(Counter)
を実行し、この関数をfirst@charlieノードとsecond@deltaノードで十分な時間動作させます。この関数は両ノードで実行され、一貫性の問題を生じることなく終了しました。同時にthird@nemoでtest:select()
を実行すると、3レコードから成るリストが常に返されます。これは、op
もreverse_op
も1回でmnesia:write
とmnesia:delete
を適用するからです。同じtestのダーティバージョンである
test:dirty_mop(Counter)
を両ノード上で同時に実行すると、多数のエラーメッセージが発生します。また、third@nemoでtest:select()
を実行すると、2レコードまたは4レコードから成るリストが返される場合もあります。クエリーリスト内包表記
Mnesiaにはテーブル内のデータとやり取りするSQL風の言語はありませんが、ErlangのQuery List Comprehensionモジュール(qlc
)を使用することで、Erlangをデータベース言語として使用し、データベースに問い合わせることができます。qlc
モジュールは、リスト内包表記という強力なErlang構文を利用します。数学的集合を定義したい場合は、集合ビルダーの表記法を用います。これは、集合の要素の特性を記述することによって、集合の要素を限定するものです。今度は、この表記法とErlangのリスト内包表記構文がどれくらい似ているかを見てみましょう。
Aは、10より小さく、かつ2乗しても自身と同じ数になる自然数(N)の集合を表しています。これを数式で表すと次のようになります。
lists:seq(0,9)
という関数呼び出しで10未満のすべての整数のリストを返すことができるので、以下の構文を使用して上述の要素のリストを取得できます。1> A = [X || X <- lists:seq(0,9), X == X * X]. [0,1]
Expression
は、Qualifier1
などの修飾子によって生成およびフィルタされた要素に対して行う一連の演算です。[Expression || Qualifier1, Qualifier2, ...]
qlc
がどのように提供しているか理解できたでしょう。ただし、このモジュールを使用する前に、システムのどこにファイル「qlc.hrl」があるかを探し、コードに以下の行を含める必要があります。-include_lib("/path_to/qlc.hrl" ).
test:select()
はtable1の全レコードからなるリストを返します。以下の行に注意してみると、Handle = qlc:q([X || X <- mnesia:table(table1)])
qlc:q
に渡している引数がリスト内包表記であることに気付くでしょう。このリスト内包表記では、テーブルの内容を返す関数mnesia:table
をジェネレータとしています。関数qlc:q
が返したクエリーハンドルは関数qlc:e
で評価され、この関数によってすべてのテーブルデータが収集され、リストとして返されます。QueryList = qlc:e(Handle).
mnesia:transaction
の内部で動作することに注意してください。関数test:join()
には、テーブルtable1とtable2を結合する簡単な例が含まれています。この部分のクエリーリスト内包表記は次のようになります。Handle = qlc:q([X#table1.number || X <- mnesia:table(table1), Y <- mnesia:table(table2), X#table1.number > 2000, X#table1.table1_id =:= Y#table2.table2_id ])
X#table1.number
は、指定されたフィールドnumber
の値を返します。X
はtable1のレコードとして評価されます。Mnesiaを探求する
Mnesiaの重要な機能をいくつか紹介してきましたが、もちろんこれですべてではありません。これまで学習してきたことが読者の好奇心を呼び起こし、Mnesiaについてもっと知りたいと思っていただければ幸いです。Mnesiaの可能性を最大限に引き出すため、ぜひ他の機能もより深く知ってもらいたいと思います。参考資料
- Erlangオンラインマニュアル
- MNESIAデータベース管理システム(プレゼンテーション)
著者紹介
Roberto Giorgetti(Roberto Giorgetti)
イタリアに拠点を置くITマネージャ、テクニカルライター。主にビジネス分野と工業分野でオープンソースの開発に従事。核工学の学位を持つ。
New Topics
Special Ad
ウマいもの情報てんこ盛り「えん食べ」 | |
「えん食べ」は、エンジョイして食べる、エンターテイメントとして食べものを楽しむための、ニュース、コラム、レシピ、動画などを提供します。 てんこ盛りをエンジョイするのは こちらから |
Hot Topics
IT Job
今週のIT求人情報