フリーランスのためのネットビジネス専門学校 ネットで独立開業を目指す人を応援
フリーランスのためのネットビジネス専門学校 ネットで独立開業を目指す人を応援

Visual Studio CodeでPHPのリモートデバッグ環境を構築する

はじめに

DB関連の処理で大量にメモリを消費するという問題に直面した際に、
バッファクエリと非バッファクエリなるものを知った。
PHPのマニュアルに書かれていることを自分なりに調べて検証してみたのですぐに思い出せるように記録を残しておく。

php.net/manual/ja/mysqlinfo.concepts.buffering.php

検証環境

CentOS 7.4
PHP 7.1.8 (cli)
MySQL 5.7.18

バッファークエリとは

PHPのマニュアルによるとバッファークエリとは

クエリは、デフォルトではバッファモードで実行されます。 つまり、クエリの結果がすぐに MySQL サーバーから PHP に転送され、 PHP プロセスのメモリ内に結果を保持し続けるということです。

図にするとこんな感じかな?
buffer_query.png

非バッファークエリとは

では非バッファークエリとはどういうものなのか?こちらについてもPHPのマニュアルに記載があります。

非バッファクエリは、クエリを実行してリソースを返しますが、 その時点ではまだデータが MySQL サーバー上にあって取得待ちになっています。

図にするとこんな感じでしょうか?
not_buffer_query.png

イメージはつかめた。
非バッファークエリにすることによる作用と副作用は何か。

非バッファークエリの作用

メモリの使用量が減る

結果が大量に返ってくることが想定できる場合は、非バッファモードを使わないといけません。

とマニュアルにあるので検証してみた。

sample.php(バッファークエリ)
try {
    $dbh = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}

$sth1 = $dbh->prepare('xxx');
$result = $sth1->execute();
$recordList1 = $sth1->fetchAll(PDO::FETCH_ASSOC);
print(memory_get_peak_usage()."n");

結果

$ php sample.php 
1399820624
sample.php(非バッファークエリ)
try {
    $dbh = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}
$dbh->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$sth1 = $dbh->prepare('xxx');
$result = $sth1->execute();
$recordList1 = $sth1->fetchAll(PDO::FETCH_ASSOC);
print(memory_get_peak_usage()."n");

結果

$ php sample.php 
883385944

500MBくらい減った。全然違う。

非バッファークエリの副作用

完全にfetchしきるまで同一データベースハンドル上でクエリ実行ができない。

PHPのマニュアルに

サーバー上の結果セットからすべての結果を取得するまで、 同じ接続上で別のクエリを実行することはできません。

とあるようにfetchしきるまで同一データベースハンドル上でクエリ実行ができません。
コードでこれを表すと下記のようなコードになります。

クエリ実行が実行できない例1
...
try {
    $dbh = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}

$dbh->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);

$sth1 = $dbh->prepare('xxxx')
// trueが返る
$result1 = $sth1->execute();

$sth2 = $dbh2->prepare('xxxx')
// falseが返る
$result2 = $sth2->execute();

// recordが取得されている
$recordList1 = $sth1->fetchAll(PDO::FETCH_ASSOC);
// recordが取得できない
$recordList2 = $sth2->fetchAll(PDO::FETCH_ASSOC);                                                                              

クエリ実行が実行できない例2
...
try {
    $dbh = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}

$dbh->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);

$sth1 = $dbh->prepare('xxxx')
// trueが返る
$result1 = $sth1->execute();
// recordが取得されている(fetchAllではなくfetch)
$recordList1 = $sth1->fetch(PDO::FETCH_ASSOC);


$sth2 = $dbh2->prepare('xxxx')
// falseが返る
$result2 = $sth2->execute();
// recordが取得できない
$recordList2 = $sth2->fetchAll(PDO::FETCH_ASSOC);                                                                              

fetchしきるまでDBにプロセスが残り続ける

PHPのマニュアルに

サーバーへの負荷は高くなります

とあったのでコードにsleepを意図的に入れて、watchコマンドでMySQLのプロセスを監視してみました。

watch -n1 'echo "SHOW FULL PROCESSLIST;" | mysql xxxxx'
...
try {
    $dbh = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}

$dbh->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);

$sth1 = $dbh->prepare('xxxx')
$result1 = $sth1->execute();

// sleep 30sec
sleep(30);

$recordList1 = $sth1->fetchAll(PDO::FETCH_ASSOC);

軽めのクエリーでは下記のような結果になり、
mysql_select_wait.png

重めのクエリーでは下記のようなプロセスがsleepの間残りました。
mysql_select_wait_2.png

MySQLのマニュアルによるとSending to clientとは下記のような状態らしいです。

The server is writing a packet to the client. This state is called Writing to net prior to MySQL 5.7.8.

dev.mysql.com/doc/refman/5.7/en/general-thread-states.html

非バッファークエリでfetchを忘れるとどうなるか

fetchしきるまでDBにプロセスが残り続けると書きましたが、
fetchを忘れるとどうなるのかが気になったのでこれも検証してみた。

検証コード
...
try {
    $dbh = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}

$dbh->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);

$sth1 = $dbh->prepare('xxxx')
$result1 = $sth1->execute();

exit;

mysql_select_wait_2.png

MySQLのプロセスは消えるみたいです。
lack of memoryでもプロセス解放されるかな?
今度検証してみよう。

おわりに

PHPのマニュアルには

バッファクエリを使うのは、 結果セットの量が限られている場合や事前に結果の行数を知りたい場合だけにとどめるべきでしょう。 結果が大量に返ってくることが想定できる場合は、非バッファモードを使わないといけません。

非バッファモードを勧めているように見える。
MySQLのプロセスとかを見ながら検証した感じだと
prepareしてexecuteしてすぐにfetchAllという流れになっているのであれば
非バッファモードを使っても問題ないように思う。
少なくともcsvダウンロードとかの機能では非バッファモード使えそう。

ちなみに、私は検証しただけで非バッファモードは採用しませんでした。
ロジックを見直してメモリ使用量を下げることにしました。

記載してある内容が間違っていたら誰がフィードバックください。
あと、バッファーモードの時との差分である500MBの詳細をご存知の方、
フィードバックいただければ幸いです。

[紹介元] PHPタグが付けられた新着投稿 – Qiita Visual Studio CodeでPHPのリモートデバッグ環境を構築する

コメント

記事に戻る

コメントを残す