データベースの含まれるレコード数が増えてくると今まで経験しなかったことを経験するようになります。

まず最初に出くわすのが、以下のようなコマンドラインでのコマンド実行のエラーです。

$ php artisan command:dump
HP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 56000000 bytes) in /vol1/usr/www/larajapan/repos/larajapan/vendor/laravel/framework/src/Illuminate/Database/Connection.php on line 330

In Connection.php line 330:
                                                                                       
  Allowed memory size of 134217728 bytes exhausted (tried to allocate 56000000 bytes)  

これは、実行しているプログラムのメモリの使用が現在設定しているphpのメモリ制限を超えようとしたよ、という親切なエラーです。確かに、php.iniで設定されているデフォルトの134217728バイト(128メガバイト)は小さ過ぎますね。

phpのプロセスのメモリ使用制限は、システム変数memory_limitで設定できるので、以下のように簡単に変更できます。
128メガバイトから倍の256メガバイトに増やします。

...
ini_set('memory_limit', 256M');
...

しかし、それでもエラーとなります。

PHP Fatal error:  Allowed memory size of 268435456 bytes exhausted (tried to allocate 2097152 bytes) in /vol1/usr/www/larajapan/repos/larajapan/vendor/laravel/framework/src/Illuminate/Database/Connection.php on line 332

In Connection.php line 332:
                                                                                      
  Allowed memory size of 268435456 bytes exhausted (tried to allocate 2097152 bytes)  

もっと使用可能なメモリを増やすべき?

ここで、どんなプログラムを実行しているか見てみましょう。

namespace App\Console\Commands;

use Illuminate\Console\Command;

use App\User;

class DumpCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:dump';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Dump data in csv format';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        ini_set('memory_limit', '256M');

        $fields  = [
            'id',
            'created_at',
            'name',
            'email'
        ];

        // CSV出力

        $fh = fopen('users.csv', 'w');

        fputs($fh, chr(0xEF).chr(0xBB).chr(0xBF));
        fputcsv($fh, $fields);

        $rows = User::all();

        foreach ($rows as $row) {
            $values = [
                $row->id,
                $row->created_at,
                $row->name,
                $row->email
            ];

            fputcsv($fh, $values);
        }

        fclose($fh);

        //ここに使用したメモリを表示
        printf("memory usage: %s\n",
            $this->formatBytes(memory_get_peak_usage(true)));

        return 0;
    }

    // バイトをメガバイトに変更
    public static function formatBytes($size, $precision = 0)
    {
        $base = log($size, 1024);
        $suffixes = array('', 'K', 'M', 'G', 'T');

        return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
    }
}

このプログラムはDBテーブルのデータをすべてCSVのファイルにエクスポートするプログラムです。プログラムの最後に、どれだけメモリを使用したかを表示するようにしていますが、先の実行では、途中でエラーとなっためでそこまで到達していません。

さらにメモリを増やしてみましょう。今度は1ギガバイトまで増やして、つまりini_set('memory_limit', '1G')と設定して実行してみます。

memory usage: 804M

実行は成功です!そして、メモリがピーク時には804メガバイト使用されていることがわかりました。

昨今のマシンでは1ギガバイトくらいのメモリ使用はたいしたことありません。しかし、実行の前に途中でエラーにならないように、前もってどこまで増やせばよいのでしょう?

今回はテストのために、50万のレコードを用意しました。私のお客さんのDBではその10倍ものレコード数があるDBテーブルがあります。

ここで、もう一度プログラムを見てみます。このプログラムは、データベースのデータをCSVのファイルにエクスポートするプログラムですが、ファイルに書き込む前に、すべてのレコードを変数:$rowsにいったん入れています、つまりメモリの保存しているわけで、レコード数が多くなればなるほどメモリが必要となります。しかし、前もってどれだけメモリを使用するかの計算は難しいです。

もっとメモリの使用を低く抑えてマネージすることはできないのでしょうか?

そこで登場するのがEloquentのchunk()、これで上のプログラムのループ部分を以下のように書き変えます。

...
        User::chunk(1000, function($rows) use($fh) {
            foreach ($rows as $row) {
                $values = [
                    $row->id,
                    $row->created_at,
                    $row->name,
                    $row->email
                ];

                fputcsv($fh, $values);
            }
        });
...

最初からすべてのレコードを取ってくるのではなく、まず、レコードを1000個のチャンクで取ってきてクロージャの部分を実行し、そして次の1000個を取得して処理と。。それゆえに、$rowsに保存するデータは前回に比べてはるかに少なくなります。

これを実行すると、使用するメモリは驚くほど少なくなりました。

memory usage: 14M

By khino