
Webアプリエンジニアの加瀬です。
バッチのリファクタを行ったのですが、データ取得の方法を少し工夫してメモリ使用量を小さくすることができました。色々な場面で活用できそうと思ったので書き留めておこうと思います。
PHP: v8.1.13
Symfony: v5.4.11
ユーザーへのメッセージの自動再送を行うバッチがあり、送信対象の件数が急増した際に設定したメモリ上限値を超えてOOM(Out Of Memory)エラーが発生してしまうことがありました。
メモリ上限を引き上げる暫定対応を行ったのですが、今後送信件数が増加した場合に再発する可能性があったのでリファクタを実施しました。
リファクタ前のコードには以下の問題点がありました。
送信件数が増えれば増えるほどメモリ使用量が比例的に増えるような状態でした。メモリ使用量を抑えるために、設計を見直す必要がありました。
どのようにリファクタするか悩んだのですが、対応方針として以下の形をとりました。

データ取得時に関連エンティティの紐付けを行っていましたが、データ取得をする段階では不要(データを処理する際のみ必要)なものも含まれていました。
そのため、「対象データのIDを取得する処理」(データ取得時に不要なエンティティはjoinしないようにする)と「ID指定で対象データを取得・関連エンティティを紐付ける処理」に分離しました。
対象データを取得・関連エンティティを紐付ける処理:
(※内容を一部変更しています。エンティティ名などは実際のものと異なります。)
<?php/** * 指定した期間のデータを取得 *@returnResendMessage[] */publicfunction getListByReservationTime(\DateTime$from, \DateTime$to):array{// 実際には30近くのエンティティの紐付けを行っている$qb=$this->createQueryBuilder('rm')->select('rm','tu','wl','c','i','jl','rmr')->innerJoin('rm.targetUser','tu')->leftJoin('tu.wishList','wl')->leftJoin('rm.company','c')->leftJoin('wl.item','i')->leftJoin('c.jobListing','jl')->leftJoin('rm.resendMessageResult','rmr')->where('rm.deletedAt IS NULL')->andWhere('rmr IS NULL')->andWhere('rm.reservationTime BETWEEN :from AND :to')->setParameters(['from'=>$from,'to'=>$to,]) ;return$qb->getQuery()->setHint(Query::HINT_FORCE_PARTIAL_LOAD,true)->getResult() ;}
対象データのIDを取得する:
<?php/** *@returnint[] */publicfunction getIdsByReservationTime(\DateTime$from, \DateTime$to):array{// whereで使用しないエンティティはjoinしない// IDのみ取得する$qb=$this->createQueryBuilder('rm')->select('rm.id')->leftJoin('rm.resendMessageResult','rmr')->where('rm.deletedAt IS NULL')->andWhere('rmr IS NULL')->andWhere('rm.reservationTime BETWEEN :from AND :to')->setParameters(['from'=>$from,'to'=>$to,]) ;return$qb->getQuery()->getResult();}
ID指定で対象データを取得・関連エンティティを紐付ける:
<?php/** *@paramint[] $ids *@returnResendMessage[] */publicfunction getListByIds(array$ids):array{// ID指定でデータを取得し、関連エンティティを紐付ける$qb=$this->createQueryBuilder('rm')->select('rm','tu','wl','c','i','jl','rmr')->innerJoin('rm.targetUser','tu')->leftJoin('tu.wishList','wl')->leftJoin('rm.company','c')->leftJoin('wl.item','i')->leftJoin('c.jobListing','jl')->leftJoin('rm.resendMessageResult','rmr')->where('rm.id IN (:ids)')->setParameter('ids',$ids) ;return$qb->getQuery()->setHint(Query::HINT_FORCE_PARTIAL_LOAD,true)->getResult() ;}
IDリストの取得だけであればメモリをあまり使用せず、またIDにインデックスを貼っているため、ID指定でデータを取得することで実行時間の短縮を狙うことができます。
データ取得部分の処理を分けた後、データ処理部分を変更しました。
送信対象のデータのIDリストを取得した後、IDリストを1000件ごとのチャンクに分けてforeachで回します。各チャンクごとに「ID指定でデータを取得」「本処理(DB登録、メッセージ送信など)」「メモリ解放」を実施するようにしました。
データ送信処理:
<?php$resendMessages=$this->resendMessageRepository->getListByReservationTime($this->from,$this->to,);$resendMessageCollection=new ResendMessageCollection($resendMessages);foreach($resendMessageCollectionas$resendMessage){// DB登録、メッセージ送信などの処理...// メモリ解放処理を行っていない}
データ送信処理:
<?php// 対象データのIDリストを取得$resendMessageIds=$this->resendMessageRepository->getIdsByReservationTime($this->from,$this->to,);// IDリストをチャンクごとに分ける(BATCH_SIZE = 1000)$chunkedIds=array_chunk($resendMessageIds,self::BATCH_SIZE);// チャンクごとに処理を実施foreach($chunkedIdsas$ids){// ID指定でデータを取得$resendMessages=$this->resendMessageRepository->getListByIds($ids);$resendMessageCollection=new ResendMessageCollection($resendMessages);// 処理内容は変更なしforeach($resendMessageCollectionas$resendMessage){// DB登録、メッセージ送信などの処理...}// メモリ解放処理を追加$entityManager->clear();gc_collect_cycles();}
リファクタにより、実行時間を短縮+メモリ使用量を減少させることができました。リファクタ前後の各平均値を記載します。
表1:平均値(同じ曜日・同じ時間帯のデータ)
| メモリ使用量[MB] | 実行時間[s] | 送信件数[件] | |
|---|---|---|---|
| 修正前 | 540.36 | 1395.13 | 1487.13 |
| 修正後 | 422.67 | 783.88 | 1519.75 |


メモリ使用量が減少し、送信件数が急増した際のOOMエラーの発生リスクを減らすことができました。実行時間が結構短くなっているのも嬉しかったです(指数関数的に増加しなくなったのは大きい)。
データ取得・処理方法を少し工夫しただけで実行時間及びメモリ使用量を減らすことができました。メモリ使用量を減らしたい場合等、少しでも実装の参考になれば幸いです。
弊社では一緒に開発していただけるエンジニアを募集しています!ご興味のある方、ぜひご検討いただけると嬉しいです。
id:t_ow884
id:k_fujimoto
id:donuthole8
id:saoha
id:m_ssk
id:m_irie
id:m_inaba
id:t_ogawa
id:ow_tk
id:bebermate
id:yito222
id:k_yamamoto_ow
id:a-iwamoto-ow
id:ikunaga_ow
id:k_dairiki
id:miriejob
id:t_moriyama_ow
id:keiozawa_ow
id:n_otake
id:wisteria3221
id:shunsuke_ikeuchi
id:s-o-o
id:kzmshx
id:seiya_hamada
id:openwork_engineer
id:openwork_writer引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。