2010年3月25日木曜日

symfony 1.4 + Propel 1.4 でメモリリーク?

さて、以前とあるWebサービスをsymfony 1.0からsymfony 1.4へアップデートを行ったが、バックエンドのバッチ処理は1.0のまま稼働させていた。また、とある事情でこのWebサービスを整理する必要があったため、バッチ処理の1.4化を行っていた。

いくつか、1.4化のための作業(ログ関係とヘルパー関係の修正等)を行いバッチ処理を開始!様子を見ていると1.0時代より非常に時間がかかる。さらに待っていると、良くあるメモリが足りないよ!というFatal Errorで死亡してしまった。あれ-、、と思いつつメモリの様子を見て見ると・・・

memory_leak

美しすぎるメモリリークの図

メモリリークしてる!あーこんな情報前に何処かで見たなと思いつつ検索してみるも発見できず、、

仕方ないのでソースコードを少しずつ実行して原因となる場所の特定をする。複数箇所で起きているようで、どうもPropelのオブジェクトを生成するときにリークするようだった。

この時点でもしかしたらリークしているわけではなく、なんらかのキャッシュ機構が働いているのかもなぁ、と思い検索して見るも発見できず。グーグルで「Propel 1.4」で検索するとこのブログが上から三番目に出てくるという、情報のなさっぷりを嘆く。

ここまでくるとソースを見るしかない。オブジェクトの生成部をたどっていくと、Propelが自動生成したBaseDataSourcePeer.phpで以下のようなソースを見つけた。

	public static function populateObjects(PDOStatement $stmt)
	{
		$results = array();
	
		// set the class once to avoid overhead in the loop
		$cls = DataSourcePeer::getOMClass(false);
		// populate the object(s)
		while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
 			$key = DataSourcePeer::getPrimaryKeyHashFromRow($row, 0);
 			if (null !== ($obj = DataSourcePeer::getInstanceFromPool($key))) {
 				// We no longer rehydrate the object, since this can cause data loss.
 				// See http://propel.phpdb.org/trac/ticket/509
 				// $obj->hydrate($row, 0, true); // rehydrate
 				$results[] = $obj;
 			} else {
				$obj = new $cls();
				$obj->hydrate($row);
				$results[] = $obj;
        DataSourcePeer::addInstanceToPool($obj, $key);
 			} // if key exists
		}
		$stmt->closeCursor();
		return $results;
	}

注目して欲しいのはDataSourcePeer::addInstanceToPoolというメソッドで、名前からして、キャッシュしてますよ!という香りが漂ってくる。試しにPropel1.2で生成したモデルと比較してみると、1.2移行のバージョンで新しく追加されたことがわかった。また上記メソッドを使用している部分を全てコメントアウトしてみると、メモリが異常に消費されることは無くなった。

さてここまでわかれば、検索でさらにキーワードを絞ることが出来る。再度検索してみると、原因はdatabases.ymlでpooling:trueと設定していることであった!(参考)databases.ymlに書いてあるあたり基本中の基本だ!何となく、へーpoolingするんだ?速そうだからtrueにしとけ、的な感覚でtrueにしたのを思い出した、、

まとめ

マニュアルはちゃんと読もう。

少し追記。場合によっては、オブジェクトをプールした方が高速になる場合があるのはわかる。しかし自動でORMで処理してしまうのは費用対効果(メモリ消費とCPU・ディスク負荷の軽減)を考えるとあまり良い方法ではないように思う。今Propel1.4で実装されているような処理では、私が遭遇したような問題が起こるかも知れないし、キャッシュヒット率を考えると、ヒット率が良いと考えられる場合はアプリーケーションレベルで都度実装した方が多くの場合よりよいだろう。

