部門の勉強会でオライリーから出ている「ソフトウェアアーキテクチャの基礎」という本を読んでいる。アーキテクチャのパターンランゲージを手に入れることを主眼にしているので、第2部から読んでる。
内容的に同意できないところもあるし、「そのネーミングはどうなのか?」と思うこともしばしばだけども知識の整理にはなるし、何より「うーん、どうなの?」という内容の方が勉強会が盛り上がるというのはあるので、楽しくやっているところ。
で、次回は15章の「スペースベースアーキテクチャ」である。「すぺーす?宇宙?」という感じでまったく聞いたことがない名前なんだけれども、インメモリDBグリッドでデータ共有したたくさんのアプリケーションサーバでリクエスト数の変動幅が大きい(ときどきバーストした大量のリクエストが飛んでくるような)システムを作ろうというようなアーキテクチャパターンのことをそう呼んでいるらしい。まあ、確かに固まった名前がないといえばないような気がする。
しかし、それがなんで「スペースベース」なのかというと、これは以下のように説明されている。
スペースベースアーキテクチャの名前は、タプルスペースの概念に由来する。タプルスペースとは、複数の並列プロセッサーが共有メモリを介して通信する技術だ。
タプルスペースというのは聞いたことがない。調べてみると、Lindaというものが昔、考えられていたらしい。
Wikipediaには「プログラミング言語」だと書いてあるが、これは並列プログラミングのモデルの一種のようだ。コルーチンとかアクターとかスレッドとかミューテックスとか、そういうものの仲間な気がする。いや、それらの仲間が「タプルスペース」でタプルスペースの実装をLindaと呼ぶのかもしれない。よくわからない。
よくわからないけども、並列プログラミングを行う上での問題というのは、並列で動作しているプロシージャ同士で共有するリソースの競合をどうやって避けるかということに尽きる。共有しているリソースが何にもなければそれぞれが勝手に動いていればいい。けど、それを並列とはいわないわけで、共有するリソースへのアクセスをどうするかということが問題になる。ロックして他のプロシージャを待たせればいいという考え方もあるし、共有リソースをプロシージャ間で通信に載せてやりとりすればいいという考え方もある。
タプルスペースというのはシンプルなリクエストキューのようなもので、どんなスキーマでも格納できるストレージにputする動作と、ある特定のスキーマだけをgetする動作だけが出来て、リクエスタとコンシューマはスキーマを取り決めておくことでメッセージをやりとり出来るというような仕組み。getする側は欲しい情報がなければブロッキングするので、単純にタプルスペースからgetするよというコードだけを書いておけば、それでリクエストの待ち受け状態に出来るというわけ。
Wikipediaによると、これが提案された1986年頃には注目されたそうだが、90年代には忘れられたらしい。まったく聞いたことがなかった。
で、dRubyの関さんがLindaのRuby実装であるrindaというライブラリを書いていて、大昔からRubyの標準ライブラリに入っていて、Railsの内部でも使われていたりするらしい。まったく知らなかった・・・というか、ここまでくると聞いたことがないわけはない気もするんだけども、関心を持っていないとそんなものである。というわけで、ちょろっと触ってみることにする。
まず、1つ目のターミナルでタプルスペースを作る。
irb(main):001:0> require 'rinda/tuplespace' => true irb(main):002:0> ts = Rinda::TupleSpace.new => #<Rinda::TupleSpace:0x0000000108141eb0 ... irb(main):003:0> DRb.start_service('druby://localhost:12345',ts) => #<DRb::DRbServer:0x000000010818e030 @config= (途中略) @uri="druby://localhost:12345"> irb(main):004:0>
3行だけである。簡単だ。requireして、タプルスペースを作って、それをdRubyでURLを付けて公開する。それだけ。
では、2つ目のターミナルで、待ち受けをしてみる。
> irb irb(main):001:0> require 'drb/drb' => true irb(main):002:0> DRb.start_service => #<DRb::DRbServer:0x0000000106dc7038 @config= (途中略) @uri="druby://192.168.0.125:59142"> irb(main):003:0> ts = DRbObject.new_with_uri('druby://localhost:12345') => #<DRb::DRbObject:0x00000001067db2c8 @ref=nil, @uri="druby://localhost:12345"> irb(main):004:0> mes = ts.take([:message, nil])
今度は4行だ。requireして、dRubyを開始して、タプルスペースに接続して、メッセージを取得する。
2行目のDRb.start_serviceはなぜ必要なのかというと、dRubyでやりとりするときに暗黙のオブジェクト公開が行われることがあるからなんだそうだ。なので、オブジェクトを公開しない場合も、おまじないで引数なしのDRb.start_serviceをやっておく。
3行目が典型的なdRubyの使い方で、DRbObject.new_with_uriするとそのURLで公開されているオブジェクトに対するDRbObjectが作られる。そのDRbObjectに対するメソッド呼び出しをすると、それがネットワークを通じて元のオブジェクトへ送られることになる。ありがちなRMI(Remote Method Invocation)だと、そのメソッドの呼び出しのためのIDL(Interface Definition Language)での記述がめんどっちかったりするわけだが、なんせRubyなのでそういうものはなく、いきなりメソッド呼び出しが出来てしまう。
なので、4行目でいきなりDRbObjectであるtsに対してtakeを実行する。こちらのプロセスはたぶんDRbObjectの実体がTapleSpaceであることは知らないまま、takeを向こうに送りつけている。で、takeは引数に渡したものをパターンとしてマッチするものを返す。見つからなければ待ち続ける。nilはワイルドカードとして扱われるので、要するに2要素で1要素目に:message
が入っているものが来るのを待ち続ける。 最後の行でプロンプトが戻ってきていないのがポイントね。
では、3つ目のターミナルを開いて、メッセージを送りつけてみよう。
> irb irb(main):001:0> require 'drb/drb' => true irb(main):002:0> DRb.start_service => #<DRb::DRbServer:0x000000010134f010 @config= (途中略) @uri="druby://192.168.0.125:59454"> irb(main):003:0> ts = DRbObject.new_with_uri('druby://localhost:12345') => #<DRb::DRbObject:0x00000001013fd5e8 @ref=nil, @uri="druby://localhost:12345"> irb(main):004:0> ts.write([:message, "hoge"]) => #<DRb::DRbObject:0x00000001014a4028 @ref=78840, @uri="druby://localhost:12345"> irb(main):005:0>
さっきとの違いは、takeじゃなくてwriteでメッセージを送っていること。これをwriteした瞬間に2つ目のターミナルに以下が表示されることになる。
irb(main):004:0> mes = ts.take([:message, nil]) ## <== さっきはここで止まってた => [:message, "hoge"] irb(main):005:0>
めちゃめちゃ簡単だ。
1つのリクエスタと1つのコンシューマしかいないと何がうれしいのかわからないが、これの素敵なところはいくらでもコンシューマを待ち受けさせてよいし、いくらでも同時のプロセスからメッセージを投げ込んでよいということだ。時間がかかるIOを伴う処理(例えば、何かのリストにもとづいてインターネットのいろんな場所から何かを集めてくるとか)を並列化したり、時間のかかる計算をコア数分だけ並列化したりしたいことはちょいちょいあるが、それがものすごく簡単に実行出来るし、まったく同じプログラムでマルチプロセスにもマルチマシンにも出来る。これはとっても便利じゃないか。なんで今まで知らなかったんだろう。覚えておこう。