目次


魅力的な Python

RPyC による分散コンピューティング

多数のマシンをシームレスに制御する Python のネイティブ・オプション

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: 魅力的な Python

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:魅力的な Python

このシリーズの続きに乞うご期待。

私は 2002年に「分散コンピューティング」に関する連載記事を書きました (「参考文献」を参照)。その当時 RPyC (Remote Python Call) は存在していませんでしたが、私は Pyro という Python のツールと xmlrpclib という Python のライブラリーについて説明しました。驚いたことに、そこで説明した内容は、この 7 年間ほとんど何も変わっていません。Pyro はまだ使用されており、アクティブに保守が行われています。そして Python には相変わらず xmlrpclib があり、同様に SimpleXMLRPCServer もあります (ただしこれらは Python 3 ではリファクタリングされ、xmlrpc.clientxmlrpc.server になっています)。

RPyC がカバーしている領域は、Pyro と XML-RPC によってカバーされています。これらの長年使用されているツールに対して RPyC が何か根本的に新しいものを実際に追加するわけではありませんが、RPyC には設計上の優れた特徴がいくつかあります。

簡単に説明すると、RPyC 3.0+ には 2 つのモードがあります。つまり RPyC のバージョン 3 より前のバージョンで既に存在していた「クラシック・モード」と、バージョン 3 で導入された「サービス・モード」です。クラシック・モードには真のセキュリティー・フレームワークがないため (これは必ずしも悪いことではありません)、リモート・マシンは単にローカル・リソースであるかのように表示されます。新しいサービス・モードは、サーバーがサポートする公開インターフェースの集合を分離し、明示的に許可されたもの以外からのアクセスをすべて禁止するため、非常にセキュアです。基本的に、クラシック・モードは Pyro と同等であり (ただし Pyro のオプションとしてのセキュリティー・フレームワークはありません)、サービス・モードは RPC (例えば XML_RPC など) と同等ですが、呼び出しの規則と実装の詳細の一部が異なります。

分散コンピューティングの背景

スタンドアロンのパーソナル・コンピューティングのパラダイムでは、アプリケーションの実行に使用されるいくつかのリソースがユーザーのワークステーションに含まれています (例えばプログラムやデータを格納するためのディスク・ストレージ、CPU、揮発性メモリー、ディスプレイ・モニター、キーボードとポインティング・デバイス、そしてプリンター、スキャナー、サウンド・システム、モデム、ゲーム・パッドといった周辺入出力機器等々)。パーソナル・コンピューターにはネットワーク機能もありますが、慣習的にネットワーク・カードは、単なる入出力デバイスと見なされています。

「分散コンピューティング」とは、コンピューティング・リソースと実際のコンピューターとの関係を多様化する方法に関する流行語です。私達はかつて、「クライアント/サーバー」や「n 階層アーキテクチャー」といった言葉でコンピューター同士の階層関係を表現していました。しかし現在では、リソース同士の関係は多種多様なものになっており、階層的な関係もあれば、格子状あるいはリング状に位置する関係など、さまざまな形のトポロジーが考えられます。傾向としてはツリー状のトポロジーから多様な形状のものへとシフトしてきています。考えられる関係の一例を以下に挙げます。

  • SAN (storage-area network) は大量のコンピューターの永続的なディスク・リソースを集中化します。
  • その逆に Gnutella や Freenet などの P2P (peer-to-peer) プロトコルはデータの保存と取得を分散化します。
  • X ウィンドウ・システムと VNC (AT&T の Virtual Network Computing) を利用すると、物理的に離れた場所にあるマシンに表示装置や入力装置を接続することができます。
  • Linux Beowulf などのプロトコルを利用すると複雑な計算処理を多くの CPU で共有することができます。その一方で、SETI@Home (NASA による地球外知的生命体探査) や GIMPS (Great Internet Mersenne Prime Search) といったプロジェクトや、さまざまな暗号化「問題」では、複雑な計算処理を行う上で CPU 間の強調動作をほとんど利用しません。
  • Ajax も (Web ブラウザー・クライアントの中に限定されていますが) 多くのソースからのリソースを利用する方法の 1 つです。

