日曜日, 7月 08, 2012

RESTに関する3つの間違い このエントリーを含むはてなブックマーク

楽観的排他制御を利用する非同期的なトランザクション実行であればスケーラビリティを損ねることなく2phase commitが可能である。これは、分散KVSにおけるスケーラビリティと一貫性の両立について で主張したように、同期的な2phase commitは密結合に誘導することになるため、矛盾するように思えるかもしれない。だがそんなことはない。
前半はまずこの話から入るが、後半ではRESTに関する間違いについて、3つほど思うところを述べたい。

楽観的排他制御と2phase commit

 reflexworksではFeedやEntry単位でatomicなトランザクション処理を行えるが2phase commitはサポートしていない。これを許すと密結合になってスケールしないからである。だが、これはあくまで同期的な処理の話であって、ネットワーク障害への耐性を考慮され、非同期処理やオフラインで使えるのであれば2phase commitでもスケールすると考えている。
 RESTにおける2phase commitについては、REST におけるトランザクションについて (Re: Web を支える技術) が詳しい。
トランザクションレイヤを RESTful に表現する手法は理解しておいて損がないものだと思います。具体的には、
  • トランザクションの開始は POST を使ったトランザクションリソースの生成
  • トランザクション開始後は POST ではなく PUT を使い、クライアントがリクエスト ID を指定することで、ネットワーク障害への耐性を確保
  • トランザクションのコミットも再送可能じゃないと困るので PUT
  • コミットの返り値を確認してからトランザクションリソースを DELETE
といったあたりが基本になると思います。
ポイントは、クライアントがリクエスト ID を指定する点。IDはPOSTで返却されたリソースIDになるが、楽観的排他制御のためには、論理シーケンス番号 (LSN)も同時に必要になるはずだ。
もう少し具体的に説明しよう。

