[PostgreSQL] mmgr

src/backend/utils/mmgr/README 번역 및 요약


Background

  • postgresql에서 대부분의 메모리 할당은 "memory context"들에서 진행된다
  • memory context란 보통 src/backend/utils/mmgr/aset.c에 구현된 AllocSet들을 의미한다
  • 오버헤드를 최소화하면서 메모리를 관리하는 방법은 메모리의 수명에 맞는 적절한 context들의 집합(set)을 정의하는 것이다
  • memory context의 기초적인 연산은 다음과 같다
    • context 생성
    • context 내에서 메모리 chunk를 할당
    • context 삭제(해당 context에 포함된 모든 메모리가 해제된다)
    • 특정 context에 할당된 메모리의 총량 조회 (context가 가지고 있는 chunk의 총량이 아닌 raw memory의 총량)
      • raw 메모리를 작게 나누면 chunk가 된다
  • CurrentMemoryContext라는 전역 변수는 항상 "현재" context를 의미한다
    • palloc은 내부적으로 "현재" context에서 메모리를 할당한다
  • MemoryContextSwitchTo 함수는 새로운 context를 현재 context로 설정하고 이전의 컨텍스트를 반환한다. 그래서 이 함수를 호출한 호출자는 이전 컨텍스트를 복원할 수 있다
  • malloc, free함수를 그대로 사용하는 것 보다 메모리 컨택스트를 통해 호출할 때의 이점은 다음과 같다
    • 하나의 컨텍스트에 포함된 여러 chunk를 한 번에 해제할 수 있다. 이것은 chunk 각각에 대해 해제 요청을 하는 것보다 빠르고 안전하다
    • postgresql에서 트랜잭션의 끝에 메모리를 정리(clean up)할 때 이 점을 이용한다. 트랜잭션 또는 더 짧은 수명을 가진 모든 활성 컨텍스트를 리셋함으로써, 모든 일시적인(transient) 메모리를 회수할 수 있다

palloc API

  • palloc은 malloc과 유사하지만 다른점도 있다
  • 메모리가 부족한 경우(Out Of Memory) palloc과 repalloc은 elog(ERROR)를 통해 종료한다. NULL을 반환하지 않는다. palloc_extended함수를 사용하면 이런 행동을 override할 수 있다
  • palloc(0)는 유효한 연산(operation)이다. NULL을 반환하지 않고 사용할 수 있는 바이트가 없는 chunk를 반환한다. 이 chunk는 이후에 repalloc을 통해 커질 수 있다. 반대로 repalloc은 크기를 0으로 재할당 하는 것을 허용한다
  • pfreerepalloc은 NULL포인터를 받지 않는다. repalloc은 현재 메모리 컨텍스트에 종속되지 않는다. 하지만 현재 어떤 컨텍스트에서 메모리를 할당하는지는 알아야 한다

The Current Memory Context

함수를 호출할 때마다 메모리 컨텍스트를 전달하는 것은 notational overhead가 되기 때문에 항상 현재의 메모리 컨텍스트를 CurrentMemoryContext를 통해 참조할 수 있다. 이러한 개념이 없다면 copyObject 함수들(routines) 을 호출하기 위해서는 항상 메모리 컨텍스트를 전달해야 하고 포인터를 반환하는 함수들도 마찬가지이다. 함수 내부에서 일시적으로 메모리를 할당하지만 사용자에게 그 포인터를 반환하지 않는 함수에게까지 메모리 컨텍스트를 명시적으로 전달할 필요가 있을까? 우리는 이렇게 코드가 복잡해지는 것을 원하지 않았다.

CurrentMemoryContext는 가능한 수명이 짧은 context를 가리키는 것이 좋다. 쿼리가 실행되는 동안 CurrentMemoryContext가 가리키는 컨텍스트는 매 tuple을 처리할 때마다 초기화된다. 극히 일부 코드에서만 트랜잭션보다 수명이 긴 메모리 컨텍스트를 가리켜야 한다. 수명이 긴 메모리 컨텍스트는 메모리 누수의 위험성을 높인다.

pfree/repalloc

pfreerepalloc 해제하려는 chunk가 현재 메모리 컨텍스트(CurrentMemoryContext)에 포함되든 말든 호출 가능하다. chunk가 포함된 메모리 컨텍스트가 자동으로 호출(invoke)된다.

Parent and Child Contexts

