2009年12月18日金曜日

#appengine でTransactionを使った保存とそうでない保存

#appengine java night #3( #ajn3 )に参加したのエントリで書いた以下の動作を確認しました。

ルートエンティティが存在しなくてもエンティティグループに対するトランザクション処理が正常に行われる。つまり、トランザクション管理で使用されるエンティティグループのタイムスタンプは、Datastoreのエンティティとは関係が無いところで管理されている可能性がある

これについて、@marblejenkaさんが詳細に書いてくれています。紹介した記事の主題はmakeAsyncCall()をフックしてProtocolBufferのレベルで詳細表示するロガーの件ですが、その動作サンプルの題材にこの記事の冒頭で書いた「ルートエンティティが存在しない場合のTransactionの動作」、が登場します。まぁこれは自分が@marblejenkaさんにおねだりしたってのもあるからかもしれないけど。

その中で、appengineのdatastoreの動作についてわかりやすい証明がされている箇所があったのでいくつか抜粋してみます。

親エンティティが存在しない子エンティティをTrasactionを使わずに保存

Javaで実行したコードにたいして、実際にサービスが呼び出された場合のサービスとアプリケーション間のRequest/Responseを紹介する形で説明していきます。いくつかの要素に関しては改行をとっぱらって表記しています。

service.allocateIds("parent", 1); // 親エンティティ用のallocateIds
request ===================
datastore_v3:AllocateIds(
  model_key <
    app: "balmysundaycandy"
    path <
      Element { type: "parent" name: "ignored" }
    >
  >
  size: 1
);

response ===================
start: 15007
end: 15007
これは特に目新しい事はありません。
service.allocateIds(parentKey, "child", 1); // 子エンティティ用のallocateIds
request ===================
datastore_v3:AllocateIds(
  model_key <
    app: "balmysundaycandy"
    path <
      Element { type: "parent" id: 15007 }
      Element { type: "child" name: "ignored" }
    >
  >
size: 1
);
response ===================
start: 1
end: 1
親エンティティのKeyをサービスに渡していることが見えます。
Entity child = new Entity(childKey); service.put(child); // 子エンティティを保存
request ===================
datastore_v3:Put(
  entity <
    key <
      app: "balmysundaycandy"
      path <
        Element { type: "parent" id: 15007 }
        Element { type: "child" id: 1 }
      >
    >
    entity_group <
      Element { type: "parent" id: 15007 }
    >
  >
);
response ===================
key <
  app: "balmysundaycandy"
  path <
    Element { type: "parent" id: 15007 }
    Element { type: "child" id: 1 }
  >
>
cost <
  index_writes: 1
  index_write_bytes: 1
  entity_writes: 1
  entity_write_bytes: 318
>
ここが重要!

最後の子エンティティのPUT時のリクエストにentity_groupという要素が登場しています。API的には隠蔽されたentity_groupという要素が存在することで、親エンティティなしでもEntityGroupも扱えるっていう仕組みになっているんですね。当然のことながらこのentity_group要素は親エンティティのKeyを保持しています。この例だとTransactionは使ってませんが、この時点で「ルートエンティティが存在しない場合のTransactionの動作」の謎が解けましたね。

ここから先は「ルートエンティティが存在しない場合のTransactionの動作」には関係がない余談となります。別の視点で面白い事も見えたので、続けてTransactionを使った場合のサービスとアプリケーション間の通信の詳細も紹介します。視点を「Transactionを使わないPUT、Transactionを使うPUTの動作の差」に切り替えましょう。上の例で注目すべきはPUT時の通信のレスポンスにあるcost/index_writesです。

Transactionを使った場合のPUT

Transaction tx = service.beginTransaction();
request ===================
datastore_v3:BeginTransaction();
response ===================
handle: 0xe99cea1e678995af
TransactionのIDがレスポンスに含まれています。
service.put(tx, child);
request ===================
datastore_v3:Put(
  entity <
    key <
      app: "balmysundaycandy"
      path <
        Element { type: "parent" id: 16006 }
        Element { type: "child" id: 1 }
      >
    >
    entity_group <
      Element { type: "parent" id: 16006 }
    >
  >
  transaction <
    handle: 0xe99cea1e678995af
  >
);
response ===================
key <
  app: "balmysundaycandy"
  path <
    Element { type: "parent" id: 16006 }
    Element { type: "child" id: 1 }
  >
