2013年5月24日金曜日

30.Google App Engine for PHPにおけるポータビリティを考える

このエントリーをはてなブックマークに追加


Ryo Yamasaki(@vierjp)です。

Google App Engine for PHPでオンプレミス向けのPHP既存アプリはどこまで動くか、
動かす際には何に注意すべきか、「前回行ったWordPressの動作確認」と「公式ドキュメント」を読んだまとめです。


GAE/PHPに関しては
・DatastoreではなくCloudSQLを推していること
・MemcacheのStubの関数の存在
・ドキュメントにWordPressの設置方法について書かれていること
などを見るに、
「既存アプリをそのままAppEngineで動かす」というケースをこれまでよりも強く意識しているように感じました。

これまでAppEngineはこのケースにおいて他のクラウドに遅れを取っていたように思いますが、
(PaaSではWindows Azureが早かった印象)
Cloud SQLやCloud Storage等の関連サービスの充実に伴い現実的になってきたと感じています。

その上でPHPの既存アプリをAppEngineに載せる際に問題になりそうな事を考えながらドキュメントを読んでみました。
と言ってもPHP素人なので考慮しきれていない事は多々あると思いますが。(わかりやすい予防線)



◯File入出力(Google Cloud Storage との連携)


・プログラム内で読み書きしたい場合

AppEngineはその制限上、ローカルのファイルシステムへのアクセスができません。
よって、ファイルの入出力は全てCloud Storageに対して行う必要があります。

・書き込み

$options = [ "gs" => [ "Content-Type" => "text/plain" ]];
$ctx = stream_context_create($options);
file_put_contents("gs://my_bucket/hello.txt", "Hello", 0, $ctx);

・書き込み(Stream)

$fp = fopen("gs://my_bucket/some_file.txt", "w");
fwrite($fp, "Hello");
fclose($fp);

