メニュー

ラベル appengine の投稿を表示しています。 すべての投稿を表示
ラベル appengine の投稿を表示しています。 すべての投稿を表示

2013年10月16日

Google App Engine 1.8.6 で Go言語の単体テストがサポート

Google App Engine 1.8.6 がリリースされました。

そしてついに、Go版で単体テスト用のパッケージ appengine/aetest が追加されました!

AppEngineのローカルでの開発サーバはPythonで実装されており、Pythonの開発サーバがGoアプリを呼び出すように動作しています。この仕組み上、Goの単体テストからAppEngineのAPIを呼び出すことが困難でした。

新たに追加された appengine/aetest を使うと、以下のように単体テストを書くことができます。
package foo_test
import (
    "testing"
    "appengine/memcache"
    "appengine/aetest"
)
func TestFoo(t *testing.T) {
    c, err := aetest.NewContext(nil)
    if err != nil {
        t.Fatal(err)
    }
    defer c.Close()
    it := &memcache.Item{
        Key:   "some-key",
        Value: []byte("some-value"),
    }
    err = memcache.Set(c, it)
    if err != nil {
        t.Fatalf("Set err: %v", err)
    }
    it, err = memcache.Get(c, "some-key")
    if err != nil {
        t.Fatalf("Get err: %v; want no error", err)
    }
    if g, w := string(it.Value), "some-value" ; g != w {
        t.Errorf("retrieved Item.Value = %q, want %q", g, w)
    }
}
この例では、memcache APIを使ったテストをしています。aetest.NewContext が appengine.Context インタフェースを返すので、これを使って各種APIを呼び出すことが出来ます。 

単体テストの実行には go test ではなくSDKに同梱されている goapp コマンドを使います。
export APPENGINE_API_SERVER=/path/to/go_appengine/api_server.py
goapp test
環境変数 APPENGINE_API_SERVER は、SDKをインストールした場所に応じて値を設定します。aetest は裏でPythonのAPI Serverを起動し、そこと通信することでAPIを動かしていますので、そのためにSDKの場所を設定する必要があります。

※aetest.NewContextするたびに、データストア等は新しい領域が使われるようで、テストをまたがってデータが永続化されてしまうことは起きないようです。
※NewContextはテストケースごとに書きますが、中ではPythonのプロセスを起動しているので、やや重い気がします。ここは今後の改善に期待したいところ。

Enjoy Testing!

2013年4月19日

Go Conference 2013 spring に参加しました

今回は100人以上も参加者がいました。もはやマイナー言語ではないですね!

GitHubでの言語ランキングでは24位ですし。

ちょっと前からGoが気に入って書いていますが、Goの波はもう来ているなと確信しました。

Go言語は、今現在C/C++/Javaといった言語が使われている領域で、新たに使われるようになると思います。Go言語は静的で強い型付けですが、LL的な側面もあり、広い領域で適正があります。



さて、僕は「Go on AppEngine」というLTをやりました。

スライドはこちら:
https://docs.google.com/presentation/d/1nYK5uM_Ac9U36fb1X9RRrFbpIRWSWXbkOgzhx2lVvEQ/edit?usp=sharing

ハッカソン中は、testbedというGoogle App Engine向けのパッケージを開発していました。

SDK 1.7.6以降で動かなくなっていた問題への対応を模索しましたが、いまのところSDK側のコードにパッチをあてる必要があります。詳しくはGitHubのREADMEで。


鵜飼さん、Derekさんのセッションは、Goの良さが伝わる素晴らしいお話でした。Goを始めたばかりの人には、そうとう刺さったのではないでしょうか。

Jxckさんのセッションでは、チャネル関連のパターンの復習になりました。closeとかあったの気づいてませんでした……。

methaneさんのセッションは、goroutineの裏側が少し分かり、ためになりました。隠蔽されているとはいえ、ある程度の仕組みが分かっていると、より効率よく書けますし。goroutineまわりは、1.1でパフォーマンスが上がるようです。

# 上記以外のセッションは、コード書いててあまり聞いてなかったです。ごめんなさい。

最後になりましたが、幹事のymotongpooさん、Jxckさん、会場提供のオラクルさん、ありがとうございました。

2013年2月12日

Go言語でGoogle App Engineの単体テストをするパッケージ

