ラベル symfony の投稿を表示しています。 すべての投稿を表示
ラベル symfony の投稿を表示しています。 すべての投稿を表示

2010年5月20日木曜日

Doctrine 関係リンク集

次のプロジェクトはDoctrineでいくことにしたのでメモ。
Doctrine2ではマジックメソッド(findBy~)が使えなくなるらしいし、そもそも処理が重いらしいので開発では使わないことにした。

初級編

上級編

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月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のまま運用してお茶を濁している、、これも追々移行したい。

2010年2月18日木曜日

symfony 1.4 CacheHelper(cache関数) のその後

前回のまとめ

symfony 1.4のCacheHelperには余分なキャッシュを保存してしまう問題がある(多分)。sfViewCacheManagerのソースを見た感じでは、module名とaction名のみを基準にキャッシュ管理をしており、独自の内部URI(default/indexのような)をもたないcacheは制御が行えず、action cacheも同時に保存してしまうような仕様になっている。

対策

結局別にCacheHelperを作成する事で対応した。元々symfony 1.0の時からCacheHelperでは対応できない機能があったので、別途CacheHelperを作成して対応していた。

ソース github ライセンスはMIT

追加した機能は

  1. 前回のブログで書いた余分なキャッシュを保存しないようにした。
  2. 別途引数にinternalURIを追加して、複数のページで同一のキャッシュを使用できるようにした。
  3. すでにキャッシュ済みか調べるための関数の追加した。

使い方

  • cache関数の代わりにhr_cache関数を使用する。
  • cache_save関数の代わりにhr_cache_save関数を使用する。
  • hr_is_cached()関数はactionですでにキャッシュ中か調べて余分な処理を行わないようにするようなときに使用する。hr_is_cachedを実行した際の結果はstatic変数に保存され、その後hr_cacheが使用されたときと結果が同じになるよう保証する。(実行されるタイミングが異なるため、lifetimeが切れて結果が異なる場合があるため)
  • 第三引数のinternaURIは例えば、URIが値を取る場合、’default/index?a=123’と’default/index?a=abc’では別のキャッシュが保存されるが、internalURIに’default/index’と指定することで共通のキャッシュが使用される。

効果

  • ディスク領域の節約になる。
  • internalURIを適切に設定するとキャッシュヒット率が上がり各種負荷が下がる。

本来であれば、componentを使えば良いが、componentを使うとtemplateファイルが分離され、設定も多少煩雑になるため、自分の場合どちらかというとCacheHelperを好んで使っている場合が多い。

ただ、殆どの場合通常のCacheHelperで問題はないと思う。効果があるのはページ数が数万~数十万単位あるサイトで、キャッシュ書き込みによりディスク負荷があがり、キャッシュファイルの増大によって(Linux等のOSの)ページキャッシュが圧迫されパフォーマンスが急激に低下するような場合それなりの効果があると思う。

2010年2月12日金曜日

symfony 1.4.1 fragment cache(cache関数)のよくわからない挙動について

 

テスト用のapp testを追加

symfony generate:app test
symfony generate:module test default

apps/test/config/settings.ymlのprodにcache:true追加。apps/test/modules/default/actions/actions.class.phpのexecuteindexをreturn sfView::SUCCESS;に書き換え。

1 この状態でアクセスしてみる。

ブラウザで/test.php/を開く。

キャッシュされたファイルを調べる。
find cache/test/prod/template/ -type f

(何も表示されない)

2 indexSuccess.phpでfragment cacheを使って見る。

例えばapps/test/modules/default/templates/indexSuccess.phpを下記の内容にする。

<?php if( !cache('cachename', 600) ):?> 
cache time - <?php echo date('Y-m-d H:i:s')?> 
<?php cache_save()?> 
<?php endif;?>

ブラウザで/test.php/を開く。

キャッシュされたファイルを調べる。
find cache/test/prod/template/ -type f

cache/test/prod/template/localhost/all/default/index/_sf_cache_key/cachename.cache - これは良いとして、、
cache/test/prod/template/localhost/all/default/index.cache - これは何?

 

3 GETで値を投げてみる。

ブラウザで/test.php/?a=1を開いてみる。
ブラウザで/test.php/?a=2を開いてみる。

キャッシュされたファイルを調べる。
find cache/test/prod/template/ -type f

cache/test/prod/template/localhost/all/default/index/_sf_cache_key/cachename.cache
cache/test/prod/template/localhost/all/default/index/a/1.cache - 増えてる!
cache/test/prod/template/localhost/all/default/index/a/2.cache - 増えてる!
cache/test/prod/template/localhost/all/default/index.cache


4 まとめ