reflexworksでは、LSNに該当するものはリビジョン番号であり、POST /buy でリソースURL(/buy/934085,1)が返却される。(この,1がリビジョン番号で更新の都度+1される)次に、 PUT /buy/934085,1 を実行。もし既に更新されていたらリビジョン番号不一致となるため、409 conflict(optimistic locking failed)が返る。DELETEも同じくリビジョン番号を指定して実行する。
前述の例では自分しか更新しない前提ではあるが複数回実行される可能性は考慮すべきところで、べき等性の保証が必要なのだが、この場合は楽観的排他エラーで「既に更新されています」というメッセージを出してあげる方が親切だと思われる。
ちなみに、同一リソースURLでリビジョン番号も同じであれば、システム全体で同じものであることが保証されている。これは、複数のノードに散在するデータの整合性を保証するためのreflexworksの基本的な考えにもなっている。(エンタープライズクラウドにおけるスケールアウトの現実解

また、POSTはリソース作成時にのみ使い、PUTは既存のリソース更新に使うのはrelfexworksも同様である。「 一般に、リソース作成時に PUT を使うべきでないとされる理由は、クライアント間で名前の競合が発生する可能性があるから」という理由はもちろんだが、CRUDの4つは明確に区別したいという理由もある。SQLにおいても、INSERTとUPDATEは区別されている。

RESTはステートレスでなければならないか

さて、ここからRESTの3つの間違いの話に入る。
ステートレスはRESTの原理原則であり、実際に疎結合にすることでシステムはスケールラブルになる。私は、ステートレスをスケーラビリティ確保の絶対条件と考え、reflexworksではセッションは使わない方針を貫いてはいた。そして、ログイン処理においてさえセッションIDは使わずに都度認証することを徹底していた。たしかにステートレスにするとスケールはする。だが、最近は度が過ぎるとかえって不自由な設計を招いてしまうことになることがわかってきて、逆にセッションはアリなんじゃないかと思うようになってきた。

はっきりいおう。私は間違っていた。


一つは、ユーザログイン管理において毎回認証を実行させる必要があり、擬似的なセッションIDを発行する必要があったこと。具体的には、認証に成功したら新たに時間制限をつけたcookieチケット(ワンタイムパスワード)を発行し擬似的なセッションIDで対応していた。また、そのチケットはあくまでログイン認証用であり、セッションオブジェクトは管理しないようにしていた。ユーザにログイン入力を強要させるわけでもなく、名前もStateless Session ID(SSID)にしていた。
 しかし、ユーザIDとワンタイムパスワードを含むトークンを毎回クライアントと通信してやりとりするのはセキィリティ上よろしくない。私は頑にステートレスを守る一方でセキュリティをないがしろにしていたわけだ。
 また、パフォーマンスも悪くなる。
前述した2phase commitでは、払い出したチケット(/buy/934085,1)は一時的な揮発性の情報であり、これらはむしろセッションオブジェクトかキャッシュメモリに置くべきである。こうすることで、パフォーマンスも格段によくなり、KVSとの間にメモリという緩衝材が入ることでI/O負荷も軽減できる。
というわけで、最近はRESTであっても必要最低限のセッションオブジェクトだけは認めてもいいと考えるになった次第である。実際に最新版のreflexworksでは標準のセッションオブジェクトが使えるようになっている。また、一時的なオブジェクトはなるべくキャッシュさせるような設計を推奨している。

リソースは本当にドメインロジックを必要としないか

2つ目の間違いはリソースとドメインロジックの置く場所についてである。
MVCの話のなかでも触れてはいるが、ドメインロジックはバウンダリだけでなくコントロールやエンティティの層においても構わない。



にもかかわらず、当初のreflexworksでは、リソースを特定するURLに対して単純なHTTPリクエストのCRUD操作(GET,PUT,POST,DELETE)しか用意していなかった。
これだと、例えばECサイトの金額チェックなどをサーバ側で行うことができない。金額を正確にチェックするにはサーバ側でマスター参照する必要があるはずである。たとえ認証を付けたところで、リクエストデータが正しいかどうかという話は別である。どんなに頑張ってクライアントでチェックしてもサーバ側では無条件では受付けられない。
だからといって、自由にサーバ側のインターフェースを作らせたくはない。
サービスを自由に作らせると、きっとこんな感じのインターフェースになってしまうだろう。

entry = getFoo(entry)
entry = updateFooByBar(entry)
・・・

これでは、サービス志向の二の舞になってしまう。せっかく、RESTでリソースを一意に定義できているのだから、こういう感じにしたい。

entry = Foo.service(entry)

 例えば、GET  /p/Foo?item=abc などと実行すると、/p servletが呼び出され、URLパラメータとentityにもとづいて処理されるといった感じになる。/p servletの中では、reflexContext.get("Foo");とすることで実際のリソースにアクセスできる。(もちろん、POST,PUT,DELETEメソッドもある)

ちなみに、/d/Fooに対して実行すると、/d servletが呼び出され、以前のバージョンと同様に、サービスは実行されずにリソースの内容がそのまま返される仕組みとなっている。(/dはdata、/pはproviderの頭文字)

最近では、単純にデータだけが提供される、BaaS(Backends as a Service)やDBaaS(Database as a Service)が流行っているが、本当にサービスのドメインロジックが必要ないか、よく検討して使うべきだろう。

イベントドリブンなアーキテクチャーは受け入れられるか

先日、MOVEが話題になった。
コントローラをOperationsとEventsに分けた方がいいという案だが、望まれなかった子にあるように、MVCの一つの解釈(あるいは間違えているの)であり、MVCを否定するものではない。
ただ、closureや、あるいは、Deferredのような非同期処理をなんとか中心に考えたいという気持ちもわからないでもない。

実は、reflexworksでもイベントドリブンな設計はいけると思った時期があった。実際にイベントをトリガーにサーバサイドJSを実行できる。でも開発者の多くが難しいと感じ、オーソドックスな方を好んだので、今は廃止しようと考えている。オーソドックスな方とは、前述したような単純な/p servletによるフィルターのような処理のことである。

単にユニークであることが使いやすいとは限らない。




また、コントローラをOperationsとEventsに分けるより大事だと思うのは、Modelをデータ(スキーマ)とドメインロジックに分けることである。スキーマは一貫させる一方でドメインロジックは各レイヤの最適な場所に配置させる。貧血症みたいなオブジェクトにはなってしまうが、それを恐れてはいけない。

以上が、RESTに関して私が犯した間違いの3点である。
だが、もしかしたら、こうやって原理原則を修正した結果、RESTのアーキテクチャーとは到底呼べないものになってしまっているかもしれない。だがそれでも構わない。なぜなら、これらは実務に落としたときに実際に問題となって一つ一つ解決してきたものだからである。

今流行のリーンスタートアップ流にいうなら、クールなアーキテクチャだと思って採用(Build)するのは結構なことだが、測定(Measure)して、学習(Learn)してから最終的に判断しないとだめである。それが本当に素晴らしいかどうかは、実務に落としたとき初めてわかるものだから。

<ご紹介>
reflexworks           <= 製品についてはこちら
reflexworksで分業開発しよう!  <= 開発案件募集中です


月曜日, 7月 02, 2012

KVS上でアプリを動作させるために必要なたった2つのこと このエントリーを含むはてなブックマーク

先の記事で、ReflexWorksにおけるデータ操作の方法について触れ、ツリー構造の仮想フォルダ管理とREST APIによるデータアクセスについて説明した。ここでは、もう少し掘り下げて、KVS上でアプリを動かすための条件とは何なのかについて考えてみたい。(たった2つのことといいながら長文失礼します)

SQL vs NoSQL: Battle of the Backends

タイムリーにも、Google IO でSQL vs NoSQL: Battle of the Backendsというセッションがあったようだ。ここでは、SQL(MySQL)とNoSQK(Datastore)を、Queries、Transactions、Consistency、Scalability、Management、Schemaの6つの項目について比較していて、前半の3つについてはSQLが、後半の3つについてはNoSQLが優れているという結果になっている。最後は両者同数でどっちもどっちだね、みたいな話になっているが、セリングトークも含まれるのでこれは真に受けて欲しくないとにかく、これまで主張してきた通り、トランザクションが増えるとRDBがボトルネックになり、オンライン処理ではそのうち限界になってくるはずだ。
 また肝心のパフォーマンスについては、「基幹系システムでCloud SQLは使えるか試してみた」を見てもわかるように、Datastoreとはまだ相当の差があるようだ。

とはいえ、丸山先生もおっしゃっているように、GAEでもMySQLが使えるようになったことは、まあ、それはそれで結構なことだとは思う。(ということでお茶を濁す)

アプリにKVSを意識させないことが大事

さて、今WebアプリといえばHTML5などのリッチコンテンツが主流である。クライアント上のネイティブアプリのように小気味好く動作することが求められている。またスマートデバイスではオフラインや非同期更新などもある。もはや、JSPやJSF、あるいはPHPでHTMLを動的に作るといった古いアーキテクチャーは通用しない。エンジニアは利用者に最大限のエクスペリエンスを与えようと頑張っており、UI/UXに多くの時間と労力をかけている。可能なかぎり画面だけに集中したいと思っているはずだ。なので、JavaScriptを中心としたフレームワークのムーブメントが今起きていると思う。

このような要求にもReflexWorksであれば対応できる。ReflexWorksは、疎結合を基本としているためクライアント開発者がUI/UXに専念できるように、なるべくサーバ開発を意識させないようにしている。クライアントシンプルなREST APIでサービスを呼び出すことだけ考えればよく、結果もJSON/XMLで取得できる。

KVSで業務アプリを動かす条件とは

冒頭で、GAEではDatastoreはよいがCloudSQLはビミョーであるといった。
一方、AWSのRDSやSQL Azureはまずまずのようだが、実は、Amazon DynamoDBや、Azure Storage Service(KVSの方)にもビミョーなところがある。

一つ目はトランザクション。
GAEではEG(エンティティグループ)のトランザクションが扱える。Azure Storage Serviceでも、同一テーブル、Partitionkeyのデータはトランザクション処理可能である。しかし、DynamoDBではトランザクションを扱えない。(ドキュメントによるとatomic counter程度はできそうだが・・)



http://www.slideshare.net/kentamagawa/amazon-dynamodb-11808513
クラウドのエンタープライズ利用を考えた場合、数msで返せるとか、数万qpsとかいっても単体のスループットであれば実はそれほど重要ではない。ブラウザ表示で数msと数百msの違いは感じられないので、単純にKVSパフォーマンス比較してもあまり意味はない。逆にどんなに高速でもトランザクション非サポートは痛い。トランザクション機能がないと結局は参照系など用途は限定されることになるため、単体性能は悪くてもノードを増やすことでリニアに性能向上できることの方がはるかに重要だ。たとえ数百msのレスポンスで数十tpsになったとしてもトランザクション機能とスケーラビリティは必須である

2つ目はセカンダリインデックス。
実はKVSであっても単純なkeyとvalueだけでなく高度な検索ができるようなものもある。例えば、GAEのDatastoreには、データがソートされて格納されている。そしてRANGE検索ができるため前方一致で検索できる。これはRDB(2次元)とKVS(1次元)の間の1.5次元の性質をもつというのだそうだ。(このあたりについては、kazunoriさんの記事:BigTableと分散KVS がとても参考になる)
キー検索以外にもプロパティ検索も欲しくなる。DynamoDBやAzure Storage Serviceでは、プロパティ検索は可能だが、GAEのproperty indexのような、Built-in indexが存在しないためプロパティ検索は全スキャンになってしまうようだ。これでは大量のデータ検索には使えない。




ReflexWorksにおける基本設計と検索について

元へ。クライアント開発では単なるJSON呼び出しなので、それほど苦労しないことは理解いただけたと思うが、サーバ側ではそれなりのサービスを準備するのが結構大変だと思われるかもしれない。だが、実はそんなことはない。以下に示すように、ReflexWorksはドキュメント指向でありORMは不要であり設計も単純明快だ。



ReflexWorksにおける設計の基本となるのはエンティティである。エンティティとは、簡単にいえば、一単位として扱われるデータのまとまりのこと。難しくいえば、貧血症をおこしたオブジェクトのこと(笑)。POJOでもJSONの連想配列でも何でもいい。とにかく、クラスやプロトタイプに複数の項目がプロパティとして付いているものをエンティティと呼んでいる。ただし、CSVのようにデータを列挙したものではなく、項目と値がペアになっておりまた親子関係も表現できる。

ReflexWorksでは、このエンティティにユニークなアドレス(URL)をつけることで、データ全体を階層管理している。そして、そのアドレスに対してREST APIで操作する。



BDBはセカンダリインデックスが使えるという(1.5次元の)KVSである。このセカンダリインデックスにより、様々なプロパティ検索やソートなどを高速に実行できる。

例) GET /d/{selfid}?{name}{=|.eq.|.lt.|.le.|.gt.|.ge.|.ne.}{value}&{name}{=|.eq.|.lt.|.le.|.gt.|.ge.|.ne.}{value}&...&l={n}


