问题描述
通过PHP,执行了一段针对MYSQL查询,然后件数据查询的结果写入到一个csv文件中,由于数据量非常大,通过WEB访问会存在超时的问题,所以程序执行是命令行下执行的。
在执行了很久之后,通过MYSQL控制台:show processlist;命令,发现刚才执行的SQL已经完成了,但是csv文件,并没有生成。
问题排查
由于程序对容错性处理不是很好,所以执行没有看到任何的错误记录,只能通过PHP或者MYSQL的错误日志来排错
-
通过查看MYSQL错误日志文件: hostname.err文件,并没有找到任务的错误记录
-
发现PHP并没有记录PHP的执行日志,所以需要在php.ini中开启PHP的日志记录
error_log = /usr/local/php/var/log/php.log -
再次执行刚才的查询,在PHP的错误日志中发现:
PHP Fatal error: Allowed memory size of 23907532800 bytes exhausted (tried to allocate 88 bytes) in /path/1.php on line 2
错误提示PHP内存溢出。
最终得到结论:是由于MYSQL返回的数据太大,超出了PHP最大内存限制,导致程序
尝试方案
既然是PHP内存溢出,那就考虑在PHP端如果优化处理
-
*** 程序是Thinkphp开发的,执行SQL调用的是Thinkphp自带的query方法,通过追踪源码发现 ***
private function getResult() { //返回数据集 $result = $this->PDOStatement->fetchAll(PDO::FETCH_ASSOC); $this->numRows = count($result); return $result; }query最终调用了该方法,fetchAll查询的资源。一下子将所有的结果集赋给了$result变量。
这种方式,显然有问题,因此优化该方法,使自己的query请求调用原生PDO,通过while,每次处理一行结果集写入到csv文件$res_source = $link->query($sql_info['sql']); $fp = fopen($csv_file, 'a'); while ($row = $res_source->fetch(PDO::FETCH_ASSOC)) { fputcsv($fp, $row); $i++; }但是最终得到的结果,还是PHP内存溢出,没有成功。结果集让然是全部缓存到了内存中。
-
*** 通过控制PHP对MYSQL的数据读取,每次逐行读取,而不是一次全发返回结果集。***
按照这个思路,通过网上查找资料,找到了一些说明资料,描述:这个问题在PHP的官方网站上叫缓冲查询和非缓冲查询(Buffered and Unbuffered queries)。PHP的查询缺省模式是缓冲模式。也就是说,查询数据结果会一次全部提取到内存里供PHP程序处理。这样给了PHP程序额外的功能,比如说,计算行数,将指针指向某一行等。更重要的是程序可以对数据集反复进行二次查询和过滤等操作。但这种缓冲查询模式的缺陷就是消耗内存,也就是用空间换速度。
相对的,另外一种PHP查询模式是非缓冲查询,数据库服务器会一条一条的返回数据,而不是一次全部返回,这样的结果就是PHP程序消耗较少的内存,但却增加了数据库服务器的压力,因为数据库会一直等待PHP来取数据,一直到数据全部取完。最终找到了一个关于PHP操作MYSQL的原生函数:
mysql_unbuffered_query,PHP官方手册这样介绍该函数:mysql_unbuffered_query — 向 MySQL 发送一条 SQL 查询,并不获取和缓存结果的行
*** 说明 ***
resource mysql_unbuffered_query ( string $query [, resource $link_identifier ] )
mysql_unbuffered_query() 向 MySQL 发送一条 SQL 查询 query,但不像 mysql_query() 那样自动获取并缓存结果集。
一方面,这在处理很大的结果集时会节省可观的内存。另一方面,可以在获取第一行后立即对结果集进行操作,而不用等到整个 SQL 语句都执行完毕。
当使用多个数据库连接时,必须指定可选参数 link_identifier。
*** Note: ***
mysql_unbuffered_query() 的好处是有代价的:在 mysql_unbuffered_query() 返回的结果集之上不能使用 mysql_num_rows() 和 mysql_data_seek()。此外在向 MySQL 发送一条新的 SQL 查询之前,必须提取掉所有未缓存的 SQL 查询所产生的结果行。所以我们在读取大量数据的时候,可以使用mysql_unbuffered_query()来替代mysql_query()。设置为非缓冲查询,设置方法:
修改PDO实例选项:<?php /* * Mysqli设置格式 * 通过对query方法设置:MYSQLI_USE_RESULT(只是读取了结果集的元信息) * 另一种默认的方法是:MYSQLI_STORE_RESULT(把结果集从mysqlServer读到客户端) */ $mysqli = new mysqli($db_hosts, $db_user, $db_pwd, $db_name); $uresult = $mysqli->query("select * from table_name", MYSQLI_USE_RESULT); /* * PDO设置格式 * 通过对setAttribute,设置非缓冲查询 */ $pdo = new PDO($db_dns, $db_user, $db_pwd); $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); /* * mysql设置格式 * 通过调用mysql_unbuffered_query方法,来替换query方法 */ $connect = mysql_connect($db_hosts, $db_user, $db_pwd); $db = mysql_select_db($db_name); $result = mysql_unbuffered_query("select * from table_name");最终通过该方法,解决了PHP读大数据内存溢出,*** 最终成功写入CSV的文件的大小:388G,写入CSV文件行数:9亿多行 ***。