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(赤木しげるではない)さん、ありがとうございました!

2009年11月24日火曜日

@bufferings さんによる #appengine #slim3 データストアドキュメントの日本語訳

bufferingsさんがやってくれました。このドキュメントはslim3についての説明というだけではなく、Google App Engineのデータストアの詳しい説明にもなっており、とてもわかりやすくなっています。オススメ。

2009年11月19日木曜日

#appengine hack-a-thon #1に参加した

もう二日も経ってしまったので、今更かよってカンジですけれども。

感想

六本木のアカデミーヒルズ40Fで開催されました。キレイ&眺めが良いフロアで、からあげ弁当・麻婆なす弁当がそれぞれ\600と\500、喫煙ルーム付き、というかなり良い環境でした。コミュニケーションは途中の昼食、終了後の懇親会がメインで、作業中はとにかくもくもくと作業するカンジ。
オンラインのコミュニケーションは、事前の予定通りTwitterで#ahack1 のハッシュタグを使おう!ってやってましたが、主に「富士山」「チョコレートの配給」「座った席の連絡(松尾さんを目印に)」「芋の配給」という話題ばかりが流れてましたw ついでに書いておくと、事前に用意したリポジトリは実に役に立ちませんでしたw でも、それなりの成果も上がっていましたし、色々と濃い話もできて楽しかったです。

私の成果

maven+eclipse(with Google Plugin for Eclipse)を使っている場合に、appengineのプロジェクトを簡単に作成するプラグインを作っていました。当日はslim3datastore+servletのシンプルなプラグインが完成し、slim3datastore+wicket用のプラグインの途中までで時間切れ。帰宅後にそれを完成させ、次の日にslim3datastore+cubby用のプラグインを完成させて、このエントリを書いています。

このプラグインを使うと以下の3手順ですぐに appengine のプロジェクトができあがり開発がすぐに開始できます。

  1. mvn archetype:generateして
  2. mvn eclipse:eclipse して
  3. eclipse に取り込む

この手順で作成したプロジェクトでできる事は以下の通り。

  • eclipseからGooglePluginを使って開発用のWebコンテナ上でアプリケーションを起動する(起動構成も生成されます)
  • eclipseからappengineの環境を利用する自動テストを実行できる
  • mavenからも自動テストを実行できる
  • makeSyncCallServletが組み込まれ、設定済みの状態になっている(デプロイ環境ではアプリケーション開発者権限を持つアカウントのみがアクセスできる設定になっています)

できない事は以下の通り。

  • JDOは一切組み込まれません。
  • mavenから開発用サーバを起動できない。mavenから開発サーバを起動するプラグインが1.2.6に対応しているかどうか未確認のため、組み込んでいません。

一番軽い構成になっているgaej-simple-pluginをが、wicketやcubby以外のフレームワーク用に拡張するのに便利だと思います。

2009年11月7日土曜日

#appengine の「データストアの読み込み専用状態」をエミュレートする

先日もメンテナンスがあり、AppEngineのデータストアが読み込み専用状態になってましたね。データストアへの書き込みを行うアプリケーションではこの「読み込み専用状態」時の対策をする必要があるのですが、「読み込み専用状態」の時の振る舞いをテストする方法を書いておきます。自動テストができる範囲は広ければ広い程よいですもんね。

ApiProxy.Delegateを実装・適用するだけ

ApiProxy.Delegateを実装して、makeSyncCall()メソッド内で「Datastoreへの書き込み」の時に ApiProxy.CapabilityDisabledException( )を投げてやるだけ。私は下記のようなユーティリティを作ってテストしてます。

static <R>R runOnReadOnlyMode(Callable<R> callable) throws Exception {
  @SuppressWarnings("unchecked")
  final ApiProxy.Delegate<Environment> backupedDelegate = ApiProxy.getDelegate();
  Delegate<Environment> delegate = new Delegate<Environment>() {

    public void log(Environment arg0, LogRecord arg1) {
      backupedDelegate.log(arg0, arg1);
    }

    public byte[] makeSyncCall(Environment arg0, String service, String method, byte[] arg3)
        throws ApiProxyException {
      if (service.equalsIgnoreCase("datastore_v3")
          && (method.equalsIgnoreCase("Put") || method.equalsIgnoreCase("Delete"))) {
        throw new ApiProxy.CapabilityDisabledException(service, method);
      }
      return backupedDelegate.makeSyncCall(arg0, service, method, arg3);
    }
  };
  ApiProxy.setDelegate(delegate);
  try {
    return callable.call();
  } finally {
    ApiProxy.setDelegate(backupedDelegate);
  }
}