[追記] 1.8.6 からSDKに単体テストのためのパッケージが追加されました。

詳しくは http://najeira.blogspot.jp/2013/10/google-app-engine-186-go.html を読んでください。

----

Google App Engine Go で単体テストするためのパッケージを作りました。

こんな感じでテストを書けます。

package yourapp

import (
 "appengine/datastore"
 "net/http"
 "testing"
 "github.com/najeira/testbed"
)

const (
 PYTHON    = `/usr/local/bin/python27`
 TESTBED   = `/usr/local/google_appengine/goroot/src/pkg/github.com/najeira/testbed/testbed.py`
 APPENGINE = `/usr/local/google_appengine`
)

func TestAllocateIDs(t *testing.T) {
 bed := testbed.NewTestbed(PYTHON, TESTBED, APPENGINE)
 bed.Start()
 defer bed.Close()
 
 // create a dummy context
 r, _ := http.NewRequest("GET", "http://example.com/", nil)
 c := bed.NewContext(r)
 
 // write your test codes here
 low, high, err := datastore.AllocateIDs(c, "Test", nil, 10)
 if err != nil {
  t.Errorf("got error: %v", err)
 }
 if high - low != 10 {
  t.Errorf("wrong values: %d, %d", low, high)
 }
}


もともとAppEngine GoのSDKでは、Python版の開発サーバを動かしGo言語側とAPI経由で通信して動くようになっています。これは、Datastore等のAppEngineのスタブがPythonで実装されており、Go版がないためです。

このため、Go言語単体では、AppEngineのAPIを使った単体テストを行うことができませんでした。

このGo言語の testbed パッケージでは、Pythonのプロセスを起動してスタブを準備させ、Go言語側からプロセス間通信で呼び出してAPIを使えるようにしました。

Pythonを起動しないといけないので、環境に応じた引数を受け取らないと動きません。ここがダサいので、何かいい方法があれば pull request お待ちしております。

2012年11月28日

AppEngine開発環境でのConsistency


AppEngineのDatastore(今では標準となったHigh Replication Datastore)では、クエリのConsistency(一貫性)はEventualです。

このため、開発環境でもHRDの設定にしている場合、Eventual Consistencyな動作になります。

実は開発環境でのDatastoreのConsistencyの動作は設定によって変えられるのですが、デフォルトはTimeBasedHRConsistencyPolicyというものになっています。

# このクラスは google.appengine.datastore.datastore_stub_util の中にあります。

これは名前の通り、時間の経過によってクエリに検出される確率があがります。

Python SDKのコードを読むと、その確率は100msで98%、300msで99%、2000msで99.5%、240秒で100%となっています。

さて、AppEngineのSDKは単体テストを開発環境だけで実行することが容易になっていますが、前述したConsistencyについて、テストの場合には注意が必要なので、解説します。

Python版では、単体テストにはtestbedというモジュールを使います。これは、テスト用の各種サービス(API)のスタブです。

testbedについては公式のドキュメントも参照してください。
https://developers.google.com/appengine/docs/python/tools/localunittesting?hl=en

単体テストの場合、ConsistencyのデフォルトはMasterSlaveConsistencyPolicyです。つまりConsistencyはStrongで、保存したデータは即座にクエリに検出されます。

単体テストではStrong Consistencyは便利なのですが、MSなのでCross Group Transactionが使えません。よって、HRDアプリのテストではHRD用のConsistencyを使う必要があります。
self.testbed = testbed.Testbed()
self.testbed.activate()
self.testbed.init_datastore_v3_stub(consistency_policy= datastore_stub_util.TimeBasedHRConsistencyPolicy())

こんな感じです。

ここで困るのは、Eventualだとテストが書きづらくなることです。

保存したデータがクエリに検出されるかどうか、というテストが、Eventualなので成功したり失敗したりします。

実は、保存したデータをGetするとインデックスが作られるので、 Put > Get > Query とすると、テストが上手く行きます。

しかし、もっとスマートにテストを書くために、TimeBasedHRConsistencyPolicy ではなく、 PseudoRandomHRConsistencyPolicy を使ってみましょう。

これはクエリに検出されるかどうかを、(時間経過によらず)ランダムに決定するポリシーです。
コンストラクタの引数で確率を0.0から1.0までで指定できます。ここで確率を1.0、つまり100%にすると、Strong相当の動作になります。
self.testbed.init_datastore_v3_stub(consistency_policy= datastore_stub_util.PseudoRandomHRConsistencyPolicy(1.0))

