2013年6月20日木曜日

38.Google Cloud Datastoreを試してみた その他環境編 (3/3)



33.Google Cloud Datastoreを試してみた 概要編 (1/3)
37.Google Cloud Datastoreを試してみた GCE編 (2/3)
38.Google Cloud Datastoreを試してみた その他環境編 (3/3)

Ryo Yamasaki(@vierjp)です。

前回に引き続き「Google Cloud Datastore」についてです。
第1回は概要や仕組み、注意点について、
第2回ではGoogle Compute Engineから実行してみました。

この第3回ではGoogleのクラウド環境以外から実行してみます。
さらに、私のローカル環境・GCE・GAE それぞれからDatastoreを操作した場合の実行時間の比較もしました。


33.Google Cloud Datastoreを試してみた 概要編 (1/3)」に書いた
以下のユースケースではGoogleのクラウド環境以外からCloud Datastoreに接続します。

・ユースケース3 Googleのクラウド環境以外からDatastoreを利用する

第1回に書いたとおり「Google Cloud Datastore」はAPI経由で操作できるので、
Googleのクラウド環境以外からも利用が可能です。
Googleのクラウド外から接続する場合にも第2回の「サンプルプログラム」の項までは
同様に必要なので、そちらを参照してください。



◯Googleのクラウド外からAPIを利用するための準備

1.Cloud Console で対象プロジェクトを開く。
2.プロダクト一覧の「APIs」を選択する。
3.画面左の「REGISTER APP」ボタンを押す。
4.アプリケーションの名前を入力する。
 (この値はプログラム等から使用しないので、わかりやすい一意な名前をつけましょう)
5.「Web Application」「Generic」を選択して「Register」ボタンを押す。
6.「Certificate」をクリックする。
7.「Generate Certificate」ボタンをクリックする。
8.「Download private key」ボタンをクリックして「Private Key」をダウンロードする。
9.「View public key」ボタンをクリックしてダイアログを閉じる。

後の作業で以下を使用します。
・「Service Account」→「Certificate」セクションに書いてある「EMAIL ADDRESS」
・「Private Key」→ダウンロードした「*.p12ファイル」
・「Dataset ID」→「CloudプロジェクトのProject ID」

これはCloud Datastore API に限らない、 Service Account Flow でGoogleのAPIを使うための準備です。
GoogleのAPIを実行するための認証方法はいくつかあるのですが、
Webサーバーや定期的に実行するバッチ等から利用する場合にはこの方式が便利です。



◯前回のサンプルコードを使って試してみる

まずは「36.Google Cloud Datastoreを試してみた GCE編 (2/3)」で使用したサンプルコードを使います。

前回書いたように、DatastoreHelper#getOptionsfromEnv() メソッドが
「Compute Engineから認証する場合」と「それ以外の環境から認証する場合」とを
環境に応じて自動で切り替えてくれるので、
環境変数の設定をするだけでコードの修正なしにそのまま動きます。

− 環境変数を設定する
まず必須なのは以下の2つの環境変数です。
export DATASTORE_SERVICE_ACCOUNT=<service-account>
export DATASTORE_PRIVATE_KEY_FILE=<path-to-private-key-file>

ローカルの開発サーバーに接続したい時は追加で下記も設定するそうです。
export DATASTORE_HOST=http://localhost:8080
export DATASTORE_DATASET=<dataset_id>

− プログラムを実行する
mavenから実行するので前回と実行方法は同じです。