もしかしたら根本的に勘違いをしているのかも。そもそも1.4だとhttp://www.symfony-project.org/jobeet/1_4/Propel/ja/21 に特にcache関数の説明がないからサポートしてない?

追記 1

symfony 1.0で同じ事をすると、
cache/test/prod/template/localhost/all/test.php/symfony/index/_sf_cache_key/cachename.cache
というファイルしかできない。個人的にはこれが理想。
現状だと、symfony1.4の挙動が仕様なのか、バグなのわからない。

cache()関数使用後に、sfViewCacheManager->isCachable(‘default/index’)するとこれがtrueを返す。symfony1.0だとそもそもcache()関数後(action実行後)にisCachableは呼び出されない。lazy_cache_key(ビューキャッシュのために遅延キャッシュキージェネレーション)の関係で後から呼び出されるのかもしれない。lazy_cache_keyの意味はわからないが、、

追記2

今のsfViewCacheManagerだと、複数回cache関数を実行すると一番最後に実行したcache()関数のlifetimeでdefault/indexのキャッシュが作成される。これは仕様としてどうなのか。(注:後から気付いたがこれは間違い)

プログラムを見た感じ、moduleとactionでisCachableを切り分けていて、cache関数を実行すると、currentのmoduleとactionでcacheがonになり、actionがキャッシュされてしまう。

追記3

sfCacheFilterのexecute()を$filterChain->execute();return;としてもキャッシュが生成される。どのタイミングで生成してるんだろう。余分に作られている(と思っている)キャッシュはactionキャッシュと同じものだが、上記のテストの方法で作った場合default/indexのcacheはenabled:falseになるので、sfCacheFilterがキャッシュチェック時(sfExecutionFilterの実行前)にはisCachableはfalseとなり、キャッシュを使用することはないし作ることに意味がないように見える。

追記4

sfFileCache::write()で書き込み状態を調べてみると、余分に作られている(と思っている)キャッシュは、ブラウザでリロードするたびに書き込まれる、、

追記5

余分に作られている(と思っている)キャッシュは、最終的にsfPHPView::render()の、
$content = $viewCache->setActionCache($uri, $content, $this->isDecorator() ? $this->getDecoratorDirectory().'/'.$this->getDecoratorTemplate() : false);
上記の部分で書き込まれている。setActionCache()はソースを見た感じ、そのアクションがisCacheableでwithLayoutの場合のみキャッシュとして保存するようになっている。本来であれば、cache.ymlで特にenabled: trueとしているわけでもないので、キャッシュとして保存されるはずはないが、cache関数を実行した時に実行されるaddCache関数がactionキャッシュのキャッシュ情報を上書きするような動作になっており、action cacheも保存ししまっているように見える。

 

全体的に見た感じバグっぽいが、、対応は後日検討。新たにcache関数の代わりのヘルパーを作るか、sfViewCacheManagerの動作を変えるか、sfPHPViewの動作を変えるかして対応することにする。

追記6

cache.ymlにてdefault/indexをenabled:trueを指定した場合cache/test/prod/config/module_default_config_cache.yml.phpに下記の内容が追加される。

$this->addCache($moduleName, 'index', array('withLayout' => false, 'lifeTime' => 600, 'clientLifeTime' => 600, 'contextual' => false, 'vary' => array ()));

ヘルパーのcache関数を実行した場合、下記のような内容が実行される。

$cache->addCache($params['module'], $params['action'], array('withLayout' => false, 'lifeTime' => $lifeTime, 'clientLifeTime' => $lifeTime, 'vary' => $vary));

この場合両者とも、module = default, action = index,となるため、cache関数を実行した場合もcache/indexをenable:trueとした場合と同じになり、default/indexのaction cacheが保存される事になる。

 

まとめ2

sfViewCacheManagerのソースを見た感じでは、module名とaction名のみを基準にキャッシュ管理をしており、独自の内部URI(default/indexのような)をもたないcacheは制御が行えず、action cacheも同時に保存してしまうような仕様なのかも知れない。
ただキャッシュファイルは保存されるが、sfCacheFilter実行時にはまだcacheヘルパーは実行されておらず、addCacheされていないためそのキャッシュファイルは使用されない。ようするにキャッシュファイルがあってもsfExecutionFilterは実行されるし、保存されたaction cacheは特に意味はない。

対策!

現状1.0から1.4へ移行しようとしているサイトは、1.0で運用している現在のキャッシュファイルだけでも数十ギガあるため、このまま1.4に置き換えるとそれだけでさらにキャッシュファイルのサイズが数倍になってしまうことが予想される。

対策として、CacheHelperの動作を変更した。ソースは後日アップ予定。