これで、シンプルにテストを書けるようになります。


なお、Javaにも同様の機能があるようです(LocalDatastoreServiceTestConfigクラス)。
https://developers.google.com/appengine/docs/java/tools/localunittesting

2012年10月24日

AppEngine 1.7.3がリリース

公式Blog
http://googleappengine.blogspot.jp/2012/10/app-engine-173-released.html

さて、1.7.3のprereleaseの中身を見た時のメモ:

  • MemcacheShardingStrategy という名前がコード中に出てくる
  • DatastoreのPBにsafe_time_microseconds、safe_replica_nameという変数が追加
  • DatastoreのPBにSnapshotInfoというクラス追加。もしかしてスナップショットとれるのか!?
  • endpointsというモジュール追加。Pythonも対応なのかな?
  • appcfgにserver_idという記述が追加。I/Oでの発表であったやつかな? server.yamlのやつ。
  • appserverにinteractive_consoleなる記述。コンソールでPythonコード書いて動かせるのかな? あると便利。
1.7.2でもDatastoreのPBにgroup_byとかdistinctとかの記述が増えているので、Datastore周りが楽しみですね。

2012年9月24日

PyCon JP 2012 でセッションしてきました

PyCon JP 2012 の併設イベントにて

Google App Engineの「Python NDB APIの紹介」というセッションを行いました。

セッションで使用したスライドは http://goo.gl/Ugif4 です。
(以前のajnで使用したものを、少しだけアップデートしています)

Youtubeでセッションの動画を見ることも出来ます。
http://www.youtube.com/watch?v=hldoRF7fZEo

2012年5月2日

Google App EngineとGoogle Cloud Storage


Google App EngineとGoogle Cloud Storageが連携できるようにAPIが用意されつつあるので試してみた。

まず、AppEngineからCloudStorageにデータを保存するのは簡単。

files.gs.createしてopenしてwriteするだけ。

こんな感じ:

write_path = files.gs.create(
  filename,
  mime_type='application/octet-stream',
  acl='public-read',
  cache_control='max-age=31536000',
  )
with files.open(write_path, 'a') as fp:
  fp.write(data)

finalizeするまでは追記可能なので、ログをどんどん書き込むといった用途にも使える。


次にImages APIのget_serving_urlで使おうとしたところ、出来なかった。

まず、GSのファイル名からBlobKeyを作成するところまでは出来る。
gs_key = blobstore.create_gs_key(filename)
次に、このBlobKeyをget_serving_urlに渡す。
images.get_serving_url(gs_key)

開発環境だと、get_serving_urlは成功するが、スタブにバグがあって、そのURLへアクセスすると500になる。

プロダクションだとget_serving_urlがInvalidBlobKeyErrorになる。

というわけで今のところPicasa経由での表示は出来ない。

これが使い方の問題なのか、まだget_serving_urlが未対応なのか、どちらかは分からない。


なお、CloudStorage上のオブジェクトに直接アクセスする分には問題ない。


追記:
現時点では、get_serving_urlはGoogle Cloud Storageをサポートしていないそうです。
http://code.google.com/p/googleappengine/issues/detail?id=7441#c2

2012年4月18日

Eventual consistencyなクエリ結果のキャッシュ

AppEngineのHigh Replication環境では、QueryはEventual consistencyです。

Twitterで @zaki50 さんのツイート:


これはその通りで、データ更新時にmemcacheをdeleteしても、次のキャッシュ作成のタイミングで更新されたデータが読み込まれない場合があり、新しく作りなおしたキャッシュが古い内容のままになる可能性があります。

対応策としては

  • キャッシュの削除ではなく、キャッシュを上書きする

が考えられます。

データを更新した時、そのコンテキストでは何を更新したか分かっています。
そこで、Eventual consistencyでクエリを行い、そのクエリ結果と更新したエンティティをマージして、
新しいキャッシュを作成し、保存します。

これにより、作成されたキャッシュは新しい内容になります。


(更新メモ)
記事作成時はクエリをStrong consistencyにする方法を紹介しましたが、HRDでの非Ancestorクエリは常にEventuallyのようなので、修正しました。

