2013年7月23日火曜日

40.Google Cloud Storageの新機能 (2013/07/23)


Ryo Yamasaki(@vierjp)です。

Cloud Platform Blog: New in Google Cloud Storage: auto-delete, regional buckets and faster uploads
によると、Google Cloud Storageに新機能が追加されたとのことなので調べてみました。
せっかくなのでメモを公開。



◯Object Lifecycle Management

Cloud Storage上のデータを一定期間経過後に自動で削除できます。
AppEngineのDatastoreのバックアップデータなんかを一定期間経過後に自動で削除するように設定しておくと便利そう。

bucketに対してこの設定をするので、そのbucket内に配置したファイル全てにこの削除ルールが適用される。
よって、バックアップデータの保持期間のポリシー毎にbucketを分けるのが良さげ。

削除ルールには二種類のパターンがあって、
・期間を指定(日単位)
・バージョニング機能を使っている場合に保持する世代数を指定(保持しておく最新のバージョン数を指定)


・lifecycle_config.xml の記述例 (365日後に削除)
<?xml version="1.0" ?>
<LifecycleConfiguration>
    <Rule>
        <Action>
            <Delete/>
        </Action>
        <Condition>
            <Age>365</Age>
        </Condition>
    </Rule>
</LifecycleConfiguration>

・以下のコマンドでルールを適用する
gsutil lifecycle set lifecycle_config.xml gs://bucket_name


・既存のルールを取得したい場合には以下のコマンドを実行する
gsutil lifecycle get gs://bucket_name > lifecycle_config.xml

設定を無効にしたい場合には以下のような設定が空のxmlファイルを作成してそれをセットする。
<?xml version="1.0" ?>
<LifecycleConfiguration/>



◯Regional Buckets

Cloud Storageのbucketは US, EUというレベルでも指定できるが、
この機能ではさらに細かいリージョンのレベルで指定することが可能。

「bucketのリージョン」を「使用しているCompute Engineのリージョン」と同じにすれば転送速度が速くて良い、とのこと。


・ロケーションの種類
Google Cloud Platformでの「ロケーション」には、とりあえず以下の3つの概念があるようだ。

・国家・大陸 (national/continental)・・・US, EU (現状この2つ)
・リージョン ・・・ US-CENTRAL1 等
・ゾーン・・・・・・us-central1-a 等
(下に行くほど狭い概念になる)


App Engineは「国家・大陸」のレベルで指定できる。(ただしEUはプレミアプランのみ)
Cloud Storage「国家・大陸」のレベルか「リージョン」レベルを指定できる。
Compute Engineではインスタンスや永続ディスク(PD)を作成する際に「ゾーン」を指定する。
(「ゾーン」は「リージョン」の下位に紐づくので、「ゾーン」を指定すると自ずと「リージョン」も決まる)


・「Regional Buckets」の作成方法
bucket作成時に以下のように 「-l」(location)オプションでリージョンを指定する。
gsutil mb -c DRA -l US-CENTRAL1 gs://myregionalbucket

・現在Cloud Storageで指定できるリージョン
US-EAST1
US-EAST2
US-EAST3
US-CENTRAL1
US-CENTRAL2
US-WEST1

・現時点で存在するCompute Engineのリージョン
US-CENTRAL1
US-CENTRAL2
EUROPE-WEST1


当然リージョンは一致しているだろうと思ったら違った。
Cloud Storageで指定できる「US-EAST」と「US-WEST」はCompute Engineには無いし、
Compute Engineで指定できるEUROPE-WESTはCloud Storageで指定できないらしい。

ということは現時点で指定するなら「US-CENTRAL1」か「US-CENTRAL2」ということになるだろうか。


・Durable Reduced Availability (DRA) でのみ利用可能?
ブログには「Durable Reduced Availability (低可用性・低価格のストレージ) で使える」と書いてある。
Cloud Storageのドキュメントにはその点についての記述が見つからないし、
ドキュメント内のコマンドの例でもDRAのオプション(「-c DRA」)を指定していない。

しかし実際にDRAオプションを指定せずにコマンドを実行した場合、エラー(500エラー)となって失敗した。
DRAオプションを指定したら成功した。