こんなカンジにできるので、他にもDatastoreTimeoutExceptionやらApiProxyException系の例外、例えばApiProxy.UnknownExceptionなんかが発生したときの振る舞いも簡単に自動試験できますね!

不思議

Datastoreについては上記の方法で簡単にApiProxy.CapabilityDisabledExceptionをエミュレートできるのですが、MemcacheServiceについては簡単には行かない事が判明しています。

  • MemcacheServiceは例外をキャッチした際に、それをログ出力するだけのハンドラでExceptionを握りつぶす実装になっている
  • 先日の読み込み専用モードの時にログが記録されていない気がする=Memcacheサービスは読み込み専用状態に書き込み要求を受けても例外をスローしない…?

前者についてはMemcacheService#setErrorHandler()メソッドに、new com.google.appengine.api.memcache.StrictErrorHandler()を設定してやればちゃんと例外が飛ぶのでまぁなんとかなるのですが、後者については次回のメンテナンスの時になるまで調査ができませんね。ひょっとするとMemcacheサービスの仕様なのかな?という気もします。「常にMemcacheから値を取得できるとは限らない」という前提なので、書き込みに失敗しても問題ない、という仕様なのかな?それであれば、そもそも試験する必要がありませんね。
どなたか前回のメンテナンス時にMemcacheサービスから書き込みエラーとか何かの例外を受け取った人がおられたら、わかる範囲で良いので教えてください!

読み込み専用モードを考慮してアプリ側で工夫できる事もある

がっつりデータストアに書き込む必要がある処理はどうしよーも無いのですが、例えばカウンタの記録など「書き込みがメインでは無いが、書き込み処理も付随する」ような機能はちょっと工夫しておけば読み込み専用時にも稼働させる事ができます。

私が稼働させているAppEngineアプリのひとつに空うさぎというFriendFeed/Twitter/RSSクライアントアプリケーションのファイル管理用の小さいアプリケーションがあるのですが、このアプリの中にファイルのダウンロード件数を記録する、という機能があります。この機能は「ファイルのイメージをレスポンスする」と同時に「ダウンロード数カウンタを増加させるTaskをQueueに投入する」という実装をしています。投入されたTaskはデータストアの書き込みに成功しない場合はHttpStatusの500を返すように実装しているので、読み込み専用時は常に500を返す事になります。その結果、読み込み専用モードが解除されるまではAppEngineのTaskQueueに残り続けるので、読み込み専用モードが解除された頃にはカウンタの整合性はいずれ正しくなる、という動作をします。AppEngineはまだベータバージョンですし、工夫でしのげる場所はうまく実装しておきたいですね。

2009年11月4日水曜日

#appengine でスキーマ変更に対応するバッチ処理を行う

2009/11/05追記

ひがさんより指摘を頂いて、30秒制限に関する補足を本文中に青字で追記しました。いつもありがとうございます、助かります>ひがさん

ここから本文

タイトルの処理について、いくつかノウハウを書いておきます。ポイントは以下の2点。

  • 全てのエンティティにスキーマバージョンを保持する
  • ローカル環境からデプロイ環境へ直結してバッチ処理を実行する事で、30秒制限なんて無視してしまう

実例をもとに説明してみます。最近、appengine java night用のまとめページとかに使おうとしているサイトを運営していて、そこに「TwitterでAppEngine関連についてつぶやかれた内容を収集する」という機能を実装しました。しかし、つぶやきを保存する際の投稿者の情報として「Name」を保持しているものの「ScreenName」を保持しておらず、投稿者のタイムラインページへのリンクを作成できないという問題がおこっていました。なので、つぶやき保存用のエンティティに「ScreenName」という属性を新たに追加し、つぶやきの保存時にはその値を取得するように修正しました。しかし、この修正以前に保存されたつぶやきについては、ScreenName属性を持たない状態になってしまっているので、その値を設定してやる必要があります。今回はこれをバッチ処理しようと思います。
あ、この問題は、バッチ処理の説明(このエントリ)を書くための布石であって、不具合では無いんですよっっ?

