Go 프로세스 메모리 사용량 분석 (1부)

왜 이 문제를 파헤쳐야 할까?

인프라 제공자로서 우리 서비스가 얼마나 많은 메모리를 사용하는지, 왜 그만큼 사용하는지 명확히 설명할 수 있어야 한다. 클라우드 개발자라면 이 정도 문제는 당연히 이해하고 있어야 한다.

§ 0x01 범위

실용성을 고려하여 Linux X86 플랫폼만 다룬다. 메모리 페이지 크기는 4KiB이다.

목표: 대규모 Go 프로세스의 메모리 소비 지점을 명확히 설명한다.

Go는 자체 런타임을 가지고 있어 메모리 구성이 복잡하다. 설명하고 이해하기 어려운 부분이 있으므로, 먼저 간단한 C 프로그램부터 시작한다.

§ 0x02 일반적인 C 프로그램

의존성이 없는 가장 간단한 프로그램을 어셈블리로 작성한다. AT&T 스타일을 선호한다:

.section .data  
.section .text  
.globl _start  
_start:  
movl $1, %eax  
movl $5, %ebx  
int $0x80

Makefile을 작성한다:

all: demo

demo: demo.o
    ld -static --no-dynamic-linker -o $@ -lc $<

demo.o: demo.s
    as -gstabs -o $@ $<

clean:
    rm ./demo ./demo.o

as로 컴파일하고 ld로 링크하여 정적 링크 바이너리를 생성한다. libc에 의존하지 않으며 Linux에서 가능한 최소 프로그램이다.

gcc와 gas 등 구성 요소가 필요하다. make 실행 후 demo 바이너리가 생성된다. 이 바이너리는 종료 코드를 설정하는 단일 시스템 호출만 있다. 실행해도 출력이 없으며, echo $?로 확인할 수 있다.

$ ./demo; echo $?
5

gdb로 프로그램을 중단시키고 메모리 사용량을 확인한다:

ulin  7116  0.0  0.0  168  24 pts/1    t   09:17   0:00 /mnt/d/code/lean/go-memory/asm/demo

RSS 값은 24KiB이다. 즉, 최소 프로그램도 24KiB를 차지한다.

왜 최소 프로그램이 24KiB를 차지하는지 분석한다. 프로세스는 바이너리가 메모리에 로드되어 형성되므로, 바이너리(ELF) 구조부터 살펴본다.

readelf -S로 섹션 크기를 확인한다:

섹션크기(B)설명
NULL0ELF의 빈 섹션, 4KiB 차지
.text (코드)1212B지만 정렬 후 16B
.symtab (심볼 테이블)144strip으로 제거 가능
.strtab (문자열)25
.shstrtab33

readelf -a -W demo로 ELF 레이아웃을 확인:

  1. ELF 헤더
  2. 프로그램 헤더
  3. 섹션들
  4. 섹션 헤더 테이블
구역오프셋크기
ELF 헤더064
프로그램 헤더6456 × 2 = 112
섹션1764096 + 16 + 144 + 25 = 4281
섹션 헤더 테이블43205 × 64 = 320

섹션 중 .text만 메모리에 로드된다. 코드가 12B인데 왜 RSS가 24KiB일까?

/proc/<pid>/smaps에 상세 메모리 레이아웃이 있다. Pss와 Rss의 차이점: Rss는 실제 세그먼트 크기, Pss는 프라이빗 세그먼트 크기다. .so 파일이 다른 프로세스와 공유되면 Pss = Rss / N이다. 실제 점유는 모든 세그먼트의 Pss 합계다.

세그먼트Pss설명
00400000-004010004KiB프로그램 헤더에 따라 NULL 섹션 로드, 읽기 전용
00401000-004020004KiB코드 세그먼트, rxp (실행 가능)
7ffff7ff9000-7ffff7ffd0004KiB환경 변수, 읽기 전용
7ffff7ffd000-7ffff7fff0004KiBvdso 세그먼트, 커널 매핑된 가상 공유 객체
7ffffffdd000-7ffffffff0008KiB스택

총 24KiB다. 메모리 페이지는 최소 단위(4KiB)로 할당되므로 코드 세그먼트가 4KiB 미만이어도 4KiB를 차지한다.

§ 0x03 요약

최소 C 프로그램의 메모리 사용량을 분석하여 RSS 구성을 명확히 했다. 전체 결론은 다음과 같다:

  1. ELF의 일부 섹션이 프로세스 메모리에 매핑된다.
  2. 프로세스의 최종 메모리 섹션은 두 가지 출처가 있다:
    • 프로그램 헤더에서 로더에게 어떤 섹션을 메모리에 로드할지와 해당 플래그를 알려준다.
    • 로더가 자체적으로 정의한 공유 세그먼트 (vdso, 스택, 환경 변수).

의문: 힙 세그먼트가 보이지 않는 이유는? 추측컨대 glibc가 정의하는 영역이며, 현재 어셈블리로 구현된 프로그램은 힙이 필요하지 않다.

태그: go 메모리관리 ELF linux 프로세스

7월 7일 03:10에 게시됨