DRuby別名DRb-Rubyの分散システムの基礎。 仕事の原理と落とし穴

最近、dRubyブック-Rubyによる分散並列コンピューティング(ライブラリの作成者自身が書いた日本語の本の翻訳)がリリースされました。 この記事では、DRbライブラリに関する本の章の概要を説明します。 このトピックをさらに詳しく知りたい場合は本を購入またはダウンロードできます 。 この投稿ではスレッドの同期についても、Rindaライブラリについても説明しません。



複数のプロセスで動作するシステムを作成しているとします。 たとえば、バックグラウンドで長時間実行されるタスクを実行するWebサーバーがあります。 または、あるプロセスから別のプロセスへのデータの転送を確実にし、それらを調整する必要があります。 このような状況では、DRbライブラリが必要です。 完全にRubyで記述されており、標準ライブラリに含まれているため、すぐに作業を開始できます。 接続するには、 require 'drb'



書くだけrequire 'drb'







DRbライブラリの長所は、主にRuby言語自体のダイナミズムにあります。

第一に、準備段階で最小限の労力を費やす場合、あるプロセスまたは別のプロセスで、ためらうことなくオブジェクトのある場所で作業します。 ライブラリは、すべての技術的な詳細を完全に隠しています。

第二に、インターフェイスをハードコードする必要はありません。 rubyオブジェクトは、そのインターフェイスを外部に公開できます。この方法では、どちらもHash



Queue



などの標準クラスのいずれかの機能を使用したり、任意のインターフェイスで独自のクラスを作成したりできます。 さらに、実行中にインターフェイスを直接変更したり、 method_missing



を使用してmethod_missing



を処理することもできます。 そしてもちろん、クライアントが署名または動作を変更したメソッドを呼び出さない場合、サーバーインターフェイスの更新はクライアントにまったく影響しません。 したがって、サーバーとクライアントは可能な限り独立しています。

そして最後に、クライアントはサーバーから返されるオブジェクトのクラスを知る必要さえありません。クライアントはそれを使用せずに使用できます。 したがって、サーバーは必要なだけ詳細を非表示にできます。

しかし、もちろん、落とし穴があり、それらはたくさんあります。 幸い、dRubyの理解は簡単ですが、その構造を理解することで、ほとんどの問題を簡単に防ぐことができます。 残念ながら、このライブラリのドキュメントでは多くの点が明確にされていないため、この記事は初心者と既にライブラリを使用したことがある人にとって興味深いものになります。