全てのエンティティにスキーマバージョンを保持する

主に今回の修正のような「属性の追加」が危険なのです。AppEngineでは「特定の属性を持たないエンティティ」というスキャンができません。インデックスを定義していても、そのインデックス定義に関する属性を持たないエンティティはそもそもインデックスのエントリが作成されないからです。そこで、エンティティの定義にスキーマバージョンを最初から用意しておく事をおすすめします。今回の修正だと、エンティティ用のクラスの修正は以下のようになっています。

ローカル環境からデプロイ環境へ直結してバッチ処理を実行

移行処理をしてやるエンティティの抽出は上に書いた「スキーマバージョン」の存在により、簡単に抽出できるようになりましたので、移行処理の準備は問題ありません。
が、移行処理自体はどのような手段を使うのか?が問題です。AppEngineでは1リクエスト30秒の制限があるので、一度に大量のデータは処理できません。

  • 特定の件数で制限してcronする?
  • 特定の件数ずつばらしてtaskqueueを使う?
まぁ、どちらでも可能です。でも、本来動作させたいプロダクトコードとは関係ないモジュールをサーバにデプロイするのもあんまり気持ちよいものではありません。そこでローカル環境からバッチ処理を行おう、というのが今回の趣旨です。データストアに対する一回の操作(Datastore#put()とかDatastore#get()とか、データストアサービスに対する一回のメソッド実行)に関しては30秒制限がある事は変わりませんが、それらを何度でも実行できるので、処理全体としては30秒制限も関係無くなりますしね。今回の処理だと、以下のような手順を行います。

  1. schemaVersion == 1の条件で移行対象のエンティティを取得する。
  2. 念のため、それらのエンティティのバックアップを保存しておく。
  3. UserId(Twitterのユーザ)で集約し、Twitterのユーザに対応するscreenNameを取得して、そのユーザに該当するエンティティを以下のように更新する
    • 取得した screenNameを設定
    • スキーマバージョンを2に設定
  4. 更新したエンティティをデプロイ環境へ保存する。

それぞれの手順内の細かい処理は省いたスケルトン部分のコードは次のようになります。

setUpBeforeClass(); // ローカルのAppEngine環境を開始する
try {
  // デプロイ環境のMakeSyncCallServletに接続するためのアカウント情報を入力させる
  getAccountInfo();
  // 移行対象のエンティティを取得する
  final List<Tweet> tweets = getOldEntities(); 
  // ローカルのデータストアにバックアップを作成する
  backupToLocalDatastore(tweets);
  // データ移行済みのエンティティを作成する
  final List<Tweet> updated = createUpdatedEntity(tweets, 50);
  // デプロイ環境側のデータストアに、データ移行済みのエンティティを保存する
  MakeSyncCallServletDelegate.runInDelegateWithAuth(new Runnable() {

    @Override
    public void run() {
      executeBatch(updated);
    }
  }, email, password, SERVER, SERVLET);
} finally {
  tearDownAfterClass(); // ローカルのAppEngine環境を終了する
}

デプロイ環境との直結には、デプロイ側にMakeSyncCallServlet、ローカル側MakeSyncCallServletDelegateという、自作のリモート接続の仕組みを使っています。ソースコードの全体は次のリンクから見る事ができます。このソースだと、AppEngineの制限よりもTwitterの制限が回避できなくて困ったりするんで、そのあたり上限の件数を設定したりしていますけど、実際にアプリ内の用件でバッチ処理を行うときにはあまり関係ないですね。あと、処理後にデプロイ環境で使われているMemcacheを全てクリアしたりもしています。プロダクトコード内の処理が走らない限りMemcacheにキャッシュされたデータを使用する設計も多いだろうし、そういう事も想定されるならMemcacheのクリアも忘れずに実行しておかないといけません。

MakeSyncCall関連については以下の資料を参考にしてください。LT用であんまり詳しくはないですけれども、大枠は資料の通りです。実際にはこのサーブレットをweb.xml内でセキュリティをかけて配置しています。

ちなみに、この直結の仕組みを使うとクライアント側アプリケーションから直接Datastore.query()..とかDatastore.put()...といった操作をする、昔のクラサバのような組み方でアプリケーションを作れるって事ですね!