メモリリークについて

Webmanは常駐メモリフレームワークであるため、メモリリークの状況に少し注意を払う必要があります。しかし、開発者はあまり心配する必要はありません。メモリリークは非常に極端な条件下で発生し、簡単に回避できます。Webmanの開発は、従来のフレームワーク開発の体験と基本的に同じであり、メモリ管理のために余分な操作を行う必要はありません。

ヒント
Webman に付属の monitor プロセスは、すべてのプロセスのメモリ使用状況を監視します。プロセスの使用メモリが php.ini で設定された memory_limit の値に達しそうになると、自動的に対応するプロセスを安全に再起動し、メモリを解放します。この間、業務には影響がありません。

メモリリークの定義

リクエストが増加し続けると、Webmanが占有するメモリも無限に増加します(注意:無限に増加です)、数百MB、さらにはそれ以上に達する場合、これがメモリリークです。もしメモリが増加しても、その後増加しない場合は、メモリリークとは見なされません。

一般的に、プロセスが数十MBのメモリを占有することは正常な状況です。プロセスが非常に大きなリクエストを処理したり、大量の接続を維持したりする場合、単一プロセスのメモリ占有が数百MBに達することもよくあります。この部分のメモリは、PHPが必ずしもすべてをオペレーティングシステムに返却しない場合があります。むしろ、再利用のために残されるので、特定の大きなリクエストを処理した後にメモリ占有が増大し、メモリが解放されない場合があり、これは正常な現象です。(gc_mem_caches() メソッドを呼び出すと、部分的に未使用のメモリを解放できます。)

メモリリークはどのように発生するのか

メモリリークが発生するためには、以下の2つの条件を満たす必要があります:

  1. 長寿命の配列が存在する(注意:長寿命の配列であり、通常の配列は問題ありません)
  2. その長寿命の配列が無限に膨張する(ビジネスが無限にデータを挿入し、データを清掃しない)

条件1と条件2が同時に満たされる場合(注意:同時に満たされる)、メモリリークが発生します。それ以外の場合は、上記の条件を満たさないか、いずれか一方の条件のみを満たす場合、メモリリークとは見なされません。

長寿命の配列

Webmanにおける長寿命の配列は次の通りです:

  1. static キーワードの配列
  2. シングルトンの配列属性
  3. 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 も長寿命の配列になります。異なるkeyのデータを $data 配列に追加し続けることで、プログラムが使用するメモリも増大し、メモリリークを引き起こします。

注意
もし Cache::instance()->set(key, value) で追加されるkeyの数が有限であれば、メモリリークは発生しません。なぜなら、$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 に付属の monitor サービスが、その都度安全にメモリリークが発生したプロセスを再起動し、メモリを解放します。

もしメモリリークを避けたいのであれば、以下の提案に従うことを検討してください。

  1. globalstatic キーワードの配列を極力使用しない。使用する場合は、無限に膨張しないことを確認してください。
  2. よく知らないクラスについては、シングルトンを使用せず、new キーワードで初期化してください。シングルトンが必要な場合、その配列属性に無限膨張がないか確認してください。