さて、以前とあるWebサービスをsymfony 1.0からsymfony 1.4へアップデートを行ったが、バックエンドのバッチ処理は1.0のまま稼働させていた。また、とある事情でこのWebサービスを整理する必要があったため、バッチ処理の1.4化を行っていた。
いくつか、1.4化のための作業(ログ関係とヘルパー関係の修正等)を行いバッチ処理を開始!様子を見ていると1.0時代より非常に時間がかかる。さらに待っていると、良くあるメモリが足りないよ!というFatal Errorで死亡してしまった。あれ-、、と思いつつメモリの様子を見て見ると・・・
美しすぎるメモリリークの図
メモリリークしてる!あーこんな情報前に何処かで見たなと思いつつ検索してみるも発見できず、、
仕方ないのでソースコードを少しずつ実行して原因となる場所の特定をする。複数箇所で起きているようで、どうも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にしても良いと思う。