https://developers.google.com/appengine/docs/python/datastore/queries#Setting_the_Read_Policy_and_Datastore_Call_Deadline
Setting the read policy to strong consistency for a non-ancestor query in the High Replication Datastore will have no effect.
「HRDではread policyをstrong consistencyに設定しても、何の効果もない」と書いてありました。

2011年9月7日

Google App Engineの新料金体系ではMax Idle Instancesの設定が必須

詳しい料金について:
http://www.google.com/enterprise/cloud/appengine/pricing.html

FAQ:
http://googledevjp.blogspot.com/search/label/app%20engine


Admin ConsoleのBilling Historyから、新料金体系の見積もりを見ることが出来るようになっています。

これによりTwitterの#gaejaをはじめとして、各地で混乱が生じているようです。

Datastore Ops、Storage、Bandwidthなどは、従来から値上がりはしますが、計算式は大きく変わらず、2倍以下の値上がりなので、個人的には許容できます。

問題は「Frontend Instance Hours」です。
現行では「CPU Time」が意味合いとしては近いです。

ところが、計算方法が大きく異なり、見積もりでの値上げ幅が非常に大きいケースがあるようです。

PlusFeedの場合、31CPUHが 880IHになり、課金額も13倍に:
https://plus.google.com/104961845171318028721/posts/DamjzZBVxd7

またTwitterで見ていても、7~8倍というTweetも見られましたし、3倍程度というTweetも見られました。

これは、インスタンスの課金の時間単位の仕組みが独特のため、アプリの内容によって、影響が大きかったり小さかったりするためです。

FAQより引用:
インスタンスは実際の稼働時間に加えて、スタートアップの費用として別の 15 分間に対しても課金されます。この費用は App Engine がインスタンスを立ち上げたり落としたりする時にかかるリソースをカバーします。したがって、1 つの on-demand インスタンスで 5 分間トラフィックを処理したとすると、5 + 15 分間分、つまり $0.08 / 60 * 20 = 2.6 セント課金されることになります。また、インスタンスが止まって、15 分経過する前に再度立ち上がった場合には、このスタートアップ費用は一度だけ課金され、この間ずっとインスタンスは動いているとみなされます。例えば、On-Demand インスタンスが 5 分間トラフィックをさばいて、その後 4 分間止まり、さらにその後 3 分間トラフィックをさばいたとするならば、課金対象の時間は (5+4+3)+15 分になり、課金額は $0.08 / 60 * 27 = 3.6 セントになります。

ほとんどアクセスのないアプリがリクエストを受け取るとインスタンスが起動しますが、最低でも15分間=0.25IH=0.02$が課金単位なので、最悪のケースでは1リクエストで0.25IHも使ってしまいます。

24IH/dayの無料枠があるので、1リクエスト=0.02$ということは起きませんが、1リクエスト=0.25IHとすると、CPUHでの計算時よりも最悪1000倍のコストになります。

ただし、アクセスがある程度あるアプリでは、上記の問題はありません。

たとえば月間500万リクエストの場合、平均すると15分間では3600リクエストを処理します。
1リクエストを500msで処理するならば、現行のCPUHの計算では0.2~0.5CPUHくらいで、新料金体系が理想に近いスケジューリングで回れば0.25IH~0.50IHで処理できます。
大きくは変わらないですよね。

逆に影響が大きいのはcronなどで定期的に処理を回しているアプリ。

いままでは毎分cron起動して1秒の処理をしていた場合、1日で0.5CPUHくらいでした。
ところが新料金体系では24IHになります。48倍です。
もちろん、24IH/dayは無料枠におさまりますが、cronからTaskQueueをキックするなどして複数のインスタンスが起動してしまう場合は24IHを超えてしまい数十セントから数ドルの課金になるでしょう。

上記のケースではTaskQueueのrate、bucket sizeを調整して、Taskがスパイクせず平準化されて実行されるように設定すると問題が緩和されると思います。

# 新料金体系ではTQをどう設定したら良いか? など、情報があれば教えてください

値上りが大きくて困っている人は、Admin ConsoleのApplication Settings内にある「Max Idle Instances」を、1などの小さな値に設定することを強くお勧めします。

どうやらMax Idle InstancesのデフォルトであるAutoは、非常に富豪的な設定で、使わなくなったインスタンスを延々と延命して、IHを浪費します。