昔ながらの PC アプリケーションのハードウェア・リソースであったものを分散させるプロトコルやプログラムは、分散コンピューティングの全体像の一部にすぎません。より抽象的なレベルで考えると、もっと興味深いもの、例えばデータ、情報、プログラム・ロジック、「オブジェクト」、そして究極的には責任までも分散させることができます。DBMS は、データを集中化し、データ取得を構造化するための、従来の方法です。それとは反対の NNTP そして最近の P2P では、大幅に分散化させた形で情報を保存します。その他、検索エンジンなどの技術では、その都度情報の集合を再構成し、そして再度集中化します。プログラム・ロジックは規定された計算のための実際のルールを記述し (さまざまなタイプの RMI と RPC ではそうしたルールを分散化します)、DCOM、CORBA、SOAP といったオブジェクトを仲介するプロトコルは OOP フレームワークの中にロジックの概念を再投入しています。もちろん、トリガーや制約、正規化などの機能を持つ昔ながらの DBMS でさえ、必ずある程度のプログラム・ロジックを備えています。こうした抽象的なリソースはすべて、ある時点で、ディスクやテープに保存されたり、メモリーの中に展開されたり、ビットストリームとしてネットワーク上に送信されたりします。

最終的に、分散されたコンピューターの間では一連の責任が共有されます。例えば、あるコンピューターは別のコンピューターに対して、ある状況下で特定の仕様を満たすビット列を、あるチャネルを介して送信することを「約束」します。こうした約束つまり「契約」が特定のハードウェア構成についての契約であることは稀であり、受信側の機能要件を満たすための契約であることがほとんどです。

なぜ RPyC は便利なのか

RPyC が何をするのかの概要を説明する前に、まずは RPyC を使って分散できるリソースや責任には以下の 3 つのカテゴリーがあることを示しておきます。

  1. 計算 (ハードウェア) リソース。一部のコンピューターは他のコンピューターよりも高速な CPU を持っており、また一部のコンピューターはプロセスの優先順位やアプリケーションの負荷を考慮すると他のコンピューターよりも CPU の空きサイクルを余分に持っています。同様に、一部のコンピューターは他のコンピューターよりも大きなメモリー容量とディスク・スペースを持っています (これは何らかの大規模な科学計算などを実行する場合には重要です)。またあるマシンには、特別な周辺機器が接続されており、他のマシンには接続されていないかもしれません。
  2. 情報リソース。一部のコンピューターは特定のデータに対して特権アクセスができるかもしれません。そうした特権にはさまざまな種類が考えられます。その一方、ある特定のマシンが実際のデータのソースとなるかもしれません (例えばそのマシンが、科学装置などの自動的にデータを収集してくれる何らかの装置に接続されていたり、あるいはユーザーがデータを入力する端末そのもの (キャッシュ・レジスター、チェックイン・デスク、観測サイトなど) であったりする場合など)。その一方でデータベースは、特権を持ったコンピューターのローカル・データベースであったり、あるいは少なくとも、そのマシンまたはアカウントが属する特定のグループのローカル・データベースであったりする可能性があります。そうした場合には、特権を持たないコンピューターがデータベースから得られる特定の集約データやフィルタリングされたデータにアクセスするためには、何らかの理由が必要になる可能性があります。
  3. ビジネス・ロジックに関する専門知識。どのような組織においても (あるいはどのような組織間においても)、特定の領域での判断ルールを決定する能力と責任を持っている人々 (個人や部門等) がいるものです。例えば、給与業務を担当する部門が病欠やボーナスに関するビジネス・ロジックを決定する (そして、そのロジックを時々変更する) かもしれません。あるいはデータベース管理者である Jane が、複雑なリレーショナル・テーブルからそのデータを最も効率的に抽出する方法の決定責任を持っているかもしれません。

RPyC を利用することで、こうしたすべてのリソースを分散させることができます。

RPyC のクラシック・モード

