エイバースの中の人

アプリとWEBサービスを作っているABARSのBLOGです。

AppEngine

AppEngineでLAN内の端末からデバッグする

AppEngineのdevserverではhttp://localhost:8084/にアクセスすることでデバッグをすることができます。iPad対応のサイトを作る場合は、実機でもテストできると便利です。何となく、http://マシンのIP:8084/で繋がりそうですが、実際は繋がりません。

LAN内の端末からアクセスできるようにする場合、GoogleAppEngineLauncherのApplicationSettingsのLaunchSettingsのExtraFlagsに、--host=0.0.0.0を追加する必要があります。

host

この設定をしておくと、http://マシンのIP:8084/、でアクセスできるようになります。

AppEngineのChannelAPIを使用して非同期通信を行う

サーバからクライアントにメッセージをPUSHしたい場合、一番簡単な実装はポーリングになります。クライアントが、1秒に1回など、サーバに更新が無いかを問い合わせることで、擬似的にPUSHを行うのです。

しかし、ポーリングで実装した場合、ラグを少なくするために、問い合わせの間隔を短くすると、クライアントとサーバの両方の負荷が大きくなります。そのため、ポーリング間隔は、ある程度大きく取る必要があり、ラグが大きくなってしまいます。

そこで、サーバとのセッションを維持し続けておくロングポーリングや、HTML5のWebSocketなどを使って、このラグを小さくすることが行われています。これらの非同期通信を、簡単に使えるようにライブラリ化したのが、GoogleAppEngineのChannelAPIです。

ChannelAPIは、サーバサイドのPythonのライブラリと、クライアントサイドのJavaScriptのライブラリがあり、サーバからクライアントへのPUSH通信が行えます。開発サーバはポーリングで実装されており、実サーバではロングポーリングもしくはWebSocketで実装されています。

ChannelAPIのサーバサイドは、何と2つのメソッドしかありません。まず、任意のユニークな64文字以下の文字列(クライアントID)から、create_channelでJavaScriptのAPIで使用するトークンを作ります。


from google.appengine.api import channel

client_id=str(user.user_id()) + "_"+ str(server_time)
token = channel.create_channel(client_id)


サーバからデータをPUSHする際の宛先にはクライアントIDを使用するので、作成したクライアントIDをDataStoreなどに保存しておきます。


room.channel_client_list.append(client_id)
room.channel_client_list_for_reconnect.append(client_id)


2つのStringListに格納しているのは、ConnectionとDisconnectionの管理のためで、後述します。

メッセージをクライアントにPUSHするには、channel.send_messageを使用します。引数は、クライアントIDとメッセージです。例外は起きません。データが確実に送信できたかも保証されません。


for client in room.channel_client_list:
 channel.send_message( client , message )


JavaScript側では、次のようにしてライブラリを読込みます。


<script src="/_ah/channel/jsapi"></script>


クライアントでは、サーバのcreate_channelで作成したトークンを引数に、Channelオブジェクトを作成します。Channelオブジェクトのopenにサーバからのメッセージを受け取るコールバック関数を指定します。


var channel = new goog.appengine.Channel( token );
var socket = channel.open({
onopen : function(){
 alert("サーバとのコネクションを確立しました。");
}
, onmessage : function(message) {
 alert("サーバから"+message.data+"を受信しました。");
}
, onerror : function(error) {
 alert("サーバとの接続でエラーが発生しました。リロードして下さい。"+error.description);
}
, onclose : function(){
 alert("サーバとの接続がクローズされました。リロードして下さい。");
}
});


クライアントからサーバにデータを送る方法は無いので、普通にHTTPでポストします。

サーバでは、新規クライアントからの接続と、切断で、POSTイベントが来ます。