mvn clean compile
mvn exec:java -Dexec.mainClass="ClientTest" -Dexec.args="[Project ID]"
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building vier-test-client 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) @ vier-test-client >>>
[INFO] 
[INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) @ vier-test-client <<<
[INFO] 
[INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ vier-test-client ---
6 12, 2013 7:04:56 午後 ClientTest main
情報: DATASTORE_DATASET:null
6 12, 2013 7:04:56 午後 ClientTest main
情報: DATASTORE_HOST:null
6 12, 2013 7:04:56 午後 ClientTest main
情報: DATASTORE_SERVICE_ACCOUNT:*************-********************************@developer.gserviceaccount.com
6 12, 2013 7:04:56 午後 ClientTest main
情報: DATASTORE_PRIVATE_KEY_FILE:/Users/User/Documents/workspace/vier-test-client/privatekey.p12
6 12, 2013 7:04:56 午後 com.google.api.services.datastore.client.DatastoreHelper getOptionsfromEnv
情報: Using JWT Service Account credential.
6 12, 2013 7:04:56 午後 ClientTest main
情報: options.getHost():https://www.googleapis.com
6 12, 2013 7:04:56 午後 ClientTest main
情報: options.getDataset():***********
6 12, 2013 7:04:58 午後 ClientTest main
情報: Meaning of Life?
> 42
6 12, 2013 7:05:02 午後 ClientTest main
情報: fascinating, extraordinary and,when you think hard about it, completely obvious.

前回Compute Engine上で実行した場合には
Using Compute Engine credential.」とログが出力されましたが、
Compute Engine上以外から環境変数を指定して実行した場合には
Using JWT Service Account credential.」と出力されます。
このように環境に応じて使用するCredentialを内部で自動的に切り替えているのがわかります。



◯クエリを実行するサンプル

Getting Started with Google Cloud Datastore and Java/Protobuf」のサンプルを少し修正しただけです。

処理内容は下記の通り。
1.Entity を1000件 put する
2.更新時刻の降順、limit5件でクエリする

* 「Putの平均時間」を計測するため、あえて batch put してません。


・ClientTest3.java

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang3.time.StopWatch;

import com.google.api.services.datastore.DatastoreV1.BlindWriteRequest;
import com.google.api.services.datastore.DatastoreV1.Entity;
import com.google.api.services.datastore.DatastoreV1.EntityResult;
import com.google.api.services.datastore.DatastoreV1.Key;
import com.google.api.services.datastore.DatastoreV1.Property;
import com.google.api.services.datastore.DatastoreV1.PropertyOrder;
import com.google.api.services.datastore.DatastoreV1.Query;
import com.google.api.services.datastore.DatastoreV1.RunQueryRequest;
import com.google.api.services.datastore.DatastoreV1.RunQueryResponse;
import com.google.api.services.datastore.DatastoreV1.Value;
import com.google.api.services.datastore.client.Datastore;
import com.google.api.services.datastore.client.DatastoreException;
import com.google.api.services.datastore.client.DatastoreFactory;
import com.google.api.services.datastore.client.DatastoreHelper;
import com.google.api.services.datastore.client.DatastoreOptions;

public class ClientTest3 {
 private static final Logger logger = Logger.getLogger(ClientTest3.class.getName());

 public static void main(String[] args) {
  if (args.length < 1) {
   System.err.println("Usage: ClientTest dataset_id");
   System.exit(1);
  }

  logger.info("DATASTORE_DATASET:" + System.getenv("DATASTORE_DATASET"));
  logger.info("DATASTORE_HOST:" + System.getenv("DATASTORE_HOST"));
  logger.info("DATASTORE_SERVICE_ACCOUNT:" + System.getenv("DATASTORE_SERVICE_ACCOUNT"));
  logger.info("DATASTORE_PRIVATE_KEY_FILE:" + System.getenv("DATASTORE_PRIVATE_KEY_FILE"));

  String datasetId = args[0];
  Datastore datastore = null;
  try {
   DatastoreOptions.Builder builder = DatastoreHelper.getOptionsfromEnv();
   DatastoreOptions options = builder.dataset(datasetId).build();
   logger.info("options.getHost():" + options.getHost());
   logger.info("options.getDataset():" + options.getDataset());

   datastore = DatastoreFactory.get().create(options);

  } catch (GeneralSecurityException exception) {
   System.err.println("Security error connecting to the datastore: " + exception.getMessage());
   System.exit(1);
  } catch (IOException exception) {
   System.err.println("I/O error connecting to the datastore: " + exception.getMessage());
   System.exit(1);
  }

  StopWatch sw = new StopWatch();
  sw.start();
  for (int i = 1; i <= 1000; i++) {
   try {
    // トランザクション外で更新するためのRPC requestを作成する
    BlindWriteRequest.Builder req = BlindWriteRequest.newBuilder();
    // 新規Entityを作成する
    Entity.Builder entity = Entity.newBuilder();
    // 一つのPathElementでKeyを生成する (親Keyなし)
    Key.Builder key = Key.newBuilder().addPathElement(
      Key.PathElement.newBuilder().setKind("ClientTest3").setName("keyName" + i));
    entity.setKey(key);
    // 文字列
    entity.addProperty(Property.newBuilder().setName("str")
      .addValue(Value.newBuilder().setStringValue("string" + i)));
    // 数値
    entity.addProperty(Property.newBuilder().setName("number")
      .addValue(Value.newBuilder().setIntegerValue(i)));
    // 作成時刻
    entity.addProperty(Property.newBuilder().setName("createDate")
      .addValue(Value.newBuilder().setTimestampMicrosecondsValue(new Date().getTime() * 1000)));
    req.getMutationBuilder().addUpsert(entity);
    // putする
    datastore.blindWrite(req.build());

    logger.info("put done count:" + i);

   } catch (DatastoreException exception) {
    logger.log(Level.SEVERE, "error", exception);
   }
  }
  sw.stop();
  logger.info("put entities " + sw.getTime() + " milliseconds.");

  sw.reset();
  sw.start();

  try {
   // クエリする
   RunQueryRequest.Builder req = RunQueryRequest.newBuilder();
   Query.Builder queryBuilder = req.getQueryBuilder();
   queryBuilder.addKindBuilder().setName("ClientTest3");
   // 作成時刻の新しい順
   queryBuilder.addOrder(DatastoreHelper.makeOrder("createDate", PropertyOrder.Direction.DESCENDING));
   // limit 5件
   queryBuilder.setLimit(5);
   // クエリ実行
   RunQueryResponse res = datastore.runQuery(req.build());

   List<EntityResult> results = res.getBatch().getEntityResultList();
   for (EntityResult result : results) {
    Entity entity = result.getEntity();

    Map<String, Object> propertyMap = DatastoreHelper.getPropertyMap(entity);
    logger.info("Entity: keyName:" + entity.getKey().getPathElement(0).getName() + " str:"
      + propertyMap.get("str") + " number:" + propertyMap.get("number") + " createDate:"
      + propertyMap.get("createDate"));
   }

  } catch (DatastoreException exception) {
   logger.log(Level.SEVERE, "error", exception);
   System.exit(1);
  }
  sw.stop();
  logger.info("query entities " + sw.getTime() + " milliseconds.");

 }
}



◯ローカル環境とCompute Engineからの処理速度比較

- ローカルで実行
iMac mid2012 Core i5 メモリ 32GB

mvn clean compile
mvn exec:java -Dexec.mainClass="ClientTest3" -Dexec.args="cloud-datastore"
(中略)
6 11, 2013 4:53:02 午後 ClientTest3 main
情報: put done count:999
6 11, 2013 4:53:02 午後 ClientTest3 main
情報: put done count:1000
6 11, 2013 4:53:02 午後 ClientTest3 main
情報: put entities 504134 milliseconds.
6 11, 2013 4:53:03 午後 ClientTest3 main
情報: Entity: keyName:keyName1000 str:string1000 number:1000 createDate:Tue Jun 11 16:53:02 JST 2013
6 11, 2013 4:53:03 午後 ClientTest3 main
情報: Entity: keyName:keyName999 str:string999 number:999 createDate:Tue Jun 11 16:53:01 JST 2013
6 11, 2013 4:53:03 午後 ClientTest3 main
情報: Entity: keyName:keyName998 str:string998 number:998 createDate:Tue Jun 11 16:53:01 JST 2013
6 11, 2013 4:53:03 午後 ClientTest3 main
情報: Entity: keyName:keyName997 str:string997 number:997 createDate:Tue Jun 11 16:53:01 JST 2013
6 11, 2013 4:53:03 午後 ClientTest3 main
情報: Entity: keyName:keyName996 str:string996 number:996 createDate:Tue Jun 11 16:53:00 JST 2013
6 11, 2013 4:53:03 午後 ClientTest3 main
情報: query entities 424 milliseconds.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8:26.646s
[INFO] Finished at: Tue Jun 11 16:53:03 JST 2013
[INFO] Final Memory: 19M/193M
[INFO] ------------------------------------------------------------------------
iMac:vier-test-client User$ 
1000回のPutに504,134ms. 約504秒→平均500ms. ぐらい


- Compute Engine上で実行
f1-micro メモリ:0.60GB ゾーン:us-central1-a

mvn clean compile
mvn exec:java -Dexec.mainClass="ClientTest3" -Dexec.args="cloud-datastore"
(中略)
6 12, 2013 3:30:36 午前 ClientTest3 main
情報: put done count:999
6 12, 2013 3:30:36 午前 ClientTest3 main
情報: put done count:1000
6 12, 2013 3:30:36 午前 ClientTest3 main
情報: put entities 251113 milliseconds.
6 12, 2013 3:30:37 午前 ClientTest3 main
情報: Entity: keyName:keyName1000 str:string1000 number:1000 createDate:Wed Jun 12 03:30:36 EDT 2013
6 12, 2013 3:30:37 午前 ClientTest3 main
情報: Entity: keyName:keyName999 str:string999 number:999 createDate:Wed Jun 12 03:30:36 EDT 2013
6 12, 2013 3:30:37 午前 ClientTest3 main
情報: Entity: keyName:keyName998 str:string998 number:998 createDate:Wed Jun 12 03:30:36 EDT 2013
6 12, 2013 3:30:37 午前 ClientTest3 main
情報: Entity: keyName:keyName997 str:string997 number:997 createDate:Wed Jun 12 03:30:35 EDT 2013
6 12, 2013 3:30:37 午前 ClientTest3 main
情報: Entity: keyName:keyName996 str:string996 number:996 createDate:Wed Jun 12 03:30:35 EDT 2013
6 12, 2013 3:30:37 午前 ClientTest3 main
情報: query entities 462 milliseconds.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4:23.316s
[INFO] Finished at: Wed Jun 12 03:30:37 EDT 2013
[INFO] Final Memory: 10M/24M
[INFO] ------------------------------------------------------------------------
[User@cdstest vier-test-client]$ 
1000回のPutに208,679ms. 約208秒平均200ms. ぐらい

一番非力な仮想マシンで試しましたが、予想通り国内からインターネット経由でAPIを実行するのと比べて
処理時間に2倍の差があります。
インスタンスを「n1-standard-1 (コア1 メモリ 3.75GB)」にしても実行時間は誤差レベルでしか変わらず。
ほとんどの時間をAPIの通信で使っているからでしょう。


- ついでにApp Engine上からのPut速度も試してみる
F1インスタンス (一番低価格のFrontendインスタンス)

Compute Engine からの put にかかった時間を見て、
「App Engine から put したらどのくらいだっけ?」と思ったので試してみました。

コードは以下の内容。
Model (Entity)の定義は前述のClientTest3とほぼ同じです。
App Engine 上から Slim3経由で Low Level APIを使って操作した場合の実行時間を調べます。

リクエストの60秒制限に引っかからないよう、こちらはPut回数を「100回」にしています。
(cronやTaskQueue使ってもいいんですけど簡単に)

package jp.vier.test.controller;

import java.util.Date;
import java.util.logging.Logger;

import jp.vier.test.model.Slim3Test;

import org.apache.commons.lang3.time.StopWatch;
import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;
import org.slim3.datastore.Datastore;

import com.google.appengine.api.datastore.Key;

public class PutTestController extends Controller {
    private static final Logger logger = Logger
        .getLogger(PutTestController.class.getName());

    @Override
    public Navigation run() throws Exception {

        StopWatch sw = new StopWatch();
        sw.start();
        for (int i = 1; i <= 100; i++) {
            Key key = Slim3Test.createKey("keyName" + i);
            Slim3Test model = new Slim3Test(key);
            model.setStr("string" + i);
            model.setNumber(i);
            model.setCreateDate(new Date());
            Datastore.put(model);
        }
        sw.stop();
        logger.info("put entities " + sw.getTime() + " milliseconds.");

        return forward("putTest.jsp");
    }
}


100回のPutに4,388ms. 約4.4秒→平均44ms. ぐらい。

GCE上から Cloud Datastore API 経由で実行するより4〜5倍速いです。


- 所感
予想通りの結果として、
Cloud Datastore API は Googleクラウド外からよりもCompute Engineから実行した方が速い
という結果になりました。 (アメリカ国内だったらここまで変わらないのでしょうけど)

「既存の別システムの処理に伴ってDatastoreを操作したいようなケース」では
既存システムから直接APIを実行するのがシンプルだと思いますが、
「大量データに対して更新処理を行うバッチ処理」のような単独の処理は
Compute Engineから実行するのが良さそうです。
ちなみに f1-micro のお値段は$0.019/時。 (起動しっぱなしでも $13.68/月ぐらい)

次に、
Compute Engineからの実行速度とApp Engineからの実行速度を比べると、
AppEngine 上から直接 Datastore を操作するのに比べてCloud Datastore API はかなり速度が落ちる
という結果も出ました。

※ 2013/6/20 追記 Google+で +Takashi Matsuo さんと +Johan Euphrosine さんからコメントをいただきました
Google でも Compute Engine から実行した際の速度については認識していて、
現在 Compute Engine と Cloud Datastore 間のネットワークレイテンシーを減らすために積極的に取り組んでいる」とのことです。
参考: High latency · Issue #5 · GoogleCloudPlatform/google-cloud-datastore
(ちなみに Johan さんは「18.Google App Engineパターン (appengine ja night #23)」の「proppy氏」)

※ 2013/7/11 追記 Githubで Johan さんから「もう一回ベンチマーク試せる?」と書き込みがあったので試してみました。
試したところ f1-micoro で put速度は3割強向上しました。
また、別途 Brian さんから「高性能なインスタンスほどネットワークのスループットも高いから、n1-standard-4 か n1-highcpu-4 インスタンス変えたらもっと速くなるかも」という書き込みがあったので、こちらも試してみました。

putの速度は n1-highcpu-4 も f1-micro とほとんどと変わりませんでしたが、クエリの速度は3割ほど速くなりました。
(ちなみに Brian さんは「appengine ja night #23」でCompute Engineについて発表してくれた「Brian氏」)

Cloud Datastoreの話から少し逸れますが、Compute EngineでWebサーバーを公開するような場合はネットワークのパフォーマンスのために高性能なインスタンスを使うのが良いかもしれません。
(軽く調べたところEC2もインスタンスタイプによってネッワークのパフォーマンスが変わる模様)


前回書いた仕組みを考えれば、
「クライアント→GAEアプリ上のAPI」間の通信時間とGAEアプリの処理時間が
そのままオーバーヘッドとなって実行時間に加算されているので、当然と言えば当然かもしれません。
(さらに APIのURLである www.googleapis.com のサーバーと App Engineアプリ の間でも通信してるのかな?)

この結果を見ると、「33.Google Cloud Datastoreを試してみた 概要編 (1/3)」に書いた
ユースケース2 Compute Engineから使うNoSQLデータベースとして利用する のように、
GCE上のWebアプリからメンテナンスフリーなKVSとしてCloud Datastoreを利用するのは速度的に苦しい気がしてきました。

現時点ではApp Engineをメインに使っているWebシステムとの連携目的で
バッチや別システムから Cloud Datastore API を使うのが現実的かもしれません。


「Cloud Datastoreの請求がPreview期間後はAppEngineとは別の請求になる」という話から考えると
その頃には今と異なる仕組みになっていそうな気もしますので、
Compute Engine から接続する場合だけでもDatastoreに高速にアクセスする手段が提供されて欲しいと思います。
App Engine を使わず Compute Engine だけで利用する場合にも、
メンテナンスフリーな Datastore の存在は運用コストに結構影響があると思うので。

個人的には、予定されているCompute Engine のLoad Balancerと
アジアのデータセンターが提供される頃に間に合うとタイミングがいいなぁと思います。



◯おまけ (トラブルシューティング)

実は今回書いた「Service Account Flowを使ってAPIを実行」はとても難航しました。
本来は前回のCompute Engineからの実行ができていれば環境変数を設定するだけで簡単に済むはずですが、
認証エラーが発生してAPIを実行できず、その調査に2,3日悩まされました。
その時のトラブルの原因は環境的なものであったと考えています。

最後にApp Engine Ver.1.8.1リリース後に作成した新規プロジェクトではスムーズに動作したので
もう問題は再現しないかもしれませんが、
Service Account Flow でうまく接続できない場合の参考に、原因の切り分けのために試した事を書いておきます。


- blindWrite実行時に 「DatastoreException(Unauthorized.)」 が発生する
当初、ドキュメント通りに環境変数の設定をしていてもこのエラーが発生してエラーになっていました。

・Compute Engine上からは問題なく動作した。
 (前回書いたようにCompute Engine上では専用のCredentialが使われるので、Credentialが異なる)

APIs Explorer からは問題なく動作した。(JSからGoogleアカウントのIDとパスワードを使った認証)

・blindWrite実行時に使用するTokenの値をデバッガで「APIs Explorerで生成したToken」に書き換えたら動作した。

・Java で Authorization Code Flow を使って Credential を取得するコードを書いたら問題なく動作した。
 (これもGoogleアカウントのIDとパスワードを使った認証)
 参考:https://developers.google.com/bigquery/docs/authorization#installed-applications

* ここまでに試した内容では取得した Service Account や Private Key を使用しません。
これらを使用するのがこの後のService Account Flow です。

ここまでで、どうやら Service Account Flow で認証した場合にだけ動作していないことがわかりました。
次に Service Account Flow での認証ができていないのかを確認しました。

・Service Account Flowで OAuth2 Token の取得はできている。

・BigQueryのScopeを追加してService Account Flowで認証し、BigQueryのAPIを実行したところ動作した。
→Service Account Flow を使った認証自体には問題なさそう。
→"Service Account Flow を使って Datastore を操作しようとした場合"に「Unauthorized」となる。

・この時対象アプリの「ah-builtin-datastoreservice」にリクエストログが記録されている。(アクセスは来ている)

これらの内容から、
「Service Account Flow を使った認証自体はできているが App Engine に対して権限が無いのではないか」
と推測しました。

そして、最後に作成して正常に動作した App Engine のアプリでは、
Admin Console の Permissions に「Cloud Consoleの Certificate セクションの EMAIL ADDRESS」、
つまり「環境変数の DATASTORE_SERVICE_ACCOUNT に指定するメールアドレス」が登録されていました。
一方、以前に作成して実行に失敗したGAEアプリには「EMAIL ADDRESS」が登録されていませんでした。
このアドレスは自分で登録することができないので(メールを受信して承認できないので)
どこかのタイミングで自動で登録されるのでしょう。
何らかの理由でこれが登録されなかった場合にエラーになるのかもしれません。

正常に動作したアプリでは、Cloud Project関連のユーザーとして
・123456789000@project.gserviceaccount.com
・123456789000@developer.gserviceaccount.com
の2つが、
追加したService Account Flow用のアプリのEMAIL ADDRESSである、
・123456789000-01g2taglb3ur4567resjqda89pod0t2g@developer.gserviceaccount.com
が登録されていました。
* アドレスの「@」以前は適当に書き換えています。

特にこの3つ目の「EMAIL ADDRESS」が無い場合が怪しいのかな、と思っています。
(確定では無いのですが、接続に失敗したアプリには今もこれが登録されていませんし、今も接続できません)


− APIのリクエストがAppEngineアプリの「ah-builtin-datastoreservice」で処理されない
これは2つのパターンがありました。
・アプリのデフォルトバージョンにリクエストが来る
・「ah-empty」という謎のバージョンが作成されてそこにアクセスが来る


「サイズ 0Bytes」、「デプロイしたユーザー名が空」ってのはレアですね。
さすがにバグっぽい気がしますが、こちらの原因と解決策はわかりません。


- 他のエラーのパターン
こちらは設定やコードにミスがある場合のパターンです。参考に。

- Cloud DatastoreAPIをONにしていない場合
DatastoreException(Access Not Configured): blindWrite 403

- Scopeが足りない場合 (DatastoreHelperを使っている限りは起こり得ませんが)
403 Forbidden Insufficient Permission


かなり苦労しましたが、おかげでGoogle APIの各種認証方法についての理解は以前より深まった気がします(;´∀`)



◯参考リンク

Google Cloud Datastore 公式ドキュメント
Activate Google Cloud Datastore - Google Cloud Datastore
Using the Local Development Server - Google Cloud Datastore
Getting Started with Google Cloud Datastore and Java/Protobuf
GoogleCloudPlatform/google-cloud-datastore · GitHub

2013年6月18日火曜日

37.Google Cloud Datastoreを試してみた GCE編 (2/3)


33.Google Cloud Datastoreを試してみた 概要編 (1/3)
37.Google Cloud Datastoreを試してみた GCE編 (2/3)
38.Google Cloud Datastoreを試してみた その他環境編 (3/3)

Ryo Yamasaki(@vierjp)です。

途中で「App Engine SDK 1.8.1のリリース」「BigQueryの新機能」「GAE for PHP用WordPressプラグイン」を挟んで
引き続き「Google Cloud Datastore」について試したことを書いていきます。

第1回は概要や仕組み、注意点について書きました。
この第2回ではGoogle Compute EngineからCloud DatastoreのAPIを実行してみます。



◯Google Cloud Console と Cloudプロジェクト

Cloud DatastoreのAPI実行時に「プロジェクト」という概念が出てくるので、
先に「Google Cloud Console」とその「プロジェクト」について簡単に書こうと思います。

先日「Blog @vierjp : 27.Google I/Oで発表されたGoogle Cloud Platformの新機能」に書いた通り、
Google I/Oで「Google Cloud Console」が発表されました。

これまでプロダクト毎に分かれていたGoogle Cloud Platformの各プロダクトを
まとめて扱うための管理画面が「Google Cloud Console」で、
その各プロダクトをまとめる単位が「Cloudプロジェクト」になります。



上の画像のように、
一つの「Cloudプロジェクト」にはAppEngine・ComputeEngine・Cloud Storageなどの各プロダクトが含まれます。

「Cloudプロジェクト」と既存プロダクトとの関連は以下のようになります。

・AppEngineの"一つのアプリケーション"とプロジェクトが1:1で紐付いている
・AppEngineの「アプリケーションID」と Cloud Consoleの「Project ID」は同じ値である
・プロジェクトに紐づくCloud Datastore は"一つのGAEアプリケーションのDatastore"を示す
・既存のAPI Projectは「Cloudプロジェクト」と1:1で紐付いている(ほぼ同等)
という点がポイントです。



◯Cloudプロジェクトの作成

- 新規に作成したGAEアプリケーションのDatastoreを使用する場合

Google Cloud Consoleを開く
・画面左上の「CREATE PROJECT」ボタンを押す
・「Project name」と「Project ID」を入力する。

* デフォルトではかなり適当なID値が表示されますが、
Project IDの値はAPI利用時に使ったり、
GAEアプリケーションのアプリケーションID(=URLの一部)にもなるので、
自分で覚えやすい名前を明示的に指定するのがオススメ。


- 既存GAEアプリケーションのDatastoreを使用する場合
第一回に書いたように Cloud Datastore API の処理は App Engine のアプリで実行されますが、
Cloud Datastore APIの実行時には「Cloudプロジェクト」を指定します。
そのため、過去に作成済みのAppEngineアプリとCloudプロジェクトを紐付けておく必要があります。

1.AppEngineのAdmin Consoleを開く
2.Application SettingsのCloud Integration セクション内「Add Project」を押す

しばらく「進行中」になるので待ちます。
完了すると成功した旨表示され、「Application Settings」のBasics セクションに
「Google APIs Console Project Number:〜〜」の記述が追加されます。


これでそのGAEアプリケーションがCloudプロジェクトと紐付きます。
(エラーになって失敗するケースもあるようですが。。)

参考:Activate Google Cloud Datastore - Google Cloud Datastore



◯サンプルプログラム

ほとんど「Getting Started with Google Cloud Datastore and Java/Protobuf」のサンプルのままです。
コメントは全て日本語に直しました。

1.TriviaというKind名でEntityを一件putする
2.Keyを指定してgetする
3.入力した値とEntityから取得した数字(42)が一致するかを判定してメッセージを出力する
というコードです。


・pom.xml
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemalocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelversion>4.0.0</modelversion>
  <groupid>vier-test-client</groupid>
  <artifactid>vier-test-client</artifactid>
  <version>0.0.1-SNAPSHOT</version>
  <name>vier-test-client</name>

<repositories>
  <repository>
    <id>sonatype-snapshots</id>
    <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupid>com.google.apis</groupid>
    <artifactid>google-api-services-datastore-protobuf</artifactid>
    <version>v1beta1-rev1-1.0.0-SNAPSHOT</version>
  </dependency>
</dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactid>maven-eclipse-plugin</artifactid>
        <configuration>
          <downloadsources>true</downloadsources>
          <downloadjavadocs>true</downloadjavadocs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

・ClentTest.java
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.api.services.datastore.DatastoreV1.BlindWriteRequest;
import com.google.api.services.datastore.DatastoreV1.Entity;
import com.google.api.services.datastore.DatastoreV1.Key;
import com.google.api.services.datastore.DatastoreV1.LookupRequest;
import com.google.api.services.datastore.DatastoreV1.LookupResponse;
import com.google.api.services.datastore.DatastoreV1.Property;
import com.google.api.services.datastore.DatastoreV1.Value;
import com.google.api.services.datastore.client.Datastore;
import com.google.api.services.datastore.client.DatastoreException;
import com.google.api.services.datastore.client.DatastoreFactory;
import com.google.api.services.datastore.client.DatastoreHelper;
import com.google.api.services.datastore.client.DatastoreOptions;

public class ClientTest {
 private static final Logger logger = Logger.getLogger(ClientTest.class.getName());

 public static void main(String[] args) {
  if (args.length < 1) {
   System.err.println("Usage: ClientTest DATASET_ID");
   System.exit(1);
  }

  // 環境変数をログ出力
  logger.info("DATASTORE_DATASET:" + System.getenv("DATASTORE_DATASET"));
  logger.info("DATASTORE_HOST:" + System.getenv("DATASTORE_HOST"));
  logger.info("DATASTORE_SERVICE_ACCOUNT:" + System.getenv("DATASTORE_SERVICE_ACCOUNT"));
  logger.info("DATASTORE_PRIVATE_KEY_FILE:" + System.getenv("DATASTORE_PRIVATE_KEY_FILE"));

  // DatasetID(ProjectID) を実行時引数から取得する
  String datasetId = args[0];
  Datastore datastore = null;
  try {
   // 生成したCredentialをセットした「DatastoreOptions」を取得する。
   // Compute Engine上て動作している場合はComputeEngine用のCredentialを、
   // それ以外の環境で、かつ環境変数が適切に設定されている場合は
   // Serivce Account FlowのCredentialを生成する。
   DatastoreOptions.Builder builder = DatastoreHelper.getOptionsfromEnv();

   DatastoreOptions options = builder.dataset(datasetId).build();
   logger.info("options.getHost():" + options.getHost());
   logger.info("options.getDataset():" + options.getDataset());

   datastore = DatastoreFactory.get().create(options);

  } catch (GeneralSecurityException exception) {
   logger.severe("Security error connecting to the datastore: " + exception.getMessage());
   System.exit(1);
  } catch (IOException exception) {
   logger.severe("I/O error connecting to the datastore: " + exception.getMessage());
   System.exit(1);
  }

  try {
   // トランザクション外で更新するためのRPC requestを作成する
   BlindWriteRequest.Builder req = BlindWriteRequest.newBuilder();
   // 新規Entityを作成する
   Entity.Builder entity = Entity.newBuilder();
   // 一つのPathElementでKeyを生成する (親Keyなし)
   Key.Builder key = Key.newBuilder().addPathElement(
     Key.PathElement.newBuilder().setKind("Trivia").setName("hgtg"));
   entity.setKey(key);
   // Entityに2つのPropertyを追加する
   // utf-8文字列の「question」
   entity.addProperty(Property.newBuilder().setName("question")
     .addValue(Value.newBuilder().setStringValue("Meaning of Life?")));
   // 64bit integerの「answer」を追加する
   entity.addProperty(Property.newBuilder().setName("answer").addValue(Value.newBuilder().setIntegerValue(42)));
   // update or insertする「mutation」として「upsert」を指定する
   // 他にも「update」「insert」「insertAutoId」「delete」「force」がある
   // (「force」はユーザーが設定したread-onlyを無視するらしい)
   req.getMutationBuilder().addUpsert(entity);
   // 同期的にRPCを実行して結果を無視する
   // (「返り値を無視する」の意味であって、システムエラー時には例外が発生する)
   datastore.blindWrite(req.build());

   // KeyでEntityを「get」するための RPC リクエストを作成する
   LookupRequest.Builder lreq = LookupRequest.newBuilder();
   // 登録したEntityを「get」するためにKeyを一つ指定する
   // (おそらく複数指定するとbatch get)
   lreq.addKey(key);
   // RPCを実行して結果を取得する
   LookupResponse lresp = datastore.lookup(lreq.build());
   // 結果として一つのEntityを取得する.
   Entity entityFound = lresp.getFound(0).getEntity();
   // 「question」 propertyの値を取得する。
   String question = entityFound.getProperty(0).getValue(0).getStringValue();
   // 「answer」 propertyの値を取得する。
   Long answer = entityFound.getProperty(1).getValue(0).getIntegerValue();
   logger.info(question);
   // コンソールからの値入力待ち
   String result = System.console().readLine("> ");
   // 入力値とEntityから取得した「answer」(=42)が一致した場合
   if (result.equals(answer.toString())) {
    logger.info("fascinating, extraordinary and,when you think hard about it, completely obvious.");

    // 一致しなかった場合
   } else {
    logger.info("Don't Panic!");
   }
  } catch (DatastoreException exception) {
   // DatastoreAPI実行時の例外をcatchする.
   logger.severe("Error while doing datastore operation");
   logger.log(Level.SEVERE, "error", exception);
   System.exit(1);
  }
 }
}



◯Compute Engine から Cloud Datastore の API を使う

前回「Blog @vierjp : "33.Google Cloud Datastoreを試してみた 概要編 (1/3)"」に書いた以下のユースケースでは
Compute EngineからCloud Datastoreに接続します。

ユースケース1 App EngineのバックエンドとしてCompute Engineを利用する
ユースケース2 Compute Engineから使うNoSQLデータベースとして利用する

前回書いたとおりCompute EngineからCloud DatastoreのAPIを利用する場合は認証が簡単です。


- Google Compute Engineを有効にする
Google Cloud Consoleで操作対象の Datastore が属する Cloudプロジェクト を開いて Compute Engine を選択します。
Compute Engine を使用するためにはここで Billing設定を有効にする必要があります。


- gcutilの導入
Google Compute Engine では CUI のツールとして gcutil が提供されています。
今回の手順では gcutil が必要なので最初に準備します。(理由は後述)

gcutil はMac、Linux環境では他のプロダクトのCUIツール同様簡単に導入できますが、
Windows環境では「Cygwin」の導入も必要です。
(おそらく gcutil のコマンドの一部に内部的に ssh や scp コマンドを使うものがあるため)

・ダウンロードして展開する
wget https://google-compute-engine-tools.googlecode.com/files/gcutil-1.8.1.tar.gz
tar xzvpf gcutil-1.8.1.tar.gz -C $HOME
rm gcutil-1.8.1.tar.gz

・Pathに追加する
echo -e "\nexport PATH=\$PATH:\$HOME/gcutil-1.8.1" > $HOME/.bashrc
source $HOME/.bashrc

参考:gcutil Tool - Google Compute Engine


- GCEインスタンスの作成
gcutil --project [Project ID] addinstance [インスタンス名]\
 --image=projects/centos-cloud/global/images/centos-6-v20130522\
 --machine_type=f1-micro\
 --persistent_boot_disk=true\
 --service_account_scopes=\
https://www.googleapis.com/auth/userinfo.email,\
https://www.googleapis.com/auth/datastore

・Project ID
作成済みのCloudプロジェクトのIDを指定します。

・インスタンス名
ここでは好きな名前を指定すればOK。
覚えやすい名前にしましょう。

・image
OSイメージを指定する。
ここでは用意されているCentOS6のイメージを使いました。

・machine_type
インスタンスタイプを指定する。
提供されているインスタンスタイプの内容と料金は以下を参照
Google Compute Engine Pricing - Google Cloud Platform — Cloud Platform

・persistent_boot_disk
起動ディスクをPersistent Diskにするかどうか。
共有インスタンスの f1-micro はPersistent Diskの利用が必須なのでtrueを指定しています。
(Compute Engineについての詳しい話はまたそのうち)

・service_account_scopes
このGCEインスタンスから利用可能なScope
Googleの各API毎に要求するScopeが異なります。
「https://www.googleapis.com/auth/userinfo.email」はユーザーを特定するために定番で指定、
「https://www.googleapis.com/auth/datastore」はCloud DatastoreのAPIを利用するために指定します。


* 現状この手順は gcutil から行う必要があります
本来 GCEインスタンス の作成は Cloud Console の GUI からもできます。
その際にAPIの利用設定(Scopeの設定)をすることが可能ですが、
Cloud Datastore API については現状そこで利用設定をすることができません。(UI的に)
そのため gcutil のコマンドで明示的に Scope を指定してインスタンスを作成する必要があります。


- GCEインスタンスにsshで接続する
gcutil --project [プロジェクト名] ssh [インスタンス名]
インスタンス作成時に指定したプロジェクト名とインスタンス名を指定してsshで接続します。
任意のsshクライアントからIPアドレスを指定して接続することもできますが、
gcutil からなら覚えやすいインスタンス名で接続できるので楽です。


- JDKのインストール
Javaプログラムを実行するのでJDKをyumでインストールします。

yum list \*java-1\* | grep open
sudo yum install java-1.7.0-openjdk.x86_64
sudo yum install java-1.7.0-openjdk-devel.x86_64

- mavenのインストール
mavenをダウンロードして設定します。

wget http://ftp.riken.jp/net/apache/maven/maven-3/3.0.5/binaries/apache-maven-3.0.5-bin.tar.gz
tar xvzf apache-maven-3.0.5-bin.tar.gz
sudo mv apache-maven-3.0.5 /usr/share
sudo ln -s /usr/share/apache-maven-3.0.5 /usr/share/maven

export M2_HOME=/usr/share/maven
export M2=$M2_HOME/bin
export PATH=$M2:$PATH

最後に以下のコマンドで動作を確認。
mvn --version

- ローカルのファイルをGCEインスタンスに転送
gcutil --project [プロジェクト名] push [インスタンス名] \
/Users/User/Documents/workspace/vier-test-client/ /home/User/vier-test-client/
こちらも ssh 同様、インスタンス名を指定して gcutil からファイルの転送ができます。
内部的には scp コマンドが実行されているようです。


- プログラムの実行
mavenから実行します。

mvn clean compile
mvn exec:java -Dexec.mainClass="ClientTest" -Dexec.args="[Project ID]"
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building vier-test-client 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) @ vier-test-client >>>
[INFO] 
[INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) @ vier-test-client <<<
[INFO] 
[INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ vier-test-client ---
6 12, 2013 11:23:56 午前 ClientTest main
情報: DATASTORE_DATASET:null
6 12, 2013 11:23:56 午前 ClientTest main
情報: DATASTORE_HOST:null
6 12, 2013 11:23:56 午前 ClientTest main
情報: DATASTORE_SERVICE_ACCOUNT:null
6 12, 2013 11:23:56 午前 ClientTest main
情報: DATASTORE_PRIVATE_KEY_FILE:null
6 12, 2013 11:23:56 午前 com.google.api.services.datastore.client.DatastoreHelper getOptionsfromEnv
情報: Using Compute Engine credential.
6 12, 2013 11:23:56 午前 ClientTest main
情報: options.getHost():https://www.googleapis.com
6 12, 2013 11:23:56 午前 ClientTest main
情報: options.getDataset():***********
6 12, 2013 11:24:01 午前 ClientTest main
情報: Meaning of Life?
> 42
6 12, 2013 11:24:07 午前 ClientTest main
情報: fascinating, extraordinary and,when you think hard about it, completely obvious.

実行時引数は「Project ID」を指定します。(コード中では datasetId としているけど同じ値)
冒頭に書いたとおり「Cloudプロジェクト」 と 「Datastore」 は 1:1 で紐付いているので、
「Cloudプロジェクト」のIDを指定することでアクセス対象のDatastoreが確定します。


DatastoreHelper#getOptionsfromEnv()メソッドの中では、
1. Compute Engine用のCredentialの取得を試みて、取得できればそれを返す
 →Compute Engine上で実行した場合はCompute Engine用のCredentialを取得できる
  (実行ログ23行目の「Using Compute Engine credential.」はこの分岐のログ)

2. (1でCredentialを取得できなければ)Service Account Flow用のCredentialを生成して返す
 →Compute Engine用のCredentialを取得できなければ、環境変数にセットした情報を用いてCredentialを生成して返す
という処理が行われています。

よってローカル環境でテストする際やCompute Engine以外の環境でも、
環境変数の設定をするだけでプログラムの修正は無しに動作する作りになっています。

(Compute Engine から Scope設定 だけでアクセスできるのは同一プロジェクト内の Datastore だけなので、
ComputeEngine 優先のフローは 別Project の Datastore にアクセスしようとした場合に困りそうな気もしますが)



◯おまけ

GCEインスタンス上ではGoogle APIを実行するためのCredentialを以下のコードで取得できるようです。

  public static Credential getComputeEngineCredential()
      throws GeneralSecurityException, IOException {
    NetHttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
    ComputeCredential credential = new ComputeCredential(transport, new JacksonFactory());
    credential.refreshToken();
    return credential;
  }

参考:
com.google.api.services.datastore.client.DatastoreHelper#getComputeEngineCredential()

「ComputeCredential」は「google-api-client-1.15.0-rc.jar」に含まれているので、
ComputeEngineのインスタンスを生成する際に必要なScopeを指定することで、
おそらくCloud Datastore以外のAPIを使うときにも使えるでしょう。



◯参考リンク

Google Cloud Datastore 公式ドキュメント

Activate Google Cloud Datastore - Google Cloud Datastore

Getting Started with Google Cloud Datastore and Java/Protobuf

gcutil Tool - Google Compute Engine



◯次回

次回は Compute Engine 以外の環境から Cloud Datastore の API を試します。
今回は put と get だけでしたが、次回はクエリも実行してみようと思います。

2013年6月14日金曜日

36.Google App Engine for PHPでWordPressを運用するためのプラグインが登場


Ryo Yamasaki(@vierjp)です。

Google App Engine for PHP用のWordPressプラグインが公開されました。
WordPress › Google App Engine for WordPress « WordPress Plugins

このプラグインを追加することで、以下の改善がなされます。
ファイルのアップロードが可能になる
メールの送信が可能になる
・問題のあった一部のUIが修正される

以前に「Blog @vierjp : 29.Google App Engine for PHPでWordPressを動かしてみた」に書いた、
「画像のアップロードができない問題」が解決されるそうです。

これは素晴らしい!というわけで早速試してみました。



◯プラグインの導入

WorkPress本体の導入方法は
Blog @vierjp : 29.Google App Engine for PHPでWordPressを動かしてみた」を参照してください。

WordPress本体の導入までできていればプラグインの追加はとても簡単です。
1.「WordPress.org」からプラグインをダウンロードする

2. zipを展開する

3.展開したディレクトリ「google-app-engine」を「/wp-content/plugins/」に配置する

4.デプロイする
/Users/User/appengine_php_1_8_0/appcfg.py update -R --runtime=php . \
--noauth_local_webserver --oauth2

5.左のメニューからプラグイン一覧画面を開いて「Google App Engine for WordPress」を「有効化」する



6.「Settings」で「メールアドレス」と「ファイルを保存するGoogle Cloud Storageのbucket名」を設定する


◯画像をアップロードしてみる

以前は管理画面でアップロードする際にエラーになっていましたが、今度は問題なくアップロードできました。




・画像を追加して公開したエントリーがこれ。



実際にGoogle App Engine 上で動作しているWordPressは以下のURLで閲覧できます。
「WordPress on Google App Engine for PHP by vierjp」
* 2013/11/10 追記 Cloud SQLの課金が地味に痛いので停止しました。

修正はかなり面倒そうに思っていましたが、
本体のコードに手を入れずプラグインの追加だけで対応できてしまうのですね。
WordPressすごい。


これでApp Engine 上でWordPressを運用することが現実的になったのではないでしょうか。


2013年6月12日水曜日

35.BigQueryの新機能 (2013/06/11)


BigQueryに新しい機能が追加されたので早速試してみました。

Cloud Platform Blog: Google BigQuery gets bigger, faster, and smarter with big result sets and new analytics functions

◯Large results

- Destination Table
クエリ結果をテーブルに出力するための指定。
「Select table」ボタンを押して出力先テーブルの「プロジェクト」「データセット」「テーブルID(名前)」を指定する。

 -Write Preference
 テーブルへの書き込み設定。
 3つの選択肢があるが、どの選択肢でも「テーブルが無ければ新規に作成して書き込む」は同じ。
 その上でテーブルが既に存在している場合の挙動が下記のように異なる。

 ・Write if empty テーブルが既に存在している場合はエラー
 ・Append to table テーブルが既に存在している場合は追記する
 ・Overwrite table テーブルが既に存在している場合はテーブルを丸ごと削除してから新規に書き込む

 -Results Size - Allow Large Results
 通常「クエリの結果は128 MB以下」という制限がありそれを超えるとエラーになっていたが、
 このオプションを指定した場合はその制限がなくなる。
 このオプションにチェックするためには「Destination Table」を指定する必要があります。


クエリ結果をテーブルに書き込む事には2つの意味がありそうな気がします。

1.大規模な結果を取得して画面に表示する過程で使う一時テーブル的な位置づけ(たぶん)
2.大規模な結果を取得してテーブルに保存する、既存の「Save as Table」の改善的な位置づけ
 →既存テーブルのデータをクエリで加工して別のテーブルとして保存することも可能。
 以前は大きいテーブルに対してこれをする場合には日付の範囲等で行を絞って小分けにテーブルとして保存して、
 さらにそれらを一度ダウンロードしてから一つのテーブルに追記Uploadする必要があったのでとても楽になります。

これまでは結果データが大きすぎると「Response too large to return.」となって結果を取得できなかったので
画面に表示して閲覧することも、結果をテーブルとして保存することもできませんでしたが、
それができるようになった・・・はずなのですが、、
「Large Results」にチェックして下記のクエリを実行したら「Response too large to return.」 になりました。ぐぬぬ。

SELECT
    title,
    id,
    language,
    wp_namespace,
    is_redirect,
    revision_id,
    contributor_ip,
    contributor_id,
    contributor_username,
    timestamp,
    is_minor,
    is_bot,
    reversion_id,
    comment,
    num_characters
FROM
    [publicdata:samples.wikipedia] 
LIMIT 500000

本当は解決してからブログ書きたかったけど、これ以上は課金が怖いので棚上げして書いちゃう。`,、('∀`) '`,、

・・・そのうちまた試してみます(´・ω・`)

* 2013/6/14 追記 Google+ 経由でコメントいただきました
ある一つのクエリについて
・「Large Results」のチェックなしではエラーになった
・「Large Results」のチェックありでは結果を取得できた
を確認できたとご連絡いただきました。

しかし、上記のサンプルデータに対するクエリを試していただいたところ
こちらはやはりエラーになってしまったそうです。
改善されたのは確かなようですが、無制限というわけではないのかな・・・?


◯Window functions(分析用関数)の追加

例えば下記のような関数があります。

・rank() 結果データを特定のカラムでランク付けした順位
 →同じ順位が二行ある場合は次の順位が一つ飛ぶ(ex.1,2,3,3,5)

・dense_rank() 結果データを特定のカラムでランク付けした順位
 →同じ順位が二行あっても次の順位が飛ばない (ex.1,2,3,3,4)

・row_number() 結果データの行番号 (1から連番)

・percentile_cont(<percentile>) パーセンタイル (統計用語らしい 解説しているサイト)


他にも色々追加されたようです。
Query Reference#Window functions



◯Queryキャッシュ

- Query Caching Use Cached Results
・「Destination Table」を指定した場合には利用できない
・これにチェックしていると、クエリの結果としてキャッシュされた結果を表示する。 (デフォルトで有効)
・クエリはユーザごとの単位でキャッシュされる。(プライバシーを維持するため、とのこと)
・最後のクエリから変更していないテーブルに対してのみ適用される (テーブルを変更するとキャッシュが使われなくなる)
・キャッシュされた結果を参照するのは無料 (ただし一日あたりのクエリ回数のQuotaカウントには加算される)
・クエリの結果は24時間保持される (「ただしベストエフォート」との事なので早めに消えることもありそう)
・bqツール、APIもデフォルトはキャッシュが有効。オプションで無効にすることが可能
→クエリが参照するテーブルが前回実行時から変更されているとキャッシュを参照しないので、
 基本的には常にキャッシュを有効にしてよさげ

クエリ文字列をKeyに最終的な結果がキャッシュされているという感じ。
試しに
1回目 ソート順を指定してlimitを指定10
2回目 ソート順を指定してlimitを5
としたところ、
2回目のクエリで参照するデータは一回目に参照したデータに含まれているが、キャッシュを使わなかった。

ついでに試したてみたところ、
クエリ内の文字のCASE違いやスペースの有無は無視される。(キャッシュが使われる)



◯UIの改善 クエリ・バリデータ, コスト推定,クエリの中断,クエリの記録

-クエリ・バリデータ

リアルタイムにエラーを通知してくれるようになった。
 該当箇所が赤字になった上に、エラーメッセージが表示される。
 (以前はクエリを実行した後でエラーになって、エラーメッセージが表示されていた)


-コスト推定

 構文が有効であれば、クエリを実行した場合にどのくらいコストがかかるかを実行前に知らせてくれる。
 画面右の緑の●を押すとクエリが処理するデータサイズが表示される。(BigQueryではクエリ時に処理するデータ量に応じて課金される)
 この機能は「--dry_run」フラグを指定するとBQツールとAPIで使用可能らしい。


-クエリの中断が可能に

以前は一度クエリを実行すると、それが完了するまで次のクエリを実行できず完了を待つ必要がありましたが、
クエリを明示的に停止してすぐに次のクエリを実行できるようになりました。

「RUN QUERY」ボタンを押してクエリが開始すると「Abondon Query」ボタンに変わる。
「Abondon Query」ボタンを押すと確認ダイアログが出て「OK」を押すとクエリが止まる。
ただしあくまでUI的な話で、サーバーサイドでは処理が継続されるので途中で止めてもコストはかかるそうです。


- クエリを記録しておけるようになった
「Save Query」で名前をつけてクエリを記録できる。


◯価格改定

より手頃な価格に値下げする。

- 全てのユーザーに対して
 データストレージのコストが$0.12/GB/monthから$0.08/GB/monthになる。
 2/3になるのでかなり下がります。
 大規模データを扱うためのBigQueryなので、これはなかなかうれしい値下げです。

- 大規模ユーザーに対して
 使えば使うほど単位あたりの価格が割安になるような価格設定が用意されるようです。
 おそらくこの「Package pricing table」で、申し込めば月額課金のプランにできるようです。
 お値段は月額$3,300から。



◯Quotaの増加

全てのユーザーに対してインタラクティブなクエリのQuotaを倍増した。

Quota Policy - Google BigQuery — Google Developers




34.Google App Engine 1.8.1リリース



Ryo Yamasaki(@vierjp)です。

Google App Engine 1.8.1がリリースされたので変更点を確認しました。(Javaのみ)

・Task Queueの非同期APIがGAになったこと
・Search API がGAになったこと
Google Cloud Storage API Functionsがdeprecatedになってそのうち廃止されること
DatastoreのEntityのKeyをID値で自動採番した場合のデフォルトの挙動が変わること
が大きな変更点だと思います。

KeyのID値の自動採番の変更は既存のシステムの作りによっては影響があるかもしれないので、
1.8.1を適用する前に影響を確認した方がいいかも。

○関連リンク

SdkForJavaReleaseNotes - googleappengine - Google App Engine Java SDK
SDKダウンロードページ


◯1.8.1変更点

- This SDK will be the last release that can deploy Java 6 apps. In 1.8.2,
  the SDK will only be compiled with the Java 7 compiler and the only target runtime will be Java 7.

  Ver.1.8.1 の SDK は Java6 アプリをデプロイできる最後のバージョンになります。
  Ver.1.8.2 では SDK は Java7 コンパイラのみでコンパイルされ、ターゲットランタイムも Java7 になります。


- The Task Queue async API is now a GA feature.
  The asynchronous methods improve utilization by allowing your app to add, lease and delete multiple tasks in parallel.

 Task Queue の非同期APIはGA機能になります。
 非同期メソッドはアプリケーションが並列に複数のタスクを追加、リース、および削除できるようにすることで、
 利便性が向上します。


- Cloud Console projects are now created by default whenever a new App Engine app is created.
  This is a Preview feature.

 新しいApp Engineのアプリが作成される際に
 Cloud Consoleプロジェクトがデフォルトで作成されます。
 これはプレビュー機能です。

 結構前に作ったのもプロジェクト自体は作成されていた気がするけど、
 今後は紐付け済みの状態で作成されるのかな・・・?
 →追記:プロジェクトは作成されましたが自動で紐付けはされませんでした。


- In an upcoming release the Experimental Google Cloud Storage API Functions will be decommissioned.
  This API and its Experimental status is documented at the following link:
 https://developers.google.com/appengine/docs/java/googlestorage/overview

 今後のリリースで、Experimentalだった「Google Cloud Storage API Functions」は廃止されます。
 このAPIとその実験的ステータスは、次のリンク先のドキュメントに書かれています:
 https://developers.google.com/appengine/docs/java/googlestorage/overview


- The Google Cloud Storage library will replace Google Cloud Storage API and is now available as a Preview feature.
  More information can be found at https://code.google.com/p/appengine-gcs-client/

 「Google Cloud Storage library」はGoogleクラウドストレージAPIを置き換えます。
 そして現在プレビュー機能として利用可能です。
 さらに詳しい情報は https://code.google.com/p/appengine-gcs-client/ で見つけることができます

 今後は「Google Cloud Storage library」を使えとのこと。
 廃止するまでに十分な時間は用意するつもりだそうです。
 Files API使いやすそうだったんだけどなぁ。。
 PHPは通常のファイルI/O用関数で読み書きできるのに。(´・ω・`)


- Bandwidth between App Engine and Google Cloud Storage is currently free of charge
  (this may change in the future for certain levels of service).

  AppEngineとGoogle Cloud Storageの間の帯域は現在無料です。(将来変更される場合があります)

  GAE・GCS間の帯域は前から無料だった気がするんですが、私の勘違い・・・?


- The Search API has graduated from Experimental to Preview.
  Apps that have billing enabled can exceed the free quota levels and will be charged for usage above these levels.

 Search APIは、Experimentalを卒業してプレビューになった。
 課金を有効にしているアプリでは、無料クォータレベルを超えることができ、
 それを超えた分に課金されます。


- Estimated number of search results will only be accurate if it is less than or equal to the number of results requested.
  By default this can be overridden by setting 'number_found_accuracy' QueryOption in the Search API.

 Search APIに関して、検索結果の推定数は要求された結果の数以下の場合に正確になります。
 デフォルトでは、検索APIの「number_found_accuracy」クエリオプションを設定することで
 オーバーライドすることができます。


- Dates, atoms, and number fields can now be found by searching without a field restriction in the Search API.

  Search APIに関して、Dates、atom、およびnumberフィールドは、
 フィールド制限なしの検索をすることで見つけることができます。


- A quoted empty string now returns atom fields with empty values for the Search API.

 Search APIに関して、引用符で囲まれた空の文字列は現在空の値を持つatomのフィールドを返します。


- Snippet and count functions are no longer allowed in sort expressions for the Search API.

  Search APIでスニペットとカウントの機能はsort式では許可されなくなります。


- The Search API now has improved error messages for user errors and internal errors.

  Search APIのユーザー·エラー、内部エラーのエラーメッセージが改善されました。


- The Datastore now assigns scattered auto ids by default. Legacy auto ids are still available via the 'auto_id_policy' option in appengine-web.xml.

  データストアは、デフォルトで"散らばったAuto ID"を割り当てます。
  レガシーな自動IDはappengine-web.xmlの'auto_id_policy'オプションを使用して引き続き利用できます。

  私はEntityのKey値として自動採番したIDを使っていないので(デメリットがあったので)よく知りませんが、
  これまではIDの数値は必ず前のput時よりも大きくなっていく挙動なんでしたっけ?
  この挙動に頼った実装をしている場合は1.8.1になってデフォルトの挙動が変わる前に
  'auto_id_policy'の設定をしないとまずいのかも。
  1.8.1は6/11の朝(日本時間)には本番環境に適用されているからSDK正式リリースしてからじゃ遅くないか、と思いきや、
  「Google Cloud Platform Blog」によると
 This change will take effect for all versions of your app uploaded with the 1.8.1 SDK.
  「1.8.1のSDKを使って(jarを含んで?appcfgの話?)Uploadすると適用される」って書いてあるからまだ大丈夫みたいですね。
  EntityのKeyに自動採番したIDを使っている人は1.8.1を適用する際には一応注意。


- The Sockets API now allows client code to call get/set options against sockets. 
  Previously, calls raised "Not Implemented" exceptions.
 When java.net.Socket.get<option>() is called, a mock value is returned, calls to set<option>() will be silently ignored.

 現在Sockets APIでは、クライアントコードがソケットに対してget/setオプションを呼び出すことができます。
 以前は、"Not Implemented"の例外が発生していました。
  java.net.Socket#get〜〜()を呼ぶとモック値が返され、set〜〜()を呼ぶと無視されます。

これらのメソッドを使っている既存ライブラリのための対応でしょうか。


- Fixed an issue with the namespace not being displayed when a user attempts to select a namespace in the Admin Console.
https://code.google.com/p/googleappengine/issues/detail?id=8164

ユーザが管理コンソールでネームスペースを選択しようとしたときにネームスペースが表示されない問題を修正しました。


- Fixed an issue in the Admin Console Logs page to correctly display 'Until' instead of 'Since' for logs search criteria.
https://code.google.com/p/googleappengine/issues/detail?id=8659

管理コンソールのLogsページで検索条件に'Since'の代わりに'Until'と表示されていた問題を修正しました。



Search APIをあまり触ってないのでその辺怪しいですが、とりあえずの訳でした。
ツッコミ・ご指摘お待ちしております!



2013年6月11日火曜日

33.Google Cloud Datastoreを試してみた 概要編 (1/3)



33.Google Cloud Datastoreを試してみた 概要編 (1/3)
37.Google Cloud Datastoreを試してみた GCE編 (2/3)
38.Google Cloud Datastoreを試してみた その他環境編 (3/3)


Ryo Yamasaki(@vierjp)です。

Google I/O 2013で発表された「Google Cloud Datastore」について調べたので、
今回から三回に分けて書いていこうと思います。


第1回は概要や仕組み、注意点についてです。

今回は以前に書いた「Blog @vierjp : 27.Google I/Oで発表されたGoogle Cloud Platformの新機能」と重なっている部分がいくらかあります。


◯概要

・App EngineのDatastoreが単独で利用可能になる
これまでAppEngine上のアプリからしか利用できまなかったDatastoreがそれ以外からも利用できるようになりました。
Compute Engineや他のクラウド環境、オンプレミス環境からも利用が可能です。

・特徴はApp EngineのDatastoreと同じ
 ・クエリの速度がデータ量に影響しない
 ・データは複数データセンターにレプリケートされる
 ・計画されたダウンタイムはなし
 ・メンテナンスフリー
 ・etc…

・環境や言語を問わず利用可能
Protocol Buffer形式かJSON形式でAPIを利用してDatastoreを操作する事が可能です。
PythonとJava、node.js向けには公式のクライアントライブラリも提供されています。

・現在「Preview Release」という扱い
まだPreviewなのでAPIが突然変わる可能性あります。
と言っても通常過去のAPIバージョンもしばらく並行して動くようにしているようなので、
おそらく安全に移行するための期間はあるのではないでしょうか。
Google APIs Explorer」を見ると、
例えばCompute EngineのAPIは現在v1beta13〜v1beta15を利用可能であることがわかります。


◯仕組み

サービス的な位置づけとしては「AppEngineのDatastoreを別のサービスとして切り出した」で良いと思いますが、
実際の仕組みは「AppEngine のDatastoreを切り出した」というよりも
既存のAppEngineのDatastoreそのものにアクセスするためのAPIを提供した
というのが正しいようです。

APIによる操作は「API実行時に指定したAppEngineアプリケーションのDatastore」に対して行われます。
(厳密には指定したCloud Projectに紐づくAppEngineアプリケーション)

そしてAPIはAppEngine上で動作するアプリとして作られているようです。
Admin Consoleの「Logs」には、以下のように「ah-builtin-datastoreservice」という暗黙的なバージョンが「Built-ins」というカテゴリで追加されています。
(一度でもCloud Datastore APIを実行すると表示されるっぽい)
そしてAPIを実行するとこのバージョンのリクエストログに記録されます。


そう考えると使用量がAppEngineの使用量に加算されそこで課金される(後述)のも納得ですし、
システム的な特性もAppEngineのDatastoreと同じということになります。

このような仕組みなので、
自分でAppEngine上にデプロイしたアプリからはこれまで通りにそのアプリケーションの Datastore を操作できます。
その上で「そのアプリケーションのDatastoreをAPIを使って外部からも操作できる」という事になります。
そのためAppEngine上の既存アプリケーションは全く修正することなく、
アプリケーション外の環境とデータを共有することができます。



◯Indexの管理

AppEngineから使っている場合はこれまでどおりに「datastore-indexes.xml」を配置してデプロイすればOKです。

加えてAppEngine"以外"から使う場合のために、
AppEngineのアプリをデプロイせずにインデックスを設定するための手段が追加されています。
と言っても「datastore-indexes.xml」を作成するのは同じですし、
コマンドからこのファイルを指定して「インデックスの更新」と「使ってないインデックスの削除ができる」というものなので、
機能的にはappcfgのインデックス関連の機能をそのまま持ってきて単体で使えるようにしたという感じです。

Index Configuration - Google Cloud Datastore — Google Developers



◯Preview期間中の制限

・Read、Write、Small Operationの回数に制限あり
1,000万/日 500/秒まで

ただしこれらは連絡すれば増やしてもらえるとのことです。



◯Memcacheの利用に関する注意

特にフレームワークでEntityの自動キャッシュ制御をしている場合には注意が必要です。
これまでは「DatastoreはAppEngine上のアプリからしか操作しない」という前提でした。
そのためアプリで使うフレームワーク(NDB等)で自動的にMemcacheにEntityデータの登録や削除を行い、
必ずその処理を通してDatastoreを操作することでDatastoreとキャッシュの内容が一致していました。

しかし、
例えば「AppEngine上のアプリから追加されたEntityをアプリ外からCloud Datastore API経由で更新する」というケースを考えた場合、
APIけ更新処理はアプリで使っているフレームワークの処理を通らないのでキャッシュが更新されません。
よってこのタイミングで Datastore のデータとキャッシュの内容がずれてしまうことになります。

同様に自前でクエリ結果をキャッシュしているような場合にも
Entityが更新されたタイミングでそのキャッシュを削除することができません。

AppEngine 上のアプリで Entity の自動キャッシュ制御をしている場合には、
外部から操作するKindだけキャッシュしないようにしたり、
それが困るならその処理だけ専用のAPIを自前で作ったり(Cloud DatastoreのAPIを使わない)とする必要があります。

APIのアプリのソースコードが公開されてAPIのアプリを置き換え可能になると良いのですが。



◯課金に関する注意

・少なくとも現時点ではAPIの使用量はAppEngineの使用量として加算されます
冒頭に書いたとおりAPIがAppEngine上で動作しているという仕組みを考えると納得ですが、
APIの使用量はAppEngineの使用量として加算され「Usage History」(「Billing History」)で確認できます。
そしてAppEngineの無料枠を超えた分が課金の対象になります。

これはPreview期間中だけの暫定仕様で、今後はAppEngineとは別の請求になる予定だそうです。
この時に Cloud Datastore の利用に無料枠があるかどうかわからないので、
もしかしたら試すには今が都合が良い時期かもしれません。

・API実行によって課金されるリソース
 ・Datastore Storage
 ・Datastore Writes
 ・Datastore Reads
 ・Small Datastore Operations
 ・Frontend Instance Hours
 ・Bandwidth Out

最初の4つはDatastoreの操作に伴うものなので当然ですが、
Frontendインスタンス利用時間と帯域使用量にも加算される点は注意です。

どちらもAPIがAppEngine上のアプリとして動作していると考えれば理解できます。
(挙動として理解はできるけどFrontendインスタンス使用量は課金対象から外してくれても良い気がする)

Bandwidth OutはAPIのレスポンスデータの通信分に適用されていると思います。
(Compute Engineから実行すればBandwidth Outはかからないかも)



◯ユースケース1 App EngineのバックエンドとしてCompute Engineを利用する



Cloud Datastore API によってApp EngineとCompute Engineの両方の環境からデータの共有が簡単になりました。

よって、
・AppEngineのアプリが保存する大量データの更新処理をCompute Engine上から行う
・DatastoreのデータをBigQueryに転送する前に加工する
・Compute Engine上で特殊なミドルウェアが必要な処理を行った上で処理結果をDatastoreに記録する

といった、App Engineで実装しづらい(できない)処理をCompute Engineで行う事なりました。
ただし前述のようにAppEngine上のアプリでキャッシュ制御をしている場合には留意してください。

Cloud Datastoreの登場によって、
AppEngineのバックエンドとしてのCompute Engineの価値が高まったと言えるでしょう。



◯ユースケース2 Compute Engineから使うNoSQLデータベースとして利用する


・App Engineの対応言語に縛られずにCompute Engine上のWebアプリからDatastoreを利用できる
・Compute Engineが使う「メンテナンスフリー・ダウンタイム無し」のデータ保存領域として利用できる

例えばCompute Engine上にnode.jsのサーバーを立ててデータ保存領域としてCloud Datastoreを使うことができます。

また、これまでCompute Engineでデータベースを利用したい場合には
Compute Engine上に自前でRDBやNoSQLデータベースを構築するかCloud SQLを使うという選択肢がありましたが、
今回新しくCloud Datastoreという選択肢が増えたと言えます。


また、Compute EngineからはCloud DatastoreのAPIを利用するための認証が簡単です。(詳しくは第2回に)



◯ユースケース 3 Googleのクラウド環境以外からDatastoreを利用する


JSON APIがあるので基本的に環境・言語を問わずに利用できます。
別のクラウド環境からも、自社にあるオンプレミスのサーバーからアクセスすることも可能です。

アプリのアップグレードに伴うデータの一括更新処理をローカル環境から実行するようなこともできます。
ただこのケースの場合にはCompute Engineから操作するのと比べて通信速度が遅いでしょうから、
Webアプリでユーザーに対してリアルタイムにレスポンスを返す目的で使うなら、
App EngineやCompute Engineから利用した方が良いかと思います。
どちらかというと、このケースはバックグラウンドでの処理や既存システムとの連携において有用でしょう。

(App Engine、Compute Engine以外からの利用は第3回に)



◯参考リンク

Ushering in the Next Generation of Cloud Computing (I/O Session)

What's New and Cool with Google Compute Engine (I/O Session)

Google Cloud Datastore 公式ドキュメント

Report of Google I/O 2013 Google Cloud Platform



◯次回以降の予定

第2回はCompute Engineから、第3回はその他の環境から、
サンプルコードを使ってCloud Datastore APIを実行してみようと思います。