모든 메모리 컨텍스트가 서로에 대해 독립적이라면 메모리 컨텍스트들을 추적하기 힘들다. 그래서 parent와 child 컨텍스트로 이루어진 tree를 만들었다. 하나의 메모리 컨텍스트를 만들 때 새로운 메모리 컨텍스트가 어떤 메모리 컨텍스트의 child가 될지 명시할 수 있다. 메모리 컨텍스트는 많은 child를 가질 수 있지만 parent는 하나만 가질 수 있다. 이론적으로는 이런 tree가 여러 개가 되어 숲을 이룰 수 있지만, 지금은 하나의 최상위 컨텍스트(TopMemoryContext)만 사용하고 있다.

하나의 메모리 컨텍스트를 삭제하면 그 하위의 모든 컨텍스트들이 연쇄적으로 삭제된다.

이러한 특성들 때문에 상위 컨텍스트 하나만 추적하면 메모리 누수에 대해 걱정할 필요가 없다. 트랜잭션이 끝날 때 삭제할 컨텍스트 하나만을 추적하고 트랜잭션보다 lifetime이 짧은 나머지 컨텍스트들은 추적하는 컨텍스트의 descendant로 생성하면 되기 때문이다.

Memory Context Reset/Delete Callbacks

Postgres 9.5부터 도입된 이 기능은 메모리 컨텍스트를 단순히 palloc으로 할당한 메모리를 관리하는 것을 넘어 다양한 자원을 관리하는데 사용할 수 있게 해준다. 이것은 "reset callback function"을 메모리 컨텍스트에 등록함으로써 가능해진다. 이렇게 등록된 함수는 컨텍스트가 리셋되거나 삭제되기 직전에 호출된다. 이 기능은 특정 컨텍스트에서 할당한 객체와 연관된 자원을 해제(give up)하기 위해 사용할 수 있다. 예를 들면 다음과 같은 상황에서 사용할 수 있다.

  • 하나의 tuplesort object와 관련 있는 파일들을 닫는다
  • 해제되는 컨텍스트에서 생성된 장기 캐시 객체들에 대한 reference count를 해제
  • palloc으로 생성한 object에 연관된 malloc 메모리 해제

마지막 예시는 순수한 Postgres 코드에 대해서는 바람직하지 못한 프로그래밍 관행이다. 모든 메모리 할당은 특정 컨텍스트에서 palloc을 사용하는 것이 좋다. 하지만 Postgres가 아닌 라이브러리의 인터페이스에 대해서는 유용하다.

이 기능을 제공하는 API는 callback의 상태를 저장하기 위해 호출자에게 MemoryCotnextCallback이라는 memory chunk를 요구한다. 일반적으로 이 메모리는 callback이 등록될(attached) 컨텍스트와 동일해야 한다. 그래야 사용된 후 자동으로 해제되기 때문이다. (callback은 컨텍스트의 리셋 또는 삭제 직전에 호출됨)

Globally Known Contexts

전역변수를 통해 접근할 수 있는 컨텍스트가 있다. 시스템에는 추가적인 컨텍스트들이 여러 개 존재할 수 있지만, 모든 추가적인 컨텍스트들은 아래의 컨텍스트의 자손이어야 한다. 그래야 error가 발생하더라도 메모리 누수를 막을 수 있다.

TopMemoryContext

컨텍스트 tree의 최상단 level의 컨텍스트 중 하나이다. 다른 모든 컨텍스트들은 이 컨텍스트의 직/간접적인 child이다. 이 컨텍스트에서 메모리를 할당하는 것은 본질적으로 malloc을 호출하는 것과 동일하다. 이 컨텍스트는 절대 reset되거나 삭제되지 않기 때문이다. 이 컨텍스트는 DB 인스턴스와 수명을 같이 하거나 제어 모듈이 적절한 시점에 삭제할 것들을 위해 사용한다. 예로는 fd.c의 open file table이 있다. 정말 필요한 것이 아닌데 이곳에 할당하는 것과 CurrentMemoryContext가 이곳을 가리키는 것은 피해야 한다.

PostmasterContext

postmaster가 일반적으로 실행되는 컨텍스트이다. 백엔드가 생성된 후 백엔드에서는 이 컨텍스트를 삭제할 수 있다. 이 컨텍스트에는 postmaster에서는 사용했지만 백엔드에서는 더 이상 사용하지 않는 메모리 복사본이 저장되어 있기 때문이다. 단, non-EXEC_BACKEND 빌드에서는 postmaster가 가지고 있는 pg_hba.conf와 pg_indent.conf 데이터 사본이 백엔드 프로세스의 인증 과정에서 직접적으로 사용된다. 따라서 이 인증 작업이 끝나기 전에는 이 컨텍스트를 삭제할 수 없다. (Postmaster는 TopMemoryContext와 ErrorContext만을 가진다. 나머지 최상위 컨텍스트들은 백엔드가 시작(startup)될 때 설정된다)

CacheMemoryContext