よほどSpin-upが遅いアプリ以外であれば、これを小さくしても問題にはならないはずなので、新料金体系になるまでの期間で、どのくらいの値でいくらになるのかを見つつ、調整していくと良いと思います。


追記:
自分で管理しているアプリでMax Idle InstancesをAutoから1に変更したところ、Front Instance Hourが1/3ほどになりました。

2011年4月15日

Queryを非同期に使う

Queryは重い処理なので、複数回呼び出すときは非同期にするとよい。
Python版では、Query.run()を使うと非同期呼び出しができる。

例えば Diary というモデルがあったとして:

q = Diary().filter(...)
iterator = q.run()

このようにしてrunメソッドを呼び出すと、
裏では非同期にAPIが呼び出される。

runの戻り値はイテレータであり、
これを使おうとしたときに結果の待ち合わせが行われる。

よって、イテレータを使わないでおいて、
他のQuery.run()を呼び出せば、複数のAPIが並列にできる:

diary_query = Diary().filter(...)
diary_iterator = diary_query.run()

comment_query = Comment().filter(...)
comment_iterator = comment_query.run()

#これ以降でdiary_iterator,comment_iteratorを使う

Query.run()はイテレータを返すので、
Query.fetch()のようにlimitを指定できない。

そのままイテレーションすると、結果と時間の許す限り、
ずっとイテレーションしてしまう。

limitを指定したければイテレーション中に件数をカウント
すればいいが、面倒なので、自前のイテレータでラップする。


class QueryIterator(object):

  def __init__(self, query, limit=None):
    self.limit = limit
    self.count = 0
    if limit:
      config = datastore_query.QueryOptions(limit=limit, prefetch_size=limit)
    else:
      config = None
    self.iterator = query.run(config=config)

  def __iter__(self):
    return self

  def next(self):
    if self.limit and self.limit <= self.count:
      raise StopIteration()
    self.count += 1
    return self.iterator.next()
 
  def get_result(self):
    return [e for e in self]


これを使うと:

q = Diary().filter(...)
iterator = QueryIterator(q, limit=100)

for entity in iterator:
  ...

# 第一引数がq.run()じゃないことに注意

このイテレータは指定された件数に達するとイテレーションを終えるので、
際限なく処理が続いてしまうことはなくなる。

また、結果をリストで欲しい時は:

q = Diary().filter(...)
iterator = QueryIterator(q, limit=100)
entities = iterator.get_result()

のように使える。

2011年1月20日

BASEトランザクションのメモ

以下のようなコードで、数値のインクリメントなどの処理をexactly onceに出来る。

class Topic(db.Model):
  count_comments = db.IntegerProperty(default=0)

class Comment(db.Model):
  topic = db.ReferenceProperty(Topic)

class Flag(db.Model):
  pass

def txn():
  comment = Comment(topic=topic)
  comment.put()
  add_task(..., transactional=True)
db.run_in_transaction(txn)

def txn():
  flag_key_name = str(comment_key) + task_id
  if Flag.get_by_key_name(flag_key_name, parent=comment_key):
    return # already exceeded
  comment = Comment.get(comment_key)
  comment.count_comments += 1
  comment.put()
  flag = Flag(key_name=flag_key_name, parent=comment_key)
  flag.put()
db.run_in_transaction(txn)

2010年11月9日

Google App Engineについて

http://togetter.com/li/66450

これに関して書いておきます。

全体の流れはTogetterを見てください。

makotokuwataさんは勉強会を開催されていて影響力あると思うので、反論しておきます。

appengineには得手不得手があり、デメリットもあります。
これについてはmakotokuwataさんと意見の相違はないのですが、総合的に考えるとメリットのほうが大きい、というのが僕の意見です。


まずAppEngineがいまいちブレークしないのは、お金を集める仕組みが用意されていないことと、Datastore (Bigtable) の使い方が難しいことの2点だと思う。
Datastoreの難しさには同意です。
ただ、お金を集める仕組みはブレークには関係あるようには思えません。
appengineはアプリの実行環境であり、PayPalなりGoogle Checkoutなり、外部のシステムと連携すればいいだけです。
Amazon EC2やレンタルサーバにもPHPにもPythonにも、お金を集める仕組みは無いです。