システムエラーなので絶対これが原因とは断言しづらいけど、
やはりブログに書いてある通りDRAの場合のみ指定できるように思える。



◯gsutil - Automatic Parallel Composite Uploads

Google I/O のセッションにもあった「大きいファイルの分割Uploadするテクニック」を自動でやってくれるらしい。
大きいファイルを扱う場合に楽になりそう。


.botoファイルで以下の設定をすることが可能。

・parallel_composite_upload_threshold
閾値の指定 ここで指定した値を超えるサイズのファイルが自動で分割Uploadされる

・parallel_composite_upload_component_size
分割ファイルのサイズ

この自動分割アップロード機能を完全に無効にしたい場合には、「parallel_composite_upload_threshold」の値を「0」にする。
(自分で分割したファイルをアップロード・サーバー上で結合したい場合にこの機能が邪魔になることがあるらしい)

・分割アップロードされた一時ファイルはサーバー上で結合された後に削除される。
・結合前に転送に失敗した場合は、リジュームを活用して再度アップロードされる。
・この場合も結合まで成功したら一時ファイルは削除される。
・アップロードが正常に完了するまで一時ファイルは残る。



◯Durable Reduced Availability Storage (DRA Storage)

これは以前からある機能だけど、関連するので補足。

Durable Reduced Availability Storage は可用性が低いけど料金が安いストレージ。
Pricing and Support - Google Cloud Storage — Google Developers

ドキュメントによれば、データのバックアップに向いているという話。
> (データのバックアップには) 高耐久性(durability)が重要ですが、最高の可用性(availability)は必須ではありません。

バックアップ時に落ちてて失敗してたら結構嫌な気もするけど、、高耐久性の方が重要そう。
「高可用性のbucket」だとしても失敗は把握できるようにしておくべきだから、
トレードオフとしてアリな気がする。


・DRAバケットの作成方法
gsutil mb -c DRA gs://<bucketname>/



・既存のバケットからデータを(事実上の)コピーするコマンド
gsutil cp -D -R gs://<standard_bucket>/* gs://<durable_reduced_availability_bucket>


現時点では「既存の標準バケット」を「DRAバケット」に変更したり、
「標準バケット」から「DRAバケット」に直接オブジェクト(ファイル)のコピーはサポートしていない。
新規にbucketを作成して、一度ダウンロードしてから再度アップロードする必要がある。

そのため「-D」で「daisy chain」オプションを指定する。
直接コピーはできないが、このコマンドを実行すると
一度ローカルのPCにデータをダウンロードして、それから新しいバケットにアップロードする形になるので
コマンド一発で「ダウンロード→アップロード」できる。

ただしこの場合に注意点が2つ。
・「ダウンロード + アップロード」をしているのでそれら両方に対して料金がかかる。
・このコマンドを実行する前にコピー先のbucketにデフォルトのACLを設定しておくべし。
 (サーバー上でのコピーではなく新規Uploadなので、コピー前のACL(アクセスコントロール)情報は失われる)



◯参考リンク

Cloud Platform Blog: New in Google Cloud Storage: auto-delete, regional buckets and faster uploads
Object Lifecycle Management - Google Cloud Storage — Google Developers
Regional Buckets - Google Cloud Storage — Google Developers
mb - Make buckets - Google Cloud Storage — Google Developers
Durable Reduced Availability Storage - Google Cloud Storage — Google Developers



そのうちバージョニングも勉強しなきゃなぁ、と思いつつ今回はここまで。ノシ

2013年7月3日水曜日

39.Google App Engine for PHPでCakePHPを動かしてみた




Ryo Yamasaki(@vierjp)です。

Google I/O 後に Limited Preview のアカウントをもらってから、
これまでGoogle App Engine for PHPに関して以下のブログを書いてきました。

28.Google App Engine for PHP (GAE/PHP) を早速試してみた
29.Google App Engine for PHPでWordPressを動かしてみた
30.Google App Engine for PHPにおけるポータビリティを考える
36.Google App Engine for PHPでWordPressを運用するためのプラグインが登場

私がPHP素人ということもありこれまでは主に「既存のPHPアプリをApp Engineで動かす」内容でしたが、
今回は「新規にアプリを作成する」ケースを想定して
GAE/PHP上でCakePHPというフレームワークを動かしてみようと思います



