PerfView와 dotnet-trace로 “느림”의 원인을 찾는다 ── .NET 성능 조사 실무 입문

· · PerfView, dotnet-trace, ETW, 성능 조사, .NET, CSharp, 불량 조사, Windows 개발, 기술 상담

지난번 「Windows 이벤트 로그·ETW 입문」에서는 ETW가 무엇인지와 EventSource로 커스텀 이벤트를 정의하는 부분까지 다루었고, PerfView를 이용한 분석은 “범위 밖”으로 남겨두었습니다. 이 글은 그 후속편입니다. 업무 앱이 “느리다”, “CPU를 다 써버린다”, “가끔 멈춘다”고 할 때, PerfView와 dotnet-trace로 원인이 되는 함수까지 찾아내는 절차를 다룹니다.

참고로 이 글은 CPU·응답 시간 조사가 주제입니다. 메모리가 계속 늘어나는 문제를 구분하는 방법은 「.NET에서 GC 대기와 메모리 누수 구별하기」에서, 크래시가 발생하는 문제는 「WinDbg + SOS로 크래시 덤프 읽기」에서 다루고 있습니다.

1. 먼저 결론

  • “느림”에는 두 종류가 있습니다. CPU를 다 써버려서 느린 경우(CPU bound)와, CPU는 한가한데 대기 때문에 느린 경우(블록)입니다. 전자는 CPU 샘플링, 후자는 ThreadTime(컨텍스트 스위치) 수집이 필요하며, 기본 CPU 수집만 봐서는 후자의 원인이 보이지 않습니다. 1
  • 판단이 안 서면 dotnet-trace부터 시작합니다. EventPipe 기반의 크로스 플랫폼 도구로, 대상 프로세스를 실행한 것과 같은 사용자라면 관리자 권한 없이 수집할 수 있습니다. 2 수집한 .nettrace는 PerfView나 Visual Studio에서 열 수 있습니다. 3
  • PerfView가 필요해지는 것은 머신 전체·네이티브·블록 시간까지 보고 싶을 때입니다. ETW 기반이므로 커널 이벤트나 네이티브 코드의 스택도 다룰 수 있습니다. 다만 ETW 세션을 시작하려면 관리자 권한이 필요합니다. 3
  • CPU 샘플링은 기본적으로 1밀리초마다(프로세서별로) 수집됩니다. 1개 샘플 = 약 1ms의 CPU 시간으로 읽고, 대략 1,000개 이상(가능하면 5,000개 정도) 모인 뒤에 판단하는 것이 기준입니다. 샘플 수가 적을 때 상위에 뜨는 함수는 우연일 가능성이 있습니다. 1
  • 숫자를 해석하는 기준은 모두 inclusive(자신 + 호출한 곳 전체)와 exclusive(자기 자신) 구분에서 시작합니다. exclusive가 큰 함수는 “실제로 CPU를 쓰고 있는 곳”, inclusive가 큰 함수는 “그 아래 어딘가가 무거운 곳”입니다.
  • 측정하기 전에, 무엇이 “느린지”를 하나의 작업으로 고정합니다. “전체적으로 느리다”는 상태로 수집해서는 읽어낼 수 없습니다. “이 보고서를 출력하는 데 40초 걸린다”처럼 작업과 시간을 고정한 뒤에 수집하세요. 개선 전후를 비교 측정하는 사고방식은 「Windows에서 프로그램 버전별 속도를 올바르게 비교하는 방법」도 참고가 됩니다.

증상별로 어디서부터 시작할지 판단표로 정리하면 다음과 같습니다.

증상 먼저 쓰는 도구 보는 것
CPU가 높은 채로 고정됨 dotnet-trace collect / PerfView의 CPU Stacks exclusive가 큰 함수
CPU는 낮은데 처리가 느리다·멈춘다 PerfView(ThreadTime 수집) 어느 스레드가 무엇을 기다리며 블록되었는지
느리지만 우선 경향만 알고 싶다 dotnet-counters CPU 사용률, GC 빈도, ThreadPool 큐 적체
GC가 너무 많은 것 같다 dotnet-counters → dotnet-trace(GC 이벤트) GC 횟수·정지 시간, 할당이 많은 곳
특정 업무 처리 중 어디가 느린지 알고 싶다 EventSource 커스텀 이벤트 + 위의 방법들 커스텀 이벤트 사이의 경과 시간과 그 구간의 스택

