とりあえず雑記帳(跡地)

Google App Engine


※上記の広告は60日以上更新のないWIKIに表示されています。更新することで広告が下部へ移動します。

WebコミックLibrary http://web-comi.appspot.com/
GAE/JとSlim3で作成してみた、各出版社から配信されているWebコミックをまとめて閲覧できるサイトです。只今、実験運用中…

参考サイト




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に追加します。



Datastore ~ Operations

  • Quotaの中でも、世の中の頭を悩ませる最大の壁
    • Datastore Read Operations
    • Datastore Write Operations
    • Datastore Small Operations

  • 結論から言えば、Memcacheでキャッシュしまくるしかないです。

とりあえず、キャッシュについて、適当に考察してみました
キャッシュの計画



インスタンス


  • GAEでいう「インスタンス」とは、計算リソースのことです。「仮想的なCPU」と考えてもいいでしょう。
    • Javaクラスのインスタンスとは全然別の概念なので、そこを取り違えないように。
    • GAEのインスタンスの属性としてclassという用語が出てくるけど、やっぱりJavaクラスとは何にも関係ないので注意。

  • 何かしらのJavaクラスのコードを実行する際には、(GAEの)インスタンスが割り当てられて実行します。
    • このとき、コードの起動方法と設定によって、割り当てられるインスタンスが決定されます。

Frontend InstanceとBackend Instance

参考
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


  • 定期的に、Servletを自動実行する仕組みです。
    • 主に、Webクローラー等の、UIを伴わないバックグラウンド処理に利用されます。
  • 具体的には、定期実行したい処理を記述したServletに対応するURLに対して、設定ファイル(cron.xml)に設定されたタイミングでリクエストを送出します。
  • 例外発生時にcatchしなかった場合でも、リトライは発生しません。
  • Cronから呼び出されることを想定しているURLは、外部から勝手に呼び出されないように、admin以外はアクセスできないようにしておきましょう(後述の「認証」を参照)。
    • 逆に言えば、管理者の場合は、ブラウザからURLを指定してリクエストを送出すれば、Cronからの起動を待たずに強制起動が可能です。
    • まぁ、結局は単なるServletなのですから、当たり前ですが…

スケジュールの指定


  • 指定可能なのは、下記2パターン。混在は出来ないようです。
    • 間隔を指定して実行
      • 例:1時間毎に実行
    • 指定日の特定の時刻に実行
      • 例:毎日12:00に実行

間隔を指定して実行

  • 下記の形式で指定
every N (hours|mins|minutes) ["from" (time) "to" (time)]
  • 2時間毎に実行の場合
every 2 hours
  • 10:00~14:00の間に、30分毎に実行の場合
every 30 minutes from 10:00 to 14:00

指定日の特定の時刻に実行

  • 下記の形式で指定
("every"|ordinal) (days) ["of" (monthspec)] (time)
  • 毎日10:00に実行の場合
every day 10:00



Task 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>でリトライの設定
<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

  • Backendsの設定をbackends.xmlに記述します。
  • backends.xmlはWEB-INF直下に配置します。

<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を参照
    • まぁ、課金しなければ、ほぼB1ですね。
  • インスタンスのタイプとしてDynamicを指定しています。
    • インスタンスには、Resident(常駐型)とDynamic(動的起動型)の2種類があります。
    • 詳しくは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というパスで作ってみました。

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が出力されています。
2012-01-01 01:23:45.678 [xxxxx/1.358322054626724163].<stdout>: null 

Cronを使って、Backendsで実行

  • Cronで、Backendsで実行させる場合は、<target>で実行させるBackendsのインスタンス名を指定します。
<?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が出力されています。
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でキャッシュされてしまう

  • 下記のように、Cache-ControlとPragmaを設定して、キャッシュを無効にしてしまう
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();
}
 



認証

  • 認証については、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アカウントにログインした人の内、管理者権限を持っている人のみがアクセス可能となる。
    <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>

OpenIDによる認証


OpenIDを使えば、Googleアカウント以外のOpenIDプロバイダを利用できます。
  • mixi
  • Yahoo!
  • はてな
等々

アプリケーションの設定

AdminConsoleにて、アプリケーションの作成時、作成後ならばApplication Settingsにて、
[Authentication Options]を"Federated Login"に設定しておきます。

/_ah/login_required

認証がされていない状態で、前述の<security-constraint>で保護されたURLにアクセスすると、
/_ah/login_requiredへリダイレクトされます。この動作はAppEngineで用意されているものなのですが、
肝心の/_ah/login_requiredのリクエストを処理するサーブレットは自前で用意する必要があります。

通常は、この/_ah/login_requiredのリクエストを処理するサーブレットで、OpenIDプロバイダの選択画面を表示します。
また、/_ah/login_requiredにリダイレクトされた際に、パラメータcontinueには、最初にリクエストされた、保護されたURLが格納されているので、
OpenIDプロバイダへのリクエスト時に、同様にパラメータcontinueにそのURLを設定することで、OpenIDプロバイダ側での認証完了後に、
continueに設定されたURLにリダイレクトしてくれます。

Slim3の場合

Slim3の場合は、/_ah/login_requiredにControllerを割り当てることができます。

[パッケージルート].controllerに、AddRouterクラスを作成します。

package jp.fujiyan.controller;
 
import org.slim3.controller.router.RouterImpl;
 
public class AppRouter extends RouterImpl {
    public AppRouter() {
        addRouting("/_ah/login_required", "/loginRequired");
    }
}
 

AppRouter#addRouting()を使うことで、第1引数で指定したURLへのリクエストで、第2引数で指定したURLをハンドルするControllerが呼び出されるようになります。
上記の場合、/_ah/login_requiredへのリクエストで、/loginRequeredをハンドルする、jp.fujiyan.controller.LoginRequiredControllerが呼び出されます。




ローカルのDatastoreのデータを削除

  • /war/WEB-INF/appengine-generated/local_db.binを削除して、同名のファイルを新規作成する。
  • 削除直後のアクセスで"Failed to load from the backing store"というログとともにスタックとレースがでるけど気にしない



ローカルテスト環境での管理コンソール

  • 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
  1. commons-loggingのjarファイルをwar/WEB-INF/libにコピー
  2. log4Jのjarファイルをwar/WEB-INF/libにコピー
  3. war/WEB-INF/appengine-web.xmlの<system-properties>にorg.apache.commons.logging.Logを追加する
<system-properties>
    ...
    <property name="org.apache.commons.logging.Log" value="org.apache.commons.logging.impl.Log4JLogger"/>
    ...
</system-properties>
  1. srcにlog4j.propertiesまたはlog4j.xmlを置き、内容を利用したい設定に変更する
  • 新規プロジェクト作成時には、デフォルトでlog4j.propertiesが作成されるが、DataNucleusのログ設定しかないので、修正が必要
  1. war/WEB-INF/logging.propertiesは不要なので削除してもよい。
  • 削除の際には、war/WEB-INF/appengine-web.xmlの<system-properties>のjava.util.logging.config.fileも削除する



JDOを使わない場合

参考
Song of Cloud
  • プロジェクトのプロパティダイアログを開き(プロジェクトを選択して、右クリック→[Properties])、[Builders]の[Configure the builders for the project]で、[Enhancer]のチェックをOFFにする
    • JDO向けのモデル拡張を行うプロセス。チェックしていると処理が実行されるので外しておく。
  • src/META-INF/jdoconfig.xmlを削除
    • JDOの設定ファイル。不要なので削除する。



GAEでStruts2



間違いの御指摘は
コメントまでm(_ _)m

更新履歴

取得中です。



総数: -
本日: -
昨日: -