이번 포스트의 내용은 지난 포스트에서 다루었던 가비지 컬렉션 모드의 연장선 상에 있는 글로써 프로세스가 어떤 가비지 컬렉션 모드를 사용하는지 알아내는 방법과 Server-GC 모드 사용시 한번쯤 고려해 보아야 할 사항에 대해 다루고 있습니다. 가비지 컬렉션에 대해 어느 정도 알고 계시는 독자 분들일지라도 가비지 컬렉션 모드에 대한 포스트는 먼저 읽어 보실 것을 권장합니다.

Using Garbage Collection Modes

지난 포스트에서 CLR이 사용하는 몇 가지 가비지 컬렉션 모드를 살펴 보았다. Workstation-GC 모드와 Server-GC 모드가 그것인데, Workstation-GC 모드는 다시 Non-Concurrent-GC 모드 그리고 Concurrent-GC 모드로 나누어 진다. 닷넷 프레임워크 버전 4.0에 와서는 Concurrent-GC의 신 버전이라 할 수 있는 Background-GC 모드가 사용된다.

Workstation-GC/Server-GC 모드 사용 여부 판단

이렇게 여러 종류의 가비지 컬렉션 모드가 존재한다면 프로세스가 어떤 가비지 컬렉션 모드를 사용하는지 아는 것이 상당히 중요하다. 기본적으로 2개 이상의 논리 프로세서(멀티 코어, 하이퍼 스레드 포함)를 가진 시스템에서라면 Concurrent-GC 모드(혹은 Background-GC 모드)가 사용된다고 보면 된다. Server-GC 모드는 명시적으로 app.config 파일에 <gcServer enabled=”true” /> 설정을 지정하지 않는 한 사용되지 않는다. 다만, ASP.NET과 SQL Server(SQLCLI)에서는 기본적으로 Server-GC 모드가 사용된다. 한편 1개의 CPU를 가진 시스템에서는 구성 파일의 설정과 무관하게 항상 Non-Concurrent-GC 모드가 사용된다는 점도 기억해 두면 된다.

프로그램적으로 현재 내 어플리케이션이 어떤 GC 모드를 사용하고 있는지 알고 싶다면 단 몇 줄의 코드를 통해 이를 알아낼 수도 있다. System.Runtime 네임스페이스의 GCSettings 클래스가 바로 GC 작동 모드에 관계된 정보를 읽거나 설정(!)도 가능하게 해준다. GCSettings.IsServerGC 속성(읽기 전용)은 현재 프로세스가 Workstation-GC 모드로 작동하는지 Server-GC 모드로 작동 중인가를 알려주는 속성이다.

일반적인 WinForm, Console, WPF 등의 어플리케이션에서 이 속성의 값을 찍어보면 당연히 false라고 나온다. 이들 어플리케이션에서<gcServer enabled=”true” /> 설정을 수행하면 이 속성의 값은 true가 된다. ASP.NET이나 SQL Server(SQLCLI) 환경에서 별다른 설정을 하지 않았다면 이 속성의 값은 true가 된다. ASP.NET의 경우에 Server-GC 모드를 끄는 방법이 있는데, 이 방법은 잠시 후에 설명 하도록 하겠다.

GC 모드 조정

기왕 말이 나왔으니 끝장을 봐 보자. 자신 있게 “내가 니 애비다, 이놈아”를 외치려면 온몸이 불타는 고통쯤은 참아야 하지 않은가? GCSettings 클래스에는 묘한 속성이 하나 더 있다. LatencyMode 란 속성이 그것인데 이 속성 값은 GCLatencyMode 열거 타입이다. LatecyMode 란 바로 대기 모드 혹은 지연 모드를 의미하며 가비지 컬렉션 수행 동안 어플리케이션이 갖는 지연 시간의 크기를 의미한다고 보면 된다. LatencyMode 속성을 GCLatencyMode 열거 타입의 3가지 값 중 하나로 설정함에 따라 GC 작동 방식을 약간이나마 변경할 수 있는 것이다.

public enum GCLatencyMode
{
    Batch = 0,
    Interactive = 1,
    LowLatency = 2,
}