RPyC のクラシック・モードとは、リモートの Python インストールが持つすべての機能をローカルの Python システムの中で実行できるようにするものです。このモードでのセキュリティーは、基本的に、そのシステムに接続するすべてのものにシェル・アカウントを与えるのと同じです。セキュリティーが問題である場合には、暗号化ライブラリー tlslite を使うことで接続を暗号化した上で、接続にログインを要求できるようになります。つまり telnet と同等のものではなく ssh と同等のものを容易に作成できるのです。この場合の利点はもちろん、こうした接続を Python スクリプトによって制御することができ、Expect のような言語を使う場合に比べて、ローカル・リソースとリモート・リソースとの間の対話動作を確実で信頼性の高いものにできる点です。

リモート・マシン上のサーバーを起動するためには、RPyC に付属している classic_server.py を実行するだけです。セキュアな接続が必要な場合には --vdb オプションを追加します。ポートをカスタマイズするには --port を使うか、あるいはサーバーに他のオプションがないかどうかを --help を使って調べます。もちろん、一般的なシステム初期化の中や cron ジョブの一部としてサーバーを起動し、確実に指定のマシンで実行させることもできます。サーバーが稼動状態になると、クライアントをいくらでも好きなだけ、それらのサーバーに接続することができます。ただし、実際にはサーバーがそれほど特別なものではないので注意してください。1 台のマシンと 1 つのプロセスから多くのサーバーを利用することができ、同時にそのマシンとプロセスは (お互いに対称的に「サービスし合う」2 台のマシンを含めて) 多くのクライアントに対してサーバーとして動作します。

これは (いくつかのサーバーが起動された後に) シェル・セッションを見るとわかります。

