Posted at

PHPと関連ライブラリのバージョンを上げたら、OMySQLのCSV出力結果が途中で途切れるようになった


PHP5系からPHP7系にバージョンを上げて、且つ古いPEARのモジュールを使い続けたら大量のMySQLの結果取得が途中で途切れるようになった


前提条件


  • CentOS7(CentOS Linux release 7.6.1810 (Core))

  • php72-php-7.2.16-1.el7.remi.x86_64

  • php72-php-mysqlnd-7.2.16-1.el7.remi.x86_64

  • php72-php-pecl-mysql-1.0.0-0.17.20160812git230a828.el7.remi.x86_64


発生した事象

PHPからMySQLに繋いで大量データをCSVに出力するなんて、よくあるシチュエーションですが

PHPのバージョンを上げるまでは正常に動いていた処理が動かなくなった。

バージョンを上げるまでは正常に動作していたので、処理中に全結果を配列に保持するような方式はとっていないのは明らかなのに。

現象的には、データを大量に出力しようとした場合に途中までの出力で止まるといった

OutOfMemoryチックなのだが、何故か「Fatal error: Allowed memory size of~」が出力されない。


対応策

地道に(古いPEARモジュールもSVNに直接コミット、管理されていたのでその中まで)デバッグしていったところ

下記のネイティブな関数のコールでDB処理が中断されていた。

@がついているので、エラーメッセージが出ないのも理解できる。

ですが、今まで大丈夫ぶだったのに、バージョン上げてからこの関数が問題になった理由が分からない。

@mysql_query

ただ、調べていくうちに下記の事が分かった。

* mysql_query()はMySQL Client Library(libmysqlclient)を利用した関数でPHP7で削除されている

* MySQL Client Library(libmysqlclient)はMySQL側が提供している接続ライブラリ

* MySQL Native Driver(php-mysqlnd)を導入することで、同様の関数を引き続き利用出来る

* MySQL Native DriverはPHPの拡張モジュールでありメモリ管理はPHPに依存する

簡単にいうと、libmysqlclient は外部ライブラリなので、メモリ管理はlibmysqlclientで管理されるが

MySQL Native DriverはPHPのモジュールなので、メモリ管理はPHPで管理となる。

https://www.php.net/manual/ja/mysqlnd.overview.php


注意: メモリ使用量の報告

MySQL Native Driver は PHP のメモリ管理システムを使っているので、 そのメモリ使用量を memory_get_usage() で追えます。 これは > libmysqlclient では不可能なことです。なぜなら libmysqlclient は C の malloc() 関数を使っているからです。


https://www.php.net/manual/ja/mysqlinfo.concepts.buffering.php


注意:

ライブラリとして libmysqlclient を使っている場合は、結果セットのデータを PHP の変数に代入するまで結果セットのメモリ利用量が PHP のメモリ制限にカウントされません。 mysqlnd の場合は、結果セットのメモリがすべて PHP のメモリ制限にもカウントされます。


mysql_query()という関数は結果セットを自動でバッファリングするため

大量のデータを扱うとPHPのメモリ制限に引っかかりやすい関数なのである。

https://www.php.net/manual/ja/function.mysql-query.php

一方、バッファリングしない関数(mysql-unbuffered-query())も存在し

こちらを利用することで、メモリ枯渇なく全データにアクセスする事が可能である。

https://www.php.net/manual/ja/function.mysql-unbuffered-query.php


対応策

多量のデータ出力の場合のみ、mysql_unbuffered_query()が利用されるように変更した。

MySQLiに置き換えるような記載が多く見られますが、それらも内部的にはMySQL Native Driverを読んでいるため

既存のソースに手を入れないで済むようにこのままとした。


最後に

昔から言われていますが「@」を付けて関数を呼ぶのは、やっぱり悪手でしょ!!

っと改めて思いました。