icorprofiler

2 개의 포스트

.NET Continuous Profiler: Memory usage (새 탭에서 열림)

Datadog의 .NET 프로파일러는 가비지 컬렉션(GC)의 효율성과 메모리 할당 패턴을 분석하여 애플리케이션의 성능 병목 현상을 진단합니다. 이 시스템은 모든 할당을 추적하는 대신 `AllocationTick` 이벤트를 활용한 샘플링 방식을 채택하여 운영 환경에서의 오버헤드를 최소화하면서도 정밀한 데이터를 제공합니다. 특히 .NET 7의 최신 API를 통해 객체의 생존 주기를 추적함으로써, CPU 부하의 원인이 되는 과도한 GC 작업과 잠재적인 메모리 누수 지점을 정확히 찾아내는 데 결론적인 도움을 줍니다. ### 가비지 컬렉터가 CPU에 미치는 영향 측정 * **전용 스레드 모니터링**: 서버 GC 설정 시 CLR이 생성하는 코어당 전용 스레드(.NET Server GC 및 .NET BGC)의 CPU 소비량을 운영체제로부터 직접 수집합니다. * **Pull 모델 채택**: GC 발생 시마다 이벤트를 받는 Push 방식과 달리, 프로파일러가 1분마다 주기적으로 GC 스레드의 CPU 사용 통계를 가져와 'Garbage Collector'라는 단일 프레임을 가진 네이티브 콜 스택 샘플로 기록합니다. * **버전별 차이**: .NET 5 이상에서는 GC 스레드 식별이 가능하여 정확한 측정이 가능하지만, 이전 버전에서는 스레드 ID 정보 부족으로 인해 이 기능을 완벽히 지원하기 어렵습니다. ### AllocationTick을 활용한 효율적인 할당 추적 * **샘플링 기반 추적**: 모든 객체 할당을 기록하는 `ObjectAllocated` 방식은 성능 저하가 극심하므로, 약 100KB의 할당이 누적될 때마다 발생하는 `AllocationTick` 이벤트를 사용하여 데이터를 수집합니다. * **상세 정보 수집**: 이벤트 페이로드에서 클래스 ID(ClassID), 타입 이름, 메모리 주소, 객체 크기뿐만 아니라 해당 객체가 할당된 힙의 종류(SOH, LOH, POH)까지 식별합니다. * **동기적 콜 스택 캡처**: 해당 이벤트는 할당을 수행한 스레드에서 동기적으로 발생하므로, 즉시 콜 스택을 워킹(Stack Walking)하여 어떤 비즈니스 로직이 메모리 압박을 유발하는지 특정할 수 있습니다. ### Weak Handle을 이용한 생존 객체 및 누수 탐지 * **객체 이동 대응**: GC의 컴팩션(Compaction) 단계에서 객체 주소가 변경되는 문제를 해결하기 위해, 샘플링된 객체에 대해 Weak 핸들을 생성하여 관리합니다. * **ICorProfilerInfo13 활용**: .NET 7에서 추가된 이 프로파일링 API를 통해, GC 이후에도 핸들이 가리키는 객체가 여전히 메모리에 살아있는지(`IsAllocated`)를 확인합니다. * **생명 주기 분석**: GC가 끝날 때마다 참조되지 않는 객체의 핸들은 제거하고, 생존한 객체들은 다음 프로필에 포함시켜 어떤 데이터가 메모리에 오래 머무르며 누수를 유발하는지 추적합니다. 운영 환경에서 메모리 문제를 분석할 때는 단순한 할당량 확인을 넘어, GC로 인한 CPU 점유율과 객체의 생존 기간을 함께 살펴야 합니다. 특히 .NET 7 이상의 최신 런타임을 활용하면 프로파일러의 Weak 핸들 추적 기능을 통해 메모리 누수 탐지의 정확도를 대폭 높일 수 있습니다.

