얼마전에 깃헙 블로그에 SL[AUO]B 할당자의 CPU Flame Graph를 간단하게 분석해서 올렸었는데, perf에서 프로파일링 하는 방식을 제대로 모른 채 사용하다보니 결과가 왜곡됐었다. 그래서 간단하게라도 기술적인 부분을 정리하려고 한다.
TMI지만 이 글 제목을 원래는 "함수 호출 스택 샘플링"이라고 하려다가, CPU 샘플링/프로파일링이라는 말을 더 많이 쓰는 것 같아서 지금의 제목이 되었다.
Sampling
우리는 워크로드에서 어떤 일이 일어나는지를 분석하고 싶어한다. 뭐 캐시 미스나 사이클당 명령어와 같은 PMC (Performance Monitoring Counter)일 수도 있고, 아니면 특정 벤치마크에서 어떤 컴포넌트가 얼마나 CPU를 사용하는지가 궁금할 수도 있다. 간단한 예로 함수 호출 스택이 궁금하다고 해보자. 어떤 함수가 얼마나 호출되는지 파악하는 가장 간단한 방법은 함수의 시작과 끝에 명령어를 삽입해서 함수의 진입과 탈출을 기록하는 것이다. (tracepoint/kprobe/kretprobe처럼 말이다.)
하지만 너무 자주 실행되는 함수들 ㅡ 초당 수백만번 실행되는 함수에 이런 명령어를 삽입해버리면 오버헤드가 감당하지 못할 정도로 커져서 production에서는 사용하지 못할 것이다.
대신 일정 주기마다 CPU가 어떤 함수를 호출하고 있었는지 스냅샷을 찍은 후에 스냅샷을 차곡차곡 모으면 워크로드에서 어떤 함수를 얼마나 호출하는지에 대한 유의미한 지표가 될 것이다. 설명하다보니 말이 길어졌는데 결국 sampling이라고 함은 모든 이벤트를 하나씩 세는 게 아니라 일부 이벤트로 전체적인 경향을 보고자 하는 통계적인 프로파일링 방법이다. 반대로 모든 이벤트를 하나씩 측정하는 것을 tracing이라고 한다.
perf
$ sudo perf record -F 99 -a -g -- sleep inf
perf record는 스택 트레이스 뿐만 아니라 다양한 성능 지표들을 perf.data라는 파일에 기록한다.
-F: 초당 몇번씩 샘플을 기록할지 주기를 나타낸다. 보통 49Hz나 99Hz를 많이쓴다. 50이나 100과 같은 값을 사용할 경우에는 lockstep sampling으로 인해 결과가 왜곡될 수 있다고 한다.
-a: 모든 CPU에 대해서 샘플을 기록하라는 의미이다. 이걸 명시하지 않으면 'sleep inf'라는 명령어에 대해서만 샘플을 기록한다.
-g --: perf.data에 호출 스택을 표시하라는 뜻이다. 이때 -- 대신 다른것을 적으면 stack unwinding하는 방법을 선택할 수 있다.
sleep inf: 실행할 명령어이다. perf는 이 명령어가 끝날 때까지 기록한다.
이렇게 perf record를 해서 기록된 perf.data 파일을 분석하면 함수 호출 스택을 분석할 수 있다. 가장 간단한 방법은 perf report 명령어로 perf.data 파일을 분석하는 것이다.
$ sudo perf report
FlameGraph
하지만 perf report는 너무 출력이 많아서 한 번에 보기가 쉽지 않다. Brendan Gregg가 개발한 FlameGraph라는 도구는 함수 호출 스택을 시각적으로 잘 보여준다.
$ git clone https://github.com/brendangregg/FlameGraph
$ sudo mv perf.data ./FlameGraph
$ cd FlameGraph
$ sudo perf script | ./stackcollapse-perf | ./flamegraph > flamegraph.svg
이때 함수 호출은 아래에서 위로 이루어지며, 바의 폭이 넓을 수록 캡처된 스냅샷의 수가 많은 것이다. 폭이 넓다고 무조건 더 많은 시간을 사용했다고 하긴 어려운데, 호출 빈도에 따라 스냅샷의 수가 달라지기 때문이다. 바의 왼쪽에서 오른쪽 순서는 큰 의미가 없다. (알파벳 순이었던 걸로 기억한다.)
perf 사용시 주의할점
샘플링을 하려면 어떤 주기적인 이벤트가 발생할 때마다 실행을 멈춘 후 어떤 함수가 호출되고 있었는지 기록해야 한다. 이 때 '주기적인 이벤트'가 무엇이냐에 따라서 결과가 정확할 수도, 왜곡될 수도 있다.
PMU(Performance Monitoring Unit)의 NMI(Non-Maskable Interrupt)기반
perf record로 샘플링을 할때, x86에서는 기본적으로 PMU에서 발생시키는 NMI (Non-Maskable Interrupt)를 사용한다. 이 방법의 장점은 인터럽트가 비활성화된 함수 호출 경로도 샘플에 포함된다는 장점이 있다. 하지만 PMU가 사용 불가능한 경우에는 사용할 수 없다. 이때 perf에서는 일정 CPU 사이클마다 NMI를 발생시키도록 해서 정해진 주기로 함수 호출 스택을 샘플링할 수 있다.
hrtimer를 활용한 소프트웨어 인터럽트 기반
만약 PMU를 사용할 수 없는 경우에는 타이머 인터럽트가 발생할 때 샘플을 수집한다. 따라서 인터럽트가 비활성화된 구간의 샘플은 수집할 수 없다. 보통 클라우드 플랫폼에서는 PMU를 일반적으로 사용할 수 없다. 가상화 기술을 사용한다고 해서 기술적으로 불가능한 건 아니지만 보통은 제공하지 않는다. 나 같은 경우에는 GCP에서 perf로 샘플링을 했는데, GCP에서 PMU를 지원하지 않아서 샘플이 왜곡된 형태로 수집되었다. 나는 슬랩 할당자를 분석하려고 했는데 spinlock을 들고있는 구간의 샘플을 통째로 수집하지 못했으니 결과가 엄청나게 왜곡되었을 것이다.
Lockstep Sampling
왜 샘플링 주기를 100Hz나 1000Hz가 아니라 99Hz, 999Hz로 해야하는지 찾다보면 lockstep sampling이 발생할 수 있어서라고 설명한다. lockstep sampling이 생기면 결과에 왜곡이 생간다고 한다. 예시가 있기는 한데 실제 프로그램의 실행에서 lockstep이 발생하는 이유를 정확히는 이해하지 못했다. (아신다면 댓글로 알려주세요...!)
함수 호출 스택 분석시 문제점
perf로 커널의 함수 호출을 분석한다고 하면 큰 문제가 없다. 왜냐하면 커널은 함수의 심볼을 찾기도 쉽고, 프레임 포인터를 사용하기 때문에 stack unwinding이 매우 쉽기 때문이다. 하지만 사용자 공간에서 실행되는 프로그램의 함수 호출 스택을 분석할 때는 골치가 아파진다. 크게 두 가지 문제가 존재하는데, 하나는 프레임 포인터를 사용하지 않아서 함수의 호출 스택을 제대로 파악하지 못하는 문제(stack unwinding에 실패하는 문제)와 함수의 호출 스택은 정확하게 파악했는데 호출된 함수의 심볼이 없어서 어떤 함수인지 알 수 없는 문제가 생길 수 있다. 둘 중 한 가지 문제라도 생기면 분석하기가 어렵다.
stack unwinding을 할 수 없는 경우
어플리케이션을 컴파일 할 때 프레임 포인터를 사용하면, 스택 프레임의 위치를 추적해서 함수의 호출 스택을 알아낼 수 있다. 하지만 이러한 stack unwinding이 불가능한 경우에는 1) 해당 프로그램에 대한 debuginfo 패키지를 찾아서 설치하거나 2) 프레임 포인터를 사용하도록 (gcc에선 -fno-omit-frame-pointer) 다시 컴파일 해야한다. 참고로 perf는 debuginfo를 사용한 stack unwinding을 지원하지만 BPF는 debuginfo를 심볼 확인용으로만 지원한다고 한다. 아직 프레임 포인터가 없을 때 stack unwinding 기술에 대해선 잘 모르기 때문에, 다른 글로 정리해보겠다.
심볼이 없는 경우
stack unwinding에 성공해서 호출한 함수의 주소를 정확히 찾았다고 하더라도, 함수의 주소만 있으면 우리는 아무것도 할 수 없다. 메모리를 덤프떠서 해당 주소에 있는 함수의 명령어를 분석해서 이름을 지어주지 않는 한은... 분석을 할 수가 없다. 커널 같은 경우에는 심볼 테이블을 따로 관리하기 때문에 함수의 이름을 찾기가 쉽지만, libc나 사용자 프로그램의 경우에는 바이너리에 심볼 테이블이 포함되어있는데, 이 심볼 테이블이 strip된 경우에는 난감해진다. 1) 이런 경우에는 해당 프로그램에 대한 debuginfo 패키지를 찾아서 설치하거나 2) 심볼 테이블이 바이너리에 포함되도록 다시 컴파일해야 한다.
더 나아가서, 샘플을 수집하는 기준이 되는 이벤트를 바꾼다면?
이 글에서 소개한 방법은 타이머 인터럽트나 PMU의 인터럽트를 사용한다. 둘 모두 주기가 거의 일정하기 때문에, 이 이벤트를 기준으로 함수 호출 스택을 수집하면 시간에 따른 함수 호출 빈도을 분석할 수 있다. 하지만 꼭 이 두 가지 이벤트가 아니라 스레드가 메모리를 할당하는 함수를 호출할 때마다 샘플을 수집한다면 메모리를 어디서 많이 할당하는지 알 수 있을 것이다. 아니면 CPU가 휴면 상태에 빠지는 함수가 호출될 때마다 샘플을 수집하면 off-cpu time이 발생하는 원인을 알아낼 수 있을 것이다.
perf record -e syscalls:sys_enter_brk -a -g -- sleep inf
예를 들어서 -e 옵션으로 brk() 시스템 호출이 발생할 때마다 샘플을 수집하면 어느 함수 호출 경로에서 힙을 많이 사용하는지 볼 수 있다.
참고 문서
뭔가 책 홍보 같지만 이 책 정말 좋다. (Brendan Gregg 그는 신이야...) 책에서는 C, Java, bash script 등등 언어별로 함수 호출을 샘플링하는 방법을 소개한다. 블로그에도 유용한 정보가 많다.
perf의 튜토리얼이다.
왜 샘플링 주기가 49/99Hz인지를 정리하려는 글인데 여기서도 결론은 명확하지 않다.
'System Performance' 카테고리의 다른 글
Linux Performance Analysis in 60 seconds (0) | 2021.12.04 |
---|
댓글