relcache, catcache 그리고 관련 모듈들이 저장되는 영구적인 저장소이다. 이 컨텍스트는 리셋되거나 삭제되지 않는다. 그렇기 때문에 TopMemoryContext와 구별하는 것이 필수적이지는 않다. 하지만 디버깅을 위해 구별할 가치가 있다. CacheMemoryContext는 자신보다 수명이 짧은 child를 가진다. relcache entry를 저장하기 위해 이런 child를 이용할 수 있다. 이렇게 하면 rule parsetree를 쉽게 해제할 수 있다.

MessageContext

이 컨텍스트는 프론트엔드가 전달한 현재 command message 뿐만 아니라 현재 메세지와 수명을 같이하는 메모리 공간(storage)에 대한 정보를 저장하고 있다. Simple-Query 모드에서 parse tree와 plan tree가 여기에 저장될 수 있다. PostgresMain의 바깥 loop의 각 사이클마다 이 컨텍스는 리셋되고 모든 child는 삭제된다. 이것은 트랜잭션 단위와 portal 단위의 컨텍스트들과 분리되어 있다. 하나의 쿼리 문자열의 수명은 하나의 트랜잭션이나 portal보다 길거나 짧을 수 있기 때문이다.

TopTransactionContext

최상위 계층(Top-level) 트랜잭션이 종료될때까지 살아있는 모든 것들을 가지고 있는 컨텍스트이다. 이 최상위 계층 트랜잭션 한 사이클이 종료되면 모든 child들은 삭제되고 이 컨텍스트는 리셋된다. 대부분의 상황에서 이 컨텍스트에 직접 공간을 할당하지는 않고 CurTransactionContext에서 할당한다. 이 컨텍스트에서 할당되는 공간에는 여러 하위 트랜잭션(subtransaction)을 관리하기 위해(그 하위 트랜잭션의 수명을 초월해서 존재하는) 필요한 정보들이 저장된다. 이 컨텍스트는 에러가 발생했다고 해서 즉시 청소(clear) 되지는 않는다. 이 컨텍스트는 해당 트랜잭션 블록이 COMMIT 또는 ROLLBACK에 의해 종료될 때까지 정보를 저장하고 있다.

CurTransactionContext

현재 트랜잭션이 종료될때까지 유지되어야 하는 정보들을 저장한다. 특히 최상위 트랜잭션 커밋 시에 필요한 정보를 저장한다. 현재 실행중인 트랜잭션이 최상위 트랜잭션이면 이 컨텍스트는 TopTransactionContext와 동일하다. 반대로 하위 트랜잭션인 경우 child context를 가리킨다. 하나의 트랜잭션이 종료(abort)되면 해당 트랜잭션의 CurTransactionContext는 종료 처리를 끝낸 뒤 제거(thrown away)된다. 하지만 커밋된 하위 트랜잭션의 CurTransactionContext는 최상위 트랜잭션이 커밋될 때까지 유지된다. 이런 매커니즘은 실패한 트랜잭션의 데이터를 필요 이상으로 오래 유지하는 것을 막는다. 이런 특성 때문에, 하위 트랜잭션 중단(abort) 과정에서 메모리를 적절히 해제해주는데 신경 써야 한다. 중단된 하위 트랜잭션의 상태(데이터)는 상위 트랜잭션의 어떤 포인터들도 가리키면 안된다. 그렇지 않으면 dangling(이미 해제된 공간을 가리키는) 포인터를 가지게 되고 상위 트랜잭션의 커밋에서 사고를 유발한다. CurTransactionContext에 저장되는 데이터의 예시에는 대기중(pending)인 NOTIFY 메세지가 있다. 이 메세지들은 하위 트랜잭션의 중단 없이 최상위 트랜잭션의 커밋된 경우에만 전송된다. 하위 트랜잭션에서 상위 트랜잭션으로 전송한다.

PortalContext

이것은 독립적으로 존재하는 트랜잭션은 아니다. 현재 활성화된 execution portal의 컨텍스트를 가리키는 전역 변수이다. 이 컨텍스트는 현재 portal이 요구하는 만큼 지속되어야 하는 데이터를 저장하는데 필요한 공간을 할당하는데 사용할 수 있다.

ErrorContext

영구적인 컨텍스트이다. 에러 복구(recovery) 과정에서 이 컨텍스트로 전환되고 recovery가 완료되면 리셋된다. 이 컨텍스트는 항상 몇 KB의 공간을 가지고 있다. 그렇기 때문에 백엔드의 메모리가 부족하더라도 에러 복구를 위한 메모리는 보장된다. 이 컨텍스트 덕분에 out-of-memory 상황도 FATAL error가 아닌 normal error로 대응할 수 있다.

references