CakePHP3でサービス開発をしているのですが、 Cake\Console\Shell
を使ったバッチ処理を実装する際に、思わぬ問題にぶち当たったので共有します。
実行環境
- CentOS 6.8
- PHP5.6.27
- CakePHP3.3.3
- MySQL5.6.32
概要
バッチ処理の最中に突如として「セグメンテーション違反(Segmentation Fault)」で処理が中断する
データベースの特定テーブルの各レコードに対してforeach()で重めのSQLを投げて、集計csvに吐き出すような、集計系のバッチでよくある処理を書いていました。
雰囲気としてはこんな感じです。(もちろん実際はもうちょい複雑です)
/* こんな感じ */
$this->out("ユーザー集計csv出力を開始します...");
$users = $this->Users->find->all();
$total = count($users);
// ユーザー1件に対して一行の集計csvを出力
foreach($users as $seq => $user){
$now = $seq + 1;
$this->out("出力中...${now}/${total}件目");
$userInfo = $this->userInfomations
->find('SomeHeavyFinder',[
'id' => $user->id
])->first();
$row = [
userInfo->table1->data1,
userInfo->table1->data2,
userInfo->table2->data1,
//(以下略)
];
fputcsv($row);
}
これを実行したところ、以下のようなログが出てバッチが停止しました。
ユーザー集計csv出力を開始します...
出力中...1/2123件目
出力中...2/2123件目
出力中...3/2123件目
(中略)
出力中...3/2123件目
出力中...3/2123件目
出力中...3/2123件目
セグメンテーション違反です
何度か実行してみると、止まる件数は800件前後で、一定しない件数でセグメンテーション違反で落ちています。実は平行して書いていた別にバッチについても同じ現象が起きており、
- foreachの繰り返し処理を使って、1000回オーダーで
Table::find()
で結果セットを取得する
という条件を満たせば、マシンスペックによりますが、概ね発生するという印象です。
さらに、Shellではなく、CakePHPのControllerでも同様の現象が発生することを確認しており、その場合は 「エラーハンドリングすらされず、500_empty_response が返ってくる」 というさらに厳しい結果が待っています。
そもそもセグメンテーション違反(Segmantation Fault / SegFault)とは
OSレベルでのエラーでした。要するに
「そのプログラムが触ってはいけないメモリ領域にアクセスしようとしたのでエラー終了しましたよ^^」
という感じでしょうか。C言語ならともかく、ガベージコレクタ任せのPHPでこんなエラーが出るとは珍しいですね・・・。
CakePHPの公式Issueでの指摘
「CakePHP3 Segmentation Fault」 で検索したところ、以下の記事がヒットしました。
PHP throws a Segmentation fault when I use the database quite a lot. I have a Shell that calls the custom finder below around 1500 times, sometimes it throws this segmentation fault, sometimes it doesn't.
まさに同じ現象です。
それに対するコメントとして
I think it might be good to report this as well to the PHP team, since this kind of errors is a problem in the language itself.
CakePHPではなくPHPの言語レベルでの問題なんじゃないの?という指摘。確かに、フレームワークレベルでちょっとやんちゃしたところで、PHPのスコープを超えたメモリアクセスが起きるとは考えづらく、CakePHPの特定の処理が、PHPのメモリ管理上の不備をダイレクトに引き当てている、という感じなんでしょうか。
ちなみに、このIssueに関しては
Closing as we cannot find a way of reproducing this issue
再現性がないからクローズ、という悲しい結末になっています・・・。
決定的な解決策は見つからず
色々と試してみたのですが、決定的な解決策が見つからず、 ある程度重いDBに対して、CakePHP3で安定的に稼働するバッチが実装することは難しいのではないか という結論に至ってしまいました...。
調査する中で、幾つかわかった点のみを共有します。
1. クエリから帰ってくるResultSetオブジェクトをunset()することで若干緩和される
// ユーザー1件に対して一行の集計csvを出力
foreach($users as $seq => $user){
$now = $seq + 1;
$this->out("出力中...${now}/${total}件目");
$userInfo = $this->userInfomations
->find('SomeHeavyFinder',[
'id' => $user->id
])->first();
$row = [
userInfo->table1->data1,
userInfo->table1->data2,
userInfo->table2->data1,
//(以下略)
];
fputcsv($userInfo);
unset()
}
こんな感じで、クエリ結果を明示的にunset()することで若干緩和されたような気がします。(とは言ってもループの度に再代入されているので普通は影響ないはずなのですが)
2. 現象の発生率はクエリ自体の動作の重さではなく、クエリを実行する回数に依存する
- 「10000件のDBに対して、10000件を全てselectする(重い)」
- 「10000件のDBに対して、limit-offsetを使用して500件ずつselectする(軽い)」
では、2の方がsegFaultが起きやすい みたいです。メモリ使用量で考えると1のほうが明らかにパフォーマンスが悪いので、この問題が単純にメモリ使用量が多いことにより起きていないことがわかります。
3. PHP5.6 -> PHP7.0 に上げると直るかも(願望
言語レベルの問題なら、バージョンを上げれば直るかも!?という荒技を思いつきますが、さすがに稼働中のサービスで試す気はなれませんでした・・・。
情報求む
Webサービスにおいてバッチ処理で反復的にDBアクセスを行うことはかなり普遍的なアプローチなので、もしこのような処理が実装できないとなると、 Web開発の選択肢からCakePHP3系を外す という結論にもなりかねないと思っています。
なにかこの件に関して情報をお持ちの方がいましたら、ぜひコメントください!