◯GAE/PHPで動かすフレームワークの条件

公式のIssue Trackerでのフレームワークについてのやりとりの中で、
Googleのエンジニアの以下の書き込みがありました。(本当はもっと細かいけど概要はこんな感じ)

1.GAEのファイルシステムはReadOnlyなので、一時ファイルはGoogle Cloud Storageに書き込む必要がある。
2.ロギングはsyslog関数を使っていること。(ファイルシステム内の指定ディレクトリへの出力はできない)

一つ目は#DevFestでも話にあった「ローカルに一時ファイルを書き出す既存のPHPフレームワークを動かすのは厳しいのではないか」と同じ内容ですね。

期待は薄かったのですが、今時フレームワークが使えない開発も厳しいと思うのでダメ元で試してみました。
PHPのフレームワークについて軽くググったところ、日本ではCakePHPが人気らしいのでCakePHPにしました。

結論から書くと、
「適切に設定をすることでApp Engine上でCakePHPを動作させることができた」
と言って良いのではないかと思います。 (全機能を試したわけではありませんが)



◯ローカル環境の構築

実はこれまでGAE/PHPに関してローカル環境で動かしたことがなかったのですが、
さすがに今回はエディタと本番環境でのテストだけではきつそうだったのでローカルに開発環境を構築しました。
以下はローカル開発環境のざっくりとした構築手順です。

・MySQLのインストール
Mac OS へのMySQLのインストール方法
こちらの手順に沿って行いました。

・MacPortsのインストール
MacPorts公式

冒頭の「“pkg” installers for Mountain Lion」のリンクからダウンロードできます。
Xcodeが入っている環境でインストール時にエラーが発生する場合はこちら

・PHPのインストール
App Engine公式ドキュメントより
sudo /opt/local/bin/port install php54-cgi php54-APC php54-calendar \
    php54-exif php54-gd php54-mysql php54-oauth php54-openssl php54-soap \
    php54-xdebug php54-xsl

・PHPStormのインストール (任意)
PHP IDE :: JetBrains PhpStorm (ダウンロードページ)
Getting Started with PhpStorm as Google App Engine PHP IDE (使い方)
最近人気のPHP用IDEだそうです。
最初から GAE/PHP に対応するプラグインが組み込まれていて、IDE上で GAE/PHP のデバッグ・デプロイが可能です。
有料ですが、最初の一ヶ月は無料で利用できます。



◯初期設定

・アプリのルートディレクトリを作成する
今回は「CakePHPTest3」という名前にしました。

・CakePHPのサイトから最新の安定版(現時点で2.3.6)のzipをダウンロードして展開する



・「CakePHPTest3」以下に全てコピーする

・app.yamlの設定

/app.yaml
application: [アプリケーションID]
version: [任意のバージョン名]
runtime: php
api_version: 1
threadsafe: true

handlers:
- url: /css
  static_dir: app/webroot/css

- url: /js
  static_dir: app/webroot/js

- url: /img
  static_dir: app/webroot/img

- url: /favicon.ico
  static_files: app/webroot/favicon.ico
  upload: app/webroot/favicon.ico

- url: /.*
  script: app/webroot/index.php

* 通常CakePHPでは apache の mod_rewrite でルーティングの設定を行うようですが、GAE/PHPではapp.yamlで行います。


・デフォルトタイムゾーンの設定
ルートディレクトリ直下に「php.ini」を作成して以下の1行を記述します。

/php.ini
date.timezone = Asia/Tokyo


・Security.salt と Security.cipherSeed の変更

