2010年11月27日土曜日

アプリケーションアーキテクチャとデータベース

引き続き右のクラウドアプリケーションアーキテクチャについて考えています。

JJUG CCC 2010 Fallでは、CQRSアーキテクチャパターンの動きを下の図を用いて説明しました。
これを実現する場合、Commandによる更新系とQueryによる問合せ系で、物理的なデータベースを分けるのがよいわけですが、さらにデータベースのシステムも性質に合わせたものを適材適所で選択するとより良い結果が得られるはずです。

問合せ系のデータベースとしては、ここまでの議論からカラム指向データベース/HBaseが候補となっています。

Commandとして投入されたEventはコミットログ的な実現方式で、データベースに格納します。このデータをここではEventログと呼ぶことにします。Eventログを格納するデータベースは、通常オペレーションではappend-onlyなので、分散ISAM的なシンプルなデータベースが向いています。shared nothingなのでshardingでスケーラビリティを確保するのも容易です。このあたりの性質を軸にデータベースを選択することになります。append-only&shardingに強い、更新が高速で信頼性の高い分散KVSが候補となりますが、まだ見つけていないので保留。現時点ではRDBMSを(ISAM的に)使うのがよいかもしれません。

問題は、Eventログの内容をカラム指向データベースに反映するところです。
カラム指向データベースは、問合せに強い反面、更新に弱い性質があります。このため、カラム指向データベースを通常のトランザクション向けデータベースとして使うのは、あまり得策ではありません。

本アーキテクチャでは、このギャップをドメインサービスにおいて、トランザクションデータの格納にインメモリデータベースを使うことで埋めることが眼目の一つになっています。インメモリデータベースはVoltDBが候補になっています。
アプリケーション実行中のトランザクションは、インメモリデータベースに格納・管理されます。カラム指向データベースはあくまでもインメモリデータベースのバックアップなので、更新に多少の時間がかかっても問題ありません。
また、データの永続性はEventログのデータベースによって担保されるため、バックアップデータベースであるカラム指向データベースへのデータ更新は、インメモリデータベースへの更新とは非同期に、バックグラウンドでゆっくり行っても大丈夫というわけです。

以上のように、分散KVS(またはRDBMS)、インメモリデータベース、カラム指向データベースを適材適所で組合わせてアプリケーションを構築すると面白い結果が得られそうです。
まだまだ机上の議論ですが、この方向で考察を深めていきたいと考えています。

2010年11月26日金曜日

データベースの選択

カラム指向データベースにHBaseを使うと、Hadoopとの連携がスムーズになるので、バランスがよさそうというお話をしました。

こうなると、インメモリデータベースも具体的に考えてみたくなります。

当初は、SQLiteのインメモリデータベースモードを使って、自分でメモリクラスタを組むようなことを考えていたのですが、色々調べてみるとどうもVoltDBがよさそうに思えてきました。

VoltDBはRDBMSなので、SQLの豊富なデータ操作機能を用いることができます。
いわゆるKVSを使うと、アプリケーション側で相当の作り込みが必要なので、この点でRDBMSは安心感があります。

VoltDBは、自動的にメモリクラスタを組んでくれるみたいなので、この点でもアプリケーションは何もしなくて済みそうです。
メモリクラスタによってスケーラビリティを高めるとともに、信頼性を向上させることができます。

VoltDBではJavaを使ったストアドプロシジャとしてプログラムを書く必要があるので、この点がちょっとクセのあるところです。
たとえば、本アーキテクチャの場合、ドメインサービスだけでなく、アプリケーションサービスもストアドプロシジャとして実現するような実現方式も検討が必要になります。
この点に問題がなければ、かなり有力な選択肢ではないかと思います。