また単純に何も考えずにアプリケーションを実装した場合、今回のようなオブジェクトプールにメモリを回すよりはDBにメモリを回した方がより効果は得られるだろう。なぜなら、各種DBはメモリをより効率的に使うため極限まで最適化され、常に開発が続けられているからである。またPHPのプロセスと違い常に起動しているものなのでよく使われるデータのみをキャッシュするなど最適化もしやすい。例外的にCPU負荷がとても高く、メモリが余っておりオブジェクト生成がボトルネックになっているような場合はオブジェクトプールをtrueにしても良いと思う。

2010年3月19日金曜日

Google App Engine + Windows

  • dev_appserver.py すると HTTPSHandlerがないよって言われる。
    ActivePythonのx64版をインストールしているならx86版をインストールする。
  • 公式のチュートリアルを一通りやる。
  • 本番にアップしようとappcfg.py updateをするとSSLモジュールがないよって言われる。
  • http://pypi.python.org/pypi/ssl をインストールしてねって言われるので、ダウンロードして
    python setup.py install
    するとこのPythonはVisual Studio 2003でビルドされてるよ!Visual Studioでコンパイルするかcygwinでためしてね!っていわれる。
  • しかたないのでcygwin版でやってみる。すでにpython2.5がインストールされていた。
  • ActivePythonはとりあえずアンインストールする。
  • cygwin版pythonで
    python setup.py install
    すると成功した。
  • appcfg.py updateも成功して無事本番にアップ完了した
    http://hanger35.appspot.com/

まとめ

Google App Engineを使うときはcygwin版pythonを使おう。

2010年3月4日木曜日

symfony1.0 から symfony1.4 への移行メモ

メモ書きなので品質低
まずsymfony1.0を1.1にアップグレードプロジェクトを1.2から1.3/1.4にアップグレードする1.3の廃止予定および削除される機能に目を通す。バージョンが違うが参考になると思う。翻訳して頂いた方々に感謝。
OpenPNE プラグイン開発者のみなさんにsymfony1.4 対応のお願いもちょっと参考になる。

pluginが読み込まれない

symfony1.4では一つ一つ手動でconfig/ProjectConfiguration.class.phpに設定する必要がある。
Symfony tutorial参照

また、フォルダの名前を何とかPluginにする必要がある。1.0はplugins/testのような名前でも大丈夫だったが、ソースを見た感じplugins/testPluginのように、Pluginという文字がフォルダに付いてないと認識しない。

Propel

PropelはPrope1.2.1-devlからPropel1.4にバージョンが上がっている。

1.4ではDB抽象化レイヤに使っていたCreoleをやめて、PDOを使うようになっているので、PDOのExtensionをインストールする。config/databases.ymlはdsnという項目が追加されている。generate:projectしたときに作成されているサンプルを参考にdsn形式で書く。

クエリを直で実行したいときに使う、Propel::getConnection()で帰ってくるインスタンスが、以前のCreoleのインスタンスからPropelPDOと変更になっているのでこれに依存してるコードはエラーになる。自分のコードの場合、ExecuteQueryみたいな関数を用意していたので、とりあえずNextやgetRow関数等を実装したアダプタを間に挟んで返すようにして対応した。また、build-modelで自動生成したモデルの何とかPeerクラスのdoSelectRSはなくなり、代わりにdoSelectStmtが追加されている。他ORM経由でクエリを処理する場合はいまのところ特に問題ないと思う。

稼働中のDBからモデルを再生成する。propelのDB設定ファイルconfig/propel.iniは1.0のものをそのままコピーしてもbuild-model時にエラーになるので、公式ページを参照して再設定する。 
symfony propel:build-schema --xml
symfony propel:build-model
以前のバージョンのPropelでもbuild-schema時に生成されるschema.xmlをそのままbuild-modelしようとするとエラーに成る場合があるが、今回のバージョンでもやはり直ってないようだ。DBはMySQL。その場合、schema.xmlのDATEやTIMESTAMPのdefault=””を削除したり、CURRENT_TIMESTAMPとあるところを'0000-00-00 00:00:00'とすると直るかも知れない。