で、2点目の「Datastore (Bigtable) が使いにくい」という点だけど、これはもうどうしようもない用に思う。つまり解決のめどが思いつかない。
これは、徐々にライブラリ等も増え、Google側も機能を増やしてくると思います。
ですので、いずれ解決する問題だと思います。



これはどちらが正しいかという話ではなくて、どちらにも利点と欠点はあるのだから、単に開発者がどちらを選ぶかという話に過ぎない。ただGAEでの問題点は、開発者には選ぶ自由がないことだ。つまり「性能はそこそこでいいから使いやすい方を選ぶ」ことができない。
利点と欠点があること、開発者が選ぶかに過ぎない、という点は同意です。
開発者には選ぶ自由がないというのは、プラットフォーム選定の段階で選んでいるので、選択の自由がないとは言えないでしょう。



GAEマンセーな人の意見はいつも、「高い性能を出すためには機能が犠牲になるのはやむを得ない」というものだけど、性能と機能のどちらを優先するかは開発者が決めることであって、プラットフォームで強制されることは、*本来は*おかしな話である。
性能と機能のどちらを優先するかは開発者が決めることなので、性能を優先するためにappengineを選んだのなら、それを「プラットフォームで強制される」とは言わず、制約ある環境を選んだだけでしょう。



あとアプリの性能は、なにもデータベースの性能だけに依存するものではない。どちらかというと、「いかにキャッシュをうまく効かせられるか」「いかにデータを分散できるか」のほうが重要であって、それだとBigtableである必要性は薄い。
データベースの性能だけに依存しない、というのは同意です。
appengineでもキャッシュは非常に重要な要素です。
ただ「データを分散」がRDBだと難しいのでappengineにメリットがあります。



いくらBigtableが高性能だといっても、RDBMSと比べるとずいぶん使い勝手が落ちるので、正直言ってプラスよりマイナスのほうが大きいんじゃないかと思う。
個人的には、appengineは多くのタイプのアプリケーションで、性能とコスト面でメリットがあるので、
マイナスよりプラスが大きいと思っています。



「高性能」と「高機能」はなかなか両立しない。そして歴史を振り返ってみると、「高性能(速いこと)」よりも「高機能(使いやすくて便利)」であるほうが好まれる傾向にあると思う。端的な例がプログラミング言語で、高性能なC++やJavaよりスクリプト言語のほうが今は人気だ。
「高性能」と「高機能」はなかなか両立しない、は同意です。
スクリプト言語に人気があるのは、ハードの性能向上により、性能の差を許容できるレベルになったからです。
逆に言えば、高性能なappengineが注目されているのは、高機能なRDBでは解決できない領域があるからです。ハードの性能があがれば、再びRDBの時代になるかもしれません。



データベースも、高性能だけど扱いにくいBigtabeよりも、性能は若干落ちるかもしれないけどずっと扱いやすいRDBMSのほうが、主流のままだと思う。
主流というのがよく分かりませんが、RDBは重要なところでこれからも使われるでしょうし、
メモリやSSDなどのハード性能向上により、さらに盛り上がる可能性もあるかなと思います。



GAEでも、BigtableにアクセスするよりもMemcacheにアクセスしたほうが高速なので、結局キャッシュしなきゃいけないなら、DBは多少遅いRDBMSでもいいと思う。
これは規模の問題で、アクセスが少なかったりデータ量が少ないならRDBでいいと思います。Datastoreはデータが数十~数百ギガバイトの単位になっても、数十ミリ秒でデータを取ってきます。RDBだと「多少遅い」ではすまないのではないでしょうか(高い商用サーバ等なら別かもしれませんが)。



あとGAEのBigtableは、現状ではどう考えてもRDBMSより信頼性が劣るので(マジでデータが消えることがある)、お金を扱うようなアプリをGAEで開発するのはおすすめしない。ブログとか掲示板とかにとどめておいたほうがいい。
5月頃に大規模障害があり、一部のデータが消えました。ただし、その後バックアップのデータは届き、手動でマージ出来ました。
RDBでもハードが壊れたら一緒なので、appengineの信頼性が低いわけではありません。レプリカなしでRDBを運用するくらいならappengineのほうが信頼できます。そして充分なバックアップ体制を用意するにはお金がかかるので、appengineにコストメリットがあります。



