「Google App Engine」の編集履歴(バックアップ)一覧に戻る
Google App Engine - (2012/05/25 (金) 01:07:01) のソース
|BGCOLOR(#eff):&bold(){WebコミックLibrary}&br()[[http://web-comi.appspot.com/>>http://web-comi.appspot.com/]]&br()GAE/JとSlim3で作成してみた、各出版社から配信されているWebコミックをまとめて閲覧できるサイトです。&br()只今、実験運用中…| *参考サイト [[公式サイト(Java)>>http://code.google.com/intl/ja/appengine/docs/java/overview.html]] ---- *Slim3 [[Slim3]]へ ---- *appcfg -My Applicationsに作成した、GAEアプリケーションの管理を行うコマンドラインツールです。 -Google Plugin for Eclipseの場合、SDKがplugins/com.google.appengine.eclipse.sdkbundle_x.x.x.../appengine-java-sdk-x.x.xに存在するので、そのbinフォルダ内にappcfg.cmdがあります。 -通常は、環境変数GAE_HOMEに上記のplugins/com.google.appengine.eclipse.sdkbundle_x.x.x.../appengine-java-sdk-x.x.xを設定し、%GAE_HOME%\binをPATHに追加します。 ---- *インスタンス 参考 [[GAE/PでのBackendsの使い方 - Tari Lari Run>>http://bygzam.seesaa.net/article/226558849.html]] -GAEでいう「インスタンス」とは、計算リソースのことです。「仮想的なCPU」と考えてもいいでしょう。 --Javaクラスのインスタンスとは全然別の概念なので、そこを取り違えないように。 --GAEのインスタンスの属性としてclassという用語が出てくるけど、やっぱりJavaクラスとは何にも関係ないので注意。 -何かしらのJavaクラスのコードを実行する際には、(GAEの)インスタンスが割り当てられて実行します。 --このとき、コードの起動方法と設定によって、割り当てられるインスタンスが決定されます。 **Frontend InstanceとBackend Instance 参考 [[Properties of Backends>>https://developers.google.com/appengine/docs/java/backends/overview#Properties_of_Backends]](公式サイトのBackends Java API Overviewより) ※Default(Frontend) InstanceとBackend Instanceの比較表です。 -通常の、ブラウザからのHTTPリクエストによってアクセスされるServletは、Frontend Instanceが割り当てられます。 -CronやTask QueueからアクセスされるServletは、FrontendかBackendが選択できる? -多分、publicのBackendにすれば、ブラウザからのHTTPリクエストに、Backend Instanceを割り当てることができるのかな? ---- *Cron 参考 [[Scheduled Tasks With Cron for Java>>https://developers.google.com/appengine/docs/java/config/cron]] -定期的に、Servletを自動実行する仕組みです。 --主に、Webクローラー等の、UIを伴わないバックグラウンド処理に利用されます。 -具体的には、定期実行したい処理を記述したServletに対応するURLに対して、設定ファイル(cron.xml)に設定されたタイミングでリクエストを送出します。 -例外発生時にcatchしなかった場合でも、リトライは発生しません。 -Cronから呼び出されることを想定しているURLは、外部から勝手に呼び出されないように、admin以外はアクセスできないようにしておきましょう(後述の「認証」を参照)。 --逆に言えば、管理者の場合は、ブラウザからURLを指定してリクエストを送出すれば、Cronからの起動を待たずに強制起動が可能です。 --まぁ、結局は単なるServletなのですから、当たり前ですが… **スケジュールの指定 参考 [[The Schedule Format - Scheduled Tasks With Cron for Java>>https://developers.google.com/appengine/docs/java/config/cron#The_Schedule_Format]] -指定可能なのは、下記2パターン。混在は出来ないようです。 --間隔を指定して実行 ---例:1時間毎に実行 --指定日の特定の時刻に実行 ---例:毎日12:00に実行 ***間隔を指定して実行 -下記の形式で指定 #pre2(black){{ every N (hours|mins|minutes) ["from" (time) "to" (time)] }} -2時間毎に実行の場合 #pre2(black){{ every 2 hours }} -10:00~14:00の間に、30分毎に実行の場合 #pre2(black){{ every 30 minutes from 10:00 to 14:00 }} ***指定日の特定の時刻に実行 -下記の形式で指定 #pre2(black){{ ("every"|ordinal) (days) ["of" (monthspec)] (time) }} -毎日10:00に実行の場合 #pre2(black){{ every day 10:00 }} ---- *Task Queue 参考 [[Java Task Queue Configuration>>https://developers.google.com/appengine/docs/java/config/queue]] -Servletを非同期実行する仕組みです。 --主に、UIを伴わないバックグラウンド処理に利用されます。処理を非同期実行可能な単位に分割し、その単位をTaskとして実行させます。 -具体的には、非同期実行したい処理を記述したServletに対応するURLを、設定ファイル(queue.xml)で定義したqueueにキューイングします。 -キューイングされたURLは、とあるタイミングで取り出されて、リクエストが送出されます。 --キューからの取り出しをApp Engineに任せるのがPush Queue --キューからの取り出しをアプリで指定(Task Queue APIまたはTask Queue REST API)するのがPull Queue -例外発生時にcatchしなかった場合、リトライされます。 --とは言え、無限にリトライすると、一気にインスタンス時間を消費するので、必ずqueue.xmlの<retry-parameters>でリトライの上限設定を行いましょう。 -Task Queueから呼び出されることを想定しているURLは、外部から勝手に呼び出されないように、admin以外はアクセスできないようにしておきましょう(後述の「認証」を参照)。 --逆に言えば、管理者の場合は、ブラウザからURLを指定してリクエストを送出すれば、強制起動が可能です。 --まぁ、これも結局は単なるServletなのですから、当たり前ですが… **リトライ -queue.xmlの<retry-parameters>でリトライの設定 #highlight(xml){{ <queue-entries> <queue> <name>testQueue</name> <retry-parameters> <task-retry-limit>5</task-retry-limit> </retry-parameters> <rate>1/s</rate> </queue> </queue-entries> }} -上記のtestQueueでは、タスクで例外が発生した場合、最大5回のリトライが実施されます。 -最大で、最初の1回+リトライ5回=6回実行されます。 -<task-retry-limit> --リトライ回数を設定します。5と指定すると、5回のリトライを試みます。 -<task-age-limit> --リトライ期間を設定します。例えば、"5d"と指定すると、最初のタスク起動から5日間、リトライを試みます。 --<task-retry-limit>と<task-age-limit>を同時に指定した場合、両方のリミットに達しするまでリトライする、とありますが、実質<task-retry-limit>が優先ですね。例え5日間経過しても、5回に達していなければリトライし続けるので。 -<min-backoff-seconds>と<max-backoff-seconds>と<max-doublings> --次のリトライまでのインターバルを決定します。 --インターバルは、リトライの度に増加します。1回目は10秒、2回目は20秒、3回目は40秒…、という感じです。 --イマイチ、インターバルの計算方法がわかりません。実際に動かすと、予想通りの時間にならない…。 ---- *CronとTask Queueの使い分け -サンプルとして、Webクローラを想定します。 -Webクローラは、サイトA、サイトB、サイトCを巡回します。 -サイトA、サイトB、サイトCは、それぞれ独立に巡回することが可能とします。 -サイトA、サイトB、サイトCの巡回処理をタスクとして、それぞれの巡回処理を行うServletのURLを/crawlSiteA、/crawlSiteB、/crawlSiteCとします。 -各巡回処理のタスクをキューイングする処理を行うServletのURLを/execCrawlersとします。 --具体的な/execCrawlersの処理は、/crawlSiteA、/crawlSiteB、/crawlSiteCをTask Queueにキューイングします。 -Cronで、/execCrawlersを定期起動するようにします。 **説明 -もし、上記のような構成ではなく、全処理を/execCrawlersのみで構成した場合 --サイトAでエラーが発生した場合、サイトBやサイトCの巡回が実行されません。 --Cronではリトライが無いので、エラーが発生しても、そのままです。 -上記構成の場合は、 --/crawlSiteAでエラーが発生しても、/crawlSiteBや/crawlSiteCは独立して実行されます。 --さらに、エラー発生したタスクについては、リトライが実施されます。 --とはいえ、クローリングの無制限のリトライは、DoS攻撃に等しいので、リトライの上限を設けておきましょう。 ---- *Backends -Backend Instanceを用いて、コードを実行させます。 **backends.xml [[Java Backends Configuration>>https://developers.google.com/appengine/docs/java/config/backends]] -Backendsの設定をbackends.xmlに記述します。 -backends.xmlはWEB-INF直下に配置します。 #highlight(xml){{ <backends> <backend name="test-instance"> <class>B1</class> <options> <dynamic>true</dynamic> </options> </backend> </backends> }} -上記の場合、test-instanceというnameのBackend Instanceを定義します。 --インスタンスの名前は全て小文字じゃないと、deploy時に怒られます。 --複数のBackend Instanceを定義したい場合は、<backend>を並べていきます。 -インスタンスのclassとしてB1を指定してます。 --classの種類については[[Instance Classes>>https://developers.google.com/appengine/docs/java/config/backends#Instance_Classes]]を参照 --まぁ、課金しなければ、ほぼB1ですね。 -インスタンスのタイプとしてDynamicを指定しています。 --インスタンスには、Resident(常駐型)とDynamic(動的起動型)の2種類があります。 --詳しくは[[Types of Backends>>https://developers.google.com/appengine/docs/java/config/backends#Types_of_Backends]]を参照 --これもまぁ、課金しなければ、ほぼDynamicですね。 -ようするに、上の設定は、一番ケチな設定です。 ***もうすこし詳しく -Backendsの無料枠は9インスタンス時間 --B1を1時間稼動させる単位を1インスタンス時間とします。 --classが1つあがる毎に、消費単位が2倍となります。 --B2で1時間稼動させれば2インスタンス時間、B4の場合は4インスタンス時間となります。 -Residentの場合、一度起動すると、手動でシャットダウンさせるまでずっと常駐します --そのため、リクエストに対してすぐに応答することができます。 --そのため、インスタンス時間をどんどん消費していきます。 ---無料枠でB1の場合、初期化される日本時間16:00から、9インスタンス時間後の25:00の間しか稼動しません。その後、再び初期化される16:00までは、そのインスタンスを利用することができません。 -Dynamicの場合、リクエストがあったときに起動され、しばらくして不要になれば除去されます。 --そのため、リクエストがあったときにインスタンスが起動されていなければ、起動に時間がかかってしまいます。 --そのため、インスタンス時間の消費量は必要な分だけになります。 **試してみる -Backendsだろうが、実行するコードはServlet(或いは、Servletベースのフレームワーク)です。 --今回はSlim3のControllerでサンプルを作ってみます。 --今回は、/testInstanceというパスで作ってみました。 #highlight(java){{ package jp.fujiyan.test.controller; import org.slim3.controller.Controller; import org.slim3.controller.Navigation; import com.google.appengine.api.backends.BackendService; import com.google.appengine.api.backends.BackendServiceFactory; public class TestInstanceController extends Controller { private BackendService service = BackendServiceFactory.getBackendService(); @Override public Navigation run() throws Exception { System.out.println(service.getCurrentBackend()); return null; } } }} -今回は、Cronで実行させるのが目的で、レスポンスを返す必要が無いので、run()の戻り値は不要です。 --Slim3 Plug-inの、build.xmlのgen-controller-without-viewで作れば手っ取り早いです。 -BackendService#getCurrentBackend()は、コードを実行しているBackendsのインスタンスの名前を返します。 --コードがFrontendで実行されている場合はnullを返します。 -ブラウザから、/testInstanceでアクセスした場合、Frontendsで上記Controllerが実行されます。 --Logには、下記の様に出力されます。 --Frontendsなので、nullが出力されています。 #pre2(black){{ 2012-01-01 01:23:45.678 [xxxxx/1.358322054626724163].<stdout>: null }} ***Cronを使って、Backendsで実行 -Cronで、Backendsで実行させる場合は、<target>で実行させるBackendsのインスタンス名を指定します。 #highlight(xml){{ <?xml version="1.0" encoding="UTF-8"?> <cronentries> <cron> <url>/testInstance</url> <description>Backends Instance Test</description> <schedule>every day 18:00</schedule> <timezone>Asia/Tokyo</timezone> <target>test-instance</target> </cron> </cronentries> }} -上記の場合、日本時間18:00に、test-instanceで/testInstanceに対応するControllerが実行されます。 --Logには、下記の様に出力されます。 --今度は、Backend Instanceの名前である、test-instanceが出力されています。 #pre2(black){{ 2012-01-01 18:00:07.752 [xxxxx/test-instance.358322297746672704].<stdout>: test-instance }} **Backend Instance実行のCronからのTask Queue呼び出し -Task Queue呼び出しの際のInstanceは、キューイング元のInstanceになるっぽいです。 --なので、targetを指定せずに、Backend Instance実行のCronからTask Queueを呼び出した場合は、同じBackend Instanceでタスクが実行されます。 **明示的にBackend Instanceそ指定して、Task Queue呼び出し -例えば、通常のWebブラウザからのリクエスト(Frontend Instance)から、Backend InstanceでTask Queueを実行したい場合は、URLのホスト名で、Backend Instanceを指定します。 -通常、リクエストの際のURLは、http://[アプリ名].appspot.com/~ですが、これをhttp://[Backend Instance名].[アプリ名].appspot.com/~とすれば、指定したBackend Instanceでリクエストを処理します。 --例えば、http://test-instance.appname.appspot.com/testInstanceとすれば、test-instanceで/testInstanceに対応するControllerが実行されます。 --つまり、通常のリクエストにおける、http://[アプリ名].appspot.com/~の形式は、インスタンスを指定しない→Default(Frontend) Instanceということですね。 ---- *LocaleとTimeZone -GAEのデフォルトでは、Localeはen_US、TimeZoneはUTCです。 --但し、ローカルの開発環境では、LocaleはOSの設定のようです。日本ならja_JP。でも、TimeZoneはUTCです。 -Locale.setDafault()は、アクセス制御により使用が禁止されていますが、TimeZone.setDefault()は利用可能なようです。 -ということで、TimeZoneはFilterでsetDefault()でJSTにしておいたほうが、何かとシアワセかもしれません。 ---- *URL Fetch APIでキャッシュされてしまう 参考 [[Disable URLFetchService cache - Google App Engine for Java | Google グループ>>http://groups.google.com/group/google-appengine-java/browse_thread/thread/a8c8a6cb9fbe652f?pli=1]] -下記のように、Cache-ControlとPragmaを設定して、キャッシュを無効にしてしまう #highlight(java){{ String url = …; URLConnection connection = new URL(url).openConnection(); connection.addRequestProperty("Cache-Control", "no-cache,max-age=0"); connection.addRequestProperty("Pragma", "no-cache"); BufferedInputStream in = new BufferedInputStream(openConnection().getInputStream()); try { … } finally { in.close(); } }} ---- *認証 参考 [[北海道を愛するプログラマの覚書>>http://d.hatena.ne.jp/tetsuya_odaka/20090917/1253177052]] -認証については、Googleアカウントを利用する。 -よって、基本的にアプリ側で認証を実装する必要は無い。但し、後述の3種類の権限よりも細かく制御したい場合には実装が必要。 -アクセス権限については、次の3種類 --A)Googleアカウントへのログイン不要(全公開) --B)Googleアカウントへのログイン必要(一般権限) --C)Googleアカウントへのログイン必要(管理者権限) -上記A)については、特に設定は不要。 -上記B)またはC)については、web.xmlの<security-constraint>に記述を行う。 -B)またはC)でアクセス制限したいURLパターンを<url-pattern>に記述する。 -B)に対して許可する場合は<role-name>*</role-name>と記述する。 -C)に対して許可する場合は<role-name>admin</role-name>と記述する。 --Googleアカウントに対して管理者権限を与えるには、アプリの管理メニューで指定する。 -下記は、URLが/member/で始まるコンテンツは、Googleアカウントにログインした人のみアクセス可能で、/admin/で始まるコンテンツは、Googleアカウントにログインした人の内、管理者権限を持っている人のみがアクセス可能となる。 #pre2(black){{ <security-constraint> <web-resource-collection> <url-pattern>/member/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>*</role-name> </auth-constraint> </security-constraint> <security-constraint> <web-resource-collection> <url-pattern>/admin/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>admin</role-name> </auth-constraint> </security-constraint> }} ---- *ローカルのDatastoreのデータを削除 参考[[プログラマ的京都生活>>http://d.hatena.ne.jp/mtoyoshi/20090506/1241590403]] -/war/WEB-INF/appengine-generated/local_db.binを削除して、同名のファイルを新規作成する。 -削除直後のアクセスで"Failed to load from the backing store"というログとともにスタックとレースがでるけど気にしない ---- *ローカルテスト環境での管理コンソール -&nolink(http://localhost:8888/_ah/admin)で、ローカルテスト環境の管理コンソールにアクセスできます。 --主に、Datastore Viewerで、ローカルのDatastoreが閲覧したい場合に。 --つうか、つい最近までコイツの存在を知りませんでした…。 ---- */work/は使わないほうがいい -パス/work/を実現しようとして、/war/work/というディレクトリを作ると、Jettyがテンポラリファイルをそこに作成してしまい、何かと面倒くさいです。 ---- *ロギング -まぁ、Log4Jは使えるけど、たとえばwarn()でログ出力しても、Administration Console上ではWarningとしては扱ってくれません。 --Info扱いですね。 -ということで、管理面を考えると、Log4Jは使わずに、おとなしく標準ロギングAPIを使ったほうが良いのですかね。 -なので、下記の記事は、参考までに… **Log4Jを使うには 参考 [[I can't>>http://fall-ken.at.webry.info/201003/article_2.html]] +commons-loggingのjarファイルをwar/WEB-INF/libにコピー +log4Jのjarファイルをwar/WEB-INF/libにコピー +war/WEB-INF/appengine-web.xmlの<system-properties>にorg.apache.commons.logging.Logを追加する #pre2(black){{ <system-properties> ... <property name="org.apache.commons.logging.Log" value="org.apache.commons.logging.impl.Log4JLogger"/> ... </system-properties> }} +srcにlog4j.propertiesまたはlog4j.xmlを置き、内容を利用したい設定に変更する -新規プロジェクト作成時には、デフォルトでlog4j.propertiesが作成されるが、DataNucleusのログ設定しかないので、修正が必要 +war/WEB-INF/logging.propertiesは不要なので削除してもよい。 -削除の際には、war/WEB-INF/appengine-web.xmlの<system-properties>のjava.util.logging.config.fileも削除する ---- *JDOを使わない場合 参考 [[Song of Cloud>>http://songofcloud.gluegent.com/2009/11/slim3-datastore2.html]] -プロジェクトのプロパティダイアログを開き(プロジェクトを選択して、右クリック→[Properties])、[Builders]の[Configure the builders for the project]で、[Enhancer]のチェックをOFFにする --JDO向けのモデル拡張を行うプロセス。チェックしていると処理が実行されるので外しておく。 -src/META-INF/jdoconfig.xmlを削除 --JDOの設定ファイル。不要なので削除する。 ---- *GAEでStruts2 [[GAEでStruts2>Google App Engine/Struts2]] ----