リスト 1. システムと Python の情報
>>> import sys,os
>>> os.uname()        # Some info about the local machine
('Darwin', 'Mary-Anns-Laptop.local', '10.0.0d1', 
'Darwin Kernel Version 10.0.0d1: Tue Jun  3 23:40:01 PDT 2008; 
root:xnu-1292.3~1/RELEASE_I386', 'i386')
>>> sys.version_info  # Some info about the local Python version
(2, 6, 1, 'final', 0)
>>> os.getcwd()
'/Users/davidmertz'

ここで RPyC をインポートし、いくつかのサーバーに接続しましょう。これを行う前に、2 つの異なるバージョンの Python からローカル・サーバーを起動し、また 1 台のリモート・マシン上で 1 つのサーバーを起動しておきます。

リスト 2. RPyC をインポートし、サーバーに接続する
>>> import rpyc
>>> conn26 = rpyc.classic.connect('localhost')
>>> conn26.modules.os.uname()
('Darwin', 'Mary-Anns-Laptop.local', '10.0.0d1',
'Darwin Kernel Version 10.0.0d1: Tue Jun  3 23:40:01 PDT 2008; 
root:xnu-1292.3~1/RELEASE_I386', 'i386')
>>> conn26.modules.sys.version_info
(2, 6, 1, 'final', 0)
>>> conn26.modules.os.getcwd()
'/Users/davidmertz/Downloads/rpyc-3.0.3/rpyc/servers'
>>> conn25 = rpyc.classic.connect('localhost',port=18813)
>>> conn25.modules.os.uname()
('Darwin', 'Mary-Anns-Laptop.local', '10.0.0d1',
'Darwin Kernel Version 10.0.0d1: Tue Jun  3 23:40:01 PDT 2008; 
root:xnu-1292.3~1/RELEASE_I386', 'i386')
>>> conn25.modules.sys.version_info
(2, 5, 1, 'final', 0)
>>> conn25.modules.os.getcwd()
'/Users/davidmertz/Downloads/rpyc-3.0.3/rpyc/servers'
>>> connGlarp = rpyc.classic.connect("71.218.122.169")
>>> connGlarp.modules.os.uname()
('FreeBSD', 'antediluvian.glarp.com', '6.1-RELEASE',
'FreeBSD 6.1-RELEASE #0: Fri Jul 18 00:01:34 MDT 2008;
root@antediluvian.glarp.com:/usr/src/sys/i386/compile/ANTEDILUVIAN',
'i386')
>>> connGlarp.modules.sys.version_info
(2, 5, 2, 'final', 0)
>>> connGlarp.modules.os.getcwd()
'/home/dmertz/tmp/rpyc-3.0.3/rpyc/servers'

これを見るとわかるように、ここでは異なるバージョンの Python をインストールした何台かの異なるマシンに接続しています。ここでは適当な機能や属性にアクセスしていますが、重要なポイントは、こうしたマシン上にある任意の関数やクラスを呼び出せるという点です。そのため、例えば antediluvian.glarp.com というマシンの中に Payroll という Python モジュールがインストールされており、このモジュールの中に get_salary() という関数があることがわかっている場合には、次のように呼び出しを行います。

リスト 3. get_salary() を呼び出す
>>> connGlarp.modules.Payroll.get_salary(last='Mertz',first='David')

antediluvian というマシンにはローカル・データベースがインストールされているかもしれず、さらには antediluvian が独自に他のリソースに接続を行っているかもしれません。しかしこの関数を呼び出したことによって返されるものは、この関数が antediluvian でローカルに実行された場合に返されるものと同じデータなのです。

リモート・マシンにコードを配置する

標準的なモジュール関数をリモート・マシン上で実行できると確かに便利ですが、独自のコードをリモートで実行できるともっと便利だと思うことがよくあるものです。RPyC のクラシック・モードでは、それをいくつかの方法で実現することができます。おそらく最も直接的な方法としては、先ほど確立した接続を介して、そのマシンに対して Python シェルを開く方法です。例えば次のようにします。

リスト 4. リモート・マシン上で Python シェルを開く
>>> conn = rpyc.classic.connect('linux-server.example.com')
>>> rpyc.classic.interact(conn)
Python 2.5.2 (r252:60911, Oct  5 2008, 19:24:49)
[GCC 4.3.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> #... run any commands on remote machine

そうしたリモート・シェルから任意の関数やクラスを定義することができ、任意の外部コードをインポートすることもでき、あるいはその他何でもローカルの Python シェルから行うことができます。

また、deliver() という RPyC 関数もあります。この関数はローカル・オブジェクトをリモート・サーバーに送信するものらしく、リモート・サーバーではローカル・コンテキストで実行されるようです。残念ながら、私はこの関数を期待どおりに動作させることはできませんでした (おそらく私が少しばかり構文を間違えていると思うのですが、ドキュメントが明確ではありません)。間に合わせの方法として、リモート・サーバーでコードを直接実行する (あるいはコードに対して eval() を実行する) ことができます。例えば次のようにします。

リスト 5. リモート・マシンでコードを実行する
>>> # Define a function (or class etc) as actual source code
>>> hello_txt = """
...  def hello():
...     import os
...     print "Hello from", os.getcwd()
...  """
>>> exec hello_txt            # Run the function definition locally
>>> hello()
Hello from /tmp/clientdir
>>> conn.execute(hello_txt)   # Run the function definition remotely
>>> remote_hello = conn.namespace['hello']
>>> remote_hello()            # Displays on remote server
>>> with rpyc.classic.redirected_stdio(conn): # Redirect to local client
...     conn.namespace['hello']()
...
Hello from /tmp/serverdir

ここで何を行ったかと言うと、関数をコンパイルしてリモートの名前空間に入れ、その関数をあたかもローカル関数であるかのように実行させているのです。しかしリモートの print を実行することによる出力を実際に見たい場合にはリモート・コンソールの出力も取得する必要があります。ただし print ではなく hello() が値を返す場合には、先ほどの例と同じように、その値はやはりローカルのコンテキストに返されます。

一時しのぎのパッチ

上記で with コンテキストを使って行ったことは、基本的に一種の「一時しのぎのパッチ」です。つまりリモートの STDIO の代わりにローカルの STDIO を一時的に使用したのです。RPyC ではこれを一般的に、システム・リソースに対して行うことができます。コードはローカル・マシン上で実行される (つまり CPU とメモリーを使用する) かもしれませんが、それでもリモート・マシン上の何らかの主要なリソースを使用します。これはサーバーの方が CPU リソースを多く持っている場合には計算リソースの面でコストの高い関数呼び出しかもしれませんが、異なるドメインにアクセスできるソケットがリモート・マシンにあり、そのソケットをリソースとして利用したい、といったことがよくあるものです。例えば次のような場合です。

リスト 6. 一時しのぎのソケット接続パッチ
import myserver
c = rpyc.classic.connect('machine-outside-firewall')
myserver.socket = c.modules.socket
# ...run myserver, which will now open sockets outside firewall

非同期リソース

リモート・マシン上のリソースを利用すると時間がかかってしまう場合には、そのリソースを使用するローカル・プログラムはリモート・アクションが完了するまでの間、止まったように見えるかもしれません。しかし、そうした呼び出しを非同期にすれば、必ずしもそうはならないはずです。結果が準備できたかどうかをリモート・オブジェクトにレポートさせ、それまでの間に他のローカル・アクション (あるいは他のリモート・サーバーを利用するアクションなど) を実行すればよいのです。

リスト 7. 非同期呼び出し
>>> conn.modules.time.sleep(15)     # This will take a while
>>> # Let the server do the waiting
>>> asleep = rpyc.async(conn.modules.time.sleep)
>>> asleep
async(<built-in function sleep>)
>>> resource = asleep(15)
>>> resource.ready
False
>>> # Do some other stuff for a while
>>> resource.ready
True
>>> print resource.value
None
>>> resource
<AsyncResult object (ready) at 0x0102e960>

この例の中にある resource.value は特に変ったことをするものではありません。しかし非同期に作成されたリモート・メソッドから値が返されると、resource.ready が True になれば、その返された値は利用可能になります。

RPyC のサービス・モード

新しい、RPyC のサービス・モードに関する説明は少しにとどめることにします。実際にクラシック・モードがより汎用的なシステムだからです (ただしクラシック・モード自体は新しいスタイルのサービスとして、ほんの数行のコードで作成されています)。RPyC の下でのサービスは XML-RPC (あるいは何らかの RPC) と、ほとんど変わりません。クラシック・モードは単にリモート・システム上のすべてを公開するサービスですが、少しのものだけを公開するサービスをわずかな行数のコードで作成することができます。例えば次のようなことができます。

リスト 8. サービス・モードを使っていくつかのメソッドを公開する
import rpyc
class DoStuffService(rpyc.Service):
   def on_connect(self):
       "Do some things when a connection is made"
   def on_disconnect(self):
       "Do some things AFTER a connection is dropped"
   def exposed_func1(self, *args, **kws):
       "Do something useful and maybe return a value"
   def exposed_func2(self, *args, **kws):
       "Like func1, but do something different"

if __name__ == '__main__':
   rpyc.utils.server.ThreadedServer(DoStuffService).start()

クライアントから見ると、このサービスは単なるクラシック・モードのサーバーのように見えますが、このサービスによって公開されるものは exposed_ という接頭辞が付いたメソッド (ただし公開時は exposed_ 接頭辞を除いたメソッド名になります) のみである点が異なります。他のメソッド (組み込みモジュールなど) を利用しようとすると失敗します。そこでクライアントは次のようになります。

リスト 9. サービスが公開されているメソッドを呼び出す
>>> import rpyc
>>> conn = rpyc.connect('dostuff.example.com')
>>> myval = conn.root.func1()   # Special 'root' of connection
>>> local_computation(myval)

まとめ

RPyC は私が説明した以上にさまざまな機能を持っています。例えば RPyC は Pyro と同じように「レジストリー」を提供します。このレジストリーを使ってサービスに名前を付けることができ、ドメイン名や IP アドレスなどではなく名前でサービスにアクセスできるようになります。これは単純であり、RPyC のドキュメントの中に説明されています。

この記事で説明したとおり、また RPyC 自体のドキュメントに明確に書かれているとおり、RPyC は多くの点で「もう 1 つの RPC パッケージ」です。例えば、上記で作成した簡単なサービスは SimpleXMLRPCServer を使って作成する場合のコードとほとんど同じです。唯一の違いはリクエストと結果に使われるワイヤー・プロトコルです。いずれにせよ、私が遭遇したような些細な問題はいくつかあるものの、RPyC は適切に構築されており、また非常に簡単に実行させることができます。ほんの数行の RPyC コードを使うことで、リソースや責任の安全かつ確実な分散化を容易に実現することができます。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux, Open source
ArticleID=385319
ArticleTitle=魅力的な Python: RPyC による分散コンピューティング
publish-date=03312009