2. 도구의 관계 정리하기 ── ETW와 EventPipe

등장하는 도구가 많아 보이지만, 기반은 두 가지뿐입니다.

  • ETW(Event Tracing for Windows): OS 전체를 관통하는 트레이스 기반입니다. 커널부터 앱까지 같은 시간축으로 기록할 수 있는 반면, 세션의 시작·종료에 관리자 권한이 필요하고 Windows 전용입니다. 3
  • EventPipe: .NET 런타임에 내장된 트레이스 메커니즘입니다. 관리자 권한이 필요 없고 모든 OS에서 동일하게 동작하는 대신, 볼 수 있는 범위가 매니지드 코드와 런타임으로 한정되어 커널 이벤트나 네이티브 스택은 얻을 수 없습니다. 2
  dotnet-trace(EventPipe) PerfView(ETW)
관리자 권한 필요 없음(동일 사용자의 프로세스 대상) 필요
대상 지정한 .NET 프로세스 1개 머신 전체(모든 프로세스 + 커널)
네이티브 스택 얻을 수 없음 얻을 수 있음
블록 시간(컨텍스트 스위치) 얻을 수 없음 ThreadTime 수집으로 얻을 수 있음
지원 OS Windows / Linux / macOS Windows만

이 대응 관계가 머릿속에 있으면, “먼저 dotnet-trace로 가볍게 수집하고, 부족하면 PerfView로 ETW까지 수집한다”는 구분이 자연스럽게 정해집니다.

3. 수집 전에 ── dotnet-counters로 경향을 10분간 살펴보기

바로 트레이스를 수집하기보다, 먼저 dotnet-counters로 런타임의 주요 지표를 살펴보는 것이 더 빠른 경우가 많습니다. 4

dotnet tool install --global dotnet-counters
dotnet-counters ps
dotnet-counters monitor --process-id <PID> --counters System.Runtime

여기서 보는 것은 CPU 사용률, GC 힙 크기와 GC 횟수, ThreadPool의 큐 길이·스레드 수, 예외 수입니다. 이 단계에서 “GC가 초당 여러 번 돌고 있다”, “ThreadPool 큐가 계속 늘어난다” 같은 경향이 보이면, 다음에 수집해야 할 트레이스 종류(할당인지, 블록인지)를 좁힐 수 있습니다. 메모리 계열 경향을 읽는 방법은 GC 대기와 메모리 누수를 구별하는 방법을 다룬 글에서 자세히 다루고 있습니다.

4. dotnet-trace로 트레이스 수집하기

dotnet-trace는 EventPipe 기반의 수집 도구입니다. 5 설치와 기본 수집은 다음과 같습니다.

dotnet tool install --global dotnet-trace
dotnet-trace ps
dotnet-trace collect --process-id <PID> --duration 00:00:00:30

옵션을 아무것도 지정하지 않으면, 런타임의 주요 이벤트와 스레드 샘플링을 포함한 기본 프로필 구성으로 수집됩니다. 과거에 존재했던 cpu-sampling이라는 프로필은 폐지되었고, 현재는 dotnet-commondotnet-sampled-thread-time의 조합이 기본값입니다. 5 용도에 따라 --profile gc-verbose(GC와 객체 할당 샘플링)나 --profile gc-collect(GC 발생만 저부하로 기록)도 선택할 수 있습니다.

지난번 글에서 만든 EventSource 커스텀 프로바이더를 함께 수집하려면 --providers를 추가합니다.

dotnet-trace collect --process-id <PID> --providers KomuraSoft-OrderService

수집 결과는 .nettrace 파일이 됩니다. 여는 방법은 세 가지로, Visual Studio나 PerfView에서 열거나, speedscope 형식으로 변환해 브라우저에서 보는 방법이 있습니다. 5

dotnet-trace convert trace.nettrace --format Speedscope

speedscope 형식은 speedscope.app에서 열 수 있는 경량 플레임 그래프로, “어느 함수 아래에서 시간을 썼는지”를 직관적으로 보는 데 적합합니다. 함수명까지 파고드는 정밀한 조사는 PerfView의 스택 뷰어가 더 강력하므로, 다음 장으로 넘어가겠습니다.

