・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 の取得はできている。
→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」という謎のバージョンが作成されてそこにアクセスが来る
さすがにバグっぽい気がしますが、こちらの原因と解決策はわかりません。
- 他のエラーのパターン
こちらは設定やコードにミスがある場合のパターンです。参考に。
- 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