すべてが機能することを確認するには、2つのirb端末を開きます。 ルビー1.8の違いがどれほど大きいかわからないので、バージョン1.9について議論していることに同意しましょう(特に1.8以降-すぐにサポートが終了します!



条件付きで、これら2つの端末はサーバーとクライアントです。 サーバーは、リクエストを受信するフロントオブジェクトを提供する必要があります。 このオブジェクトには、組み込み型のオブジェクト、特別に作成されたインターフェイスを持つモジュールなど、任意のオブジェクトを指定できます。 次に、クライアントはサーバーに接続し、このオブジェクトと対話します。

たとえば、最初の端末でサーバーを起動し、通常の配列を配置しましょう。



 require 'drb' front = [] DRb.start_service('druby://localhost:1234', front) front << 'first' #       ,    -    DRb.thread.join
      
      





クライアントを接続します。 配列の最初の要素を認識し、別の要素を配列に書き込みます



 require 'drb' DRb.start_service remote_obj = DRbObject.new_with_uri('druby://localhost:1234') p remote_obj p remote_obj[0] remote_obj << 'second'
      
      







これで、最初の端末からfront[1]



呼び出して、文字列'second'



ます。 また、別のクライアントを接続し、そこから同じ正面オブジェクトを操作することもできます。



既にDRb.start_service



に、サーバーはDRb.start_service



コマンドによって起動されます(レジスタに注意してください!)。 このメソッドは、引数として'druby://hostname:port'



という形式'druby://hostname:port'



アドレスとフロントエンドオブジェクトを持つ文字列を取ります。 フロントエンドオブジェクトは、リクエストを受信するオブジェクトです。

サーバーが別のスクリプトで起動したら、スクリプトの最後にDRb.thread.join



を記述します。 実際、DRbサーバーは別のスレッドで起動され、メインスレッドが完了するとすぐにRubyがプログラムをシャットダウンします。 したがって、メインスレッドがDRbサーバーストリームのクローズを待たない場合、両方のストリームがスケジュールより早く完了し、サーバーはすぐに利用できなくなります。 DRb.Thread.join



メソッドを実行すると、サーバーの電源が切れるまで現在のスレッドがブロックされるという事実に備えてください。



サーバーに接続するには、 DRbObject.new_with_uri



メソッドを呼び出し、サーバーが起動するアドレスを引数として渡す必要があります。 このメソッドは、 remote_obj



プロキシオブジェクトを返します。 プロキシオブジェクトへのリクエスト(メソッド呼び出し)は、リモートサーバー上のオブジェクトに自動的に転送され、呼び出されたメソッドがそこで実行された後、メソッドを呼び出しているクライアントに結果が返されます。 (ただし、すべてのメソッドがサーバーで呼び出されるわけではありません。たとえば、動作によって判断すると、 #class



メソッド#class



ローカル#class



実行されます)

クライアントは、 DRb.start_service



コマンドの意味DRb.start_service



少し後で説明します。



最後に、リモートオブジェクトのメソッドがどのように実行されるかを理解しましょう。 これを行うには、プロキシオブジェクトのメソッドを呼び出すと、メソッドの名前と引数のリストがシリアル化(マーシャリング)され、TCPプロトコルを使用して結果の文字列がサーバーに転送されます。サーバーは呼び出し引数を逆シリアル化し、フロントエンドオブジェクトでメソッドを実行し、結果をシリアル化してクライアントに返します。 すべてがシンプルに見えます。 実際、通常のリモートオブジェクトと同じ方法でリモートオブジェクトを操作し、メソッド、プロキシオブジェクト、およびサーバーのリモート実行のための多くのアクションが非表示になります。



しかし、それほど単純ではありません。 リモートメソッドの呼び出しは高価です。 サーバー上のメソッドが多くの引数メソッドを「プル」することを想像してください。 これは、サーバーとクライアントが計算を行う代わりに、時間の大部分が比較的遅いプロトコルを使用して互いにアクセスすることを意味します(2つのプロセスが同じマシンに配置されていると便利です)。 これを防ぐために、プロセス間の引数と結果は参照ではなく値で渡されます(オブジェクトのマーシャライゼーションはオブジェクトの内部状態のみを格納し、そのobject_id



認識しません-したがって、オブジェクトは最初にシリアル化され、その後、逆シリアル化は元のオブジェクトのコピーになります、同じオブジェクトによるものではないため、転送はコピーによって自動的に行われます)。 Rubyでは通常、すべてが参照によって渡され、dRubyでは通常は値によって渡されます。 したがって、 front[0].upcase!



を実行すると サーバーでは、 front[0]



値が変更され、 remote_obj[0].upcase!



を実行すると 、最初の要素を大文字で取得しますが、 remote_obj.[](0)



は最初の要素のコピーであるため、サーバー上の値は変更されません。 この呼び出しは、 front[0].dup.upcase!



と同様と見なすことができますfront[0].dup.upcase!





ただし、引数と結果を参照で渡すような方法でdRubyの動作をいつでも定義できますが、これについては後で詳しく説明します。



今こそ、最初の問題について話し合うときです。 すべてのオブジェクトが整列化されるわけではありません。 たとえば、ProcおよびIOオブジェクト、およびスレッド(スレッドオブジェクト)は、コピーを介してマーシャリングおよび送信できません。 この場合のdRubyは次のように進みます。マーシャライゼーションが機能しなかった場合、オブジェクトは参照渡しされます。

では、オブジェクトはどのように参照渡しされますか? Cを思い出してください。 そこでは、ポインターがこの目的に使用されます。 Rubyでは、ポインターの役割はobject_id



によって実行されます。 オブジェクトを参照渡しするには、 DRbObject



クラスのオブジェクトがDRbObject



ます。

実際、 DRbObject



は参照渡しのプロキシオブジェクトです。 このクラスのインスタンスDRbObject.new(my_obj)



object_id



オブジェクトmy_obj



object_id



オブジェクトのmy_obj



のサーバーのURIが含まれています。 これにより、メソッド呼び出しをインターセプトして、メソッドが意図されたリモートマシン(または別の端末)上のオブジェクトに渡すことができます。



サーバーをメソッドにしましょう



 def front.[](ind) DRbObject.new(super) end
      
      





そして、クライアントからコードを実行します。



 remote_obj.[0].upcase!
      
      







新しいメソッド#[]



は、最初の要素のコピーではなく、リンクを返しました。そのため、 upcase!



メソッドを実行した後にupcase!



フロントオブジェクトが変更された場合、これは、たとえばクライアントとサーバーからそれぞれputs remote_obj



またはputs front



コマンドを実行することで簡単に確認できます。



しかし、 DRbObject.new



を書くDRbObject.new



-怠DRbObject.new



です。 幸いなことに、値ではなく参照によってオブジェクトを渡す別の方法があります。 これを行うには、オブジェクトを非整列化可能にすれば十分です。 これは簡単で、オブジェクトにDRbUndumped



モジュールを含めるだけです。

 my_obj.extend DRbUndumped class Foo; include DRbUndumped; end
      
      





これで、オブジェクトmy_obj



Foo



クラスのすべてのオブジェクトが参照によって自動的に渡されます(そしてMarshal.dump(my_obj)



TypeError 'can\'t dump'



TypeError 'can\'t dump'



ます)。



実際に出会った例を挙げましょう。 サーバーはハッシュをフロントオブジェクトとして設定します。この場合、値はチケットです(チケット内からはステートマシンです)。 次に、 remote_obj[ticket_id]



はチケットのコピーを提供します。 ただし、これによりサーバー上のチケットステータスを変更することはできず、ローカルでのみ変更できます。 Ticket



クラスにDRbUndumped



を取得しましょう。 これで、ハッシュからチケットのコピーではなく、チケットへのリンクが得られます。そして、それに対するアクションは、クライアント上ではなくサーバー上で直接発生します。



そして今、約束を思い出して、クライアントでDRb.start_service



を呼び出す必要がある理由を教えてください。 最初の例のように、サーバー上のフロントオブジェクトによって配列が示されることを想像してください。

ここで、クライアントがremote_obj.map{|x| x.upcase}



メソッドを呼び出しますremote_obj.map{|x| x.upcase}



remote_obj.map{|x| x.upcase}





実際、ブロック引数を持つmapメソッドは、フロントオブジェクトで呼び出されます。 そして、私たちが思い出すように、私たちはマーシャリングできません。 したがって、このブロック引数は参照渡しされます。 サーバー上のmap



メソッドはyield



でアクセスします。これは、クライアントがサーバーであることを意味します! ただし、クライアントは時々サーバーである必要があるため、 start_service



メソッドを使用してDRbサーバーも起動する必要があります。 このサーバーのURIを指定する必要はありません。 内部からどのように機能するか、私にはわかりませんが、機能します。 そして、すでにお気付きのように、クライアントとサーバーの違いは見た目よりも小さいです。



新しい迷惑につまずくリスクがあります。 メソッドが、メソッドで直接生成されたオブジェクトへのリンク(コピーではなく)を返したとします。 サーバーがこのオブジェクトを別の場所に個別に保存しなかった場合(たとえば、特別なハッシュに入れなかった場合)、サーバーへのリンクはありません。 リモートマシン上のクライアントは持っていますが、サーバーは持っていません! したがって、遅かれ早かれ、このオブジェクトのメールのためにトロールがあなたのところに来ます。GC-ガベージコレクターです。 これは、しばらくすると、クライアントのDRbObject



ようなリンクが「不良」になり、どこにもリンクされないことを意味します。 このオブジェクトのメソッドにアクセスしようとすると、エラーが発生します。

したがって、少なくともサーバーによって使用されるまで、返されたオブジェクトへのリンクをサーバーが保管するように注意する必要があります。 これにはいくつかの解決策があります。

1)参照によって渡されたすべての返されたオブジェクトを配列に保存します-リンクが使用されるため、ガベージコレクターはそれらを収集しません。