5. PerfView로 CPU 조사하기 ── 스택 뷰어 읽는 법

PerfView는 Microsoft가 무상으로 공개하는 성능 조사 도구로, GitHub 릴리스 페이지에서 PerfView.exe 하나만 내려받으면 바로 동작합니다(설치 불필요). 6

5.1 수집하기

GUI에서는 “Collect > Collect”로 다이얼로그를 열고, Start Collection → 대상 작업 재현 → Stop Collection이 기본 흐름입니다. 명령줄로는 다음과 같은 형태가 됩니다(ETW 세션을 시작하므로 관리자 권한으로 실행합니다 3).

PerfView collect /nogui /acceptEULA /maxCollectSec:30

기본 수집은 모든 프로세서에서 1밀리초마다의 CPU 샘플(콜 스택 포함)과 CLR의 주요 이벤트를 포함합니다. 1 수집 중 오버헤드는 기본 구성에서 대체로 몇 % 정도로 알려져 있습니다. 7

5.2 CPU Stacks 읽기

수집 결과(.etl.zip)를 열고 “CPU Stacks”에서 대상 프로세스를 선택하면 스택 뷰어가 열립니다. 가장 먼저 보는 것은 By Name 탭의 두 열입니다.

  • Exc(exclusive): 그 함수 자체가 실행 중이던 샘플 수. 실제로 CPU를 소비하고 있는 곳입니다.
  • Inc(inclusive): 그 함수와 그로부터 호출된 곳 전체를 합친 샘플 수. 그 아래 어딘가가 무겁다는 것을 나타냅니다.

읽는 방법의 기본은 Exc 상위부터 “내 코드인가, 런타임·라이브러리인가”를 구분하는 것입니다. 내 코드가 Exc 상위에 나온다면 그 함수의 알고리즘이나 루프가 직접적인 대상입니다. System.String 계열이나 JSON 시리얼라이저가 Exc 상위라면, Callers 탭에서 그것을 Inc로 거슬러 올라가 내 코드의 어디서 대량으로 호출하고 있는지를 찾아냅니다.

또 하나 중요한 것이 샘플 수 자체의 확인입니다. CPU 샘플은 1ms 간격의 통계이므로 샘플 수가 적으면 우연에 좌우됩니다. 기준으로 총 1,000개 이상, 가능하면 5,000개 정도 모은 뒤에 판단하고, 부족하면 대상 작업을 반복 실행해 수집 시간을 늘리세요. 1

참고로 자사 모듈의 함수명이 주소 표시로 나온다면 심볼(PDB)이 해석되지 않은 상태입니다. 빌드 산물의 PDB를 손에 준비해 두는 것 자체가 조사 가능성으로 이어집니다. 이 이야기는 「PDB(프로그램 데이터베이스)란 무엇인가」에서 자세히 다루고 있습니다.

6. “CPU는 한가한데 느리다” ── ThreadTime으로 블록 시간 보기

실무의 “느림” 중 절반 이상은 CPU가 아니라 대기입니다. 락 대기, DB·HTTP 응답 대기, 파일 I/O, Task.Result에 의한 데드락에 가까운 대기. 이런 종류의 문제는 CPU 샘플에는 “애초에 CPU를 쓰지 않던 시간”으로 남기 때문에, 기본 수집으로는 원인이 보이지 않습니다.

PerfView에서는 수집 시 Thread Time 옵션을 켜면, 컨텍스트 스위치 이벤트가 추가로 기록되어 각 스레드가 “CPU를 쓰던 시간”과 “블록되어 있던 시간”을 함께 추적할 수 있게 됩니다. 1

PerfView collect /nogui /acceptEULA /threadTime /maxCollectSec:30

분석할 때는 “Thread Time Stacks” 뷰를 엽니다. CPU Stacks와 같은 스택 뷰어이지만, 샘플에 CPU 시간뿐 아니라 BLOCKED_TIME(블록되어 있던 시간)도 포함된다는 점이 다릅니다. 느린 작업의 시간 범위로 좁힌 뒤, 처리를 담당한 스레드의 BLOCKED_TIME이 어느 스택(어떤 대기)에 쌓여 있는지를 보면, “총 40초 중 35초는 이 HTTP 호출 대기였다”는 형태로 내역이 드러납니다.

