メモリリークについて
webmanは常駐メモリフレームワークですので、メモリリークには少し注意が必要です。ただし、開発者が過度に心配する必要はありません。なぜなら、メモリリークは非常に極端な条件で発生し、かつ容易に回避できるからです。webmanの開発は従来のフレームワークとほぼ同じですので、メモリ管理について余分な操作を行う必要はありません。
ヒント
webmanにはモニタープロセスが搭載されており、すべてのプロセスのメモリ使用状況を監視しています。プロセスのメモリ使用量がphp.iniのmemory_limitで設定された値にほぼ達すると、該当プロセスを自動的に安全に再起動し、メモリを解放します。この間、ビジネスに影響を与えません。
メモリリークの定義
リクエストが増えるにつれてwebmanのメモリ使用量が増加するのは正常な現象です。一般的に、プロセスがある一定のリクエスト量(通常は百万件程度)に達すると、メモリ使用量は増加を止めるか、ごくわずかに増加することがあります。
ほとんどのビジネスでは、単一プロセスのメモリ使用量は最終的に10M~100M程度で安定します。単一プロセスのメモリが100Mを超えなければ心配する必要はありません。
また、大容量ファイルの処理、大規模リクエストの処理、データベースからの大量データ読み込みなどの業務では、PHPは大量のメモリを確保します。PHPは使用後のメモリの一部を再利用するために保持し、すべてをオペレーティングシステムに返さない場合があります。このためメモリ使用量が大きくなる現象が発生することがありますが、メモリは再利用されるため心配する必要はありません。
ヒント
pharパッケージやバイナリパッケージのプロジェクトで、パッケージ自体のサイズが大きい場合、パッケージ済みプロジェクトのメモリ使用量が100Mを超えることは正常です。
メモリリークの確認方法
プロセスのリクエスト数が百万件を超え、メモリが100Mを超え、かつ各リクエスト後にメモリがまだ増加し続ける場合、メモリリークが発生している可能性があります。
メモリリークの特定方法
シンプルな方法は、各インターフェースに負荷テストを行い、百万件以上のリクエスト後もメモリが増加し続けるインターフェースを特定することです。
問題のインターフェースが見つかったら、二分探索法を用いて、ビジネスコードの半分を毎回コメントアウトし、問題のあるコードを特定するまで繰り返します。
メモリリークが発生する原因
メモリリークは、以下の2つの条件を同時に満たす場合に発生します:
- 長寿命の配列が存在する(通常の配列ではありません)
- そして、この長寿命の配列が無限に拡張される(ビジネスが終わることなくデータが挿入され続ける)
1と2の条件が同時に満たされると、メモリリークが発生します。条件を満たさないか、または1つの条件のみを満たす場合はメモリリークではありません。
長寿命の配列
webmanでの長寿命の配列は次の通りです:
- staticキーワードを使用した配列
- シングルトンの配列プロパティ
- globalキーワードを使用した配列
注意
webmanでは、長寿命のデータを使用することができますが、そのデータ内の要素が有限であること、また要素数が無限に増えないことを保証する必要があります。
以下にそれぞれの例を示します。
無限膨張するstatic配列
class Foo
{
public static $data = [];
public function index(Request $request)
{
self::$data[] = time();
return response('hello');
}
}
staticキーワードで定義された$data配列は長寿命の配列です。そして、この例では、$data配列がリクエストごとに増加し続け、メモリリークを引き起こします。
無限膨張するシングルトン配列プロパティ
class Cache
{
protected static $instance;
public $data = [];
public function instance()
{
if (!self::$instance) {
self::$instance = new self;
}
return self::$instance;
}
public function set($key, $value)
{
$this->data[$key] = $value;
}
}
呼び出しコード
class Foo
{
public function index(Request $request)
{
Cache::instance()->set(time(), time());
return response('hello');
}
}
Cache::instance()はCacheのシングルトンを返しますが、これは長寿命のクラスインスタンスです。$dataプロパティにstaticキーワードは使用されていませんが、クラス自体が長寿命であるため、$dataも長寿命の配列です。異なるキーのデータを$data配列に継続的に追加すると、プログラムのメモリ使用量が増大し、メモリリークが発生します。
注意
Cache::instance()->set(key, value)で追加されるキーが有限であれば、メモリリークは発生しません。なぜなら$data配列が無制限に膨張しないからです。
無限膨張するglobal配列
class Index
{
public function index(Request $request)
{
global $data;
$data[] = time();
return response($foo->sayHello());
}
}
関数やクラスメソッドの実行が終わってもglobalキーワードで定義された配列は回収されませんので、これは長寿命の配列です。上記のコードはリクエストが増加するとメモリリークを引き起こします。同様に、関数やメソッド内でstaticキーワードで定義された配列も長寿命の配列です。もし配列が無限に膨張すればメモリリークが発生します。例えば次のような場合です:
class Index
{
public function index(Request $request)
{
static $data = [];
$data[] = time();
return response($foo->sayHello());
}
}
アドバイス
メモリリークに特に注意を払う必要はありません。なぜなら、それは非常にまれなことですし、もし不幸にも発生した場合は、負荷テストを行い、リークを発生させるコードを特定することができます。開発者がリークポイントを見つけられなくても、webmanによって提供されるモニターサービスが必要な時にプロセスを安全に再起動し、メモリを解放します。
もしできるだけメモリリークを回避したい場合は、以下のアドバイスに従うことをお勧めします。
global,staticキーワードで配列を使用しないようにし、使用する場合はその配列が無制限に膨張しないようにする- 不慣れなクラスについては、シングルトンを使用するのではなく、
newキーワードで初期化するようにします。もしシングルトンが必要な場合は、そのクラスが無限に膨張する配列プロパティを持っていないかを確認してください。