Batch 값은 가비지 컬렉션 동안 어플리케이션이 계속 지연 됨을 의미한다. 지난 포스트에서 살펴보았던 Non-Concurrent-GC나 Server-GC의 LatencyMode 속성 값이 바로 Batch 이다. Interactive 값은 UI를 가진 어플리케이션과 같이 지연 시간이 응답 시간(response-time)을 해치지 않도록 짧게 짧게 어플리케이션이 지연됨을 의미한다. 눈치챘겠지만 Concurrent-GC 혹은 Background-GC 모드에서 LatencyMode 속성의 값이 Interactive 가 된다. 마지막으로 LowLatency 값이 당황스러운 녀석으로써 이 값은 시스템이 전반적으로 메모리 부족 현상을 겪지 않는 한, 2 세대 가비지 컬렉션(풀 가비지 컬렉션이라고도 한다)을 수행하지 않는 지연 모드이다. 이전 포스트부터 수 차례 언급했지만 커다란 힙을 가진 어플리케이션에서 2세대 가비지 컬렉션은 무시 못할 정도의 시간을 요구한다. 따라서 LowLatency 값을 LatencyMode에 할당함으로써 긴 시간을 요구할지도 모르는 2 세대 가비지 컬렉션을 수행하지 않는 것이다.

Server-GC 모드에서는 LatencyMode 값으로 Batch가 고정되며 다른 값으로 변경할 수 없다(속성 값이 변경되지 않는다). Workstation-GC 모드에서는 Concurrent-GC 모드가 활성화 되어 있는 상황에서 이 속성을 Batch로 설정하면 Non-Concurrent-GC와 동일하게 가비지 컬렉션이 수행되며 Interactive 값은 기본적인 Concurrent-GC 모드와 동일하게 작동한다. 마지막으로 LowLatency 값은 2세대 가비지 컬렉션이 발생하지 않게 된다. 반면 Non-Concurrent-GC 모드를 사용하는 경우라도 LatecyMode 속성의 값을 바꿀 수 있도록 되어 있다. 필자가 Non-Concurrent-GC 모드(<gcConcurrent enabled=”false” />  설정)에서 이 속성의 값을 Interactive 로 바꾸어 보았지만 Concurrent-GC 모드와 동등하게 작동하지는 않았다. 왜 그런 현상을 보이는지 필자도 확인할 방법은 없었다(대략 귀차니즘으로… ㅡ,.ㅡ).

MSDN의 Latency Modes 란 토픽에서 설명하듯이 LowLatency 모드는 매우 주의 깊게 사용해야 한다. 이 모드를 사용함으로써 응답시간의 극대화(2세대 GC가 발생하지 않으니…)를 꾀할 수 있지만 0, 1세대 가비지 컬렉션만 수행해서는 언젠가 OOM(Out-Of-Memory) 예외를 뚜드려 맞을 수도 있기 대문이다. 따라서 이 토픽에서는 중요한 그래픽 렌더링(rendering)이나 타임 크리티컬(time-critical) 한 작업을 하는 동안만 짧게 LowLatency를 사용할 것을 권장하고 있으며 LowLatency 모드로 작동하는 동안 과도한 메모리 할당을 하지 말 것을 주문하고 있다. 필자가 테스트 삼아 LowLatency 모드로 설정된 상황에서 메모리 할당을 좀 사용해 봤는데 아주 잠깐 동안 0 세대 및  1세대 가비지 컬렉션이 수백 번 발생했고 2세대 가비지 컬렉션이 단 한번도 발생하지 않았었다. 상당히 조심스럽게 사용해야 할 지연 모드이며 반드시 이 모드에 대한 테스트를 수행해 보아야 할 것이다. 자신이 무슨 짓을 하는지 정확하게 모른다면 아예 LatencyMode 속성은 건드리지 않는 것이 좋을지도 모른다.

Summary of GC Modes

이제 대충 가비지 컬렉션 모드들에 대해 요약해 보자. 졸라 햇갈릴 거 같은데 다음 표로써 깔끔하게 정리가 되었으면 하는 바램이다.

  Non-Concurrent-GC Concurrent-GC