じゃあGAEは使い物にならないかというと、もちろんそんなことはなく、(A)収益は特に考えない(無料で使えればそれでいい)、(B)信頼性もそこそこでいい、(C)複雑なデータモデルは扱わない、という条件を満たすならGAEはお勧め。具体的にはブログとか掲示板ね。
ブログや掲示板が向いているというのは同意ですが、規模が必要ないならWordpressのほうが早いです(コードを書く必要すらありません)。
「信頼性」がデータという意味なら、僕は信頼にたるプラットフォームだと思っています。時々重くなったり、APIが失敗するという意味なら、たしかに1000回に1回はエラーが出ます。ただし自前のサーバで信頼性を担保するにはお金がかかります。よって、コストをかけてappengineより信頼性の高いシステムを作るか、コストメリットをとるか、です。



こういう条件を満たすアプリはけっこう多いはず。BTSとかグループウェアとか写真管理とか。反対に、在庫管理とか受注管理とか会計のシステムは、データモデルや検索条件も複雑だし、無理にBigtableやKVSを使うよりRDBMSを使ったほうがいいと思う。
在庫管理とか受注管理などはRDBがいい、には同意です。
その理由はデータ量が少ないからRDBでも性能要件を満たせるであろうから、です。
モデルが複雑だとしても性能要件が高いのであれば、appengineはありだと思います。




というわけで、Togetterをご覧になって、appengineをネガティブに捉えてた人もいらっしゃるかも知れませんが、
appengineは、いわゆるWeb開発では非常に大きなメリットがありますので、

ドキュメントをご覧になっていただいたり、
http://code.google.com/intl/ja/appengine/docs/

本も発売されていますし、
http://www.amazon.co.jp/%E3%82%AA%E3%83%BC%E3%83%97%E3%83%B3%E3%82%BD%E3%83%BC%E3%82%B9%E5%BE%B9%E5%BA%95%E6%B4%BB%E7%94%A8-Slim3-Google-Engine-Java/dp/4798026999/

勉強会に参加されたり、
http://atnd.org/events/9571

Twitterに #appengine タグで質問してみてください。

2010年10月8日

エンティティの更新スループット

チケットの購入システムのようなものをGoogle App Engineで実装する場合、エンティティの更新スループットが気になります。

在庫数を気にしないのであれば、購入ログを追記していくだけなので、ものすごいスループットが出るのですが、在庫数の管理をするのであれば、あるアイテムの在庫数のデクリメントが必要なので、衝突の可能性が出てきます。

そこで、実際の購入処理に近いように、Web経由で負荷テストしてみました。
abを使って、以下のようなコマンドを実行しました。
ab.exe -n 100 -c 10 example.jp/test_datastore_throughput
ハンドラは、グローバルなカウンタをインクリメントするだけ。

ある程度abで暖めておいてから、テストを実施しました。

3回実施したところ、

  • 成功76、失敗24、約9.0秒
  • 成功79、失敗21、約7.7秒
  • 成功68、失敗32、約8.3秒

となりました。

それなりに衝突が発生していることが分かります。

秒間あたりの成功数は8.4、10.2、8.1回です。

Google App Engineに関する記事によると
http://code.google.com/intl/ja/appengine/articles/sharding_counters.html
1 つのエンティティまたはエンティティグループの更新は毎秒 5 回ほど
とあるので、記事とは大きな差はないようです。


モデル:
class TestCounter(db.Model):
  count = db.IntegerProperty(required=True, default=0)

  @classmethod
  def incr(cls, key_name):
    def txn():
      obj = cls.get_by_key_name(key_name)
      if not obj:
        obj = cls(key_name=key_name)
      obj.count += 1
      obj.put()
      return obj
    return db.run_in_transaction(txn)

ハンドラ内:
TestCounter.incr('test_datastore_throughput')

2010年8月11日

Google App EngineでListPropertyを使おう

ListPropertyは複数の値が格納できる、便利なプロパティです。

複合インデックスでのインデックス爆発という問題はありますが、ListPropertyはGoogle App Engineには欠かせません。

複数選択が可能な(formでチェックボックスになるもの)は、ListPropertyを使うと実装しやすいです。

例えば、好きな動物というデータがあったとして:

class User(db.Model):
  fav = db.StringListProperty()

user = User()
user.fav = ['cat', 'dog', 'penguin']

としておけば、

