Go 프로그램에서 힙 메모리 관련

Go 프로그램에서 힙 메모리 관련 팁이다.


나는 간단한 툴은 Go로 짜면서 C/C++에서와는 다르게 스택이나 힙 메모리 관리를 잊어버리고 있었는데, 3rd party 사에 Go로 구현해서 릴리즈해 준 웹 서버가 가동 시간이 오래되면 OOM(Out Of Memory) Killer에 의해 웹 서버가 중단되는 사실을 알게 되었다. 🙄
3rd party에서 사용중인 서버는 AWS EC2의 T2 Micro로 메모리가 1GiB여서 Ubuntu를 올린 이후에는 가용 메모리가 넉넉치 않은 상황이었다.
Go 프로그램은 자체 GC(Garbage Collector)를 가지고 있어서 메모리가 부족해지면 알아서 GC가 수행되어 메모리를 확보할 줄 알았는데, 그렇게 되지 못해서 메모리 부족으로 OOM Killer가 서버 프로그램을 강제로 죽이는 상황이 발생하는 것이었다.


그래서 Go에서의 메모리 관리를 조금 찾아 보면서 이슈를 수정한 후, 간단히 정리해 보았다.

Heap 메모리 확인

스택 메모리는 메모리가 누적되거나 누수되지 않지만, 힙 메모리는 계속해서 메모리를 차지하므로 이것이 늘어나기만 하고 해제되지 않으면 프로그램이 사용하는 메모리는 계속해서 늘어나게 될 것이다.
Go에서 heap 사용 여부 확인은 빌드시 -gcflags "-m" 옵션을 추가하면 된다. 결과로 해당 메모리가 스택에 할당되는지 힙에 할당되는지의 정보가 나온다.
만약 함수 depth가 한 단계 더 출력되게 하려면 -gcflags "-m=2"와 같이 -m 옵션 뒤에 숫자를 증가시키면 된다.

그런데 이 로그를 분석하는 것이 결코 쉽지 않다. 역시 메모리를 자동으로 관리해 주는 편리함에는 그 만큼의 대가를 지불해야 하는가 보다.
예를 들어 C/C++는 프로그래머가 직접 메모리를 관리하므로, 조금 번거롭긴 하지만 실행 파일의 크기가 작고 메모리 누수를 찾기에도 오히려 쉬운 장점이 있다.

출력 로그 중에서도 특히 힙에 할당되는 메모리를 주목해야 하는데, “escapes to heap” 메시지가 stack이 아닌 heap에 할당된다는 의미이다. 힙에 할당된 메모리는 Go의 GC(Garbage Collector)의 대상이 되며, 참조하는 곳이 없으면 GC에 의해 해당 영역은 해제될 수 있다.


따라서 힙에 할당되는 메모리를 조사하여 해제가 되지 않아서 누수가 발생하는지 찾고, 찾았으면 해당 버그를 수정하면 된다. 그런데 나의 경우에는 이런 누수는 찾을 수 없었고 아무래도 메모리 누수 문제로 보이지는 않았다.
그래서 대안으로 GC가 원래보다 빠르게 수행되도록 하는 방법을 찾아 보았다.

프로그램에서 사용 메모리 확인

실행 중에 프로그램이 사용하는 메모리를 확인하고 싶으면 아래와 같은 코드를 추가하면 된다.

import (
    "runtime"
)

func main() {
    var mem runtime.MemStats

    runtime.ReadMemStats(&mem)
    fmt.Println("HeapAlloc:", mem.HeapAlloc, "\tNumGC:", mem.NumGC, "\tMallocs:", mem.Mallocs, "\tFrees:", mem.Frees)
    fmt.Println("live alloced:", mem.Mallocs-mem.Frees, "\tHeapReleased:", mem.HeapReleased)
}

또는 OS에서 제공하는 process가 사용하는 메모리 양을 보여주는 툴을 사용하면 된다.

GC trigger 세팅

가용 메모리 대비하여 얼마만큼의 메모리가 더 할당되면 GC가 트리거 되는지에 대해 백분율로 세팅할 수 있다. 디폴트는 100%이고, 아래 코드 예는 메모리가 가용 메모리 대비 50% 정도일 때 GC가 트리거 되도록 한다.

import (
    "runtime/debug"
)
func main() {
    debug.SetGCPercent(50)
}

프로그램에서 수동으로 GC 수행

또는 프로그램에서 수동으로 원하는 시점에 GC를 실행시키고 싶으면 아래 코드와 같이 호출하면 된다.

import (
    "runtime/debug"
)

func main() {
    debug.FreeOSMemory()
}

나의 경우에는 위 코드를 이용하여 주기적으로 GC를 수행시켜서 결과로 1GiB의 메모리만 가진 서버에서도 오랫동안 이슈없이 서버 프로그램을 동작시킬 수 있게 되었다. 🍺

카테고리:

업데이트: