메모리 누수에 대한 내용

웹맨은 메모리 상주 프레임워크이므로 메모리 누수에 대해 조금 주의할 필요가 있습니다. 그러나 개발자는 너무 걱정할 필요는 없습니다. 메모리 누수는 매우 극단적인 조건에서 발생하며 쉽게 피할 수 있습니다. 웹맨의 개발은 전통적인 프레임워크 개발 경험과 거의 동일하며 메모리 관리에 대해 추가 조치를 취할 필요는 없습니다.


웹맨에 내장된 모니터 프로세스는 모든 프로세스의 메모리 사용 상황을 모니터링하며, 프로세스의 메모리 사용이 php.ini 파일에 지정된 memory_limit 값에 거의 도달할 때 자동으로 해당 프로세스를 안전하게 다시 시작하여 메모리를 해제합니다. 이 동안 비즈니스에는 영향을 미치지 않습니다.

메모리 누수 정의

요청이 계속 증가함에 따라 웹맨이 사용하는 메모리 또한 무한히 증가 하여 수백 메가바이트 이상으로 끝날 때 메모리 누수가 발생합니다. 그러나 이후에 더 이상 증가하지 않는 경우에는 메모리 누수로 간주하지 않습니다.

일반적으로 프로세스가 수십 메가바이트의 메모리를 소비하는 것은 매우 정상적인 상황이며, 프로세스가 대규모 요청을 처리하거나 대량의 연결을 유지하는 경우 단일 프로세스의 메모리 사용량은 수백 메가바이트에 달할 수 있습니다. 이러한 메모리 사용 후에 php는 모든 메모리를 반환하지 않을 수 있습니다. 대신 재사용하기 때문에 어떤 대형 요청을 처리한 후에 메모리 사용이 증가하여 메모리를 해제하지 않을 수 있습니다. 이는 정상적인 현상입니다. (gc_mem_caches() 메서드를 호출하여 일부 빈 메모리를 해제할 수 있습니다)

메모리 누수는 어떻게 발생하는가

메모리 누수가 발생하기 위해선 다음 두 가지 조건이 충족되어야 합니다:

  1. 긴 생명 주기의 배열이 존재해야 합니다 (일반적인 배열은 괜찮습니다)
  2. 긴 생명 주기의 배열은 무한히 확장되어야 합니다 (비즈니스가 끊임없이 데이터를 삽입하고 데이터를 정리하지 않는다)

만약 1, 2번 조건이 동시에 충족된다면 메모리 누수가 발생할 것입니다. 그렇지 않고 위의 조건을 충족시키지 않거나 둘 중 하나만 충족되지 않는다면 메모리 누수가 아닙니다.

긴 생명 주기의 배열

웹맨에서 긴 생명 주기의 배열은 다음과 같습니다:

  1. static 키워드로 정의된 배열
  2. 싱글톤의 배열 속성
  3. global 키워드로 정의된 배열

참고
웹맨에서는 긴 생명 주기의 데이터를 사용할 수 있지만, 배열 내의 데이터가 유한하며 요소 수가 무한히 확장되지 않도록 보장해야 합니다.

아래는 예시를 통해 설명합니다

무한히 확장되는 static 배열

class Foo
{
    public static $data = [];
    public function index(Request $request)
    {
        self::$data[] = time();
        return response('hello');
    }
}

요청 사이에 계속해서 늘어나는 $data 배열은 static 키워드로 정의되었으며, 예시에서는 요청이 계속되면서 $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()는 싱글톤을 반환하며, 이는 긴 생명 주기의 클래스 인스턴스입니다. 여기서 $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());
    }
}

건의사항

개발자들이 메모리 누수에 대해 특별히 주의할 필요는 없습니다. 메모리 누수는 매우 드물게 발생하며, 불행히도 발생할 경우에는 부하 테스트를 통해 어떤 부분에서 누수가 발생했는지 찾아내어 문제를 해결할 수 있습니다. 심지어 개발자가 누수 지점을 찾지 못해도 웹맨에 내장된 모니터 서비스가 누수가 발생한 프로세스를 적시에 안전하게 재시작하여 메모리를 해제합니다.

만일 메모리 누수를 최대한 피하려고 한다면 다음과 같은 건의사항을 따를 수 있습니다.

  1. global, static 키워드의 배열을 사용하지 않으려 노력하되, 사용해야 한다면 무한히 확장되지 않도록 보장합니다
  2. 익숙하지 않은 클래스의 경우 싱글톤을 사용하지 말고 new 키워드를 사용하여 초기화합니다. 싱글톤이 필요한 경우 해당 클래스가 무한히 확장되는 배열 속성을 가지는지 확인하세요