関数的にはPHP標準のものだと思いますが、
Pathの指定方法がGoogle Cloud Storage特有なので
アプリ内でファイルの入出力をしている場合には修正が必要になるでしょう。
(デフォルトのディレクトリとして「gs://[my_bucket]」と指定できたら楽そうですが)


・ブラウザからアップロードしたファイルを保存したい場合

これは前回「29.Google App Engine for PHPでWordPressを動かしてみた」で困った内容です。
※ 2013/6/16追記 WordPressに関してはWordPress用のGoogleの公式プラグインが提供されました。
36.Google App Engine for PHP上でWordPressを運用するためのプラグインが登場


この場合はもう少し大変です。
Direct file uploads to your POST handler, without using the App Engine upload agent, are not supported and will fail.
と書いてある通り、
multipartでアップロードされたファイルの情報を「$_FILES」変数から直接参照することができません。
前回書いたようにダウンロードしてきたWordPressそのままのコードでは画像のUploadができませんでした。

この場合の対応方法は「Uploading Directly to Google Cloud Storage」に書いてあり、
以下のフローにする必要があります。

1.「createUploadUrl」関数でアップロード先のURLを生成してformのactionとして指定する。
2.「createUploadUrlで生成したURL」からPOSTされる「Handler」を作成する。
 「Handler」では一時ファイルの情報を「$_FILES」から取得することができるので、
  ここで「move_uploaded_file」関数を使ってCloud Storageに書き込む。(移動する)

アップロード時の処理のフローは以下のようになります。


元の処理内容によっては簡単に修正できるかもしれません。
CloudStorageTools::createUploadUrl('[元のアップロード先URL]', $options);
formのactionに指定するURLを上記のように生成すれば
「元のアップロード先URL(=Handler)」で一時ファイルの情報を「$_FILES」から取得でき、
その後「move_uploaded_file」で指定するPathをCloud Storageの形式にしてやれば「辻褄が合いそう」な気がします。


ただ、WordPressで修正方針を検討したところでは若干面倒そうでした。
WordPressは「元のアップロード先URL」でCookieから取得したユーザー情報に基づく権限チェック等を行なっています。
しかし、上図③の通り「uploadUrl」から「Handler」には「サーバー間でPOST」しているので、「Handler」ではブラウザが保持するCookieを取得できません。

そうであれば、ユーザー情報に基づく権限チェックは「Handlerがリクエストを受けたタイミング」ではなく
「createUploadUrl」でURLを生成するタイミングで行うことになるでしょうか。
「createUploadUrl」で生成されるURLは推測不可能な文字列を含んでいてかつ有効期間は10分間なので、
ここでチェックしていれば概ね安全そうな気がします。
(さらにHandlerにadmin権限つけて外部からアクセスできないようにできればさらに良い)

ただ、そのためには「元のアップロード先URL」で行なっているチェック処理を
ごっそり「アップロード画面表示時の処理」に移動する必要があります。

Javaの場合でも1分間(AppEngineのリクエストがタイムアウトする時間)でアップロードが終わらないような
サイズの大きいファイルをアップロードする際にはこの「createUploadUrl」を使ったアップロード方法が必要ですが、
この方法しか使えないとなると、既存アプリを修正する場合に元のアプリの作りや処理内容次第では意外と面倒な作業になるかもしれません。

処理フローのレベルで修正が必要になってしまう事を考えると、
ポータビリティの観点では1分制限があってもいいから透過的にやって欲しいなぁというのが正直な想いです。
今のままでは既存アプリのファイルアップロード機能は全て修正が必要になってしまうので。
WordPressを載せてみた感想的には「あとちょっとなのにもったいないなぁ」という気持ちです(´・ω・`)

参考に、上記の挙動を確認した際のログです。

・投稿画面を開いた際のログ(アクセス元IPアドレスが「61.201.***.***」でGET)

・uploadUrlからHandlerにPOSTした際のログ(アクセス元IPアドレスが「0.1.***.***」でPOST)

UserAgentはブラウザから送信された情報を引き継いでいるようなので
Cookieも全部送り直してくれれば辻褄が合いそうですが、それは行儀が悪いからしないのかな(´・ω・`)
それとも何かが間違っていて、もっと簡単な方法で修正できるのかな・・・?

 2013/6/14追記 WordPressに関してはこの問題を解決するプラグインが公開されました。
Blog @vierjp : 36.Google App Engine for PHP上でWordPressを運用するためのプラグインが登場


参考:
ファイル入出力 | PHP Labo
Google Cloud Storage PHP API Overview - Google App Engine — Google Developers



◯MySQLの利用 (Google Cloud SQL との連携)

Using Google Cloud SQL with App Engine PHP SDK - Google App Engine — Google Developers

・PDO
・mysql_connect
・msqli
を使用可能。

WordPressを試してみた限りではDB周りは特に問題無さそうでした。
(どちらかというとCloud SQLについてちゃんと調べた方が良いかも)


◯ログ出力

ググって見たところ、PHP標準のログ出力関数は以下の様なものがあるのでしょうか。
・error_log関数
・syslog関数

ドキュメントに書かれているのは「syslog関数」のみで
「error_log関数」については記載がありませんが、以下の挙動になります。

・log_test.php
<?php
error_log('##### 0 tset Message', 0);
error_log('##### 1 tset Message', 1);
error_log('##### 3 tset Message', 3, '/var/tmp/app.log');
//error_log('##### 3-2 tset Message', 3, 'gs://[bucket名]/app.log'); // エラー
error_log('##### 4 tset Message', 4);
echo "log_test";
?>

・出力されたログ

・ファイル名を指定しているケースはログ出力不可(代わりにWarning)
・Cloud StorageのPathを指定した場合はエラー
・それ以外はsyslogをErrorレベルで出力した場合と同様の結果
となりました。


参考:
unoh.github.com by unoh
Logs PHP API Overview - Google App Engine — Google Developers


◯Session

The PHP Runtime Environment - Google App Engine — Google Developersに書いてあるとおり、

・Sessionに保存

session_start();
$_SESSION['Foo'] = 'Bar';

・Sesionから取得

session_start();
print $_SESSION['Foo'];
という感じに普通に使えます。


しかし一点気になる記述がありました。
By default the App Engine runtime will use memcache to store session information using the MemcacheSessionHandler class.
(中略)
However data in App Engine memcache may be flushed periodically, meaning any session information will be lost.
For longer-lived sessions, it may be preferable to use an alternative storage service such as Cloud SQL.
・デフォルトではSession情報をMemcacheに保存している
・Memcacheのデータはクリアされるかもしれないのでそれによってセッション情報が失われる可能性がある。
・長い期間Sessionを維持したければ、Cloud SQL等にセッション情報を保存した方が良い。

Javaの場合はDatastoreにSessionデータが記録されていますが、
(Memcacheも併用していると聞いた事がある気もするけど、とにかく永続化されている)
PHPの場合はMemcacheにしか保存していないようです。
実際プログラム中でSessionを使ってもJavaと違ってDatastoreにSession情報を保存するためのEntityは生成されません。

大量のクライアントからのアクセスがあった場合にはSession情報が頻繁にMemcacheからクリアされてしまうかもしれません。
それが困る場合はCloud SQLを使った自前のSessionHandlerを作ってそれを使うべし、との事のようです。

ググってみたところではセッションをMYSQLに書き込む方法としては以下のサイトが見つかりました。
PHP/セッション管理 - がしまっくす
( 「MySQLを使ったセッション管理」-「session_mysql.php」の部分)

ただ、近年は自分がJavaでAppEngineのアプリを作成する場合、基本的にセッションを使わずステートレスに作っていたので、
設計次第ではセッションをがっつり使わなくてもそれほど困らないかと思います。
というか、AppEngineに限らず新規に作る場合には可能であればできるだけ使わない方が良いと思っています。

ただし、既存のシステムをAppEngine上に持ってくる場合にはこの点について留意した方が良いでしょう。


◯mbstring

※2013/6/16追記 Google App Engine 1.8.1で mbstringが追加されています。
http://phpinfo.vier-test.appspot.com/

気になるツイートを見かけたので少し試してみました。
phpinfo見たところ mbstring, Zend multibyteが off のようだけど
これらはPHPでマルチバイトを扱う際に必要なのでしょうか。
WordPressを触ったところではマルチバイト関連での問題は起きていませんが、
どういう場合に困るんでしょ。

ググって見よう見まねでphp.iniを書いたところ、
phpinfoで「zend.multibyte ON」にできました。(「Zend Multibyte Support」はdisabledのままですが)
mbstringについては今のところ有効にすることができていません。

・php.ini
;参考
;http://d.hatena.ne.jp/do_aki/20111208/1323315995
;http://wiki.ohgaki.net/index.php?PHP%2Ftips%2F%E6%97%A5%E6%9C%AC%E8%AA%9E%E7%92%B0%E5%A2%83php.ini%E8%A8%AD%E5%AE%9A
;http://www.phpbook.jp/install/phpini/index5.html

google_app_engine.enable_functions = "phpversion, phpinfo"
output_buffering = "On"

default_charset="UTF-8"
magic_quotes_gpc=off

[mbstring]
mbstring.language=Japanese
mbstring.internal_encoding=UTF-8
mbstring.http_input = pass
mbstring.http_output = pass
mbstring.encoding_translation = Off
mbstring.detect_order = UTF-8,SJIS,EUC-JP,JIS,ASCII
mbstring.substitute_charactor=none
mbstring.strict_detection = Off
mbstring.input_encoding=pass
mbstring.output_encoding=pass

zend.multibyte = On
zend.script_encoding = UTF-8

・mbstring.php
<?php
  if ( extension_loaded('mbstring') ) {
    echo "mbstring is loaded";
  }else{
    echo "mbstring is not loaded"; //←こちらが出力される
  }
?>


他に「この設定を試してみろ」等あれば是非w


◯AppEngine固有サービスの対応状況

ドキュメントに書かれているのは以下の6つです。

Logs
Mail(送信、受信、バウンス)
Memcache
  Javaでは見たことがないstubの関数(呼び出すことはできるが何もしない)がいくつも用意されています
URL Fetch
Users
Task Queues(Pushのみ、Pullは無し)


以下のサービス等はまだ使えないようです。
・Channel API
・Images API
・Prospective Search
・Full Text Search
・Sockets
(他にもPHPにはまだ無いものがあります。正確に知りたい場合はJavaやPythonと比べてみましょう)

Datastore関連の機能もドキュメントにはありません。
(別サービスの「Cloud Datastore」はJSON API経由で使えると思います)


◯まとめ

環境の管理が不要なPaaSのAppEngineはIaaSやオンプレミス環境と比べると比較的制限は多く、
また、PHPはまだLimited Previewです。

と前置きした上で、

オンプレミス環境向けに作られた既存のPHPアプリをAppEngine上で動作させる場合には、
前述のファイルアップロードやSessionのように
「ドキュメントを読んだ上で自分が載せようとしているアプリについてその制限が問題とならないかどうか」
を検証する必要があるでしょう。

ファイル入出力についてはAPI的にはローカルのファイルシステムと同様に行うことができますが、
実際にはネットワークをまたがって別サーバーに対してファイルの読み書きをしているわけですから
Webサーバーのローカルのファイルシステムに対して読み書きする場合と比べて速度は低下するでしょう。
読み書きするファイルの使い道次第ではこれがパフォーマンス的な問題に繋がる事があるかもしれません。

また、プログラムからのファイルの出力先は必ずCloud Storageなので、
「アプリケーションのディレクトリ内に*.phpファイルを生成する仕組み」や、
「*.phpファイルを含むファイルをアップロードするような仕組み」
(WordPressのテーマを管理画面からアップロードする場合なんかはそうなのかな?)
に対応するのは難しいでしょう。


機能的な制限については「The PHP Runtime Environment」から読むのが良いと思います。
その上で各機能毎のページをチェックしてみましょう。
前回のWordPressの例のようにとりあえず載せて動かしてみて、
エラーになったら調べてみるというのも手っ取り早いかもしれません。


Cloud SQLもCloud Storageも無かった頃と比べると、
オンプレミス環境向けに作られた既存アプリを動かすための環境は徐々に整ってきています。
特にPHPはまだLimited Previewなので、今後の発展に期待したいと思います。(`・ω・´)


*ご意見・ご指摘等ありましたらコメントでもGoogle+のメッセージでもメールでも、ご連絡いただけましたら幸いです。




このエントリーをはてなブックマークに追加

4 件のコメント:

  1. セッションは、Railsやwebapp2と同じCookieで値保持できる、FuelPHPやCodeIgniterといったフレームワークベースなら、問題なさそうです。

    ファイルアップロードは、Cloud Storageをファイルシステムにマッピングする仕組みでもないと、有用なプロダクトの大半は移植で挫けそうです。

    MODX RevolutionというCMSだと、ファイルストレージを抽象化していて、ローカルのファイルシステムとAmazon S3を自由に選べます。S3互換でCloud Storageもいけるかも。

    PHPの場合、ユーザーも規模も大きいプロダクトほど、古い設計も引きずってて、ModelのDatastore化含め、現状のappengine対応は難しそうです。

    返信削除
  2. むむ?「セッションのID」ではなく「セッションに格納するデータそのもの」をCookieに入れるフレームワークがあるのですか?

    ファイルアップロードはやっぱり透過的にできて欲しいですよ(*´・ω・)(・ω・`*)ネー

    データストレージに関しては既存アプリを載せるなら「Cloud Datastore」を使うよりもMySQL互換の「Cloud SQL」を使うのが楽でしょうね。RDB向けの設計をKVSに対応させるのはAppEngineにかぎらず大変でしょうから。
    工数をかけてでもスケーラビリティが必要であれば「Cloud Datastore」化も良いのでしょうけど。

    返信削除
  3. sessionデータのCookie格納は、サーバーサイドsessionの実装が「Restfulじゃない」「スケーラブルじゃない」といった理由で生まれたものだったと思います。Railsが採用して、色んなWebフレームワークに普及してるっぽいです。
    pythonのwebapp2も標準はsecure cookiesで、memcacheやDatastoreへの格納はextra扱いです。私も最初はぎょっとしましたが、sessionにそんなデカいデータ入れない上、appengineなスケールアウトにしっくり来て、ほぼCookieで済ませてます。

    http://qiita.com/items/32efc5b7c73aba3500ff
    http://webapp-improved.appspot.com/api/webapp2_extras/sessions.html


    WordPressやMediaWikiだと、プラグインの価値が大きい反面、結構ごりごりSQL生で持ってて、更にポーティング邪魔しそうです。

    返信削除
  4. >sessionデータのCookie格納は・・・
    へぇぇ!勉強になりました。こういうのがあるのですね。
    参考URLありがとうございます。
    たしかにこれなら言語標準のセッションの必要性は下がりますね。

    返信削除