(Background-GC)
Server-GC
용도 단일 프로세서에서 처리량(throughput) 극대화 목표 UI 어플리케이션에서 응답 시간(response time)을 극대화 목표 서버 어플리케이션의 최대 효율 목표
설정 <gcConcurrent enabled=”false” /> <gcConcurrent enabled=”true” />
혹은 설정하지 않음
<gcServer enabled=”true” />
<gcConcurrent> 설정 무시
GC 스레드 GC을 유발한 스레드 GC를 유발한 스레드(0, 1세대)와 GC 전용 스레드(2 세대) 논리 프로세서 당 하나씩 할당된 GC 전용 스레드들
어플리케이션 중단 기간 GC가 완료 될 때까지 쭈욱 중단 0,1 세대 힙을 정리하는 동안 및 2세대 힙을 정리하는 동안 짧게 짧게 자주 중단 GC가 완료 될 때까지 쭈욱 중단
관리되는 힙 개수 1 1 논리 프로세서 당 1개
필요한 논리
프로세서 개수
N/A 2개 이상 2개 이상
IsServerGC 속성값 false false true
LatencyMode 속성 기본값 Batch Interactive Batch
기본적으로 사용되는 상황 1개의 CPU를 가진 시스템에서 항상 사용됨 디폴트로 사용됨.
(논리 프로세서 2개 이상)
ASP.NET/SQL Server 기본 사용 (논리 프로세서 2개 이상)

위 표에 대해 너무 심각하게 생각할 필요는 없다. 필자의 생각으론 90% 는 이 따위 표는 상관없이, 아니 가비지 컬렉션 모드란 것을 전혀 몰랐던 상태 대로 어플리케이션을 작성하면 될 것이다. 나머지 10%는… 음… 음… 필자도 먹고 살아야 하니깐……

Server-GC 모드 사용 시 발생할 수도 있는 문제

좀 극단적일 수도 있지만, 그러나 무시할 수 없는 예제 한가지를 들어 보자. 어떤 서버 컴퓨터가 쿼드 코어 CPU 4개가 장착되어 있다고 가정해 보자. 그리고 이 서버는 웹 서버 및 어플리케이션 서버로 사용되어 다수의 WCF 서비스를 IIS에서 호스팅하고 있다. 쿼드 코어 CPU가 4개 이므로 하이퍼 스레드를 사용하지 않더라도 논리 프로세서의 개수는 16개 이다. 여기에 CPU 활용도를 높이고 가용성을 극대화하기 위해 IIS의 웹 가든을 구성하여 작업 프로세스(w3wp.exe)를 8개 구동시켰다고 가정해 보자(좋은 선택이다).

이 경우, 각 IIS 작업 프로세스에는 ASP.NET이 구동될 것이므로 Server-GC 모드가 암시적으로 사용된다. 고로, 각 작업 프로세스에는 16개의 가비지 컬렉션 스레드가 생성된다. 그리고 작업 프로세스가 8개 이므로 모두 128개(= 16 * 8)의 가비지 컬렉션 스레드가 생성될 것이다(으헉!). 일반적인 상황에서 메모리가 충분하다면 이들 가비지 컬렉션 스레드가 최적(?)의 효율로 가비지 컬렉션 작업을 수행할 것이다. 하지만 8개의 작업 스레드가 동시에 가비지 컬렉션, 그것도 2세대 (0, 1세대를 포함하며 시간이 오래 걸리는) 가비지 컬렉션이 발생한다면 어떻게 될까?

먼저, 8개의 작업 프로세스가 동시에 가비지 컬렉션을 수행하는 상황이 올 수 있는가부터 생각해 보자. 지난 포스트에서 가비지 컬렉션이 수행되는 상황을 언급할 때, 메모리 할당이 발생할 때뿐만 아니라 시스템이 전반적으로 메모리 부족 현상을 겪을 때에도 가비지 컬렉션이 발생한다고 했었다. 따라서 시스템에 물리 메모리가 많이 꼽혀 있지 않은 상황이 아니라면 시스템이 메모리 부족 상황을 겪게 될 것이고 이를 극복하기 위해 정의감에 불타는 CLR은 적극적으로 가비지 컬렉션을 수행하게 된다. 바로 이런 상황이 8개의 작업 프로세스가 (약간의 시간 차이는 있을 수 있겠지만) 동시에 가비지 컬렉션을 수행할 수 있는 상황이다. 돈 좀 아끼겠다고 프로세서가 16개나 되는 시스템에 꼴랑 메모리 2GB를 꼽아 놨다든가 하는 쫌생이 시츄에이션에선 충분히 “재현”도 가능한 상황이다.