application = webapp.WSGIApplication(
('/_ah/channel/connected/',ChatConnected),
('/_ah/channel/disconnected/',ChatDisconnected),


イベントのfrom引数にトークンに対応したクライアントIDが届きます。


class ChatConnected(webapp.RequestHandler):
 def post(self):
  client_id = self.request.get('from')

class ChatDisconnected(webapp.RequestHandler):
 def post(self):
  client_id = self.request.get('from')


チャットなどでは、現在ログインしているクライアントIDのリストを適当なStringListPropertyのchannel_client_listなどで管理しておき、/_ah/channel/disconnected/でchannel_client_listからクライアントIDを除外します。

ここで注意しないといけないのは、PCでは接続->切断まで一回しかイベントが来ませんが、iOSなどでは接続->切断->接続->切断などのように、同じクライアントIDから、定期的に再接続が来たりします。具体的に、MobileSafariでホームボタンを押すと切断、ブラウザを再び起動すると接続が来ますし、何もしてなくても一定時間で再接続が来たります。

そのため、チャットなどでは、現在ログインしているクライアントIDを管理するchannel_client_listの他に、一度でも接続しているクライアントIDのリストchannel_client_list_for_reconnectなども準備しておき、/_ah/channel/connected/が呼ばれた場合に、channel_client_list_for_reconnectに含まれる場合は、channel_client_listに再び追加する必要があります。


class ChatConnected(webapp.RequestHandler):
 def post(self):
  client_id = self.request.get('from')

  #再接続先のチャットルームを探す
  query=ChatRoom.all()
  query.filter("channel_client_list_for_reconnect =",client_id)
  room_list=query.fetch(offset=0,limit=100)

  #再接続
  for room in room_list:
   if(not(client_id in room.channel_client_list)):
    Chat.add_user(room.key(),client_id)


また、Disconnectが呼ばれないこともあるので、2時間経過したユーザは取り除くとかしておくとよいです。2時間というのは、ChannelAPIのトークンの有効期間です。また、切断された場合にJavaScript側にイベントが来ないこともあるので、POSTする際に、クライアントIDも送って、サーバのchannel_client_listに含まれていなかった場合はエラーを返すなどもした方がよいです。

ということで、ChannelAPIを使用して、HTML5で動くお絵かきチャットを作りました。iOSやAndroidでもChannelAPIが動くことを確認できました。



サーバ側のコードは以下に置いておきました。

Chat.py
ChatConnected.py
ChatDisconnected.py
ChatRoom.py

ChannelAPIは手軽に非同期通信できて便利です。リアルタイム系のゲームもGAEでいけそうですね。

参考文献:[GAE] [Python] Google App EngineのChannel APIを試してみた

AppEngineでIndexed=Falseを活用して課金額を下げる

AppEngineでdb.Modelを定義した場合に、Propertyには自動で昇降順のIndexが付きますが、これはQuotaのDatastore Writeに影響します。具体的に、オブジェクトをputした際、オブジェクトが書き込まれると同時に、Propertyに対応したIndexも更新され、そのIndexの更新に課金されます。このIndexは、DataStoreのfilterで使用されますが、検索に使用しないPropertyにもIndexが自動的に作成され、課金されています。

そこで、queryのfilterに使用しない項目については、次のようにpropertyにindexed=Falseを付けると、Indexが作成されなくなり、オブジェクトのput時のDatastoreWriteを抑制でき、課金額が下がります。(参考:PropartyにIndexを付けないことは課金チューニングになるが、そもそも課金処理おかしくないか?(おかしくなかった)


class Analyze(db.Model):
bbs_key =db.ReferenceProperty()
ip = db.StringProperty(indexed=False)
adr = db.StringListProperty(indexed=False)


ただし、indexed=Trueに再度変更した場合でも、インデックスの追加は次回のputからになるので、慎重にindexed=Falseを設定する必要があります。

AppEngineのPython2.7でPILを使う(MountainLion)

AppEngineで画像を扱うにはPIL(PythonInstallLibrary)を使用します。しかし、PILはデフォルトでインストールされていないので、自前でインストールする必要があります。

まず、MacPortをインストールします。次に、Xcodeをインストールして、Preference->DownloadsからCommandLineToolsをインストールします。

コマンドラインから次のコマンドを打って、PILをインストールします。

sudo port install PIL


この状態では、AppleのPython2.7(/usr/bin/python)と、MacPortのPython2.7(/opt/local/bin/python)が存在しています。AppEngineのデフォルトではAppleのPython2.7(/usr/bin/python)を使用するようになっているので、AppEngineLauncherのPreferenceのPythonPathを、/opt/local/bin/python2.7に変更し、MacPortのPILを含んだPython2.7を使用するようにします。

path


これで、PILが使えるようになります。

AppEngineにおけるBackendsの使い方

Backendsを使うと、フロントのインスタンスとは別の、バックエンドのインスタンスでプログラムを実行できます。定期的なランキングの作成などの重めの処理に使うと便利です。バックエンドは、タスクキューやcronの実行先ターゲットに、target引数でバックエンドを指定するだけで使うことができます。バックエンドの無料枠が9インスタンス時間もあるので、活用しないのは損です。また、バックエンドに処理を逃した分だけ、フロントの処理が空くので、サーバのレスポンスも改善すると思います。尚、バックエンドのドキュメントはまだ日本語化されていないので、Backends Python API Overviewを参照します。

まず、開発用サーバでBackendsを有効にするために、開発用サーバを--backendsオプションを付けて起動します。

backends


次に、backends.yamlを作成します。バックエンドは同時に1つのリクエストしか処理できません。instancesを指定しないと、最大で20個までインスタンスが作成されて大変なことになるので、とりあえず1とかに設定しておいた方が無難です。

backends:
- name: backend1
class: B1
instances: 1
options: dynamic


queue.yamlのTaskQueueの実行ターゲットをバックエンドに設定します。

queue:
- name: counter
target: backend1
rate: 1/s


cron.yamlでも同様に実行ターゲットをバックエンドに設定します。

cron:
- description: daily hoge
url: /hoge
schedule: every 6 hours
target: backend1


後は、開発サーバでテストして、デプロイします。フロントとバックエンドは別管理されているようで、AppEngineLauncherからはデプロイできません。

コマンドラインから、以下のように入力してデプロイする必要があります。

appcfg backends . update


デプロイし忘れが起こりそうなので、次のようなdeploy.shを作って、常にコマンドラインからフロントとバックエンドを同時にデプロイするようにした方が良さそうです。Uploading, Downloading, and Managing a Python Appにコマンドラインからのコマンド一覧があります。

appcfg.py update .
appcfg.py backends . update


バックエンドが動作しているかは、AppEngineのAdminConsoleで確認できます。

backends

AppEngineのDataStoreのバックアップ方法

ファーストサーバのデータ消失が話題です。データ消失はサービス運営において一番のリスクで怖いですね。レプリケーションではプログラムのミスによるデータ消去を回避できないので、定期的に、別系統へのリードオンリーのコピーを作成しておいて、最悪、そこまではロールバックできるようにしておく必要があります。

そこで、今回はGoogleAppEngineでのDataStoreのバックアップ方法を紹介します。AppEngineでDataStoreのバックアップを取るには、AdminConsoleにログインして、DataStoreAdminを選択します。

backup1


保存したいEntityを選択して、BackupEntitiesをクリックします。

backup2


バックアップは、2GBのデータで30分程度で終了します。

保存先は、BlobstoreとGoogleCloudStorageを選択できます。GoogleCloudStorageは5GBまで無料で$0.12/GB/Monthです。BlobStoreも5GBまで無料で$0.129/ GB/Monthです。ほとんど変わらないのでお好みで選択して大丈夫そうです。

バックアップデータからの復元も、DataStoreAdminから簡単に行えます。

restore


後は自動スケジューリングが出来ればばっちりですね。ここは、バージョンアップに期待です。

AppEngineのHRDデータストアで整合性を取る

AppEngineのHRDデータストアはMSデータストアとは異なり、データの整合性が保証されません。そのため、putした直後にqueryを投げると、putした要素を取得できないことがあります。これにより、AppEngine公式サイトの一番簡単なサンプルプログラムも動作しないという、衝撃的な状況となっています。

具体的に、サンプルプログラムでは、 greeting.put()した後にredirectしてgreetings = db.GqlQuery("SELECT * FROM Greeting ORDER BY date DESC LIMIT 10")を投げていますが、put()がインデックスに反映されるまでに、数十ms〜2sec程度かかるため、putしたデータは表示されません。リロードすると表示されます。

この問題を回避するために、Googleとしては、memcacheとの併用を勧めています。

上記の高レプリケーションのサンプル コードでは、ゲストブックごとに 1 つのエンティティ グループに書き込みを行います。このため、1 つのゲストブックに対するクエリには強い整合性がありますが、ゲストブックの変更は 1 秒あたり 1 件の書き込み(エンティティ グループに対してサポートされる制限)に制限されます。このため、頻繁な使用が予想される場合は、ゲストブックごとに 1 つのエンティティ グループに書き込むことはおすすめしません。アプリケーションで頻繁な書き込みが発生しそうな場合は別の方法を使用することを検討してください。たとえば、最近の投稿を期限付きで memcache に配置し、memcache 内の最近の投稿とデータストアから取得した投稿を合わせて表示することができます。

しかし、MSデータストアからの移行時に、既存のアプリケーションをすべて書き換えるのは現実的ではありません。

そこで、put_sync()的に、インデックスに反映するまでウエイトするようなメソッドがあるといいのですが、存在しません。ということで、対処療法ですが、put_sync的な動作をするメソッドを作りました。使い方としては、整合性を取りたいオブジェクトをobj.put()する代わりにSyncPut.put_sync(obj)とします。これで、putした後、インデックスに反映されるまで待機することができます。

実装としては、putした後、インデックスに反映されるまでqueryを投げ続けています。乱数とオブジェクトの型、現在の経過秒数からほぼユニークなsandとなる文字列を作っておき、sandでfilterすることで、最新の情報に更新されたかを確認しています。

#!-*- coding:utf-8 -*-
#!/usr/bin/env python

from google.appengine.ext import db

import random
import time

class SyncPut():
	@staticmethod
	def put_sync(obj):
		#ほぼユニークなsandを作成
		rand=random.randint(0, (1<<30))
		sand=""+str(type(obj))+"_"+str(time.time())+"_"+str(rand)
		
		#putする
		obj.sand=sand	#Modelにsand=db.StringProperty()を追加すること
		obj.put()
	
		#インデックスに反映されるまで待機
		for i in range(10):
			#objのオブジェクト全体からsandを持つオブジェクトの数を取得
			cnt=obj.all().filter("sand =",sand).count()
			if(cnt>=1):
				break
			
			#まだ反映されていなかったので待機
			time.sleep(1)

実際に運用に使っているコードは、https://github.com/abars/illustbook/blob/master/myapp/SyncPut.pyから確認頂けます。

これで、おおむね動作してはいるのですが、実際には1つのオブジェクトに対して複数のインデックスが対応するため、sandのインデックスは更新されたが、他のインデックスは更新されていない可能性があり、完全ではありません。

厳密な整合性が要求される用途では、db.getは強い整合性が保証されているので、db.keyのリストを持たせてQueryを使わない必要があるかと思います。また、Googleには、ぜひ、公式にput_sync的なメソッドを用意して欲しいと思います。公式のサンプルが動かないのはさすがに厳しいと思います。

AppEngineでmemcacheリクエストをまとめて発行する

memcacheのgetを10回とか連続で行った場合、各getが前のgetの終了を待機するのでオーバヘッドが大きいです。そういう時は、memcache.get_multiがオススメです。

例えば、複数のgetをしてディレクトリに格納するコード
dic=[]
dic["id1"]=memcache.get("id1")
dic["id2"]=memcache.get("id2")
dic["id3"]=memcache.get("id3")
は、次のように書けます。
key_list=["id1","id2","id3"]
dic=memcache.get_multi(key_list,key_prefix="")

key_prefixには、各キーの先頭に付ける文字列を指定することができます。指定した場合でも、返り値のディレクトリのキーにはプリフィックスは付きません。便利です。

このget_multiはかなり高速で、10回分のgetでも、2回分程度のgetと同じ時間で処理してくれます。記事の一覧の取得など、記事単位でキャッシュしている場合は、get_multiでまとめて取ってくるのがオススメです。

AppEngineでデータストアアクセス無しでReferencePropertyのKey値を取得する方法

ReferencePropertyを持つdb.Modelがあったとします。
class MesThread(db.Model):
 bbs = db.ReferenceProperty(Bbs)

キャッシュ処理などで、データは不要でキーだけが欲しい場合に、thread.bbs.key()としてしまうと、bbsのデータストアアクセスが発生してしまいます。同様に、if(thread.bbs)のような処理でも、データストアアクセスが発生します。

これは、次のように記述することで、データストアアクセスを回避してdb.Keyを取得することができます。
bbs_key=MesThread.bbs.get_value_for_datastore(thread)

また、queryを作る際も、MesThread.all()ではなく、次のように記述することでキー値だけを取得することができます。高速化に便利です。
query = db.Query(MesThread,keys_only=True)

AppEngineにおけるputのフックとキャッシュコントロール

データストアのgetは遅いのでmemcacheを利用してキャッシュしようとした場合に、キャッシュの無効化をどのように実装すればよいのか迷うことがあります。

例えば、あるthreadオブジェクトがあって、あるタイミングで24時間キャッシュし、別のタイミングでキャッシュから読み込んだとします。
TimeA : memcache.put("id",thread,60*60*24)
TimeB : thread=memcache.get("id")

キャッシュから読み込んだ際にthreadが更新されていなければ問題は置きません。しかし、threadが更新されていた場合に、キャッシュを無効化していなければ、24時間の間、古いデータが表示されてしまいます。

これを解決する一番簡単な方法は、thread.put()をしている場所でdeleteを入れてしまうことです。しかし、これだと、put()している全ての場所にdeleteを入れる必要があり、大変面倒です。
thread.put()
memcache.delete("id")


ということで、次のようにputをフックするのがオススメです。thread.put()が呼ばれると自動的にmemcache.deleteが呼ばれます。
class Thread(db.Model):
 data_member=db.StringProperty()

 def put(self,**kwargs):
  super(Thread, self).put(**kwargs)
  memcache.delete("id")


また、AppEngineは全てのオブジェクトに独立したキーが与えられます。次のように、キーにプリフィックスをつけた形でそのまま使って格納すると管理が楽です。PREFIXを付けておくことで、PREFIXの名前でバージョン管理ができます。
TimingA : memcache.get(PREFIX+str(thread.key()))
HookedPut : memcache.delete(PREFIX+str(thread.key()))


イラストブックのお絵かき掲示板では、10個のイラストを1ページで表示するのですが、イラストのオブジェクトを全てこの方法でキャッシュすることによって、1200msかかっていた処理が600msまで短縮されました。

ちなみにAppEngineのmemcacheにはdb.Modelをそのまま突っ込めます。データへのキーではなく、ちゃんとデータで保存されるので、memcache.getした後にそのdb.Modelにアクセスしても、db.getは発生しません。(AppStatsで確認しました)

AppEngineは11月7日から新料金体系になるので、memcacheでキャッシュしまくるのがオススメです。

実際の運用コードは、https://github.com/abars/illustbook/blob/master/myapp/MesThread.pyで確認頂けます。

AppEngineにおけるReferencePropertyのListの正しい記述法

AppEngineで、とある1データへの参照は
 class MesThread(db.Model):
  bbs_ref = db.ReferenceProperty(Bbs)
のように書けるので特に問題無いのですが、1データではなく複数データへマッピングしたい場合は、ReferenceListPropertyが存在しないため、定義の仕方を考える必要があります。

ぱっと思いつくのは、
bbs_list = db.StringListProperty()
のように、文字列のリストを定義しておいて、str(bbs.key())のようにデータのキーを文字列化して入れることです。これだと、とりあえずは動いてしまうのですが、問題があります。

例えば、直近の話題としては、Python2.7がM/Sデータストアで提供されないため、HRDデータストアへの移行が必要ですが、移行をするとキー値が変化してしまいます。Googleの提供するM/S型データストアからHRDデータストアの移行ツールでは、ReferencePropertyのキー値の自動変換をしてくれますが、StringListPropertyは変換してくれません。

ということで正解は、
bbs_key_list = db.ListProperty(db.Key)
のように、db.Keyのリストを定義しておいて、db.Key(str(bbs.key()))のようにデータのキー値をdb.Key型に変換して入れておくことです。こうすると、HRDデータストアの移行ツールで認識して、自動でキー値を書き換えてくれます。今回のHRDへの移行のように、可能性としてはデータセンターの統廃合とかでもキー値が変わることは有り得そうな気がするので、できるだけこの形で格納しておくとよいようです。

GoogleAppEngineのプロファイラであるAppStatsが素晴らしい件

できるだけ速く表示されるサイトを作ることは、ユーザビリティの面でかなり重要です。そのためには、実際に今のサイトのどこが重いのかを知る必要があります。そのために使うのがプロファイラです。

例えば、
・WindowsアプリではIntelのvTunes
・AndroidアプリではTraceView
・iPhoneアプリではInstruments
がデファクトですが、GoogleAppEngineではAppStatsというものが用意されています。公式ドキュメントは”Appstats for Python”で、その日本語訳が”appengine/pythonのappstatsのページを訳してみるよ”で公開されています。

AppStatsを使うのは簡単で、appengine_config.pyを作ります。

def webapp_add_wsgi_middleware(app):
 from google.appengine.ext.appstats import recording
 app = recording.appstats_wsgi_middleware(app)
 return app

次に、setting.pyを作ります。

MIDDLEWARE_CLASSES = (
 'google.appengine.ext.appstats.recording.AppStatsDjangoMiddleware',
)

最後に、app.yamlに次の行を追加します。

- url: /stats.*
 script: $PYTHON_LIB/google/appengine/ext/appstats/ui.py

後は、http://www.illustbook.net/statsのようにstatsディレクトリにアクセスすればOKです。(管理者権限が必要です)

すると、次のような画面になります。

top


AppStatsでは、データストアアクセスなどのRPCの数と処理時間がカウントされています。重い順にランキングで表示されます。右端の数字をクリックすると、特定のHTTPアクセスのプロファイル結果を取得することができます。


profile



こんな感じで、どの処理に時間がかかるかグラフィカルに分かります。想像以上にdb.getが多いですね。


code



グラフのバーをクリックすると、RPCの詳細が見えます。


count



ソースコードの名前をクリックすると、ソースコードのどの位置で呼ばれたかまで分かります。思ってもみなかった所で無駄な処理をしていたりするので、一度、AppStatsで自分のサイトを見てみるのは有意義だと思います。前述したようにイラストブックでも使わないカウンター値をdb.getしまくっていて悲惨な感じだったので直しておきました。

result


memcached軽すぎる!

AppEngineにおけるページの読み込みスピードの高速化(主にブラウザキャッシュの話)

WEBサービスのページの読み込みスピードの高速化の基本は、
 
にまとまっています。本書には、バックエンドよりもフロントエンドの最適化の方が重要ということが書かれています。具体的に、WEBサービスのアーキテクチャを変えずに簡単にできる項目を抜き出すと次の三つになります。

(1)CSSは先頭に書く
(2)JavaScriptは末尾に書く
(3)できるだけブラウザキャッシュに乗せる

これらをAppEngineでサーブされているWEBサービスに適用する場合、(1)と(2)は特に問題無いのですが、(3)をどうするかが重要になってきます。最近のサービスでは特に画像が重いので、画像に重点を当てて考えます。

ブラウザのキャッシュは三段階あって、

(1)普通に画像を取りに行くケース
(2)条件付き GET リクエストを実行するケース(304 Not Modified)
(3)ブラウザ内のキャッシュを使用するケース(Expiration)

の順に高速になります。

(1)は普通にHTTPリクエストをして画像を取りに行くケースです。このケースを高速化するには、データストアへのアクセスをどれだけ減らすかが重要になってきます。したがって、使える所は全てmemcacheを使って、出来る限りデータストアにアクセスしにいかないような構成にすれば高速化されます。

(2)はブラウザが、ローカルにキャッシュしているファイルのeTagと更新日時をサーバに送って、更新されていなければ304 Not Modifiedだけを受け取って、画像の実データを取得しない仕組みです。AppEngineの場合はapp.yamlで
 - url: /static_files
  static_dir: static_files
のように書いて、static_dirでサーブすると、自動的に条件付きGETリクエストが有効になります。何もしなくていいので楽です。問題は動的にデータストア内の画像を返す場合で、Pythonを通して画像を返す場合は自分で条件付きGETリクエストに対処しなければなりません。ということで、Blogging on App Engine, part 1: Static servingのコードのように書けば条件付きGETが有効になり高速化されます。イラストブックでの運用コードはhttps://github.com/abars/illustbook/blob/master/myapp/ImageFile.pyから確認頂けます。

(3)は静的なファイルは出来る限りExpirationを長くして、そもそもGETリクエストを発行させない方法です。app.yamlに
 - url: /static_files
  static_dir: static_files
  expiration: "7d"
のように書けば、7日間はキャッシュが有効になります。7日間は更新確認リクエストも出さないのでファイルを更新する場合はファイル名を変えるのが望ましいです。キャッシュ期間をどれくらいに設定するかはなかなか悩ましいのですが、できるだけ長くした方が効率は良くなります。default_expirationという設定もありますが、GAEでサイト取り消しの可能性がある危険なパラメータで紹介されているように、default_expirationはなかなか危険なケースもあるので注意しながら使用しましょう。

ということでAppEngineで運用する場合はこの三つを考えればそれなりに高速化されそうです。きちんとExpirationが動いているかの確認は、Googleの提供しているPageSpeedがオススメです。

Googleはページの読み込みスピードも検索結果のランキングに反映しているので、高速化は何気に重要だったりします。

ということでAppEngineで運用しているイラストブックもぜひぜひどうぞ。
Search
Profile

abars

アプリとWEBサービスを開発しています。最近はUnityとGAE/pyが主戦場。

ブラウザ向けMMOのメトセライズデストラクタ、イラストSNSのイラストブック、東証の適時開示情報を検索できるTDnetSearchを開発しています。

かつてエンターブレインのTECH Win誌でATULADOを連載しました。

サイト:ABARS
Twitter:abars
Github:abars

Twitter
TopHatenar
HotEntry
Counter

アクセス解析付きカウンター。あなたのBLOGにもどうですか?登録はこちらから。

TOP/ BLOG/ LECTURE/ ONLINE/ RUINA/ ADDON/ THREAD/ METHUSELAYZE/ IPHONE/ MET_IPHONE/ ENGLISH/ RANKING