Google App Engine / Python 上での開発で最初から知ってればよかった、ってことをいくつか

23rd May, 2011 (Updated on 29th Jun, 2011) | Google App Engine python

ここ数ヶ月、Google App Engine/Pythonを使い、初めてちょっとしたものを作ってみているのだけど、開発初期から知っておけばよかったなー、と思うノウハウ/tips的なものをずらずらと書いてみる。

基本的な環境設定は、 以前書いた まま。

0. 公式ドキュメントを良く読む

言うまでもなく、だけど、 マニュアル はもちろん、 この辺 の下の読み物も、流し読みだけでもしておいたほうがいい。

datastoreとmodel的なところ

1. key nameを使いこなす

key nameは、レコードの作成時に指定できる(RDBでいう)primary keyの別名みたいなもの。primary key自体は自動的で作成されるので開発者が指定できるのはkey nameだけ。

key nameをうまく使うことで、datastoreを使いやすくすることができる。特にdatastore上で"unique"を表現したいときに活躍する。

たとえば、 Modelクラスのget_or_insert では、key nameを使うことで簡単にuniqueなレコードを作成することができる。get_or_insert()と言う名前通り、 key nameで指定したレコードが存在したらget、しなかったらそのままinsertといういうことができる。この処理はatomicに行われるので、同じkey nameのレコードが複数存在してしまうこともない。

get_or_insert()の注意点としては、 get時には渡した値ではupdateされない、ということにということがある。そのまんまget or inssert。

2. どういうkey nameを使うべきか

  • 他システムから取り込んだレコードでuniqueとされている値

例えば、twitter botとか作るなら(作ってないけど)、twitterのユーザIDとか、tweet毎のIDとかそのまま key nameに入れてしまうのが良さそう。
今作っているシステムでは、別システムのMongoDB上で作ったデータをそのまま取り込んでいる部分がある。そういうデータは MongoDBのBSON ID を文字列にしたのをそのまま key nameにしている。なかなかいい感じ。

  • クライアント側で生成されたデータ

クライアント側で生成されるようなデータをdatastoreに格納する場合、クライアント側でUUIDみたいのを作成し、それをそのままkey nameにするのもなかなかよさそう。

このUUIDをkey nameにしてget_or_insert()することで、クライアント端末やネットワークの不調等で同一データが2回POSTされたりしても、データが二重になったりしないのがなかなか気に入っている。もちろんUUIDを偽造されても問題ないような作りにしておく必要はあるけど。

  • 複合キー

key nameは一つしか設定できないのだけど、ただの文字列なので上記の値を結合して関連テーブルを作成したりできる。これは場合によってはなかなか便利。

  • その他

uniqueっぽいものがないモデルの場合でも、とりあえずランダムで長めの文字列を指定しておくとよいかも。後からは指定できない。

3. GQLで key nameで検索

key nameでのオブジェクトの取得は、基本的には、 get_by_key_name を使えばいいのだけど、WebのadminコンソールとかGQLしか使えないようなところでは、こんな感じに書ける。

WHERE __key__ = KEY('FooModel', 'cff0611529520910195ed357dd69f908')

4. get_or_insert() でgetされたかinsertされたかを知る

get_or_insert() を実行した結果、getされたかinsertされたかがそのままでは簡単にはわからないのが、これを知りたいときがある。例えば「get_or_insert()してgetだった場合には〜する」みたいなとき。

こういうときのために、次のようなclass methodを作っている。

@classmethod
def get_or_insert2(cls, key_name, **kwds):
  def txn():
    entity = cls.get_by_key_name(key_name, parent=kwds.get('parent'))
    if entity is None:
      entity = cls(key_name=key_name, **kwds)
      entity.put()
      return (True,entity)
    return (False,entity)
  return run_in_transaction(txn)

または、 insertに成功した場合だけオブジェクトを返し、既に存在する場合はNoneを返す処理

@classmethod
def insert_or_fail(cls, key_name, **kwds):
  def txn():
    entity = cls.get_by_key_name(key_name, parent=kwds.get('parent'))
    if entity is None:
      entity = cls(key_name=key_name, **kwds)
      entity.put()
      return entity
    return None
  return run_in_transaction(txn)

5. unique制約

