.NET 애플리케이션에서 높은 CPU 사용량 추적 및 분석
게시 됨: 2022-03-11소프트웨어 개발은 매우 복잡한 프로세스일 수 있습니다. 우리는 개발자로서 다양한 변수를 고려해야 합니다. 일부는 우리 통제하에 있지 않고, 일부는 실제 코드 실행 순간에 우리에게 알려지지 않고, 일부는 우리가 직접 통제합니다. 그리고 .NET 개발자도 예외는 아닙니다.
이러한 현실을 감안할 때 우리가 통제된 환경에서 작업할 때 일반적으로 일이 계획대로 진행됩니다. 예를 들어 우리의 개발 머신 또는 전체 액세스 권한이 있는 통합 환경이 있습니다. 이러한 상황에서 우리는 코드와 소프트웨어에 영향을 미치는 다양한 변수를 분석하기 위한 도구를 마음대로 사용할 수 있습니다. 이 경우 서버의 과부하나 같은 작업을 동시에 수행하려는 동시 사용자를 처리할 필요도 없습니다.
설명되고 안전한 상황에서는 코드가 제대로 작동하지만 과부하 또는 기타 외부 요인이 있는 프로덕션에서는 예기치 않은 문제가 발생할 수 있습니다. 프로덕션의 소프트웨어 성능은 분석하기 어렵습니다. 대부분의 경우 이론적 시나리오에서 잠재적인 문제를 처리해야 합니다. 문제가 발생할 수 있다는 것을 알고 있지만 테스트할 수는 없습니다. 그렇기 때문에 우리는 우리가 사용하는 언어에 대한 모범 사례와 문서를 기반으로 개발하고 일반적인 실수를 피해야 합니다.
언급한 바와 같이 소프트웨어가 실행되면 일이 잘못될 수 있고 계획하지 않은 방식으로 코드가 실행되기 시작할 수 있습니다. 디버깅 능력이 없거나 무슨 일이 일어나고 있는지 확실히 알 수 없는 문제를 처리해야 하는 상황에 처할 수 있습니다. 이 경우 우리는 무엇을 할 수 있습니까?
이 기사에서는 Windows 기반 서버에서 .NET 웹 응용 프로그램의 CPU 사용량이 높은 실제 사례 시나리오, 문제를 식별하는 데 관련된 프로세스, 더 중요하게는 이 문제가 처음에 발생한 이유와 방법을 분석할 것입니다. 그것을 해결하십시오.
CPU 사용량 및 메모리 소비는 널리 논의되는 주제입니다. 일반적으로 특정 프로세스가 사용해야 하는 리소스(CPU, RAM, I/O)의 양이 얼마인지, 얼마 동안 사용해야 하는지 확실히 아는 것은 매우 어렵습니다. 한 가지 확실한 것은 프로세스가 장기간 CPU의 90% 이상을 사용하고 있다면 이 상황에서 서버가 다른 요청을 처리할 수 없다는 사실 때문에 문제가 됩니다.
이것은 프로세스 자체에 문제가 있음을 의미합니까? 반드시는 아닙니다. 프로세스에 더 많은 처리 능력이 필요하거나 많은 데이터를 처리하고 있을 수 있습니다. 우선, 우리가 할 수 있는 유일한 일은 왜 이런 일이 일어나는지 확인하는 것입니다.
모든 운영 체제에는 서버에서 진행 중인 작업을 모니터링하기 위한 여러 가지 도구가 있습니다. Windows 서버에는 특히 작업 관리자인 성능 모니터가 있거나 우리의 경우 서버 모니터링을 위한 훌륭한 도구인 New Relic Server를 사용했습니다.
첫 번째 증상 및 문제 분석
애플리케이션을 배포한 후 처음 2주의 시간 경과 동안 서버에 CPU 사용량이 정점에 도달하여 서버가 응답하지 않는 것을 보기 시작했습니다. 다시 사용할 수 있도록 하려면 다시 시작해야 했으며 이 이벤트는 해당 기간 동안 세 번 발생했습니다. 앞서 언급했듯이 New Relic Servers를 서버 모니터로 사용했는데 서버가 충돌했을 때 w3wp.exe
프로세스가 CPU의 94%를 사용하고 있는 것으로 나타났습니다.
IIS(인터넷 정보 서비스) 작업자 프로세스는 웹 응용 프로그램을 실행하는 Windows 프로세스( w3wp.exe
)이며 특정 응용 프로그램 풀에 대해 웹 서버로 전송된 요청을 처리합니다. IIS 서버에는 문제를 일으킬 수 있는 여러 응용 프로그램 풀(및 여러 w3wp.exe
프로세스)이 있을 수 있습니다. 프로세스의 사용자(New Relic 보고서에 표시됨)를 기반으로 우리는 문제가 .NET C# 웹 양식 레거시 애플리케이션임을 식별했습니다.
.NET Framework는 Windows 디버깅 도구와 긴밀하게 통합되어 있으므로 가장 먼저 이벤트 뷰어 및 응용 프로그램 로그 파일을 살펴보고 진행 상황에 대한 유용한 정보를 찾았습니다. 이벤트 뷰어에 일부 예외가 기록되었는지 여부에 관계없이 분석할 충분한 데이터를 제공하지 않았습니다. 그래서 한발 더 나아가 더 많은 데이터를 수집하기로 결정하고 이벤트가 다시 발생하면 준비할 수 있도록 했습니다.
데이터 수집
사용자 모드 프로세스 덤프를 수집하는 가장 쉬운 방법은 디버그 진단 도구 v2.0 또는 단순히 DebugDiag를 사용하는 것입니다. DebugDiag에는 데이터 수집(DebugDiag Collection) 및 데이터 분석(DebugDiag 분석)을 위한 도구 세트가 있습니다.
이제 디버그 진단 도구를 사용하여 데이터를 수집하기 위한 규칙을 정의하기 시작하겠습니다.
DebugDiag 컬렉션을 열고
Performance
을 선택합니다.-
Performance Counters
를 선택하고Next
을 클릭합니다. -
Add Perf Triggers
를 클릭합니다. -
Processor
(Process
아님) 개체를 확장하고% Processor Time
을 선택합니다. Windows Server 2008 R2를 사용 중이고 프로세서가 64개 이상인 경우 프로세서 개체 대신Processor
Processor Information
개체를 선택하십시오. - 인스턴스 목록에서
_Total
을 선택합니다. -
Add
를 클릭한 다음OK
을 클릭합니다. 새로 추가된 트리거를 선택하고
Edit Thresholds
을 클릭합니다.- 드롭다운에서
Above
를 선택합니다. - 임계값을
80
으로 변경합니다. 초 수로
20
을 입력합니다. 필요한 경우 이 값을 조정할 수 있지만 잘못된 트리거를 방지하기 위해 짧은 시간(초)을 지정하지 않도록 주의하십시오.-
OK
을 클릭합니다. -
Next
을 클릭합니다. -
Add Dump Target
를 클릭합니다. - 드롭다운에서
Web Application Pool
을 선택합니다. - 앱 풀 목록에서 애플리케이션 풀을 선택합니다.
-
OK
을 클릭합니다. -
Next
을 클릭합니다. -
Next
을 다시 클릭합니다. - 원하는 경우 규칙 이름을 입력하고 덤프가 저장될 위치를 기록해 둡니다. 원하는 경우 이 위치를 변경할 수 있습니다.
-
Next
을 클릭합니다. -
Activate the Rule Now
선택하고Finish
을 클릭합니다.
설명된 규칙은 크기가 상당히 작은 미니덤프 파일 세트를 생성합니다. 최종 덤프는 전체 메모리가 있는 덤프가 되며 해당 덤프는 훨씬 더 커질 것입니다. 이제 높은 CPU 이벤트가 다시 발생할 때까지 기다리기만 하면 됩니다.
선택한 폴더에 덤프 파일이 있으면 수집된 데이터를 분석하기 위해 DebugDiag 분석 도구를 사용합니다.
성능 분석기를 선택합니다.
덤프 파일을 추가합니다.
분석을 시작합니다.
DebugDiag는 덤프를 구문 분석하고 분석을 제공하는 데 몇 분(또는 몇 분)이 걸립니다. 분석이 완료되면 다음과 유사한 요약 및 스레드에 대한 많은 정보가 포함된 웹 페이지가 표시됩니다.
요약에서 볼 수 있듯이 "하나 이상의 스레드에서 덤프 파일 간의 높은 CPU 사용량이 감지되었습니다."라는 경고가 있습니다. 권장 사항을 클릭하면 응용 프로그램에 문제가 있는 부분을 이해하기 시작합니다. 예제 보고서는 다음과 같습니다.
보고서에서 볼 수 있듯이 CPU 사용량과 관련된 패턴이 있습니다. CPU 사용량이 높은 모든 스레드는 동일한 클래스에 연결됩니다. 코드로 넘어가기 전에 첫 번째 코드를 살펴보겠습니다.
이것은 우리 문제의 첫 번째 스레드에 대한 세부 정보입니다. 흥미로운 부분은 다음과 같습니다.
여기에서 문제가 있는 작업을 트리거한 GameHub.OnDisconnected()
코드에 대한 호출이 있지만 그 호출 전에 무슨 일이 일어나고 있는지에 대한 아이디어를 제공할 수 있는 두 개의 사전 호출이 있습니다. .NET 코드에서 해당 메서드가 수행하는 작업을 살펴보겠습니다.
public override Task OnDisconnected() { try { var userId = GetUserId(); string connId; if (onlineSessions.TryGetValue(userId, out connId)) onlineSessions.Remove(userId); } catch (Exception) { // ignored } return base.OnDisconnected(); }
분명히 여기에 문제가 있습니다. 보고서의 호출 스택에 따르면 문제는 사전에 있으며 이 코드에서는 사전에 액세스하고 있으며 특히 문제를 일으키는 라인은 다음과 같습니다.
if (onlineSessions.TryGetValue(userId, out connId))
이것은 사전 선언입니다:
static Dictionary<int, string> onlineSessions = new Dictionary<int, string>();
이 .NET 코드의 문제점은 무엇입니까?
객체 지향 프로그래밍 경험이 있는 모든 사람은 정적 변수가 이 클래스의 모든 인스턴스에서 공유된다는 것을 알고 있습니다. .NET 세계에서 정적이 무엇을 의미하는지 자세히 살펴보겠습니다.
.NET C# 사양에 따르면:
정적 한정자를 사용하여 특정 개체가 아닌 형식 자체에 속하는 정적 멤버를 선언합니다.
.NET C# 언어 사양은 정적 클래스 및 멤버에 대해 다음과 같이 말합니다.
모든 클래스 형식의 경우와 마찬가지로 정적 클래스에 대한 형식 정보는 클래스를 참조하는 프로그램이 로드될 때 .NET Framework CLR(공용 언어 런타임)에 의해 로드됩니다. 프로그램은 클래스가 로드되는 시기를 정확히 지정할 수 없습니다. 그러나 프로그램에서 클래스가 처음으로 참조되기 전에 로드되고 해당 필드가 초기화되고 정적 생성자가 호출되는 것이 보장됩니다. 정적 생성자는 한 번만 호출되며 정적 클래스는 프로그램이 있는 응용 프로그램 도메인의 수명 동안 메모리에 남아 있습니다.
비정적 클래스에는 정적 메서드, 필드, 속성 또는 이벤트가 포함될 수 있습니다. 클래스의 인스턴스가 생성되지 않은 경우에도 정적 멤버는 클래스에서 호출할 수 있습니다. 정적 멤버는 항상 인스턴스 이름이 아닌 클래스 이름으로 액세스합니다. 클래스의 인스턴스 수에 관계없이 정적 멤버의 복사본은 하나만 존재합니다. 정적 메서드 및 속성은 포함하는 유형의 비정적 필드 및 이벤트에 액세스할 수 없으며 메서드 매개 변수에 명시적으로 전달되지 않는 한 개체의 인스턴스 변수에 액세스할 수 없습니다.
즉, 정적 멤버는 개체가 아니라 형식 자체에 속합니다. 또한 CLR에 의해 응용 프로그램 도메인으로 로드되므로 정적 멤버는 특정 스레드가 아니라 응용 프로그램을 호스팅하는 프로세스에 속합니다.
웹 환경이 다중 스레드 환경이라는 사실을 감안할 때 모든 요청은 w3wp.exe
프로세스에 의해 생성되는 새 스레드이기 때문입니다. 정적 멤버가 프로세스의 일부인 경우 여러 스레드가 정적(여러 스레드에서 공유) 변수의 데이터에 액세스하려고 하는 시나리오가 있을 수 있으며, 이는 결국 멀티스레딩 문제로 이어질 수 있습니다.
스레드 안전성에 대한 사전 문서에는 다음과 같이 명시되어 있습니다.
컬렉션이 수정되지 않는 한
Dictionary<TKey, TValue>
는 여러 판독기를 동시에 지원할 수 있습니다. 그럼에도 불구하고 컬렉션을 열거하는 것은 본질적으로 스레드로부터 안전한 절차가 아닙니다. 열거형이 쓰기 액세스와 경합하는 드문 경우지만 전체 열거형 동안 컬렉션을 잠가야 합니다. 읽기 및 쓰기를 위해 여러 스레드에서 컬렉션에 액세스할 수 있도록 하려면 고유한 동기화를 구현해야 합니다.
이 진술은 우리가 이 문제를 가질 수 있는 이유를 설명합니다. 덤프 정보에 따르면 문제는 사전 FindEntry 메서드에 있었습니다.
FindEntry 구현 사전을 보면 메서드가 값을 찾기 위해 내부 구조(버킷)를 반복하는 것을 볼 수 있습니다.
따라서 다음 .NET 코드는 스레드로부터 안전한 작업이 아닌 컬렉션을 열거합니다.
public override Task OnDisconnected() { try { var userId = GetUserId(); string connId; if (onlineSessions.TryGetValue(userId, out connId)) onlineSessions.Remove(userId); } catch (Exception) { // ignored } return base.OnDisconnected(); }
결론
덤프에서 보았듯이 공유 리소스(정적 사전)를 동시에 반복하고 수정하려는 여러 스레드가 있으며, 결국 반복이 무한 루프에 들어가 스레드가 CPU의 90% 이상을 소비하게 만듭니다. .
이 문제에 대한 몇 가지 가능한 솔루션이 있습니다. 우리가 먼저 구현한 것은 성능 손실을 대가로 사전에 대한 액세스를 잠그고 동기화하는 것이었습니다. 그 당시에는 서버가 매일 충돌하고 있었기 때문에 가능한 한 빨리 이 문제를 수정해야 했습니다. 이것이 최적의 솔루션이 아니더라도 문제를 해결했습니다.
이 문제를 해결하기 위한 다음 단계는 코드를 분석하고 이에 대한 최적의 솔루션을 찾는 것입니다. 코드를 리팩토링하는 것은 옵션입니다. 새로운 ConcurrentDictionary 클래스는 전체 성능을 향상시키는 버킷 수준에서만 잠기기 때문에 이 문제를 해결할 수 있습니다. 그러나 이것은 큰 단계이며 추가 분석이 필요합니다.