TEXTで定義したフィールドは自分の環境だとCHARSETをbinaryにしているためか、propel:build-schemaで自動生成した場合BLOBになってしまっている。その場合Propel1.2.1-devの時は問題なかったが、単純に->getFieldName()とした場合リソースが返されるだけで、値が取得できなくなっている。その場合fgets( $resouce )等すれば取得できが、今回はbuild-schema --xml時に生成されるXMLを編集して、type=”BLOB”と成っているところを、type=”LONGVARCHAR”と変更して対応した。

web debugツールバーで実行したクエリを表示するにはログを取る必要がある。ログを取る場合は、databases.ymlでDebugPDOを指定する。

Propel1.4の新機能については下記のブログが参考になる。
Propel 1.4のWhatsNewの超訳

フォーム・バリデータ関係

1.0でのvalidater.ymlやhandleErrorメソッドなどのVlidate処理は、sfForm関に吸収されたような形になっている。

このあたりは気合いを入れて書き直すしかないと思う。フォームが多いシステムの場合移行に際して一番大変になると思う。sfFormはなれれば割と使いやすいと思うが癖が強い。ただフォーム処理なんて言うのは大体こういうものなので気にしないこととする。自分の場合表示には使わずバリデータにしかつかっていない。フォームをプログラムで自動生成するのはデザインとの関係もあるので余り好きではない。

ビュー関係

component slotを使用していて、ビューをmodule.ymlで変更しているとき、別のモジュールから他のモジュールのcomponent slotを呼ぶとビューの変更が適用されず、デフォルトのsfViewPartialで実行されエラーになる。この辺の設定はモジュールごとに設定されているためだが、カレントのモジュールの設定しか読み込まれないためエラーになる。仕方ないのであまり良い方法とは思えないが、defaultモジュールを別途設定するために適当な場所に下記のコードを入れて対応した
sfConfig::set('mod_default_view_class','hrPHPTAL');
sfConfig::set('mod_default_partial_view_class','hrPHPTAL');

また、デフォルトのビュー(sfPHPView)を使用している場合、デフォルトで自動的にエスケープされるよう修正されているので修正する必要がある。

 

ルーティング関係

クラス構成やメソッドが大きく変わっているため、依存しているコードがあれば作り直しが必要になる。大体代わりの機能は用意されているので頑張って書き換える。

YAML関係

YAMLのパーサがSPYCからsfYAMLとなり、YAML1.2の仕様となった。大きな変更点として、offやnoは文字列として扱われるようになり、修正が必要になる。一部symfony側でoffやnoの場合でも、偽となるよう処理されているところもあるが、cache.ymlでoffと指定しているのにキャッシュされてしまうなどの問題もあったので、offやnoは全部falseに書き換え、onやyesはtrueに書き換えた。
またSPYCで通っていたものがsfYamlでエラーになる。
自分のパターンでは、正規表現を指定する部分で、例えば、 type: [a-z]{1,3}と指定しているとエラーで、これをtype: “[a-z]{1,3}”と指定してやれば問題なくなった。{}はYAMLでは連想配列を指定するのに使うので微妙な扱いの変更だと思う。

キャッシュ関係

CacheHelperについては以前書いたのでこちらを参照。

アクションキャッシュについては、sfViewCacheManager->setSuffix()(違うかも)が無くなってしまったため、ログインが必要なページでログイン前とログイン後で別々のキャッシュファイルを生成するようにしていが、それが出来なくなってしまった。そのため別途FileCacheクラスを作成して対応した。

まとめ

今回symofny1.4へ移行した理由は、symfony1.0のサポートが切れ、今後も長く続くプロジェクトでシステムの拡張・修正が必要だったため。Propel関係はうまくやれば修正にそれほど時間はかからないが、フォームの修正、キャッシュ関係の修正、動作確認にかなりの時間が必要だった。特に理由がない限り既存のプロジェクトをsymfony1.4に移行するのはあまりオススメできない。
そういえば上記には書いていなかったが、admin generatorも移行に相当時間が必要そうだったため、symfony1.0のまま運用してお茶を濁している、、これも追々移行したい。