이 경우, 모두 128개의 스레드가 동시에 가비지 컬렉션을 수행하고자 할 것이다. 비록 논리 프로세서가 16개나 된다 할지라도 윈도우 시스템 내부의 작업 스레드나 핵심 서비스의 스레드들도 자신에게 할당된 작업을 수행해야 한다. 따라서 128개의 스레드가 높은 우선 순위를 가지더라도 논리 프로세서들을 모두 독점하긴 어렵다. 이런 이유에서 몇몇 가비지 컬렉션 스레드가 프로세서 스케줄링에서 뒤처질 수 있다. 지난 포스트의 Server-GC 모드를 설명한 그림에서 볼 수 있듯이, Server-GC 모드에서 어플리케이션 스레드들이 재 시작 하는 시점은 가비지 컬렉션 스레드들이 모두 작업을 완료하는 시점임을 명심해야 한다. 따라서 백여 개의 가비지 컬렉션 스레드가 동시에 수행되는 동안, 작업 프로세스의 어떤 가비지 컬렉션 스레드가 스케줄링 등등의 문제로 자신의 작업을 완료하는데 오랜 시간이 소요된다면 해당 작업 프로세스가 클라이언트 요청을 다시 처리할 때까지의 지연 시간이 크게 늘어날 수도 있게 된다. 더 심각한 것은 시스템이 전반적으로 메모리 부족현상을 겪고 있다면 프로세스들이 서로의 메모리를 빼앗고 뺏기는 일이 자주 발생하기 때문에 이렇게 백여 개의 가비지 컬렉션이  개떼같이(필자는 소떼, 양떼는 본적이 있지만 개떼는 본적이 없는데… 봐 본 사람?) 달려드는 상황이 매우 자주 발생할 수 있다는 것이다. 고로 시스템의 전반적인 성능이 크게 떨어질 수 있다!

Workstation-GC for Server Application

따라서 다수의 논리 프로세서를 가진 시스템에서 다수의 닷넷 프로세스가 구동되는 상황이라면 이들 프로세스에 Server-GC 모드를 적용하는 것에 대해 생각을 해 보아야 한다. 물론, 메모리가 매우 충분하여 닷넷 프로세스들이 개떼같이 가비지 컬렉션을 수행하는 상황이 발생하지 않는다면 아무런 문제가 없을 것이다. 만약 그런 장담을 하기 어렵다면 Non-Concurrent-GC 모드를 사용하는 것이 전체적인 성능상 더 효과적일 수도 있다. Non-Concurrent-GC 모드에서는 관리되는 힙이 하나뿐이기 때문에 메모리 할당에서 약간의 지연이 발생하고 커다란 힙을 정리하는데 시간이 더 걸릴 것이다. 하지만 다수의 가비지 컬렉션 스레드가 동시에 프로세서를 경쟁하는 상황보다는 더 나은 대안을 제시할 수도 있다.

Concurrent-GC 모드 혹은 Background-GC 모드 대신 굳이 Non-Concurrent-GC 모드를 사용하는 이유는, Concurrent-GC와 Background-GC는 UI를 갖는 어플리케이션의 응답 속도를 향상시키기 위한 기법이지 서버 측 어플리케이션을 위한 향상 기법이 아니기 때문이다. 가비지 컬렉션 스레드가 2세대 힙을 정리하는 동안 중간 중간 자주 작업 스레드들이 중단될 수 있기 때문에 서버 어플리케이션에서는 오히려 좋지 못한 결과를 초래할 수도 있다. 아쌀하게 모든 스레드들을 종료하고 깔금하게 힙을 정리하자는 것이다.

