メモリ管理
はじめに
MySQL Native Driver は、MySQL Client Library とは異なるやり方でメモリを管理します。 メモリを確保し、解放するやり方が異なります。 つまり、MySQL から結果を読み取る際にメモリをチャンクで確保するやり方や、 デバッグや開発時のオプションが存在するかどうかや、 MySQL から結果を読み取り、PHP の変数に結びつけるやり方が違うということです。
以下の説明では、 C言語のコードレベルで MySQL Native Driver を理解することに関心があるユーザ向けに、メモリ管理の紹介とまとめを示します。
利用するメモリ管理関数
メモリの確保や解放は、 PHP のメモリ管理関数を使って行います。 よって、mysqlnd のメモリ消費量は、 memory_get_usage() のような PHP の API をコールすることで追跡できます。 PHP のメモリ管理機構を使ってメモリが確保され、かつ解放されるので、 メモリを変更してもオペレーティングシステムからはすぐに見えない可能性があります。 PHP のメモリ管理の仕組みは、プロキシとして振る舞うため、 システムのメモリの解放が遅延する可能性があります。 よって、MySQL Native Driver と MySQL Client Library のメモリ利用状況を比較することは困難です。 MySQL Client Library はオペレーティングシステムのメモリ管理機構を直接コールするので、 オペレーティングシステムからメモリへの変更が直接見えるからです。
PHP によって強制されるあらゆるメモリに対する制限が、 MySQL Native Driver にも影響します。 このため、 PHP が利用できるメモリの残量を超える大きな結果セットを取得しようとした場合に、 メモリ不足エラーが発生する可能性があります。 MySQL Client Library は PHP のメモリ管理関数を使っていないため、 PHP のメモリ制限に従いません。 MySQL Client Library を使っている場合、 PHP の配置の仕方によっては、 PHP プロセスのメモリのフットプリントが PHP のメモリ制限を超えるかもしれません。 しかしながら、PHP エンジンの制御を超えた結果セット分のメモリを確保するため、 PHP スクリプトも大きな結果セットを扱える可能性があります。
PHP のメモリ管理関数は、薄いラッパーを通じて MySQL Native Driver からコールされます。ラッパーによって、デバッグが容易になります。
結果セットの処理
様々な MySQL サーバー と クライアントAPI が、 結果セットを バッファリングするかしないか という点で違いがあります。 結果セットをバッファリングしない場合、 結果セットは一行ずつ MySQL からクライアントに送られ、 クライアントはそれらの結果を走査します。 結果セットをバッファリングした場合、 クライアントライブラリが結果全体を取得し、それらをクライアントに渡します。
MySQL Native Driver は、 MySQL とのネットワーク通信のために PHP ストリームを使います。 MySQL が送信した結果セットは、 PHP ストリームのネットワークバッファから取得され、 mysqlnd の結果バッファに格納されます。 結果セットは zval で構築されます。 次のステップとして、結果セットが PHP スクリプトから利用できるようになります。 結果セットのバッファから PHP の変数に転送される最後のタイミングがメモリ消費量に影響し、 結果セットをバッファリングした場合にほとんどの違いが出る部分です。
デフォルトでは、MySQL Native Driver はバッファリング済みの結果セットをメモリに2度保持するのを避けようとします。 結果は内部的な結果バッファと zval に一度だけ保持されます。 PHP スクリプトによって 結果が PHP の変数に転送される際、 PHP の変数は内部的な結果バッファを参照しています。 データベースへの問い合わせ結果はコピーされることなく、 メモリ上に一度だけ保持されます。 ユーザーがデータベースへの問い合わせ結果を保持する変数を変更する際は、 内部的な結果バッファが変更されるのを避けるため、 コピーオンライトが実行されるはずです。 バッファの内容を変更してはいけません。なぜなら、 ユーザーが結果セットを再度読み取るかもしれないからです。 コピーオンライトの仕組みは、追加の参照リストを管理する仕組みと、 zval 標準のリファレンスカウンタを使って実装されています。 コピーオンライトは、ユーザーが結果セットを PHP の変数を読み取る際と、 変数を unset する前に結果セットを解放する前に行わなければいけません。
一般的に、 このパターンは PHP スクリプトが結果セットを一度だけ読み取り、 保持している結果の変数を変更しない場合にうまくいきます。 このパターンの一番の欠点は、mysqlnd の参照管理の仕組みがその変数を参照するのをやめるまで、保持している結果全体を解放できないことから、 追加の参照管理のためにメモリへのオーバーヘッドが増えることです。 MySQL Native Driver は、結果が解放されたりコピーオンライトが実行された場合にユーザー変数への参照を削除します。 メモリ使用量を観察していると、結果セットが開放されるまで使用量が増えることがわかるでしょう。 PHPスクリプト が結果セットを明示的に解放しているかどうかや、 ドライバが暗黙のうちに解放しているためにメモリが必要以上に長く使われていないかを調べるのに、統計情報 を使いましょう。 統計情報を見ると、コピーオンライトが何回行われているかを調べるのにも役立ちます。
while ($row = $res->fetch_assoc()) { ... }
と同じ意味のコードを使い、
バッファリング済みの結果セットから小さな行をたくさん読み取っている場合、
参照ではなくコピーを要求することでメモリ使用量を最適化できるかもしれません。
コピーを要求するということは、
結果を二回メモリに格納するということではありますが、
結果セットを走査し、結果セットそれ自体を解放する前の
$row
のコピーを PHP が解放できるからです。
サーバーが高負荷の場合は、
ピーク時のメモリ使用量を最適化することでシステム全体のパフォーマンスを改善できるかもしれません。
但し、個別のスクリプトがコピーするアプローチは、
追加のメモリ確保やメモリコピーの操作によって速度が落ちる可能性があります。
メモリのモニタリングとデバッグ
MySQL Native Driver のメモリ使用状況を追跡する方法は複数あります。 高レベルの使用状況の概要をすばやく取得したり、 PHP スクリプトの効率性を検証するのが目的な場合は、 ライブラリが収集する 統計情報 をチェックしましょう。 統計情報を使うと、 たとえば PHPスクリプトが処理する以上の結果を生成する SQL文 を見つけたりできます。
メモリ管理機構の呼び出しを記録するために、 debug トレースログを設定できます。 これを使うと、 メモリがいつ確保されたり、解放されたりしているかがわかります。 しかし、リクエストされたメモリのチャンクのサイズはわからないかもしれません。
MySQL Native Driver の最近のバージョンによっては、 メモリ不足の状況をランダムにエミュレートする機能があります。 この機能は、C言語を使うライブラリの開発者や、 mysqlnd プラグイン を書く人だけが使うものです。 詳細については、 対応する PHP 設定のソースコードを検索して下さい。 この機能は外部に公開されていないため、 事前に警告することなく変更される可能性があります。