2)クライアントにブロックへのリンクを送信します。 例:

このコードの代わりに:

 Ticket.send :include, DRbUndumped def front.get_ticket Ticket.new end foo = remote_obj.get_ticket foo.start foo.closed? #   foo       . ,   start   .
      
      







次のように書かなければなりません:

 Ticket.send :include, DRbUndumped def front.get_ticket object_to_reference = Ticket.new yield object_to_reference end remote_obj.get_ticket do |foo| foo.start foo.closed? end
      
      





サーバー上の有効なローカル変数は、ガベージコレクターによって収集できません。 したがって、ブロック内では、リンクが機能することが保証されています。

3)この本は別の方法を説明しています-オブジェクトのobject_id



受け取る段階でリンクを作成するプロセスにobject_id



この時点でガベージコレクションプロセスを何らかの方法で遅らせる必要があります。 ハッシュに要素を自動的に追加し、オブジェクトを永久に保存することができます(推測できるように、メモリは遅かれ早かれ使い果たされるでしょう)、オブジェクトへのリンクを保存して手動でクリアできます。このハッシュは数分ごとにクリアできます。

最後のメソッドは、次のようにして実装できます。

 require 'drb/timeridconv' DRb.install_id_conv(DRb::TimerIdConv.new)
      
      





サーバーを起動する前に。 詳細については、本の第11章-ガベージコレクションの処理を参照してください。 私には興味深いようです。おそらくそれを読んだ後、ガベージコレクションプロセスの操作を使用する新しい方法が見つかるでしょう。 それでも、実際には2番目の方法を使用して、ブロックへのリンクを与える方が良いと思います。 より信頼性が高く理解しやすい。