datastore上で、unique制約を作るのはちょっと大変。ユーザ情報を格納するUserモデル内でemailアドレスをuniqueにしたい、とか非常にありがちなことが簡単にはできない。

こういう場合にも、key nameがうまく使える。unique制約を保持するためのモデルを別に作って、uniqueにしたいデータ(この場合emailアドレス)をkey nameにしてしまうのがいい感じ。上記の、insert_or_fail で作成してみて、作成できたらそのemailアドレスは使える、という感じ。(何か他に定石がありそうなんだけど。)

6. データの一括更新

システムのアップグレード時など、データを一括で更新したくなるときがある。SQLだとUPDATE一発みたいな更新でも、datastoreだとちょっと大変。こういうときは map/reduce を使うと素早く簡単に全件の更新ができてよい感じ。(件数多いと、CPU時間==お金を一気に消費しちゃうけど)

(このappengine-mapreduceはmap/reduceってか、単に全レコードに対して並列で何かができるだけなんだけど。本当のmap/reduceはそのうち導入されるらしいので楽しみ。)

7. ランキング

ちょっとしたゲームっぽいものを作ると「スコアランキング」みたいなものが必要になったりするものだけど、このランキングってのはRDBでもそれなりに厄介なもので、何も考えないで作ると、大量のアクセスが発生してしまい使いものにならなくなる。その結果、バッチ処理で静的なランキングを作成するとかちょっとダサい感じになってしまいがち。

で、ちょっと自分で色々なアルゴリズムで実装してみたりしてたのだけど、便利なライブラリを見つけてしまったのでそれを使っている。 このライブラリ。

8. 複数レコードの保存/削除は db.put() / db.delete() を使って一括で行う

スピードが全然違う。db.delete() はクエリをそのまま突っ込んだりもできる。

9. 全文検索

全文検索は、現在の Google App Engine ではまともに実装されていないらしい。(Googleのくせに! 近い将来実装されると発表されたが)

ただ、簡易的には SearchableModel が使える。使い方は、 コードの中のコメント が一番詳しいような気がする。

そのコメントにあるように、かなり低機能で日本語も使えない。 ただ、とりあえずそれっぽく動かすだけなら簡単に使える。

ログ関連

10. loggingを使ってログを取る

logging moduleを使うことで、実行時に色々なログを取れる。Google App Engineともうまく統合されていて、Web上のadminインタフェースで、httpアクセス毎に参照できてとても便利。

11. POSTされたデータとそれに対応するresponse bodyをすべてログに

loggingを使ってPOSTの中身とresponseをそのままログに取っておくとデバッグ時に便利。Google App Engineの管理画面からアクセス毎にPOSTとそれに対応するresponseの内容を見ることができる。

これをするためのMiddlewareを書いた。(実際には 前回 書いたようにセッション管理用の SessionMiddleware も挟んでいる。)