ASP.NET이 Server-GC 모드를 사용하지 않고 Non-Concurrent-GC 모드를 사용하고자 하면 닷넷 프레임워크 설치 디렉터리에서 aspnet.config 파일을 수정하면 된다. 이 파일의 위치는 프레임워크 버전 및 32/64비트 버전 별로 다음과 같다.

  • x86 닷넷 프레임워크
    C:\Windows\Microsoft.NET\Framework\<version>\aspnet.config
  • x64 닷넷 프레임워크
    C:\Windows\Microsoft.NET\Framework64\<version>\aspnet.config

이 파일은 IIS 작업 프로세스에서 ASP.NET 런타임이 구동됨에 따라 읽혀져 CLR을 호스팅 할 때 읽혀져 CLR을 초기화 하는데 적용되는 <runtime> 요소가 포함되어 있다. 이 요소를 다음과 같이 수정함으로써 Server-GC 모드 대신 Non-Concurrent-GC 모드를 사용하도록 강제할 수 있다.

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
  <runtime>
    <!--- Server GC 대신 Non-Concurrent GC 사용 -->
    <gcServer enabled="false" />
    <gcConcurrent enabled="false" />
    ......
  </runtime>
  ......
</configuration>

여기서 잔소리를 하자면, Server-GC 대신 Non-Concurrent-GC를 선택해야겠다면 반드시, 꼭, 정말로 Server-GC 모드에 문제가 있는지 확인을 해야 한다는 것이다. 메모리가 충분한 상황에서 Server-GC 모드는 서버 어플리케이션에게 결코 문제를 발생하지 않으며 Non-Concurrent-GC 모드 보다 훨씬 더 나은 효과를 낸다. 따라서 많은 논리 프로세서가 존재하고 Server-GC 모드를 사용하는 다수의 프로세스가 존재하며, 메모리 부족 시에 성능 저하가 확인 되는 경우에만 Server-GC 대신 Non-Concurrent-GC 모드를 적용해야 할 것이다. 그리고 운영 서버에 적용하기 전에 개발 서버나 테스트 서버를 사용하여 충분한 성능 테스트 및 안정성 검사를 반드시 진행해야만 한다. 많은 논리 프로세서를 가진 서버들은 대개 물리 메모리도 무쟈게 충분하므로 아마도 Server-GC 모드 대신 Non-Concurrent-GC 모드를 사용해야 하는 경우는 거의 없으리라 생각된다. 이 글을 보고 또 쪼르르 달려가서 운영(!) 서버의 aspnet.config 파일을 무조건 수정해 놓고 성능이 느려졌네, 이 글이 틀렸네, 물어내라는 둥 띵깡을 부린다면, 아주 중요한 데를 발로 차버릴 것이니 신중하게 판단하기 바라는 바이다(라고 쓰고 책임 회피라 읽는다). 잘 모르겠으면 건드리지 않는 것이 정신건강과 육체건강에 도움이 된다. (이래서 이런 내용은 가급적 피하고 싶다능……)

다음 포스트는……

사실, 이번 포스트는 계획에 없었던 내용이었다. 어영 부영 글을 쓰다 보니 일케 되었다. 그렇다고 필자가 시간이 뎀비는 줄 알아선 곤란하다. 다 잠자는 시간, WOW(내 캐릭 돌리도!) 할 시간을 줄이거나 없애고 이 짓을 하는 것이니 말이다. 다음 포스트는 가비지 컬렉션 시리즈의 마지막(일까?) 포스트로써 관리되는 힙의 내부 구조에 대해 살펴보도록 하겠다. 이 포스트는 가상 메모리(Virtual Memory)에 대한 기본 지식을 필요로 하기 때문에 가상 메모리 관련 포스트들이 어느 정도 올라온 후에 작성되지 않을까 싶다. 응? 그럼 가상 메모리에 대한 글도 쓴다는 얘기야? 글쎄~


경고 : 이 글을 무단으로 복제/스크랩하여 타 게시판, 블로그에 게시하는 것은 허용하지 않습니다.