delete는 map의 capacity를 줄이지 않는다

computerphilosopher·2022년 11월 20일
2

개요

golang에서는 map의 원소를 지우기 위해 delete(mapName, key) 함수를 이용한다. 그런데 이 함수를 통해 map의 원소를 지운다고 해서 map이 메모리에서 차지하고 있는 공간이 해제되지는 않는다. map은 원소가 지워진 공간을 해제하지 않고 남겨놓았다가 다른 아이템이 들어오면 사용한다. 맵의 논리적인 크기(length)은 줄어들지만 물리적인 크기(capacity)는 줄어들지 않는 것이다.

웹서버와 같이 종료되지 않고 돌아가는 응용에서 전역 map을 사용할 경우 map이 차지하는 공간이 점점 늘어나서 OOM kill을 당할 가능성이 있다. 이번 포스팅에서는 map delete 연산이 메모리 사용량을 줄이지 않는다는 것을 코드로 검증하고 대안을 모색해본다.

검증

다음 코드는 mode=alloc 플래그를 주면 map을 무한히 할당하고, mode=delete 플래그를 주면 중간에 원소를 지운다. 혹시 GC에 의해 수거되지 않은 메모리가 OOM을 유발하는 것을 방지하기 위해 GC를 직접 실행시켰다. 빠르게 결과를 확인하기 위해 사전에 capacity를 110억으로 설정해두었다.

package main

import (
	"flag"
	"fmt"
	"runtime"
)

const capacity = 11000000000
const maxCap = 10000

func infiniteAlloc() {
	m := make(map[int]int, capacity)
	i := 0

	for {
		m[i] = i
		i += 1
	}

}

func infiniteAllocAndDelete() {
	m := make(map[int]int, capacity)
	i := 0

	for {
		m[i] = i
		i += 1

		if len(m)%maxCap == 0 {
			fmt.Printf("map length is %d, let's delete map\n", len(m))
			for k, _ := range m {
				delete(m, k)
			}
			runtime.GC()
		}
	}

}

func main() {
	mode := flag.String("mode", "alloc", "runninng mode")
	flag.Parse()
	if *mode == "alloc" {
		fmt.Println("infiniteAlloc")
		infiniteAlloc()
		return
	}
	if *mode == "delete" {
		fmt.Println("infiniteAllocAndDelete")
		infiniteAllocAndDelete()
		return
	}
    fmt.Printf("wrong flag: %s\n", *mode)
}

위 코드를 실행해 보니 delete 실행 여부와 관계없이 30초 내에 OOM을 당하는 것을 확인할 수 있었다.

대안

map의 원소를 전부 delete 하는 대신 새로운 map을 할당해서 대입하면 GC가 메모리를 수거해간다.

func infiniteAllocAndFree() {
	m := make(map[int]int, capacity)
	i := 0

	for {
		m[i] = i
		i += 1

		if len(m)%maxCap == 0 {
			fmt.Println("map length is %d, let's free map", i)
			m = map[int]int{}
			runtime.GC()
		}
	}

}

위 코드를 실행해보면 10분 이상의 시간이 지나도 메모리 사용량이 늘지 않는 것을 확인할 수 있다.

테스트 환경

  • MacBook Pro 2021년형
    • OS version: Monterey
    • CPU: Apple M1 Pro chip
    • Memory: 16GB
  • golang 버전: 1.19.1
  • 사용 도구: time, htop

참고 자료

golang 이슈 #20135

0개의 댓글