Moodle 4.1 PHP CLI百万行CSV导出脚本内存持续增长问题排查
解决Moodle CLI脚本导出大数据时的内存增长问题
一、优先优化现有脚本(无需分进程)
你的内存增长问题大概率来自get_records_sql一次性将1万行数据加载为数组,加上Moodle内部对象的状态积累,以下是针对性优化:
- 改用数据库记录集(Recordset)替代数组查询
get_records_sql会把所有查询结果一次性存入数组,内存占用高且容易残留引用。换成get_recordset_sql返回迭代器,逐行处理后立即释放内存:
$limit = 10000; $rowCount = $table->getRowsCount(); fputcsv($file, $columns,';'); for ($offset = 0; $offset < $rowCount; $offset += $limit) { // 使用recordset而非get_records_sql $recordset = $DB->get_recordset_sql($sql . " LIMIT $limit OFFSET $offset", $params); foreach ($recordset as $row) { $formattedRow = array_values($table->format_row($row)); fputcsv($file, $formattedRow, ';'); // 立即清理单行数据引用 unset($row, $formattedRow); } // 必须关闭recordset释放数据库资源 $recordset->close(); echo "\n offset $offset memory: " . (memory_get_usage() / 1024) . "KB\n" ; gc_collect_cycles(); }
- 清理$table对象的潜在缓存
如果format_row方法内部存在缓存机制,可尝试在每次分块后重置相关状态。若Moodle API未提供直接的清理方法,可考虑在分块循环内重新初始化$table(注意性能影响,仅在内存泄漏严重时使用):
// 在分块循环内重新初始化table $table = custom_report_table_view::create(264); $table->setup(); $table->download = 'excel';
- 确保文件操作的资源释放
虽然你已经关闭了文件,但要确保在循环中没有其他未释放的资源,比如临时变量、数据库连接的隐式引用。
二、分进程方案(上述优化无效时使用)
如果内存泄漏问题无法通过脚本优化解决,可将每个分块作为独立进程执行,进程退出后内存会自动释放:
1. 主脚本(负责调度分块)
<?php define('CLI_SCRIPT', true); require "config.php"; use core_reportbuilder\table\custom_report_table_view; $table = custom_report_table_view::create(264); $table->setup(); $table->download = 'excel'; $columns = (new table_dataformat_export_format($table, $table->download))->format_data($table->headers); [$sql, $params] = $table->getPaginatedDataSQL(); $filePath = '/var/excelreport/tmp_264_' . date("d-m-Y_H:i:s") . '.csv'; // 先写入表头 try { $file = fopen($filePath, 'w'); fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF)); fputcsv($file, $columns,';'); fclose($file); echo "文件 '$filePath' 已创建并写入表头。\n"; } catch (\Throwable $throwable) { echo $throwable->getMessage(); echo "创建文件 '$filePath' 失败。\n"; exit(1); } $limit = 10000; $rowCount = $table->getRowsCount(); $totalChunks = ceil($rowCount / $limit); // 将SQL和参数存入临时文件,供子脚本读取 $tempSqlFile = tempnam(sys_get_temp_dir(), 'moodle_export'); file_put_contents($tempSqlFile, json_encode(['sql' => $sql, 'params' => $params])); // 逐个调度分块进程 for ($chunkIndex = 0; $chunkIndex < $totalChunks; $chunkIndex++) { $offset = $chunkIndex * $limit; $command = "php /path/to/export_chunk.php --file '$filePath' --offset $offset --limit $limit --sql-file '$tempSqlFile'"; echo "正在处理分块 $chunkIndex(偏移量 $offset)...\n"; exec($command, $output, $returnCode); if ($returnCode !== 0) { echo "分块 $chunkIndex 处理失败:" . implode("\n", $output) . "\n"; } } // 清理临时文件 unlink($tempSqlFile); echo "所有分块处理完成。\n";
2. 子脚本(处理单个分块)
<?php define('CLI_SCRIPT', true); require "config.php"; use core_reportbuilder\table\custom_report_table_view; // 解析命令行参数 $options = getopt('', ['file:', 'offset:', 'limit:', 'sql-file:']); if (!isset($options['file'], $options['offset'], $options['limit'], $options['sql-file'])) { echo "缺少必要参数。\n"; exit(1); } $filePath = $options['file']; $offset = (int)$options['offset']; $limit = (int)$options['limit']; $sqlData = json_decode(file_get_contents($options['sql-file']), true); $sql = $sqlData['sql']; $params = $sqlData['params']; // 初始化table(子进程独立运行,需重新初始化) $table = custom_report_table_view::create(264); $table->setup(); $table->download = 'excel'; // 追加模式打开文件 $file = fopen($filePath, 'a'); if (!$file) { echo "无法打开文件 '$filePath' 进行追加。\n"; exit(1); } // 使用recordset逐行处理 $recordset = $DB->get_recordset_sql($sql . " LIMIT $limit OFFSET $offset", $params); foreach ($recordset as $row) { $formattedRow = array_values($table->format_row($row)); fputcsv($file, $formattedRow, ';'); unset($row, $formattedRow); } $recordset->close(); fclose($file); echo "分块偏移量 $offset 处理完成。\n"; exit(0);
内容的提问来源于stack exchange,提问作者Kolt Stevenson
相关产品推荐
相关产品推荐

