왜 CPU 프로파일링이 필요한가
서비스 응답이 느려지거나 특정 워커의 CPU가 100%에 붙어 있을 때, 로그와 메트릭만으로는 "어느 코드가 CPU를 태우는지"를 알 수 없다. top이 알려주는 것은 프로세스 단위 점유율일 뿐, 콜스택 단위의 비용은 보이지 않는다. 이때 필요한 것이 샘플링 프로파일러다. Linux perf는 커널 성능 카운터를 이용해 주기적으로 스택을 캡처하고, 이를 플레임그래프로 시각화하면 어떤 함수 경로가 전체 CPU 시간의 몇 %를 차지하는지 한눈에 드러난다.
perf가 동작하는 방식
perf는 기본적으로 CPU 사이클마다 인터럽트를 걸어 그 순간의 콜스택을 샘플링한다. 예를 들어 99Hz로 샘플링하면 초당 99번 스택을 뜨고, 특정 함수가 자주 잡힐수록 그 함수가 CPU를 오래 점유한다는 뜻이다. 이는 코드에 계측을 삽입하는 방식이 아니라 관측만 하는 방식이라 오버헤드가 낮고 프로덕션에서도 쓸 수 있다.
다만 정확한 콜스택을 얻으려면 프레임 포인터나 DWARF 정보가 필요하다. 최적화 빌드에서 프레임 포인터가 생략되면 스택이 끊기므로, 이 점이 첫 번째 함정이다.
기본 사용법: record와 report
대상 PID를 30초간 99Hz로 샘플링하는 가장 흔한 패턴이다. -g로 콜그래프를 함께 수집한다.
sudo perf record -F 99 -p $(pgrep -f my-service) -g -- sleep 30
# 콜그래프 방식을 명시하려면
sudo perf record -F 99 -p 12345 --call-graph dwarf -- sleep 30
# 텍스트로 상위 소비 함수 확인
sudo perf report --stdio --sort overhead -n | head -40
--call-graph fp는 프레임 포인터, dwarf는 디버그 정보를 이용한 스택 언와인딩이다. dwarf는 프레임 포인터가 없어도 동작하지만 샘플 크기가 크고 오버헤드가 높다.
플레임그래프로 시각화하기
report의 텍스트는 넓은 콜트리를 파악하기 어렵다. Brendan Gregg의 FlameGraph 스크립트로 SVG를 만들면 가로축은 샘플 비율, 세로축은 스택 깊이를 나타내 병목 경로가 넓은 봉우리로 보인다.
sudo perf script > out.perf
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > cpu.svg
# 커널 심볼까지 보려면 커널 심볼 접근 허용이 필요
echo 0 | sudo tee /proc/sys/kernel/kallsyms_restrict
생성된 SVG에서 특정 함수를 클릭하면 해당 하위 스택만 확대된다. "넓지만 낮은" 봉우리는 스스로 CPU를 쓰는 함수(self time), "깊게 쌓인" 스택은 호출 경로가 긴 것이다.
심볼이 깨질 때: 인터프리터와 JIT
Python·Node·JVM 같은 런타임은 네이티브 스택만 봐서는 함수 이름이 나오지 않고 주소나 [unknown]으로 표시된다. 언어별 심볼 매핑이 필요하다.
# Python: 네이티브 스택 대신 py-spy로 파이썬 프레임 수집
py-spy record -o py.svg --pid 12345 --duration 30
# JVM: perf-map-agent로 /tmp/perf-.map 생성 후 perf 사용
java -XX:+PreserveFramePointer -jar app.jar
JVM은 -XX:+PreserveFramePointer가 없으면 스택이 끊기므로 프로파일링 대상 프로세스에 이 옵션을 켜 두는 것이 좋다.
on-CPU vs off-CPU, 무엇을 볼 것인가
perf의 기본 CPU 프로파일링은 "실행 중"인 시간만 잡는다. 스레드가 락 대기, I/O, 슬립으로 멈춘 시간은 잡히지 않는다. 응답이 느린데 CPU는 낮다면 off-CPU 분석이 필요하다.
| 구분 | 대상 | 도구 |
|---|---|---|
| on-CPU | 연산·바쁜 루프 | perf record -g |
| off-CPU | 락 대기·I/O 블로킹 | bpftrace / offcputime |
즉 CPU가 높으면 on-CPU 플레임그래프, CPU가 낮은데 느리면 off-CPU를 봐야 원인을 정확히 짚는다.
실무 주의점
첫째, 컨테이너 환경에서는 perf가 SYS_ADMIN 또는 perf_event_paranoid 완화를 요구한다. echo 1 | sudo tee /proc/sys/kernel/perf_event_paranoid로 조정하되 보안 정책을 확인한다. 둘째, 샘플링 주기를 너무 높이면(-F 999 이상) 관측 자체가 부하를 만든다. 프로덕션은 99Hz 수준이 무난하다. 셋째, 릴리스 바이너리는 심볼이 스트립되어 있으니 디버그 심볼 패키지를 함께 배포하거나 별도 debuginfo를 준비해야 함수 이름이 나온다. 마지막으로 플레임그래프는 "평균"을 보여줄 뿐이므로, 짧은 스파이크는 여러 번 캡처해 비교하는 것이 안전하다.