/app/Config/core.php
/**
 * A random string used in security hashing methods.
 */
 Configure::write('Security.salt', 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi');

/**
 * A random numeric string (digits only) used to encrypt/decrypt strings.
 */
 Configure::write('Security.cipherSeed', '76859309657453542496749683645');

上記はデフォルト値です。
この2つのセキュリティ的な値を適当に変更します。
「Security.salt」は文字列、「Security.cipherSeed」は数値です。


・App EngineのProduction環境とローカル環境で分岐するための処理を作成

データベースの接続先の切り替え等で使うのでまずはこれを作ります。

/app/Utility/AppEngine.php
<?php

class AppEngine {
    public static function isProduction() {
        if(isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'],'Google App Engine') !== false) {
            //syslog(LOG_WARNING, 'is Production.');
            return true;
        }else{
            //syslog(LOG_WARNING, 'is DevServer.');
            return false;
        }
    }
}

* 「$_SERVER['SERVER_SOFTWARE']」の値はProduction環境では「Google App Engine/1.8.1」、ローカル環境では「Development/2.0」になります。


・作成したUtilityを利用できるようにcore.phpの冒頭に下記を記述
/app/Config/core.php
App::uses('AppEngine', 'Utility');



・データベースの設定
最初から存在する「database.php.default」をコピーして「database.php」という名前で保存し、
下記の内容に書き換えます。

/app/Config/database.php
<?php
class DATABASE_CONFIG {

 public $default = NULL;

    public $prod = array(
        'datasource' => 'Database/Mysql',
        'persistent' => false,
        'unix_socket' => '/cloudsql/******:*****',
        'login' => 'cake_user',
        'password' => '********',
        'database' => 'cake_db',
        'encoding' => 'utf8',
    );

    public $dev = array(
        'datasource' => 'Database/Mysql',
        'persistent' => false,
        'host' => 'localhost',
        'login' => 'cake_user',
        'password' => '********',
        'database' => 'cake_db',
        'encoding' => 'utf8',
    );

 public $test = array(
  'datasource' => 'Database/Mysql',
  'persistent' => false,
  'host' => 'localhost',
  'login' => 'user',
  'password' => 'password',
  'database' => 'test_database_name',
  'encoding' => 'utf8',
 );

    function __construct(){
        if(AppEngine::isProduction()) {
            $this->default = $this->prod;
        }else{
            $this->default = $this->dev;
        }
    }
}


・ローカルのMySQLにDBを作成する

CREATE DATABASE IF NOT EXISTS cake_db;
CREATE USER 'cake_user'@'localhost' IDENTIFIED BY '********';
GRANT ALL PRIVILEGES ON cake_db.* TO 'cake_user'@'localhost';


・dev_serverを起動してブラウザでアクセスします

PHPStormから起動する場合、
初回起動時の設定画面でPHPのPathを入力する必要があります。
上記手順でMacPortsからインストールした場合は、
「Path to php-cgi executable」に「/opt/local/bin/php-cgi54」と設定すればOKです。


デフォルトのトップページはCakePHPの設定を確認するためのページが開きます。
(ここまでの手順もこのページにアクセスしながら設定を変えていくと、変える度に警告が消えていきます)

dev_serverは制限が緩く「ファイルシステムへの書き込み」ができてしまうので、下記画像のように問題なくページが開きます。

*制限が緩いのでローカル環境では「Your tmp directory is writable.」と表示されます。


しかし、App Engine上では「ファイルシステムへの書き込み」ができないためエラーになってしまいます。

デフォルトのCakePHPは下記画像のように、「cache」や「Log」を「tmp」ディレクトリ以下に出力します。
(「sessions」「tests」はデフォルト設定ではファイルが書き込まれないので問題なし)



*App EngineでPHPの「$_SESSION」を使った場合にはMemcacheに書き込むようになっています。

初期設定のままのCakePHPの場合、予想どおり一時ディレクトリへのキャッシュの書き込みでエラーになります。
また、ログもファイルに出力する設定なので、App Engineのログには出力されません。

GAE/PHPでは「ファイルI/OはCloud Storageに対して行うべし」というのが基本のようなので、
当初一時ディレクトリをCloud Storageにするため、index.phpに下記を追加しました。
define('TMP', "gs://[bucket名]/");

しかし、これは期待通りに動作しませんでした。
GAE/PHP上で「gs://〜〜」のPathを指定してファイルI/O関連の操作をすると透過的にCloud Storageの読み書きをするWrapperに処理が移譲されるのですが、
ディレクトリの存在チェック処理で「ディレクトリが存在しない」という扱いでエラーになってしまいます。
(事前にCloud Storage上にFolderを作成しておいてもダメ)


* CakePHPに限らず、既存のアプリのファイル出力先をCloud Storageに変更しただけではこういったケースは他にもあるかもしれません。

Issue Trackerでのやりとりに書かれていた通り一時ファイルとログ出力が問題になりました。
また、単純に書き込み先のPathをCloud Storageにすれば良い、というわけにもいかないようです。
次項からこれらについて対処していきます。



◯cacheの設定を変更する

前述の通りデフォルト設定のCakePHPは一時データを「tmp」ディレクトリ以下にファイルとして保存しますが、
一時データの保存領域としてMemcacheやAPC(後述)を利用するためのコードも用意されています。
また、パフォーマンス的にもMemcacheやAPCの方がファイルキャッシュより高速とされているそうです。

・bootstrap.phpの冒頭でキャッシュ設定を変更する

/app/Config/bootstrap.php
// Setup a 'default' cache configuration for use in the application.
//Cache::config('default', array('engine' => 'File'));
if(AppEngine::isProduction()) {
    Cache::config('default', array('engine' => 'Apc'));
}else{
    Cache::config('default', array('engine' => 'File'));
}

・core.phpの最後の方でキャッシュ設定を変更する
/app/Config/core.php
//$engine = 'File';
// in Production Apc, in Devserver File
if(AppEngine::isProduction()) {
    $engine = 'Apc';
}else{
    $engine = 'File';
}


ここではAPCを使うように変更しました。
* ローカル環境でもAPCを使えますが、開発時にはtmpディレクトリ内にファイルそのものが見えた方がわかりやすいかも、ということで分岐してみました。

再度デフォルトのトップページを開くと以下のようになります。

上から3つ目の項目が「The ApcEngine is being used for core caching.」になっているのがポイント。
前述のローカル環境で実行した結果のキャプチャと比べるとわかりますが、
変更前は「The FileEngine is being used for core caching.」となっていました。

*「Your tmp directory is NOT writable.」と警告が出ていますが、tmpディレクトリを使用しないので問題ありません。


・APC (Alternative PHP Cache)について
基本的には「PHPの中間コードのキャッシュや最適化を行う拡張モジュール」ですが、
MemcacheのようなKeyValue型のメモリキャッシュとしても使えるそうです。

APCはMemcacheと比べると以下のような特性があります。
 ・サーバーのローカルメモリを利用
 ・Memcacheより読み書きが高速
 ・キャッシュはサーバー間で共有されない
 ・サーバーを再起動するとキャッシュは消える

App Engineはアプリのバージョン名毎にインスタンスが別なので、
デプロイ後に古いコードのキャッシュが残ってしまうことはありません。
(同じバージョン名に対して上書きする形でデプロイした場合には一度インスタンスが全て停止するのでやはりキャッシュはクリアされます)


・APCではなくMemcacheを使う場合についても考えてみる
 ・App Engineでは頻繁にサーバーの起動・停止をするのが半ば前提なので、Memcacheの方がキャッシュを共有できる点では効率は良さそう。
 ・この場合デプロイ時にキャッシュをクリアしないと最新のコードや設定と内容がずれるだろうからそのタイミングでMemcacheのクリアが必要と思われる。
 ・複数バージョンを同時に操作した場合に競合しそう。(キャッシュのKeyのPrefixにバージョン名を追加する等で対応はできそう)
 ・データ量の多いサイトでDBのデータをMemcacheにキャッシュしている場合にはMemcacheが枯渇してCakePHPのキャッシュが活きづらい状況はあるかも。


今回は無難そうなAPCを使いましたが、
APCとMemcacheのどちらが良いかは実際に様々な条件で比較してみなければなんとも言えません。
システムの性質によって最適なキャッシュ方法が変わる可能性もありそうです。



◯ログの設定を変更する

Issue Trackerでのやりとりにある通り、ログの出力をsyslog関数を使って行うことでApp  Engineのログに記録する事ができます。

CakePHPではログの出力先(出力方法)も設定で変更できます。

・Syslogに出力するためのコードを取得する
最新の安定版(2.3.6)にはログをSyslogに出力するコードは含まれていませんが、
開発中のバージョンには「lib/Cake/Log/Engine/SyslogLog.php」という、Syslogに出力するためのコードが含まれています。

このファイルをGithubにあるCakePHP 2.4のbranchから取得して「app/Log/Engine/SyslogLog.php」にコピーします。(「app/Log/Engine/」ディレクトリも作成)

・bootstrap.phpを修正する

/app/Config/bootstrap.php
/**
 * Configures default file logging options
 */
//App::uses('CakeLog', 'Log');
//CakeLog::config('debug', array(
// 'engine' => 'FileLog',
// 'types' => array('notice', 'info', 'debug'),
// 'file' => 'debug',
//));
//CakeLog::config('error', array(
// 'engine' => 'FileLog',
// 'types' => array('warning', 'error', 'critical', 'alert', 'emergency'),
// 'file' => 'error',
//));
if(AppEngine::isProduction()) {
    $engine = 'SyslogLog';
}else{
    $engine = 'FileLog';
}

App::uses('CakeLog', 'Log');
CakeLog::config('debug', array(
 'engine' => $engine,
 'types' => array('notice', 'info', 'debug'),
 'file' => 'debug',
));
CakeLog::config('error', array(
 'engine' => $engine,
 'types' => array('warning', 'error', 'critical', 'alert', 'emergency'),
 'file' => 'error',
));

・php.iniの修正
この状態では以下の警告が出力されるので、php.iniに追記します
PHP Warning:  php_sapi_name() has been disabled for security reasons. It can be re-enabled by adding it to the google_app_engine.enable_functions ini variable in your applications php.ini

/php.ini
google_app_engine.enable_functions = "php_sapi_name, gc_enabled"
date.timezone = Asia/Tokyo

* 一行目を追加しています。

これでApp Engineのログにシステムログが出力されるようになります。



◯DebugKit pluginの設定をする

後回しにした「DebugKit plugin」も少し修正することで利用可能です。

・DebugiKitをGitリポジトリから取得する

・/app/Plugin/DebugKit ディレクトリを作成して、取得したファイルをコピー

・bootstrap.php に以下の記述を追加
/app/Config/bootstrap.php
CakePlugin::load('DebugKit');


・AppControlleを修正する
/app/Controller/AppController.php
class AppController extends Controller {
    public $components = array('DebugKit.Toolbar'); // ←この行を追加する
}

・ToolbarComponentの修正

このコードの391行目でCacheにFile Engineを使ってしまっているのでこちらもAPCに変えます。
/app/Plugin/DebugKit/Controller/Component/ToolbarComponent.php
 protected function _createCacheConfig() {
        if (Configure::read('Cache.disable') !== true) {
            $cache = array(
                'duration' => $this->cacheDuration,
                'engine' => 'Apc',
                'path' => CACHE
            );
            if (isset($this->settings['cache'])) {
                $cache = array_merge($cache, $this->settings['cache']);
            }
            Cache::config('debug_kit', $cache);
        }
    }


・app.yaml に以下を追記 (最後の「- url: /.*」より先に定義する)

/app.yaml
- url: /debug_kit/css
  static_dir: app/Plugin/DebugKit/webroot/css

- url: /debug_kit/js
  static_dir: app/Plugin/DebugKit/webroot/js

- url: /debug_kit/img
  static_dir: app/Plugin/DebugKit/webroot/img





◯環境変数の問題を解決する

一部の環境変数の値を期待どおりに取得できずに画面遷移において発生する問題を解決するため、以下の記述を追加する。

Issue Trackerにも似たような内容が挙がっています。
関連: Issue 9369: PHP $_SERVER['SCRIPT_NAME'] variable does not match the expected implementation

・index.php の最後の方に2行追加する
/app/webroot/index.php
App::uses('Dispatcher', 'Routing');

// ここから
$_SERVER['SCRIPT_FILENAME'] = __FILE__;
$_SERVER['PHP_SELF'] = 'webroot/index.php';
// ここまで

$Dispatcher = new Dispatcher();
$Dispatcher->dispatch(
 new CakeRequest(),
 new CakeResponse()
);

* そのうちIssueが修正されてこの部分の記述は不要になるかもしれません。




◯Sessionの保存場所を変える

以前に「30.Google App Engine for PHPにおけるポータビリティを考える」に書いたとおり、
現状GAE/PHPでは「$_SESSION」を使った場合のSessionデータの格納先は「Memcache」となっています。
そのためドキュメントには
・Memcacheのデータはクリアされるかもしれないのでそれによってセッション情報が失われる可能性がある。  
・長い期間Sessionを維持したければ、Cloud SQL等にセッション情報を保存した方が良い。
と書かれています。
この手順をしなくても一応CakePHPは動作しますが、ついでにこの対策もしてみます。


◯セッションをCloudSQLに保存する場合

CakePHPには「Sessionをデータベースに書き込むためのプログラム」が付属しているので、
DBにテーブルを用意した上でこの仕組みを使うようにCakePHPの設定を変更するだけで、
Sessionデータの格納場所を変更することができます。

・DBにSession格納用テーブルを作成する
USE cake_db;
CREATE TABLE IF NOT EXISTS cake_sessions (
  id varchar(255) NOT NULL,
  data text NOT NULL,
  expires int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

・core.phpを修正する
/app/Config/core.php
// Configure::write('Session', array(
//  'defaults' => 'php'
// ));

Configure::write('Session', array(
    'defaults' => 'database'
));


AppController.php を修正して'Session'を追加する
/app/Controller/AppController.php
public $components = array('Session','DebugKit.Toolbar');


◯MemcacheとCloudSQLを併用する場合

・前述の「Cloud SQLに保存する場合」と同様にテーブルを作成する

・「キャッシュとDBを併用するためのクラス」を作成する

/app/Model/Datasource/Session/ComboSession.php
<?php
App::uses('DatabaseSession', 'Model/Datasource/Session');

class ComboSession extends DatabaseSession implements CakeSessionHandlerInterface {
    public $cacheKey;

    public function __construct() {
        CakeLog::write('debug',"ComboSession#__construct");
        $this->cacheKey = Configure::read('Session.handler.cache');
        parent::__construct();
    }

    // セッションからデータ読み込み
    public function read($id) {
        CakeLog::write('debug',"ComboSession#read id:".$id);
        $result = Cache::read($id, $this->cacheKey);
        if ($result) {
            CakeLog::write('debug',"ComboSession#read result from cache result:".$result);
            return $result;
        }
        CakeLog::write('debug',"ComboSession#read result from db result:".$result);
        return parent::read($id);
    }

    // セッションへデータ書き込み
    public function write($id, $data) {
        CakeLog::write('debug',"ComboSession#write id:".$id." data:".$data);
        $result = Cache::write($id, $data, $this->cacheKey);
        CakeLog::write('debug',"ComboSession#write cache id:".$id);
        if ($result) {
            CakeLog::write('debug',"ComboSession#write db id:".$id);
            return parent::write($id, $data);
        }
        CakeLog::write('error',"ComboSession#write failed to write session:".$id);
        return false;
    }

    // セッションの破棄
    public function destroy($id) {
        CakeLog::write('debug',"ComboSession#destroy id:".$id);
        $result = Cache::delete($id, $this->cacheKey);
        if ($result) {
            return parent::destroy($id);
        }
        return false;
    }

    // 期限切れセッションの削除
    public function gc($expires = null) {
        return Cache::gc($this->cacheKey) && parent::gc($expires);
    }
}

・core.phpを修正する
/app/Config/core.php
// Configure::write('Session', array(
//  'defaults' => 'php'
// ));
    Configure::write('Session', array(
        'defaults' => 'database',
        'handler' => array(
            'engine' => 'ComboSession',
            'model' => 'Session',
            'cache' => 'sessionCache'
        )
    ));

    // セッション用Memcache設定の定義
    Cache::config('sessionCache',array(
        "engine" => "Memcache",
        "duration"=> 3600,
        "probability"=> 100,
        "compress" => false,
    ));

・前述の「Cloud SQLに保存する場合」と同様にAppController を修正して'Session'を追加する

コードは公式のサンプルに検証用のログを追加しただけです。
参照時にMemcacheにキャッシュがあればそれを利用し無ければDBを参照する、という処理になっているので、
パフォーマンスの向上と課金額の低下を期待できます。

公式のサンプルではAPCとDBの組み合わせにしていますが、
複数のサーバーが起動するのが(ほぼ)前提のApp EngineではWebサーバーのローカルメモリに書き込むAPCでセッション情報をキャッシュするのはまずいでしょうから、
MemcacheとDBの組み合わせにしました。
(スティッキーセッションではないのでどこのサーバーにリクエストが振り分けられるかわからない、よって全てのWebサーバーが共有する領域にキャッシュする必要がある)

キャッシュポリシーは名前をつけて複数設定できるので、セッションハンドラ専用のMemcacheの設定を追加してそれを利用しています。



◯ドットインストールのPHP入門に沿ってアプリを作ってみる

「一覧・登録・編集・削除」機能を持つシンプルなCRUD処理を行うWebアプリです。


ドットインストールの動画に沿って21回目の機能まで作って動作しました。
(その後のAjaxやコメント機能の追加はフレームワークの動作確認としては必要ないかな、という判断でここまで。ていうか力尽きました)

* このサンプルでは登録・更新・削除後に表示するメッセージの管理にSessionを使っています
 「setFlash」という関数を使った際にCakePHPが内部的に
 1.登録・更新・削除処理時にSessionにメッセージをセット
 2.一覧ページへリダイレクト
 3.Sessionからメッセージを取得して表示、Sessionから削除
 という処理をしているようです。


ドットインストールのPHP入門に沿って作ったサンプルアプリを以下に配置しました。
自由に読み書きしてもらって構いません。(投稿内容は常識の範囲でお願いします^-^;)
* 2013/11/10 追記 Cloud SQLの課金が地味に痛いので停止しました。

今回のサンプルアプリのコードはまるごとGithubに公開してあります。
https://github.com/vierjp/CakePHPTest3

* core.phpの「Security.salt と Security.cipherSeed」、database.phpの「接続先とパスワード」は伏せているので動作させる場合には書き換えてください。



◯まとめ

ドットインストールの動画の解説はおそらくCakePHPでのオーソドックスなCRUD処理のコーディング方法に沿っていると思いますので、
このサンプルの動作確認をもって「CakePHPが動いた」と言ってもいいんじゃないでしょうか。
もちろん全機能試したわけではないので「完全に動く」と断言まではできないのですが。

当初はフレームワークの選択肢が少ないか最悪利用が難しいのではと考えていただけに、
有名なフレームワークが動作したことでGAE/PHP上で動作させるアプリを新規に開発するにあたっての懸念が一つ減ったように感じています。

ついでに#Devfestの時には「mbstring が使えない」件も懸念されていましたが、
Google App Engine Ver.1.8.1のリリース時にmcrypt、iconv と一緒にこれも追加されています。

GAE/PHPでは拡張モジュールを自分で追加することはできませんが、
mbstringmcrypticonv は Issue Tracker に挙げられた要望が取り入れられた結果としてGAE/PHPに標準で導入されました。
「多くの人にとって価値があるような汎用的なモジュール」であれば、
要望を挙げることによって追加されるかもしれません。


今回試してみた感想として、
・JavaでDIをするように簡単にI/Oの対象を切り替える仕組みを最初から持っているCakePHPは良い
・ちょっとググれば日本語の情報が大量に見つかるという点でPHPという言語は良い(情報の得やすさはJava以上かも?)
・ローカル環境は制限が緩いのでフレームワークの基本動作や既存アプリは実際にデプロイして動作確認した方が良い
と思いました。


余談ですが、公式の Issue Trackerでのフレームワークや拡張モジュール関連の話題としては、
Phalcon Framework というCの拡張モジュールで書かれたPHP用の超高速フレームワークを入れてくれ」
というスレッドが盛り上がっているようです。
(現時点でスターの数はダントツ。盛り上がりすぎたのかコメント禁止ステータスになっていますがw)



◯参考リンク

Installing the PHP SDK on Mac OS X - Google App Engine — Google Developers
CakePHPインストール - CakePHPの使い方
Ayman R. Bedair: Google App Engine
Re: Anyone tried installing CakePHP on Google App Engine (since PHP now support)
CakePHP DebugKit の導入手順
APCとmemcachedの比較 : アシアルブログ
「PHP」の「PECL::APC」をキーバリュー型のメモリキャッシュとして使う。 – FlatLabs
セッション — CakePHP Cookbook v2.x documentation
[ステップアップ! CakePHP] キャッシュに memcached を使う | バシャログ。
CakePHP入門 (全32回) - プログラミングならドットインストール
Issue 9336:Install the Phalcon Framework PHP extension


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