users = User.all().filter('fav =', 'penguin')

のように、ペンギンが好きなユーザをクエリできます。

ここで問題なのは「どの動物も好きではない」ユーザをクエリで検索できないことです。
GQLではListPropertyが空であるという条件を表現できません。

そこで、ListPropertyのサイズも保存しておきます。

class User(db.Model):
  fav = db.StringListProperty()
  fav_len = db.IntegerProperty()

user = User()
user.fav = ['cat', 'dog', 'penguin']
user.fav_len = len(user.fav)

こうしておくと、fav_lenに要素数が入ります。
「どの動物も好きではない」ユーザは要素数がゼロですので、

users = User.all().filter('fav_len =', 0)

としてクエリで取得することが出来るようになります。

なお、他のプロパティと連動するプロパティにはComputedPropertyが便利です。
上記のUserを書き換えると:

class User(db.Model):
  fav = db.StringListProperty()
  @db.ComputedProperty
  def fav_len(self):
    return len(self.fav)

となります。これでエンティティが保存されるときに、自動でfav_lenがセットされます。

2010年8月8日

Google App Engineで、ときどきImportError

Google App Engineで、ときどきImportErrorが出ます。

もちろん開発サーバでは出ませんし、本番でも普通は出ません。ところが、ときどきImportErrorを出しまくるインスタンスがいます。

App Engineでは、一度importされたモジュールはキャッシュされ、2度目以降のimportはキャッシュされたものが使われます。

ところが、初回起動時のimportの途中でDeadlineExceededExceptionが発生した場合に、中途半端なimportが残ってしまい、ImportError多発となるようです。

Issue:
http://code.google.com/p/googleappengine/issues/detail?id=1409

この状態になってしまったインスタンスは、ずっとエラーを投げるので、どうしようもありません。そのインスタンスを終わらせる(スピンダウン)するしかありません。

簡単な方法としては、デプロイがあります。
新たにデプロイをすると、新しいバージョンで起動していくので、ImportErrorから復帰することが出来ます。

もうひとつは @higayasuo さんから聞きましたが、メモリ不足にすると、インスタンスは終了するようです。これはメモリリークのためのリセットの仕組みですね。

試しにメモリを確保しまくってみたところ、以下のログが記録されました。
Exceeded soft memory limit with 278.605 MB after servicing 2 requests total

これによれば、Pythonでは300MBくらいのヒープがあるようです。

"servicing 2 requests" はインスタンスが起動してからのリクエスト処理数ですので、あまり意味はありません。
# メモリリークの調査であれば意味はありますが

また、以下のようにもログが記録されます。

After handling this request, the process that handled this request was found to be using too much memory and
was terminated. This is likely to cause a new process to be used for the next request to your application.
If you see this message frequently, you may have a memory leak in your application.

訳すと

メモリ不足のため、このリクエストの終了後にプロセスは終了します。
これにより、次のリクエストの処理には新しいプロセスが利用されます。
このメッセージが頻繁に出る場合、アプリケーションにメモリリークの可能性があります。

というわけです。

ImportErrorの発生したインスタンスを強制的にスピンダウンするため、
わざとメモリ不足を発生させる……のもありかもしれません。

本当は、import中のDEEで、中途半端なimportをクリア出来ればいいのですが、方法が分かる方がいらっしゃいましたら、教えて頂けるとありがたいです。

2010年8月4日

Google App Engine 1.3.6 プレリリース

Google App Engine 1.3.6 のプレリリースがありました。

Python SDKの中身を見てみました:
  • db.is_in_transactionが追加
    # 正式APIなのはありがたい
  • fancy_urllibが追加
  • countの1000上限が撤廃
  • Datastore関連でunapplied_log_timestampというPBあり
    # なんだろう?
  • AllocateIdsでMAXが指定できる。allocate_id_rangeも復活
    # むかし、SDKにこっそりあったけど、途中でなくなったよね?
  • inbound_servicesにwarmupが追加
    # スピンアップ高速化のための予備インスタンスとか?
  • 500のエラーハンドラが設定可能に
    # これは素晴らしい! 標準のエラー画面がひどすぎるので……。
  • memcacheのPBにCAS向けの値が追加。APIは1.3.7か?
  • Blobstoreの画像のリサイズしたものへのURLを生成できる
    # Appでリサイズしなくていい!