UI가 멈추는 증상(작업하면 몇 초간 응답이 없어짐)도 실체는 대부분 UI 스레드의 블록입니다. ThreadTime으로 UI 스레드가 어디서 블록되는지 찾은 뒤, 근본적인 대책으로는 동기 대기를 비동기로 바꾸는 설계로 옮겨갑니다. 이 설계 쪽 정리는 「WPF/WinForms의 async와 UI 스레드를 한 장으로 정리」를 참고하세요.

주의할 점으로, ThreadTime 수집은 컨텍스트 스위치마다 이벤트를 기록하므로 기본 수집보다 데이터 양이 명확히 늘어납니다. 수집 시간은 짧게 자르고, 운영 환경에서 장시간 수집하는 것은 피하세요.

7. EventSource와 조합하기 ── “어느 구간이 느린지”를 업무 언어로 자르기

CPU Stacks도 Thread Time도 프로세스 전체의 집계입니다. “주문 처리 한 건 안에서 어디가 느린지”처럼 업무 처리 단위로 자르고 싶다면, 지난번 글에서 다룬 EventSource의 Start/Stop 이벤트가 효과를 발휘합니다.

  • 앱 쪽에서 OrderProcessingStart / OrderProcessingStop 같은 이벤트를 계측해 둔다(구현은 지난번 글 참고)
  • PerfView의 “Events” 뷰에서 그 이벤트의 시각을 확인하고, 스택 뷰어의 시간 범위(Start/End 필터)를 그 구간으로 좁힌다
  • 좁힌 범위에서 CPU/블록 시간의 내역을 읽는다

이렇게 하면 “느렸던 그 한 번”만 대상으로 분석할 수 있어, 평상시의 노이즈에 묻히지 않습니다. 커스텀 이벤트는 dotnet-trace로도 똑같이 수집할 수 있으므로, 계측을 한 번 넣어두면 개발 머신에서도 운영 환경에서도 같은 조사 방법이 통용됩니다. 성능 조사는 도구를 다루는 솜씨보다 “앱 쪽에 관측 지점이 있는가”로 난이도가 결정된다는 것이 실무에서의 체감입니다.

정리

  • “느림”을 CPU bound와 블록으로 나누는 것이 출발점입니다. 전자는 CPU 샘플링(1샘플≒1ms), 후자는 ThreadTime 수집이 아니면 보이지 않습니다.
  • 먼저 관리자 권한 없이 쓸 수 있는 dotnet-trace, 머신 전체·네이티브·블록 시간이 필요하면 PerfView라는 구분이 기본입니다. 수집한 .nettrace를 PerfView로 읽는 병용도 흔히 쓰입니다.
  • 스택 뷰어는 exclusive와 inclusive의 구분이 읽는 법의 핵심이며, 샘플 수가 충분한지 항상 확인합니다.
  • EventSource 커스텀 이벤트로 업무 처리 구간을 자를 수 있게 해두면 조사 정밀도가 한 단계 올라갑니다.

합동회사 코무라소프트는 “느리다”, “멈춘다”, “CPU가 고정된다” 같은 Windows 업무 앱의 성능 문제 조사와, 측정하기 쉽게 만들기 위한 계측·설계 상담을 다루고 있습니다. 재현 절차와 트레이스 파일이 있다면 그 분석만 의뢰하는 것도 가능합니다.

관련 문서

관련 상담 분야