おそらく最後の瞬間を照らすために残っています。 Foo



オブジェクトをリンクとして渡すと仮定します。 クライアントはFoo



クラスについては知りませんが、それでもオブジェクトでの動作を妨げることはありません。 基本的に、クライアントはDRbObject



クラスのオブジェクトを操作します。 すべてがいつも通りです。

ここで、リンクではなくコピーを送信していると想像してください。 サーバーでのシリアル化により、オブジェクトの状態とそのクラスの名前が保存されました。 クライアントは文字列を受信し、それをデシリアライズしようとしています。 もちろん、クライアントは存在しないクラスFoo



オブジェクトを作成できないため、これは彼には機能しません。 次に、逆シリアル化DRb::DRbUnknown



DRb::DRbUnknown



型のオブジェクトが返され、マーシャリングされたオブジェクトと共にバッファーが格納されます。 このオブジェクトは(たとえば、タスクキューに)渡すことができます。 また、クラスの名前を確認し、適切なライブラリをクラスにロードして、 reload



メソッドを呼び出すこともできます。その後、逆シリアル化が再度試行されます。 それは



いいえ、まだこれが最後の瞬間ではありません。 同期については書かないことを約束しましたが、それでもいくつか言葉を述べます。

分散プログラミングの場合、アクションの同期と操作の原子性は重要な概念です。 サーバーは別のスレッドで起動します。 サーバーへのリクエストごとに、このリクエストが処理される個別のストリームが自動的に作成されます。 したがって、異なるスレッドが同じ情報に同時にアクセスすることを禁止する必要があります。 そのため、分散システムおよび並列システムをプログラミングする場合は、次を使用します。

1)構築lock = Mutex.new; lock.synchronize{ do_smth }



lock = Mutex.new; lock.synchronize{ do_smth }





2)標準MonitorMixin



ライブラリのモジュール

3)標準ライブラリクラスのQueue



SizedQueue







DRbを使用して頑張ってください! 破壊的な方法を使用しているにもかかわらず、オブジェクトが変更されない理由、ブロックを受け入れる方法をクライアントで機能させる方法、受信したリンクが機能し動作した理由を理解しようとして、誰かが長い時間を費やさないように願っています。

ただし、この本では、特にRindaライブラリとその同等物について、さらに多くを見つけることができます。



All Articles