重要なことは、どんなに分散しても全体の階層管理は崩れず一貫した操作が可能になっているということ。RDBであればデータ量が増えるとテーブル分割などで頭を悩ませることになるが、ReflexWorksであればアプリはBDBのことを気にせず単にREST APIを実行すればよく、裏にあるノードの数なども気にする必要はない。

ReflexWorksにおけるトランザクション処理

トランザクションに関して説明する。
reflexworksでは、最小単位のentryにaliasという別名フォルダを複数付けることができ、またatomicな更新ができる仕組みを提供している。別名フォルダとは、/foo/doc1.xmlを/bar/doc1.xmlでもアクセス可能にする仕組みで、UNIXファイルシステムのシンボリックリンクだとおもっていただければ結構だ。


atomicity(原子性)、isolation(分離性)、consistency(一貫性)
  • (ATOMの)entryはトランザクションの最小単位であり原子性をもつ
    • つまり、RDBの1レコードに相当
  • alias(別名)の追加削除はentry内であるためatomicでありisolationが保たれる
    • 例えば、entryがフォルダ移動しても2重に見えたりはしない
  • 複数のentryはfeedで括られる
  • feedのPOST/PUTは1トランザクションで実行されるため、どれか一つでも失敗するとロールバックする(一貫性は保たれる)

