PHP内存溢出解决方法

9

我正在尝试将Postgres数据库中的数据插入到MySQL数据库中。 我需要导入约100000条记录。 但是,我总是遇到内存不足的问题。

Out of memory (allocated 1705508864) (tried to allocate 222764 bytes)

我在使用Laravel 5进行操作,以下是代码:

// to avoid memory limit or time out issue
ini_set('memory_limit', '-1');
ini_set('max_input_time', '-1');
ini_set('max_execution_time', '0');
set_time_limit(0);

// this speeds up things a bit
DB::disableQueryLog();

$importableModels = [
    // array of table names
];

$failedChunks = 0;

foreach ($importableModels as $postGresModel => $mysqlModel) {

    $total = $postGresModel::count();
    $chunkSize = getChunkSize($total);

    // customize chunk size in case of certain tables to avoid too many place holders error
    if ($postGresModel === 'ApplicationFormsPostgres') {
        $chunkSize = 300;
    }

    $class = 'App\\Models\\' . $mysqlModel;
    $object = new $class;

    // trucate prev data //
    Eloquent::unguard();
    DB::statement('SET FOREIGN_KEY_CHECKS=0;');
    $object->truncate();
    DB::statement('SET FOREIGN_KEY_CHECKS=1;');
    Eloquent::reguard();

    $postGresModel::chunk($chunkSize, function ($chunk) use ($postGresModel, $mysqlModel, $failedChunks, $object) {

        // make any adjustments
        $fixedChunk = $chunk->map(function ($item, $key) use ($postGresModel) {

            $appendableAttributes = $postGresModel::APPEND_FIELDS;
            $attributes = $item->getAttributes();

            // replace null/no values with empty string
            foreach ($attributes as $key => $attribute) {
                if ($attribute === null) {
                    $attributes[$key] = '';
                }
            }

            // add customized attributes and values
            foreach ($appendableAttributes as $appendField) {
                if ($appendField === 'ssn') {
                    $value = $attributes['number'];
                    $attributes[$appendField] = substr($value, 0, 4);
                } else {
                    $attributes[$appendField] = '';
                }

            }

            return $attributes;
        });

        // insert chunk of data in db now
        if (!$object->insert($fixedChunk->toArray())) {
            $failedChunks++;
        }

    });    
}

当插入大约80000行后,内存问题会出现。

我怀疑是集合map函数或map函数内部的循环有问题。我甚至尝试将内存设置和时间限制设置为无限制,但都没有用。也许需要使用引用变量之类的东西,但我不确定如何操作。

上述代码是否可以进行任何优化以减少内存使用?

或者如何通过代码高效地从大型PostgreSQL数据库导入大量数据到MySQL?

有人能告诉我在这里做错了什么或为什么整个内存被消耗掉了吗?

PS:我正在使用具有4GB RAM(Windows 8)的本地开发机。PHP版本:5.6.16