class PostLoggerMiddleware(object):
  def __init__(self, app):
      self.app = app
  
  def __call__(self, env, start_response):
      request = webapp.Request(env)
      if request.body:
          logging.info("request: " + str(request.body))
      
      def my_start_response(status, response_headers, exc_info=None):
        write = start_response(status, response_headers, exc_info)
        def my_write(body):
          logging.info("response: " + str(body))
          write(body)
        return my_write
      
      return self.app(env, my_start_response)
  
  application = PostLoggerMiddleware(webapp.WSGIApplication([
  ...

12. exceptionをtrackback付きでログに

基本的に何もしなければ、Traceback付きでログに乗る。しかし、コード側で、 handle_exception を定義してexceptionをすべてcatchして処理を続けたいときもある。

たとえば、現在JSON APIをGoogle App Engine上で作っているのだけど、例外が起きた時もJSONでエラーを返すことにしている。

こういう場合には、その中で JSONを返す処理を書きつつ、logging.exception(exception) と書いておくと、admin画面上では普通にTraceback付きでエラーが見える。

その他

13. 自分のアプリを複数バージョン同時に持てる。ただし datastore は共通。

app.yaml で指定する。

最初知ったときは、datastore共通で何に使うんだよ、と思ったんだけど色々便利なこともあった。

  • 新バージョンのテスト。わりとメインの使い方か。でも、datastoreは一個なのであまり思い切ったことはできない。
  • バージョン毎にログが別になる。Google App Engineのadmin画面はバージョン毎なので、ログも別々になる。map/reduceとかを同じバージョンでやるとログがそれで溢れてしまってうざいことがあるんだけど、それも別にしておくといいかもしれない。
  • propertyの型を変えるとかちょっとしたmigrationをするときに別のバージョンでやるのがいい(下記参照)。

14. 非同期でいい処理にはできる限り Task Queue を使う

リアルタイムでなくていい処理は全部 Task Queue に回してしまってもいいぐらいかも。

主な理由は二つ。

  • HTTPアクセスの処理を早く終わらせるため

これはもちろんユーザのために、という意味もあるが、Google App Engineの設計ではとにかく早く処理を返すことがスケール面でも有効らしいので大事。

  • リトライしてくれる

Google App Engineはそれなりに不安定なので、Google App Engineの都合でエラーがそれなりの頻度で起こる。そのさい task queue に入れておけばGoogle App Engineが直るまでretryしてくれる。

特に、mail送信などのAPIはしばしばtimeout (例えばこんなエラー: “DeadlineExceededError: The API call mail.Send() took too long to respond and was cancelled”) とかするので、これは必ず task queue に入れておいたほうがよさそう。メールは元々非同期で問題ないので相性もいい。

15. Modelのpropertyの型の変更

datastore自体はどんな型でもレコード毎に柔軟に持てるのだけど、その上で動くModelクラスは型にとても厳しくなっている。floatからintegerへなどの型も自動的には変換してくれない。その上read時にexceptionを出してくれてしまうので、結構厄介。

運用を初めてから型を変更する必要がでてきた場合には、以下の方法がいいみたい。

db.Expando クラスを使って値を更新する。db.Expandoクラスは、Modelのサブクラスなんだけど、設定していないpropertyも読み書きができる。また、システムを止めずに更新したい場合には、アプリの別バージョンを作成し、その上で行うと影響をあまり与えずにできる。

例:

class HogeUser(db.Model):
  foo = db.StringProperty()
  bar = db.StringProperty()
  score = db.FloatProperty()

このscoreをfloatからintegerに変更したくなったとする。

こういう場合には、このクラスを一時的にdb.Expandoから継承するようにして、かつscoreを削除してしまう(データ自体は消えない)。

class HogeUser(db.Expando):
  foo = db.StringProperty()
  bar = db.StringProperty()
  # score = db.FloatProperty()

そして、以下のようなmap/reduceを走らせる。

def alter_score_to_int(hoge_user):
  if "score" in hoge_user.dynamic_properties() and type(hoge_user.score) is float:
    hoge_user.score = int(hoge_user.score)
    hoge_user.put()

終わったら、元に戻して Int に変更する

class HogeUser(db.Model):
  foo = db.StringProperty()
  bar = db.StringProperty()
  score = db.IntegerProperty()

16. エラーをメール通知

http://code.google.com/appengine/articles/python/recording_exceptions_with_ereporter.html

標準で入っているこのライブラリを使うと、発生したエラーをdailyでまとめてメールしてくれる。それほど高機能ではないが、同じようなエラーはまとめるという最低限なことはしてくれる。自分でメールの内容をカスタマイズもできるみたい。

こんな感じで最初の方に書いている。ローカルでのSDK環境の場合、エラーになってしまうのでスキップしている。

in_local_sdk = (os.environ.get("SERVER_SOFTWARE") == "Development/1.0") or os.environ.get("TERM")
if not in_local_sdk:
    from google.appengine.ext import ereporter
    ereporter.register_logger()

その他の設定は、上のリンク先のまま。

17. 死活監視

Google App Engine上のアプリの死活監視をする場合の注意点として、Google App Engineが不安定になっている場合、readはokで、writeだけがエラーになるケースがあるようなので、writeが発生するような処理で監視をしたほうがよさそう。

参考にしたところ

http://www.mail-archive.com/google-appengine@googlegroups.com/msg20008.html
http://stackoverflow.com/questions/4742875/change-integerproperty-to-floatproperty-of-existing-appengine-datastore
http://devblog.miumeet.com/2011/02/schedule-mapreduce-daily-on-appengine.html
http://googleappengine.blogspot.com/2009/06/10-things-you-probably-didnt-know-about.html

他多数、だが忘れた。


記事の内容についての質問、苦情、間違いの指摘等なんでもtwitterでどうぞ。