例えば、本を買う例でいえば、本はカートに入れられるか、入れるに失敗するかでなければならない。あるいは、買うか買わないかである。




これは、フォルダをカートとみなせば、注文データ(Document)がフォルダ移動する問題に置き換えることができる。以下のように、異なるフォルダで同時に見えてはいけないし、どちらにもないという状態があってはいけない。




これを、reflexworksで実装すると以下のようになる。selfはこのドキュメントの実体があるフォルダを示す。(固定)一方のalternate(別名)のフォルダがfolder1からfolder2に更新されることで上記のフォルダ移動を実現する。entryの更新はatomicなので一貫性は保たれる。


最後に

KVS上でアプリを動作させるために必要なこととは、トランザクション処理とセカンダリインデックスの2つということを説明してきたが、あくまでこれはオンライン業務のアプリに関してであり、一貫性を必要としないような検索系のアプリなどは他のKVSを使ってさらに高速化できるだろう。あるいは、BDB以外でトランザクション処理とセカンダリインデックスをサポートするKVSがあるかもしれない。実はReflexWorksはBDBに特化したものではなく、他のKVS上でも動作するように、TaggingServiceとDatastoreはレイヤーを別けて作ってある。実際にGAE上でも動作するし、本当は、DynamoDBやAzure上でも動かしたいと思っている。 繰り返しになるが、ReflexWorksであれば、単純なRESTサービスのAPI実行になるため、下位のレイヤーがどのようなものになろうと開発者は意識する必要がない。また、こうすることでベンダーロックインを防ぐことにもなる。
 ちなみに、先日からFacebook上で開発案件を募集しはじめたので、あわせて紹介しておきたい。 


 ReflexWorksで分業開発をしよう! 
「HTML5や画面デザインなどをお得意とされている方は画面開発だけに集中し、サービスのことは考えなくて構いません。サービスの実装部分はJavaなので、サービス開発担当、もしくは弊社メンバーがお手伝いできます。お互いに得意なところを役割分担しながら分業開発できればいいなと考えています。」


 
© 2006-2015 Virtual Technology
当サイトではGoogle Analyticsを使ってウェブサイトのトラフィック情報を収集しています。詳しくは、プライバシーポリシーを参照してください。