你尝试过减小块的大小吗? - OrderAndChaos
@ Sarcoma:是的,我尝试将“at”设置为较小的值,但奇怪的是,它在大约80000条记录上再次耗尽了内存。 - dev02
@taylorcressy:没有限制,我不知道为什么它没有使用更多的内存,因为已经分配了1705508864字节,相当于1705MB/1.7GB。也许代码有问题,但我在代码中找不到优化点:( - dev02
我说我怀疑有内存泄漏,因为在数组中有8000个条目本身不应该是问题。但是在循环中的某个地方会添加到内存中并且没有正确清除。我会尝试使用分析器来检测内存泄漏。如果没有或者我只想完成工作并继续前进,那么我会将8000拆分为500或1000个批次,在批次之间取消设置所有变量,并调整批次值,直到内存使用量在限制范围内。 - Elzo Valugi
1
有什么是“32位”应用程序吗?大约1.7G是这样的应用程序崩溃的临界点。 - Rick James
显示剩余16条评论
6个回答

4

是的,您可以更改'memory_limit'。但这仅适用于今天,而不是明天,当您将需要更多的内存时。

计划A:

相反,多写一点代码...将数据分成每次1000行。创建一个带有所有行的单个INSERT语句。通过自己的事务执行它。

计划B:

创建一个包含所有行的CSV文件,然后使用LOAD DATA INFILE进行批量插入。

无论选择哪种计划,都要避免一次性将所有行加载到RAM中。对于PHP中的标量和数组,存在大量的开销。


我已经使用 Eloquent 的 chunk 方法对数据进行了分块,如代码所示,无法将其分成超过 500 个块以避免参数过多的错误,并且 CSV 不是选项,需要通过代码实现。代码中存在内存泄漏的问题,需要识别并修复。谢谢。 - dev02

2
肯定有一个内存泄漏的地方。我猜是在$chunk->map() 或者 $object->insert($fixedChunk->toArray()) 之间的某个位置。由于实现是隐藏的,我们只能猜测。
然而,尽可能地使用生成器。代码可能看起来像下面这样:
function getAllItems() {
  $step = 2000;

  for ($offset = 0 ;; $offset += $step) {
    $q = "SELECT * FROM items_table LIMIT $offset, $step";

    if (! $items = Db::fetchAll($q)) {
      break;
    }

    foreach ($items as $i) {
      yield $i;
    }
  }
}

foreach (getAllItems() as $item) {
  import_item($item);
}

我敢说,使用生成器,您将能够从一个数据库导入几乎任何数量的数据到另一个数据库。

感谢生成器和步骤的贡献。我稍微修改了代码,以便按块插入数据,但还是要感谢这个想法。 - dev02

1
当您获取您的PostgreSQL数据时,请尝试LIMIT返回的内容大小(http://www.postgresql.org/docs/8.1/static/queries-limit.html)到合理的值,然后进行迭代。例如,如果您一次取20000行,则应该执行'SELECT .. BLAH .. LIMIT 20000 OFFSET 0',下一次迭代将是'SELECT .. BLAH .. LIMIT 20000 OFFSET 20000'(OFFSET为20000 * 您的循环计数器)。重复此过程,直到没有行为止。

1

1.- 尝试对数据处理逻辑进行注释,以检查内存泄漏是否在此代码中:

$postGresModel::chunk($chunkSize, function ($chunk) use ($postGresModel, $mysqlModel, $failedChunks, $object) {

        // make any adjustments
        $fixedChunk = $chunk->map(function ($item, $key) use ($postGresModel) {

              ///Nothing to do 
       }
}

2.- 如果您仍然遇到相同的错误,可能是mysql驱动程序(PDO?)在尝试从查询结果中转储所有行时产生了内存泄漏,将所有行缓冲到内存中。

就像PostgreSQL unbuffered queries and PHP (cursors)中所述,您可以使用游标更改hoy postgreSql提取行的行为:

$curSql = "DECLARE cursor1 CURSOR FOR SELECT * FROM big_table";
$con = new PDO("pgsql:host=dbhost dbname=database", "user", "pass");
$con->beginTransaction(); // cursors require a transaction.
$stmt = $con->prepare($curSql);
$stmt->execute();

$innerStatement = $con->prepare("FETCH 1 FROM cursor1");

while($innerStatement->execute() && $row = $innerStatement->fetch(PDO::FETCH_ASSOC)) {
    echo $row['field'];
}

0

几个建议。

  • 在每个循环中实例化一个新的$object对象。根据MySqlModel的实际结构和项目数量,它肯定会使用大量内存(也因为GC尚未工作,参见第二个建议)。在每个循环结束时将其设置为NULL,即

$object = NULL;

  • 如果执行时间不是问题,请在每个循环之间插入一些延迟。这允许PHP垃圾收集器做一些工作并释放未使用的资源。

0

map会返回你的集合的一个新实例。垃圾回收会清理得太晚。

试着替换

$chunk = $chunk->map(function... 

使用

$newchunk = $chunk->map(function... 

当然,在插入时使用新的块,$object->insert($newchunk->toArray())。你也可以使用transform而不是map

GC现在应该收集它了,但是您可以在插入后添加unset($newchunk);以确保。在代码倒数第二行中添加unset($object);也不会有任何问题。


你尝试过unset吗?我还有一个想法:你可以尝试将foreach中的$key变量替换为$key1,例如foreach ($attributes as $key1 => $attribute) { if ($attribute === null) { $attributes[$key1] = ''; } }?不知道是否会导致内存泄漏,但感觉有点可疑且可能会累积。 - Solarflare

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接