>
cost <
  index_writes: 0
  index_write_bytes: 0
  entity_writes: 0
  entity_write_bytes: 0
>
先のTransactionを使わない場合に比べて、リクエストにTransactionのIDが含まれているのが見えます。そしてもうひとつ、cost/index_writes1ではなく0になっています。
tx.commit();
request ===================
datastore_v3:Commit(
  handle: 0xe99cea1e678995af
);
response ===================
cost <
  index_writes: 1
  index_write_bytes: 1
  entity_writes: 1
  entity_write_bytes: 318
>
commitをしたときにcost/index_writes1となっています。

Transactionを使う場合と使わない場合のインデックスの作成のタイミングが違う(Transactionを使用しない場合はPut時にインデックスの作成が実行され、Transaction使用時はCommit時にインデックスの作成が実行される)という仕様について、実際に見ることができました。というお話でした。@marblejenkaさんありがとう、まだ垢BANされてなかったんですね!

2009年12月5日土曜日

#appengine java night #3( #ajn3 )に参加した

今回の本編では、TaskQueueをメインテーマにして開催されました。

@bluerabbit777jpさん:実際に作ってわかったApp Engineの困ったところ

大量のメール送信を行うために、「Datastoreにメールをキューしておき、TQでメールを拾う→送信→Datastoreに送信完了フラグを書き込む」というパターン。これは最後の「送信完了フラグの書き込み」のフェーズに失敗する状況だとメールが多重送信されてしまう事がありうる。
fmfm、確かにそうだなぁ。こういったエラーをよしとするポイントが必要になるという事ですね。もちろん、アプリケーションの特性のよってそこへかけるコストと妥協できるボーダーは変わってくるわけですが、完璧を求めることは難しいですもんね。

時間がかかるバッチ処理を分割してTQで処理すると並列で走るので早くなる。でも、並列実行だと難しい設計だった場合は、TQ内のTaskのチェイン(Taskから次のTaskをQueueに投入)をする事でシーケンスに処理を行う事もできる。
並列実行がどうしても難しい場合はこういった手もアリですね。できるだけ並列処理できるように設計したいけど、難しい場合もありますもんねぇ。

途中で発生した「エンティティグループのルートエンティティを永続化しなかったらどーなるの?」という疑問は、疑問が出たその場ではTQに投入され、@bluerabbit777jpのプレゼン中に並列で検証が行われることとなった。勉強会の途中でこういう進行をするのも面白いですね!!勉強会に参加していなかったがリモートから@ashigeruが協力してくれた結果、検証も無事完了。結論は以下の通り。これは自分にとっては結構インパクトがある結果でした。

  • ルートエンティティが存在しなくてもエンティティグループに対するトランザクション処理が正常に行われる。つまり、トランザクション管理で使用されるエンティティグループのタイムスタンプは、Datastoreのエンティティとは関係が無いところで管理されている可能性がある

ここだけの話、バックグラウンドでこの検証をやっていたので、途中から20分ほどプレゼンをあまり聞けてなかったり…。

ぶいてく竹嵜さん:ぶいてく流 スケーラブルアプリの作り方

以前からPDFの大量処理で注目していた方のプレゼンなので、生でお話を聞くことができて良かった。

エンティティのバージョニングを行うために、「更新無し追加のみ・物理削除なし・論理削除のみ、ですべての履歴を保持、エンティティをアプリケーション的なキー+リビジョン番号でユニークに管理する」という戦略で設計されていたのは興味深かった。
このあたりはどこまで厳密に整合性を保証するのか、読み込み時の整合性をどこまでリアルタイムに保証する必要があるのか、などなどがアプリケーションによって求められるレベルが違うと思うので、正解は無いと思う。たぶん、いくつかのパターンが出てくるのは従来通り。…なんだけど、それをBigTableでやるには?という意味で、どんどん「俺はこうやってる!」という話が出てくるのは大変ありがたい。やってみなきゃワカラン問題がたくさんあるんですよねぇー。今後の竹崎さんの方針にも注目ですね!