.NET 지속적 프로파일러: 예외 및 락 경합 (새 탭에서 열림)

Datadog의 .NET 컨티뉴어스 프로파일러는 애플리케이션의 성능에 보이지 않는 영향을 미치는 예외(Exception)와 락 경합(Lock Contention)을 정밀하게 추적합니다. 단순히 발생 횟수를 세는 것을 넘어, 로우 레벨 CLR 콜백과 메타데이터 분석을 통해 예외 메시지와 락 유지 시간 등 구체적인 컨텍스트를 제공하여 성능 병목의 원인을 정확히 파악하도록 돕습니다. 이를 통해 개발자는 무분별한 예외 발생으로 인한 CPU 낭비와 병렬 알고리즘의 지연 시간을 효과적으로 최적화할 수 있습니다. **예외 발생 데이터 수집 및 타입 분석** * 예외가 던져질 때 CLR은 `ICorProfilerCallback::ExceptionThrown`을 호출하며, 프로파일러는 이를 통해 예외 객체의 `ObjectID`를 획득합니다. * `ICorProfilerInfo::GetClassFromObject`를 사용하여 예외 인스턴스의 `ClassID`를 구하고, 이를 `FrameStore`와 연동하여 클래스 이름을 확인하고 캐싱합니다. * 예외 처리는 `catch` 및 `finally` 블록의 실행을 보장하기 위해 런타임에서 많은 CPU 사이클을 소모하므로, 발생 지점과 타입별 통계를 파악하는 것이 중요합니다. **System.Exception 메시지 추출을 위한 메타데이터 탐색** * 예외의 세부 내용을 파악하기 위해 `_message` 필드의 값을 읽어야 하며, 이를 위해서는 해당 필드의 정확한 메모리 오프셋을 알아야 합니다. * .NET 버전(Framework 또는 Core)에 따라 `mscorlib`나 `System.Private.CoreLib` 모듈을 식별한 후, `IMetaDataImport`를 통해 `System.Exception` 클래스의 메타데이터 토큰을 찾습니다. * `ICorProfilerInfo2::GetClassLayout`을 호출하여 클래스의 필드 레이아웃 정보를 얻고, `_message` 필드의 `COR_FIELD_OFFSET`을 계산하여 문자열 버퍼의 위치를 특정합니다. **락 경합의 지속 시간 및 원인 식별** * .NET 런타임은 `Monitor.Enter` 등을 사용하는 락 패턴에 대해 `ContentionStart`와 `ContentionStop` 이벤트를 발생시킵니다. * .NET Framework와 같이 이벤트 자체에서 지속 시간을 제공하지 않는 경우, 프로파일러가 직접 스레드별로 시작 시점의 타임스탬프를 관리하여 대기 시간을 계산합니다. * .NET 8부터는 `ContentionStart` 이벤트에 락의 `ObjectID`와 현재 락을 점유 중인 스레드 정보가 포함되어, 어떤 스레드가 다른 스레드를 대기하게 만드는지 구체적으로 가시화할 수 있습니다. **EventPipe를 통한 효율적인 실시간 이벤트 모니터링** * .NET 5 이상에서는 `ICorProfilerCallback10::EventPipeEventDelivered` 메서드를 통해 CLR 이벤트를 동기적으로 수신할 수 있습니다. * `ClrEventParser` 클래스는 이벤트 페이로드에서 ID와 키워드를 기반으로 필요한 필드만 추출하여 성능 부하를 최소화합니다. * 이러한 메커니즘을 통해 애플리케이션 실행에 거의 영향을 주지 않으면서도(Negligible impact) 상세한 프로파일링 데이터를 확보합니다. 성능 최적화를 위해서는 `Parse` 대신 `TryParse`를 사용하여 불필요한 예외 비용을 줄이고, 프로파일러가 제공하는 락 지속 시간 데이터를 바탕으로 과도한 동기화 구문을 개선하는 실용적인 접근이 필요합니다.