インメモリデータベースの弱点は、メモリクラスタが完全にダウンした場合の永続性の確保と、大規模データがメモリに載り切らないリスクです。
本アーキテクチャでは、Eventログを用いて永続性は確保できているので、メモリにデータが載り切る場合には、Eventログの分散KVS(または普通のRDBMS)とVoltDBだけでシステムが組むこともできそうです。(AWSの場合、メモリモデルとして7.5GB, 15GB, 34.2GB, 68.4GBが用意されており、一般的なアプリケーションではメモリのみで運用することが可能です。すでにそういう時代に入っています。)
ただし、メモリクラスタ再起動時にEventログから最新の状態を復元するのに時間がかかりそうなので、何らかのチェックポイントをどこかに保存して置く必要があります。
そうなると、結局のところバックアップデータベースとしてHBaseを併用するのがバランスがよさそうです。

図の左側にあるマスターデータはローディングが早ければ何でもよいので、HBaseに相乗りで十分でしょう。

以上の考察から、今の所:

  • Eventログ用DB:append-only&shardingで高速書き込みできる分散KVSの何か
  • ドメインサービスのインメモリデータベース:VoltDB
  • バックアップデータベース&Hadoop:HBase
  • マスターデータベース:HBase(に相乗り)
というのが面白そうと考えています。

2010年11月25日木曜日

Hadoopの置き場所

土曜日に書いたアーキテクチャは、Hadoop座談会で得られたインスピレーションによるものでしたが、肝心のHadoopの置き場所を考えるのを忘れていました。
そこで、考えてみたのが以下の図です。


アーキテクチャ的には、MapReduceやその実装としてのHadoopだけでなく、より汎用的な概念の導入が有効ではないかということでBackground Computingとしています。
さらに細かく考えていくと、Background ComputingもBatchとRealtime(Streaming)に分けることができそうですが、これはいずれ。

ボクは、クラウドアプリケーションとは、非同期に発生するイベントを連続して受け取りながら、イベント受信をうけて内部状態を刻一刻と遷移させていくオブジェクトと考えています。右の図はこのあたりをJJUG CCC 2010 Fallでお話した時に使ったものです。
この中で、インデクサとしているものが、上の図のBackground Computingに相当します。
クラウドアプリケーション自身はイベント駆動で動作しますが、その応答性能や結果精度を向上させるために、必要な情報をバックグラウンドで常に作り続けているのも、クラウドアプリケーションのもうひとつの側面になります。
この部分の実現機構として、MapReduce/Hadoopが重要な選択肢というわけですね。

Hadoopを組み合わせるということで具体的に考えてみると、HBaseはカラム指向データベースということに思い至りました。アーキテクチャ素案では、ドメインサービスのメインデータベースはインメモリデータベースで、このバックアップとしてカラム指向データベースを併用しています。このバックアップデータベースをHBaseとすることで、Hadoopとの連携をシームレスに行う事ができるようになります。

Background Computingの結果は、バックアップデータベースに直接戻すケースと、イベントとして通常のデータ更新ルートに載せるケースの両方が必要でしょう。
そのあたりを図に追加しています。

2010年11月20日土曜日

クラウドアプリケーションのアーキテクチャ素案

きのうはHadoop座談会(第3回)。佐藤先生と萩原さんのお話を聞くことができました。
並行・並列・分散技術。RDBMS JoinからDryad。旬のテーマで参考になる点が多々ありました。
これに加えて、懇親会で萩原さんからお聞きしたカラム指向データベースのお話が刺激的。クラウドアプリケーション・アーキテクチャのインスピレーションが浮かんだので、図にまとめてみました。


CQRS、Event Sourceのアーキテクチャの上で:

  • 各種データベースの選択とアーキテクチャ責務分割
  • プレゼンテーション、アプリケーション、ドメインの各サービスのアーキテクチャ責務分割
  • 可用性の担保
  • スケーラビリティの担保
