메모리 누수에 대해

webman은 상주 메모리 프레임워크이므로, 우리는 메모리 누수 상황을 약간 주의해야 합니다. 그러나 개발자는 너무 걱정할 필요는 없으며, 메모리 누수는 매우 극단적인 조건에서 발생하며, 쉽게 피할 수 있습니다. webman 개발은 전통적인 프레임워크 개발 경험과 거의 일치하므로 메모리 관리에 대해 불필요한 작업을 할 필요가 없습니다.


webman에 내장된 monitor 프로세스는 모든 프로세스의 메모리 사용 상황을 모니터링합니다. 만약 프로세스에서 사용하는 메모리가 php.ini에서 설정된 memory_limit에 도달할 경우, 해당 프로세스가 자동으로 안전하게 재시작되어 메모리 해제가 이루어지며, 이 과정에서 비즈니스에 영향을 미치지 않습니다.

메모리 누수 정의

요청이 계속 증가함에 따라, webman이 차지하는 메모리도 무한 증가합니다(여기서 말하는 것은 무한 증가입니다), 수백 메가바이트 심지어 그 이상에 도달하는 경우, 이것이 바로 메모리 누수입니다. 만약 메모리가 증가하더라도 이후 더 이상 증가하지 않는다면 이는 메모리 누수가 아닙니다.

일반적으로 프로세스가 수십 메가바이트의 메모리를 사용하는 것은 매우 정상적인 상황입니다. 프로세스가 초대형 요청을 처리하거나 방대한 연결을 유지할 때에는 단일 프로세스의 메모리 사용량이 수백 메가바이트에 이를 수 있습니다. 이 부분의 메모리는 PHP가 운영 체제에 전부 되돌리지 않을 수 있으며, 재사용을 위해 남겨질 수 있습니다. 따라서 특정 큰 요청을 처리한 후 메모리 사용량이 증가하고 메모리가 해제되지 않는 현상은 정상입니다. (여기서 gc_mem_caches() 메서드를 호출하면 일부 여유 메모리를 해제할 수 있습니다.)

메모리 누수가 발생하는 방식

메모리 누수가 발생하려면 다음 두 가지 조건을 모두 만족해야 합니다:

  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 역시 긴 생명 주기의 배열입니다. 여러 다른 키의 데이터를 $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. 가능하면 global, static 키워드를 사용하는 배열을 사용하지 마세요. 사용할 경우 무한히 팽창하지 않을 것인지 확인하세요.
  2. 익숙하지 않은 클래스에 대해서는 가능한 한 싱글톤을 사용하지 말고 new 키워드로 초기화하세요. 싱글톤이 필요하다면 무한히 팽창하는 배열 속성이 있는지 확인하세요.