perf 소프트웨어 이벤트로 CPU 병목 진단하기

클라우드 VM이나 컨테이너에서 perf stat -e cycles를 돌리면 “not supported”만 뜨고 아무것도 안 나오는 경우가 흔하다. 하이퍼바이저가 하드웨어 성능 카운터(PMU)를 게스트에 노출하지 않기 때문인데, IPC나 캐시 미스 같은 마이크로아키텍처 지표는 이 상태에서 영영 못 얻는다. 하지만 커널이 직접 세는 소프트웨어 이벤트(task-clock, context-switches, cpu-migrations)와 perf sched의 스케줄링 지연 통계는 하드웨어 PMU와 무관하게 항상 쓸 수 있고, 이것만으로도 “CPU가 부족해서 느린 건지” 여부는 충분히 진단할 수 있다. 이 글에서는 실제로 hardware PMU가 없는 VM에서 이 대안으로 CPU 경쟁 상황을 실측해본다.

hardware PMU가 없는 환경 확인

$ sudo perf stat -e cycles,instructions ls /
Error:
No supported events found.
Access to performance monitoring and observability operations is limited.
...
The cycles event is not supported.

root 권한으로도 동일하게 실패한다 — 권한 문제가 아니라 하이퍼바이저가 vPMU를 게스트에 전달하지 않는 구조적 제약이다. perf list로 확인하면 software: 항목(task-clock, context-switches, cpu-migrations, page-faults 등)은 이런 환경에서도 그대로 사용 가능하다.

perf stat 소프트웨어 이벤트로 경쟁 여부 판단

순수 CPU-bound 워크로드로 무한 루프를 도는 C 프로그램을 하나 만들었다.

#include <stdint.h>

int main(void)
{
	volatile uint64_t x = 0;
	while (1)
		x++;
	return 0;
}

이 프로세스 하나만 코어 0에 고정해 4초간 소프트웨어 이벤트를 측정했다.

$ taskset -c 0 ./busyloop &
$ sudo perf stat -p $(pgrep busyloop) \
    -e task-clock,context-switches,cpu-migrations,page-faults -- sleep 4

          4,025.80 msec task-clock
                12      context-switches
                 0      cpu-migrations
                 0      page-faults

       4.027771059 seconds time elapsed

4초 내내 CPU를 거의 독점했다(task-clock 4025ms ≈ elapsed 4028ms). 이제 같은 코어 0에 동일한 busyloop을 3개 더 추가해 총 4개가 경쟁하는 상태에서 그중 하나만 다시 측정했다.

$ taskset -c 0 ./busyloop &  # 총 4개, 모두 코어 0에 고정
$ taskset -c 0 ./busyloop &
$ taskset -c 0 ./busyloop &
$ taskset -c 0 ./busyloop &
$ sudo perf stat -p 29233 \  # 4개 중 첫 번째로 띄운 프로세스
    -e task-clock,context-switches,cpu-migrations,page-faults -- sleep 4

            999.32 msec task-clock
               347      context-switches
                 0      cpu-migrations
                 0      page-faults

       4.002216705 seconds time elapsed

같은 4초를 재도 task-clock이 999ms로 뚝 떨어지고(코어 하나를 넷이 나눠 쓰니 정확히 25%), context-switches는 12회에서 347회로 30배 가까이 뛰었다. hardware PMU 없이도 task-clock/elapsed 비율이 낮으면서 context-switches가 비정상적으로 많다면, CPU 자체가 느린 게 아니라 코어 수 대비 실행 대기 중인 프로세스가 너무 많다는 뜻으로 읽을 수 있다.

perf sched latency로 대기시간 직접 재기

context-switches 횟수는 경쟁이 있다는 신호일 뿐, 그로 인해 실제로 얼마나 기다렸는지는 알려주지 않는다. perf sched는 스케줄러 트레이스포인트를 기록해 이 대기시간을 직접 계산한다. 앞의 두 상황(단독 실행/4개 경쟁)을 각각 perf sched record로 3초씩 기록하고 perf sched latency로 집계했다.

$ sudo perf sched record -o perf.data -- sleep 3
$ sudo perf sched latency -i perf.data | grep busyloop

# 단독 실행
  busyloop:29812         |   3007.839 ms |       10 | avg:   0.008 ms | max:   0.015 ms

# 코어 0에서 4개 경쟁
  busyloop:(4)            |   3013.261 ms |     1050 | avg:   8.577 ms | max:  17.313 ms

평균 대기시간이 0.008ms에서 8.577ms로, 최대 대기시간은 0.015ms에서 17.313ms로 뛰었다 — 1000배 안팎의 차이다. context-switches 횟수만으로는 “얼마나 심각한지” 감이 안 오지만, perf sched latency의 avg/max 값은 밀리초 단위로 바로 비교할 수 있어 실제 지연시간에 민감한 서비스에서 SLA 위반 여부를 판단하기에 더 직접적이다.

주의사항

항목내용
IPC·캐시 미스는 여전히 못 잰다소프트웨어 이벤트와 perf sched는 “코어가 부족한지”만 알려준다. 명령어 하나당 사이클 수, 캐시 적중률 같은 마이크로아키텍처 수준 분석은 하드웨어 PMU가 있는 베어메탈이나 vPMU passthrough를 지원하는 VM에서만 가능하다
perf_event_paranoid로 권한이 막힐 수 있다소프트웨어 이벤트도 perf_event_paranoid 값이 높으면(이 환경은 4) 일반 사용자는 접근이 막힌다. sudo로 실행하거나 CAP_PERFMON을 부여할 것
task-clock은 코어 수만큼 초과될 수 있다멀티스레드 프로세스를 여러 코어에서 동시에 측정하면 task-clock이 elapsed보다 커질 수 있다. 이 글의 예제는 단일 스레드를 코어 하나에 고정했기 때문에 elapsed가 상한이다
perf sched record는 오버헤드가 크다모든 스케줄링 이벤트를 기록하므로 장시간 프로덕션에 걸어두면 안 된다. 문제가 재현되는 짧은 구간만 기록할 것

마무리

hardware PMU가 막힌 VM이라고 CPU 병목 진단 자체가 불가능한 건 아니다. task-clock·context-switches로 경쟁 여부를 먼저 잡아내고, perf sched latency로 실제 대기시간을 밀리초 단위로 확인하면 “코어가 부족해서 느린지”는 하드웨어 카운터 없이도 충분히 구분된다. 다만 그 이상의 원인(캐시 미스, 분기 예측 실패 등)까지 파고들려면 결국 실제 하드웨어가 필요하다.

참고

답글 남기기