といったものを織り込んでいます。
Domain Serviceのクラスタを構築してin memory databaseでワークデータの管理をするのが面白いかなと思っているのですが、Domain Service初期化時にすべてのデータをメモリにローディングするのは現実的ではないので、バックエンドのデータベースが必要となります。ここにカラム指向データベースがぴったりはまるのではないか、というのがインスピレーション。RDBMSでもよいところですが、スケーラビリティを高めるにはカラム指向データベースという選択が有効と考えられます。
エージェント,というとやや大げさですが、ストアドプロシジャとかクロージャとか、そういった技術でデータの近くで処理を行って結果を返す機能が必要になるのは明らかなので、そのあたりもアーキテクチャに取り込みたいところ。ここは、Domain Service界隈で実現できるのではないかと考えています。
他にも色々と話題があるので、この図をもとにブログで実現方式を整理していこうと思っています。

2010年10月13日水曜日

[g3]データストア3

金曜日の続きです。
日曜日に公開したg3 0.2.1でg3アプリケーションDataStoreCrud.scala(以下に再掲)を実行してみましょう。

DataStoreCrud.scala
package org.goldenport.g3.app

import org.goldenport.g3._
import org.goldenport.g3.atom._
import org.goldenport.g3.messages._
import org.goldenport.g3.messages.datastore.{Create, Fetch, Query, Insert, \
    Update, Delete, Drop}