참고 링크

  1. GitHub, PerfView User’s Guide. 기본 수집이 프로세서별로 1밀리초 간격의 CPU 샘플(콜 스택 포함)이라는 점, 판단에는 1,000~5,000개 정도의 샘플이 바람직하다는 점, Thread Time 옵션으로 컨텍스트 스위치를 수집해 블록 시간까지 분석할 수 있다는 점에 대해(PerfView 자체의 Help에서도 참조 가능).  2 3 4 5

  2. Microsoft Learn, EventPipe Overview. EventPipe가 관리자 권한 같은 고권한 컴포넌트에 의존하지 않고 .NET 앱을 트레이스하기 위한 메커니즘이라는 점, 대상이 매니지드 코드와 런타임으로 한정되어 커널 이벤트나 네이티브 스택은 얻을 수 없다는 점에 대해.  2

  3. Microsoft Learn, Collect and View EventSource Traces. ETW 트레이스 수집에는 항상 관리자 권한이 필요하다는 점, PerfView와 Visual Studio가 .nettrace 파일을 열 수 있다는 점에 대해.  2 3 4

  4. Microsoft Learn, dotnet-counters diagnostic tool. System.Runtime 카운터를 이용한 실행 중 프로세스의 CPU·GC·ThreadPool 등 지표 모니터링에 대해. 

  5. Microsoft Learn, dotnet-trace diagnostic tool. collect 명령의 사용법, 기본으로 활성화되는 프로필(dotnet-common / dotnet-sampled-thread-time)과 cpu-sampling 프로필의 폐지, gc-verbose / gc-collect 등의 프로필, Speedscope / Chromium 형식으로의 변환에 대해.  2 3

  6. GitHub, microsoft/perfview. PerfView가 CPU·메모리 관련 성능 문제를 조사하기 위한 무상 성능 분석 도구이며, 릴리스 페이지에서 실행 파일을 바로 받을 수 있다는 점에 대해. 

  7. GitHub, microsoft/perfview Issue #598. PerfView 관리자가 설명한, 기본 수집 시 오버헤드가 대략 3% 정도라는 점, 그리고 /CpuSampleMSec으로 샘플링 간격을 조정해 부하를 낮출 수 있다는 점에 대해. 

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

PerfView와 dotnet-trace, 어느 쪽을 써야 하나요?
먼저 dotnet-trace부터 시도하는 것을 권장합니다. 관리자 권한 없이 사용할 수 있고, 대상 프로세스만 지정해서 수집할 수 있으며, 명령어 한 줄로 조작이 끝납니다. 반면, 네이티브 코드의 스택까지 보고 싶거나, 머신 전체 상황(다른 프로세스나 커널의 영향)까지 보고 싶거나, 블록 시간을 컨텍스트 스위치 단위로 추적하고 싶다면 ETW 기반의 PerfView가 필요합니다. 수집한 .nettrace 파일을 PerfView로 열어 분석하는 병용 방식도 흔히 사용됩니다.
Visual Studio의 프로파일러와는 어떤 관계인가요?
개발 머신에서 재현되는 문제라면 Visual Studio의 CPU 사용량 도구나 메모리 도구가 더 간편하며, 우선은 그것으로 충분합니다. PerfView나 dotnet-trace가 위력을 발휘하는 것은 Visual Studio를 설치할 수 없는 검증 서버·운영 서버에서 수집해야 하는 경우, 수집과 분석을 별도의 머신으로 나누고 싶은 경우, 커스텀 EventSource나 커널 이벤트까지 포함한 ETW 이벤트를 보고 싶은 경우입니다. 참고로 Visual Studio도 dotnet-trace가 출력하는 .nettrace 파일을 열 수 있습니다.
운영 서버에서 실행해도 괜찮나요?
두 도구 모두 운영 환경에서의 단시간 수집을 염두에 두고 만들어졌지만, 무조건 안전한 것은 아닙니다. 수집 시간을 수십 초~수 분으로 제한하고, 먼저 검증 환경에서 같은 명령을 시험해 오버헤드와 파일 크기를 확인하고, 디스크 여유 공간을 확인하는 절차를 거치세요. 특히 ThreadTime(컨텍스트 스위치) 수집이나 할당 추적은 이벤트 양이 많아 파일이 급격히 커집니다. 또한 트레이스에는 명령줄 인수 같은 정보가 포함되므로, 수집한 파일을 다룰 때도 주의가 필요합니다.
WPR/WPA와는 어떻게 구분해서 사용하나요?
WPR(Windows Performance Recorder)과 WPA(Windows Performance Analyzer)는 같은 ETW를 기반으로 하는, 좀 더 OS에 가까운 조사 도구입니다. 디스크 I/O·전원·부팅 시간 등 OS 전체의 상세 분석에는 WPR/WPA가 강력하고, .NET 앱의 CPU·GC·블록 시간 조사에는 매니지드 코드 표시에 최적화된 PerfView가 더 읽기 쉽습니다. 업무 앱 조사가 목적이라면 PerfView와 dotnet-trace만으로 대부분 충분합니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기