スキップしてメイン コンテンツに移動

続・Google App Engine のデータストア料金(課金)対策


以前書いたGoogle App Engine のデータストア料金(課金)対策の続きです。

前回の概要を簡単に書いておくと、
GAEに静的ファイルだけでデプロイしてしまう方法、
つまりHTML等をあらかじめ全部ローカルで生成しておいて
アップロード(デプロイ)するだけ、
「そもそもプログラムとして動かさない」という方法でした。

そんな方法ならデータストアどころかインスタンス時間すら(ほぼ)使わないので
まず課金されないでしょう、ということでした。


でもその方法では "静的ファイル数は1万まで" といった制約がいくつかあり
自由度が低くなってしまいます。

今回は、その方法にもう少し手を加えて
いろいろと解決できるような対策について書いてみたいと思います。

やっぱりデータストアを使う

まず、静的ファイルで何もかもやろうというのには
いつしか無理が生じるのは目に見えています。

なのでデータストアを使っていきます。
しかし使い方を工夫します。

文章で書いても伝わりにくいと思うのでとにかく例で示していきます。

Kind(テーブル)のレイアウトイメージです。

  • uri: String
  • source: Blob
  • timestamp: Date
  • contentType: String

そして web.xml はこうです。

<servlet>
<servlet-name>ViewServlet</servlet-name>
<servlet-class>example.ViewServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ViewServlet</servlet-name>
<url-pattern>*.html</url-pattern>
<url-pattern>*.css</url-pattern>
<url-pattern>*.js</url-pattern>
<url-pattern>*.xml</url-pattern>
<url-pattern>*.jpg</url-pattern>
<url-pattern>*.png</url-pattern>
<url-pattern>*.ico</url-pattern>



</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>


おそらくもう想像が付いたと思いますが・・・

ViewServlet#doGet() で以下の処理をします。

  1. String uri = req.getRequestURI();
  2. uri をキーにテーブルからEntity(レコード)を取得。取得値を仮に rec とします。
  3. 取得できなれければ resp.sendError(HttpServletResponse.SC_NOT_FOUND); return;
  4. resp.setDateHeader("Last-Modified", rec.getTimestamp.getTime());
  5. resp.setContentType(rec.getContentType());
  6. String html = new String(rec.getSource.getBytes(), "UTF-8"); // 画像等は適宜処理してください
  7. resp.getOutputStream().write(html.getBytes("UTF-8"));


前回同様、静的ファイルをローカルで生成します。
(GAE上で生成しても構いませんが、課金対策という事でなるべくローカルで処理する方向で)
そこまでは前回と同じです。

そして生成した静的ファイルを、GAE上のデータストアに格納します。
前回は静的ファイルのままアップロードしましたが
今回はデータストアに入れるわけです。
入れる部分の実装はお任せします。


これで、クライアントからのリクエスト1回につき、
データストアからの読み出しが1レコードだけで済みます。
相当なアクセス数に無課金で耐えられます。

さらに対策

上記だけでも充分に対策になるとは思いますが、
もう少し工夫できます。

MemCache を使う

エンティティを MemCache に入れて、再利用しましょう。
index.html のように頻繁に同じ URI にアクセスがある場合
データストアへのアクセスを2度目以降省略できるわけですから
非常に効果的です。

データを圧縮する

データが増えると、今度はデータストアの容量で課金されてしまいます。
その対策としてデータを圧縮すると効果的です。

テーブルレイアウトで source を String ではなく Blob で実装しているのはこのためです。
(画像とかを格納できるという目的もありますが)

幸い Java には zip 圧縮・展開処理が付いていますから
それを活用します。

ViewServlet の処理6の前で

 source = unZip(source);

を実行します。
unZip メソッドは次のとおりです。

private static Blob unZip(Blob source) throws IOException { ByteArrayInputStream bytes = new ByteArrayInputStream(source.getBytes()); ZipInputStream zis = new ZipInputStream(bytes); try { zis.getNextEntry(); byte[] buf = new byte[BUFFER_SIZE]; // サイズを指定してください ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE); for (;;) { int len = zis.read(buf); if (len < 0) { break; } out.write(buf, 0, len); } return new Blob(out.toByteArray()); } finally { zis.close(); } }


ちなみに格納時に使う圧縮のほうはこんな感じです。
こちらはローカルで処理しても良いでしょう。

private static Blob zip(Blob source) throws IOException { byte[] bytes = source.getBytes(); return new Blob(zip(bytes)); } private static byte[] zip(byte[] bytes) throws IOException { ByteArrayOutputStream os = new ByteArrayOutputStream(BUFFER_SIZE); // サイズを指定してください ZipOutputStream zos = new ZipOutputStream(os); zos.setLevel(9); // 最高レベルの圧縮指定 try { ZipEntry ze = new ZipEntry("z"); // zはファイル名。今回の例では特に使わないので何でも良い。 zos.putNextEntry(ze); zos.write(bytes); } finally { zos.close(); } return os.toByteArray(); }


なお、圧縮・展開処理は非常に軽量だったので、
インスタンス課金への影響はほとんど無いかと思います。
参考までに teiki.ko2.info でHTTPヘッダ X-unzipproc に展開時間をセットしていますが、ほとんど1ミリ秒です。

実行時に HTML を書き換える

ここまでではクライアントに返す HTML は固定でした。
しかし一部は動的に変更したい場合があります。

そのようなときは ViewServlet の 処理6と処理7の間で
HTML を動的に加工すれば OK です。

たとえば
あらかじめHTMLの置換したい場所に
「CURRENT_TIME」 といった文字列を埋め込んでおいて

html = html.replaceAll("CURRENT_TIME", 〜);
のような感じで置換します。

ちょっとした自作テンプレートエンジンですね。

ただしあまり複雑なこと(データアクセス等)をしたら
課金されて本末転倒になりかねないので注意が必要です。

データストアに格納したくないファイルについて

URI のデータをなにもかもすべてデータストアに入れたくない場合。
appengine-web.xml に以下のように記述します。

<static-files>
<include path="/**.ico" />
<include path="/**.png" />
<include path="/**.jpg" />
</static-files>

ここに記述した URI は ViewServlet には渡らなくなり、
静的ファイルでデプロイしたものがクライアントに返されます。
(web.xmlからの該当箇所の削除も忘れずに)


長くなりましたが...

いかがでしたでしょうか。

プログラムを動かせる環境なのに、敢えて静的な方向に持っていくというのは
ちょっと矛盾したようなやり方に感じますが
ゲームプログラミングでは良くやっていることだと聞きます。

そして課金対策というのは GAE を使う側だけでなく
Google も望んでいること(情報処理の最適化)であると思います。
世界全体で見ればサーバの消費電力なんかも随分変わってくるでしょうし。。。

というわけで、長くなりましたが
何か開発のお役に立てばと思います。