TT Lab
블로그 시작하기
배우기 러닝패스 코스

프로파일링: 느린 요청 하나를 함수까지

힙·고루틴·잠금: CPU 밖의 프로파일

TT Lab 에서 이어서 보기

목표

같은 pricing 에서 heap·goroutine·mutex·block 프로파일을 받아, 각 프로파일이 서로 다른 결함을 가리키는 것을 확인합니다. CPU 프로파일만 보는 습관이 무엇을 놓치는지 손으로 겪습니다.

왜 중요한가

「느리다」의 원인이 늘 CPU 는 아닙니다. 메모리를 붙잡아 GC 가 잦아질 수 있고, 고루틴이 쌓여 결국 OOM 으로 죽을 수 있고, 잠금을 기다리느라 CPU 는 놀고 있는데 응답은 느릴 수 있습니다. CPU 프로파일은 이 셋을 보여 주지 않습니다. 잠금을 기다리는 동안은 CPU 를 쓰지 않기 때문입니다(off-CPU).

Go 는 이 프로파일들을 표준 라이브러리로 내주지만 성격이 다릅니다. heap 은 「지금 붙잡고 있는 것(inuse)」과 「지금까지 할당한 것(alloc)」을 함께 담고, goroutine 은 지금 이 순간의 스냅숏이며, mutex·block 은 기본으로 꺼져 있어서 켜지 않으면 빈 파일이 나옵니다. 어느 프로파일이 무엇을 언제부터 모으는지 알아야 빈 결과를 「문제가 없다」로 잘못 읽지 않습니다.

단계

  1. pricing 을 새로 띄우고 -c 4 -d 12s 부하가 끝난 뒤 heap 프로파일을 /root/prof/profiles/m4-heap.pb.gz 에 저장하세요.
  2. inuse_space 와 alloc_space 각각의 flat 1위 함수를 /root/prof/answers/heap.json 에 적으세요.
  3. 큰 주문을 늘린 부하(-big 0.3 -d 10s) 뒤 goroutine 프로파일을 /root/prof/profiles/m4-goroutine-1.pb.gz 에 받고, 고루틴이 가장 많이 멈춘 main 함수와 그 수를 /root/prof/answers/goroutine.json 에 적으세요.
  4. 같은 부하를 한 번 더 준 뒤 /root/prof/profiles/m4-goroutine-2.pb.gz 를 받고 두 수를 /root/prof/answers/goroutine-growth.json 에 적으세요.
  5. 지금 설정 그대로 mutex 프로파일을 /root/prof/profiles/m4-mutex-off.pb.gz 에 받고 표본 수를 /root/prof/answers/mutex-off.txt 에 적으세요.
  6. PPROF_MUTEX_FRACTION=5 PPROF_BLOCK_RATE=100000 으로 pricing 을 다시 띄우고 -c 16 -coupon 0 -d 10s 부하 뒤 /root/prof/profiles/m4-mutex.pb.gz·/root/prof/profiles/m4-block.pb.gz 를 받으세요.
  7. 프로파일마다 가장 큰 비중의 main 함수(잎에 가장 가까운 것)를 /root/prof/answers/m4-summary.json 에 적으세요.

참고

부하 뒤의 힙

prof-svc up pricing 으로 새로 띄우고 loadgen -c 4 -d 12s -out /root/prof/runs/m4-load.json 이 끝난 뒤 /debug/pprof/heap 을 /root/prof/profiles/m4-heap.pb.gz 에 저장하세요.

heap 프로파일은 받는 순간의 상태입니다. 새로 띄운 서비스는 아직 아무것도 붙잡지 않았으니, 부하를 준 뒤에 받아야 붙잡힌 메모리가 보입니다.

붙잡은 것과 할당한 것

같은 heap 파일에서 -sample_index=inuse_space 와 -sample_index=alloc_space 의 flat 1위 함수를 각각 inuse_top·alloc_top 으로 /root/prof/answers/heap.json 에 적으세요.

inuse 는 GC 뒤에도 살아 있는 것, alloc 은 프로세스가 뜬 뒤 할당한 모든 것입니다. 많이 할당하고 곧 버리는 함수와 조금씩 영원히 붙잡는 함수는 서로 다른 칸에서 1위가 됩니다.

멈춰 있는 고루틴

loadgen -big 0.3 -d 10s -out /root/prof/runs/m4-big1.json 뒤 /debug/pprof/goroutine 을 /root/prof/profiles/m4-goroutine-1.pb.gz 에 저장하고, 고루틴이 가장 많이 멈춰 있는 main 함수와 그 수를 leak_function·count 로 /root/prof/answers/goroutine.json 에 적으세요.

goroutine 프로파일의 값은 시간이 아니라 「그 스택에 지금 있는 고루틴 수」입니다. 잎은 대개 runtime.gopark 이니, 그 아래에서 main 패키지 함수를 찾으세요(-top -cum).

누수의 증거는 두 장이다

같은 부하를 한 번 더(-out /root/prof/runs/m4-big2.json) 준 뒤 /root/prof/profiles/m4-goroutine-2.pb.gz 를 받고, 같은 함수의 고루틴 수를 before·after 로 /root/prof/answers/goroutine-growth.json 에 적으세요.

고루틴이 많다는 것만으로는 누수가 아닙니다. 일이 끝났는데도 줄지 않고 부하에 비례해 쌓여야 누수입니다. 스냅숏 두 장을 견줍니다.

켜지 않은 프로파일은 비어 있다

지금 떠 있는 pricing(기본 설정)에서 /debug/pprof/mutex 를 /root/prof/profiles/m4-mutex-off.pb.gz 에 받고, 표본 수(contentions 합)를 /root/prof/answers/mutex-off.txt 에 숫자로 적으세요.

Go 는 잠금 경합을 기본으로 모으지 않습니다. runtime.SetMutexProfileFraction 으로 켜야 합니다. 빈 프로파일은 「경합이 없다」가 아니라 「보고 있지 않다」입니다.

잠금을 기다린 시간

PPROF_MUTEX_FRACTION=5 PPROF_BLOCK_RATE=100000 prof-svc up pricing 으로 다시 띄우고 loadgen -c 16 -coupon 0 -d 10s -out /root/prof/runs/m4-locks.json 뒤 /root/prof/profiles/m4-mutex.pb.gz 와 /root/prof/profiles/m4-block.pb.gz 를 받으세요.

mutex 프로파일은 잠금을 놓는 쪽(Unlock)에서 「다른 고루틴을 얼마나 기다리게 했나」를, block 프로파일은 기다린 쪽에서 「얼마나 막혔나」를 적습니다. 같은 경합을 두 방향에서 봅니다.

프로파일마다 다른 범인

heap(inuse_space)·goroutine(두 번째 스냅숏)·mutex·block 프로파일 각각에서 가장 큰 비중을 가진, 잎에 가장 가까운 main 함수를 inuse·goroutine·mutex·block 키로 /root/prof/answers/m4-summary.json 에 적으세요.

표본마다 잎에서 뿌리 쪽으로 올라가며 처음 만나는 main 함수(main.quote 계열 제외)에 값을 더합니다. 3단계에서 쓴 방법을 네 파일에 그대로 적용하면 됩니다.