Parca

6편. 실전 사례 ① – CPU 사용률은 정상인데, 성능은 느린 이유

SysBPF 2025. 12. 18. 20:27

실전 사례 ①

CPU 사용률은 정상인데, 성능은 느린 이유

운영 중인 서버에서 다음과 같은 리포트를 받았다고 가정해 보자.

  • CPU 사용률: 평균 40~50%
  • Load Average: 정상
  • 메모리 여유 충분
  • APM: 주요 API 응답 시간 “정상 범위”
  • 사용자 체감: “전체적으로 느림”

이 상황은 의외로 매우 흔하다.
그리고 가장 골치 아픈 유형이기도 하다.


1. 문제의 핵심: “정상 지표”의 함정

전통적 모니터링 지표

CPU: 45%
Memory: 62%
Disk IO: 낮음
Network: 정상

→ 문제 없어 보인다.


APM 지표

/api/search 평균 110ms
/api/order 평균 180ms
에러율 0%

 

→ 역시 문제 없어 보인다.

이 단계에서 흔히 나오는 결론은 이것이다.

“일시적인 네트워크 문제였나 봅니다.”

하지만 사용자는 여전히 느리다고 말한다.


2. 문제를 다시 정의하기

여기서 질문을 바꿔야 한다.

❌ “CPU가 얼마나 쓰이고 있는가?”
“CPU가 실제로 무엇을 하고 있는가?”

이 질문은
기존 메트릭이나 APM으로는 답할 수 없다.

이 지점에서 Parca를 투입한다.


3. Parca로 본 Flamegraph 첫 화면

Flamegraph를 처음 보면 이런 특징이 보인다.

  • 전체 폭은 넓지 않다 (CPU 사용률이 낮으니)
  • 그런데 커널 함수 비중이 유난히 크다
  • __schedule() / futex_wait() 가 반복 등장

이 화면이 말해주는 메시지는 명확하다.

CPU는 계산을 안 하고,
기다리는 데 시간을 쓰고 있다.


4. Flamegraph 해석 단계별 사고 과정

1단계: 바닥부터 보기

바닥(entry)에 다음과 같은 경로가 반복된다.

sys_futex
 └─ futex_wait
     └─ __schedule
 

이 패턴은 거의 공식처럼 해석할 수 있다.

“스레드가 락을 기다리며 잠들었다.”


2단계: 유저 코드와의 연결점 찾기

커널 스택 위쪽을 따라 올라가면
유저 공간 함수가 보이기 시작한다.

pthread_mutex_lock
 └─ cache_update()
     └─ handle_request()

이제 그림이 그려진다.

  • 요청 처리 중
  • 특정 cache_update 로직에서
  • mutex lock 대기 발생
  • 스레드가 잠들며 context switch

5. 왜 CPU 사용률은 낮았는가?

이 질문이 핵심이다.

CPU 사용률이 낮다는 것은:

  • CPU가 놀고 있다 ❌
  • 실행할 준비가 된 스레드가 없다 ⭕

락 대기로 스레드들이 잠들어 있으면
CPU는 놀게 된다.

즉,

CPU 사용률은 낮지만
시스템은 느릴 수 있다.


6. APM으로는 왜 보이지 않았을까?

APM의 관점은 “요청”이다.

  • 요청 시작
  • DB 쿼리
  • 비즈니스 로직
  • 응답 반환

락 대기는 보통 이렇게 보인다.

Business logic: 180ms

APM은 말하지 않는다.

  • 그 180ms 중
  • 몇 ms가 락 대기인지
  • 몇 ms가 실제 계산인지

Parca는 이 공백을 메운다.


7. 코드 레벨에서의 문제 예시

문제의 원인이 된 코드를 단순화하면 다음과 같았다.

pthread_mutex_lock(&cache_lock);

/* cache 갱신 */
update_global_cache(req);

pthread_mutex_unlock(&cache_lock);

 

문제는 이 코드가:

  • 모든 요청 경로에서
  • 동일한 전역 락을 사용
  • cache update 빈도가 높음

결과적으로:

  • 요청 수 증가
  • 락 경합 증가
  • 스레드 대기 증가
  • 체감 성능 저하

8. Parca가 아니었다면?

이 문제를 Parca 없이 찾으려면:

  • perf 수동 실행
  • 문제 시점 재현 시도
  • 운 좋게 같은 상황 발생
  • 커널 스택 해석

운영 환경에서는 거의 불가능에 가깝다.

Parca는 이미 문제 시점의 흔적
저장해 두고 있었다.


9. 개선 후 Diff Flamegraph

개선은 단순했다.

  • 전역 mutex 제거
  • shard 기반 lock 분리
  • cache update 최소화

배포 전/후 Diff Flamegraph 비교

Parca의 Diff Flamegraph
두 시점(또는 두 배포 버전)의 CPU 실행 경로 변화를 색상으로 직관적으로 보여준다.

  • 빨간색: CPU 사용이 증가한 경로
  • 파란색: CPU 사용이 감소한 경로
  • 회색/중립: 큰 변화 없음

배포 전 (Before)

  • __schedule()
  • futex_wait()
  • pthread_mutex_lock()

경로가 넓고 반복적으로 등장
→ 스레드가 자주 잠들고 깨어남
→ 락 경합 + 스케줄링 오버헤드


배포 후 (After)

  • 커널 스케줄링 경로 파란색으로 감소
  • 유저 함수 실행 경로 상대적으로 증가
  • Flamegraph의 “톱니형 분기” 감소

이는 다음을 의미한다.

CPU가 기다리는 시간은 줄고,
실제 일을 하는 시간은 늘어났다.


이 Diff 화면이 주는 결정적 가치

이 한 장의 Diff Flamegraph만으로도 다음을 동시에 설명할 수 있다.

  • 왜 체감 성능이 개선되었는지
  • 왜 CPU 사용률이 크게 변하지 않았는지
  • 왜 APM 지표는 큰 변화가 없었는지

즉,

“성능 개선이 실제로 CPU 실행 경로를 바꿨다”
는 증거가 된다.

10. 이 사례가 주는 교훈

이 사례의 핵심은 이것이다.

  • CPU 사용률은 결과일 뿐
  • 원인은 실행 경로에 있다
  • APM과 메트릭은 충분조건이 아니다

Parca는 다음 질문에 답해준다.

“CPU 시간이 그렇게 쓰였는가?”


체크리스트: 비슷한 증상을 만났다면

  • CPU 사용률은 정상인데 느리다
  • APM은 정상인데 체감이 나쁘다
  • 요청 수가 늘수록 더 느려진다
  • 컨텍스트 스위치가 많아 보인다

Parca로 커널 스택부터 확인하라


이 편의 결론

“느리다”는 말은 감정이지만,
Flamegraph는 증거다.

Parca는
설명하기 어려운 성능 문제를
말로 설명 가능하게 만들어 주는 도구다.


다음 편 예고

다음 편에서는
이번보다 한 단계 더 들어가서,

  • 컨테이너 환경
  • Kubernetes
  • PID / namespace / cgroup

에서 Parca를 어떻게 해석해야 하는지를 다룬다.


다음 편

7편. 컨테이너·Kubernetes 환경에서의 Parca – PID, Namespace, cgroup 관점