class DataStoreCrud extends G3Application with UseRecord {
  val KIND_NAME = 'g3crud
  val schema = Schema(
    IdField,
    ('name, XToken),
    ('zip, XToken),
    ('address, XString),
    ('phone, XToken, ZeroMore),
    ('comment, XString))

  datastore('appds)

  val create = Create(KIND_NAME, schema)

  val fetch = Fetch(KIND_NAME, 5)

  val query = Query(KIND_NAME, Id(5))

  val insert = Insert(
    KIND_NAME,
    Record('id -> 5, 'name -> "Yamada Taro",
           'zip -> "1234567", 'address -> "Yokohama",
           'phone -> "0451234567", 'comment -> "omlet rice"))

  val update = Update(
    KIND_NAME,
    Record('id -> 5, 'name -> "Suzuki Hanako"))

  val delete = Delete(KIND_NAME,
                      Record('id -> 5))

  val drop = Drop(KIND_NAME)

  port("/create") agents(create) invoke("appds")
  port("/fetch") agents(fetch) invoke("appds")
  port("/query") agents(query) invoke("appds")
  port("/insert") agents(insert) invoke("appds")
  port("/update") agents(update) invoke("appds")
  port("/delete") agents(delete) invoke("appds")
  port("/drop") agents(drop) invoke("appds")
}

org.goldenport.g3.app.DataStoreCurdはg3に組み込まれているので、以下のように実行します。

$ g3 -g3.application:org.goldenport.g3.app.DataStoreCurd \
    -g3.server

ルート「/」にアクセスすると、DataStoreCurdでは対応するポートがないのでエラーとなります。(エラーメッセージは改良する予定。)



以下、Create、Insert、Fetch、Update、Query、Delete、Dropの順に実行すると以下のようになります。特に画面の定義はしていませんが、ポートチャネルを実行して結果のメッセージに応じて自動的に整形して表示するようになっています。









おまけ


g3 0.2.1では、本当は以下のようにヘッダー、サイドバー、フッターを自動的に挿入した画面が表示されるはずでしたが、バグで挿入されなくなってました。ちょっと残念。次のバージョンから自動的に挿入されるようになります。


2010年10月10日日曜日

[g3]g3 version 0.2.1

メッセージング・フレームワークg3のバージョン0.2.1を公開しました。


配布物は以下の2つです。用途に合わせてどちらかをダウンロードしてください。

  • g3-0.2-bin.zip:配布バイナリ。unzipして使用。
  • g3-0.2-jar-with-dependencies.jar:実行形式。java -jar g3-0.2-jar-with-dependencies.jarで直接使用。

g3 0.2.1は、Google AppEngine、データストア、WebUI周りの改良を行ないました。

2010年10月8日金曜日

[g3]データストア2

前回の続きです。


データストアチャネル


データストアはデータストアチャネル経由でアクセスします。

ここでは、以下のようにappdsという名前のデータストアチャネルを定義しています。


  datastore('appds)

データストアチャネルはデフォルトではg3に組み込んでいるRDBMSのDerbyを使用します。また、AppEngine上で動作させるとAppEngineのデータストアを使用します。

データストアチャネルの定義あるいは外部定義ファイルによって、任意のJDBC URLやJDBCドライバを指定することができます。ただし、現時点の実装ではデータベース固有のデータ型に対応していないので、Derby以外のRDBMSは事実上動作しないと思われます。いずれ、MySQLやPostgresなどのデータベースにもアクセス可能にする予定です。


カインドの作成


カインドの作成は、Createコマンドをデータストアチャネルに送信することで行ないます。Createコマンドには、カインド名とスキーマを設定します。


  val create = Create(KIND_NAME, schema)


  port("/create") agents(create) invoke("appds")

agentsエージェントは、メッセージを受信すると、引数に指定されたコマンドを発行するエージェントです。この場合は、Createコマンドをinvokeエージェントに送信しています。invokeエージェントは同期型でデータストアチャネルappdsにメッセージを送り、データストアアクセスの結果を受け取ります。 invokeエージェントは、このチャネルの最後のエージェントなので、invokeエージェントが受け取ったメッセージが、このチャネルの最終結果となります。


レコードのインサート


カインドに対するレコードのインサートは、Insertコマンドをデータストアチャネルに送信することで行ないます。Insertコマンドには、カインド名とインサートするレコードを設定します。


  val insert = Insert(
    KIND_NAME,
    Record('id -> 5, 'name -> "Yamada Taro",
           'zip -> "1234567", 'address -> "Yokohama",
           'phone -> "0451234567", 'comment -> "omlet rice"))


  port("/insert") agents(insert) invoke("appds")


レコードのアップデート


カインドに格納されているレコードのアップデートは、Updateコマンドをデータストアチャネルに送信することで行ないます。Updateコマンドには、カインド名とアップデートするレコードを設定します。レコードには、IDと更新するフィールドのみを設定すればOKです。

SQLの場合はUPDATE文で、AppEngineの場合は読み込みと書き戻しをデータストアチャネル側で行ないます。


  val update = Update(
    KIND_NAME,
    Record('id -> 5, 'name -> "Suzuki Hanako"))


  port("/update") agents(update) invoke("appds")


レコードのフェッチ


カインドに格納されているレコードの取り出しは、Fetchコマンドをデータストアチャネルに送信することで行ないます。Fetchコマンドには、カインド名とIDを設定します。


  val fetch = Fetch(KIND_NAME, 5)


  port("/fetch") agents(fetch) invoke("appds")


レコードのクエリ


カインドに格納されているレコードの問合せは、Queryコマンドをデータストアチャネルに送信することで行ないます。Queryコマンドには、カインド名と問合せ式を設定します。

ここでは「Id(5)」という問合せ式で「IDが5」のレコードの問合せを行っています。


  val query = Query(KIND_NAME, Id(5))


  port("/query") agents(query) invoke("appds")


レコードの削除


カインドに格納されているレコードの削除は、Deleteコマンドをデータストアチャネルに送信することで行ないます。ここでは、Queryコマンドには、カインド名とIDを設定したレコードを設定しています。


  val delete = Delete(KIND_NAME,
                      Record('id -> 5))


  port("/delete") agents(delete) invoke("appds")


カインドの削除


カインドの削除は、Dropコマンドをデータストアチャネルに送信することで行ないます。Dropコマンドにはカインド名を設定しています。


  val drop = Drop(KIND_NAME)


  port("/drop") agents(drop) invoke("appds")

次回に続きます。