「TQがスケールしない。色々タイミングなどの工夫をしたけど、インスタンスが4つ以上に増えてくれない」という話も面白かった。工夫の仕方も面白いし、なによりスケールしない理由がわからなさすぎるのが面白いw
このあたりはLT@WdWeaverのLTでも語られることだし、@WdWeaverの検証の話を知っているので結構想像できたり、逆に知っていることもあるからこそ想像できなかったりwこの竹崎さんの例は「スケールしないパターン」という意味で、とても貴重な例だと思いました。これもまた、やってみなきゃわからないし、やってみたから少し垣間見える世界もあるのですよね。@WdWeaverの世界だけだと、スケールしないパターンは見えませんから。

@WdWeaverさん:スケールアウトの真実?

個人的には、Appengine上に本格的なアプリケーションを展開する人全員が絶対に知っておかなきゃならない、ある意味基礎知識といえる話だと思ってます。もちろんアプリケーションの設計パターンと言う意味では今回のメインテーマのTaskQueueなども重要だけど、もっと低レイヤの前提条件として、みんな知っておくべきお話でした。

ずいぶん前にもこの件の資料が公開されていたので、ある程度アンテナを張っている人は知っていた話だと思うけど、リアルタイムにデモを見せてもらえたのは感激。ひがさんの号令のもと皆があれこれ試して推測した結果をうらづける結果がリアルタイムに、納得できる形で確認できたのが嬉しい。そして、今回はさらに「竹崎さんのスケールしないパターン」の話がどうなの?ってのも出てきたわけだし(warmアップしたらいいんじゃね?という話もあるが、それだけじゃ不足な気がする)、まだまだスケールする仕組みは検証する価値がありそうです。楽しいなぁ!

@tmatsuoさん:AppEngine/Py用のフレームワーク「Kay」

PythonでもJava同様にspin upに時間がかかる問題があるんだと判明したのは自分にとっては大きな発見!

管理機能としてDatastoreのデータをExportできる機能があるのは知っていたんだけど、そこで謎に感じていた「Kind一覧はどーやって取得してるんだろう?」という問題に対しては「モデルクラスのソースを読んでる」との事。なるほどね、確かにそれならできるなぁ、と謎が解けました。

@marblejenkaさん:makeSyncCallでいろいろ試してみた

LowLevelAPIよりもさらに低いレベルのAPIの話。このあたりは、みんなが詳しく触る必要がある話ではないのだけれども、@WdWeaverのスケールの仕組みの話と同様、「こういう世界もある」という事を知っておいた方がいいと思ってたので、こうやって紹介されるのは大変良いことだと思います。

1.2.8から追加されたmakeASyncCall()という機能については実はめっちゃくちゃ重要な話で、今後のAppEngine上のアプリケーションの設計方針が大きく変わる可能性がある機能。これの紹介もあったのはとても良かったと思います。ウチの同僚も興味津々だったようですし。

帰宅してからハッシュタグを追いかけていて気づいたんだけど、@marblejenkaはひとり違う視点でつぶやいているのが大変おもしろいです。イケナイbyte配列を投げすぎて垢BANされるとしたら、世界で最も早く垢BANされるのは@marblejenkaじゃないかな、と真面目に考えてます。

@kimteaがハッシュタグをまとめてくれてます

まとめありがとうございます!追いかけると色々と面白い発見がありました。

感想とか

今までで一番濃い会になったかもしれないな、と思いました。ひがさんがおられると、色々重要な議論を沸き起こせて大変助かります。特に今回は議論で発生した新たな疑問をその場で検証してみたりとか、新たな展開もあって良かったです。次回からその場検証の担当としてもっと武装(デプロイ環境でもすぐに検証できるアプリを用意しておくとかね)していこうかなぁとか思ったw

例のごとく、時間がおしまくったため、予定されていたLTが4本消化できずに終わりましたw でも、いいですね、ひがさんがおっしゃった内容で「時間通り終わるのが目的じゃない、密に時間をスゴスのが大事なんだ」というのがありましたが、それが実現できている会だなと思います。これからもこの調子でやっていけると良いな、と思います。

感謝

懇親会もできるひろーーい会場を提供してくださったグリー株式会社様、いちいさん、大変ありがとうございました。特に夜遅くまで面倒を見てくださったいちいさん、お疲れ様でした&ありがとうございました。今回も開催に関するしきりをしてくださったスティルハウス佐藤さん、有益な話を聴かせてくださったスピーカーの皆様、会場やTwitterで盛り上げてくださった参加者の皆様、リアルタイム検証をリモートから手伝ってくれた@ashigeru(赤木しげるではない)さん、ありがとうございました!