태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

ASP.NET 응용 프로그램의 확장 전략
Richard Campbell and Kent Alstad

 

 

 

사람들이 ASP.NET 성능에 대해 자문을 구할 때는 보통 프로젝트에서 이미 문제가 발생한 후입니다.
응용 프로그램이 프로덕션 환경에 배포되기 전에 자문을 구하는 경우는 드뭅니다.
개발자 입장에서 문제없이 작동한다고 해서 사용자에게도 문제가 발생하지 않으라는 법은 없습니다.
사이트가너무 느리다는 불만이 접수되었다고 가정해 봅시다.
경영진은 왜 테스트에서 이 문제가 발견되지 않았는지 의아해 합니다.
개발자는 문제를 재현하지 못합니다.
그 와중에 꼭 누군가는 ASP.NET은 확장이 안 된다고 말합니다.
여러분에게도 익숙한 상황인가요?
전 세계적으로 이용자 수가 가장 많은 몇몇 웹 사이트는 ASP.NET을 기반으로 운영됩니다.
MySpace가 대표적인 예입니다.
이 사이트는 여러 플랫폼을 거쳐 ASP.NET으로 마이그레이션되었습니다.
중요한 점은 웹 응용 프로그램은 확장에 따라 성능 문제가 발생할 수 있고 문제가 발생하면 실질적 원인을 확인하고 최적의 문제 해결 전략을 찾아야 한다는 사실입니다.
가장 어려운 일은 응용 프로그램의 성능을 포괄적으로 나타내는 일련의 척도를 만드는 일입니다.
문제를 전체적으로 확인하지 않고는 어디에 중점을 두어야 할지 알 수 없으니까요.


 

성능 방정식
2006년 9월, NetForecast의 Peter Sevcik과 Rebecca Wetzel는 "Field Guide to Application Delivery Systems"라는 백서를 발표했습니다.
이 백서는 WAN(광역 네트워크) 응용 프로그램의 성능 향상을 중점적으로 다루며, 그림 1의 방정식도 포함하고 있습니다.
이 식은 본래 WAN 성능을 측정하는 식이지만 약간만 수정하면 웹 응용 프로그램 성능을 측정하는 데 사용할 수 있습니다.
수정된 방정식은 그림 2에 나와 있으며 그림 3에는 각 요소가 설명되어 있습니다.
 

변수 정의
R 응답 시간입니다.사용자가 링크 클릭 등을 통해 페이지를 요청한 시점부터 사용자의 컴퓨터에 전체 페이지가 렌더링되기까지 소요되는 총 시간입니다.일반적으로 초 단위로 측정됩니다.
페이로드 태그와 모든 리소스(예: CSS, JS 및 이미지 파일)를 비롯하여 브라우저로 전송된 총 바이트입니다.
대역폭 브라우저에서 보내고 받는 전송 속도입니다.보내는 속도와 받는 속도가 다를 수 있으며 페이지가 여러 소스로부터 생성되는 경우 복수의 속도를 나타낼 수 있습니다.대개 평균값을 구해 초당 바이트 수(bps) 단위의 단일 대역폭으로 표시합니다.
AppTurns 특정 페이지에 필요한 리소스 파일의 수입니다.이러한 리소스 파일에는 CSS, JS, 이미지 및 기타 브라우저에서 페이지 렌더링 중에 가져오는 모든 파일이 포함됩니다.방정식에서는 AppTurns 식 앞에 RTT(왕복 시간)를 추가하여 HTML 페이지를 별도로 계산합니다.
RTT 전송된 바이트에 관계없이 왕복하는 데 소요된 시간입니다.모든 요청에는 최소 하나(페이지 자체에 대해)의 RTT가 소요됩니다.일반적으로 밀리초 단위로 측정됩니다.
동시 요청 브라우저에서 리소스 파일에 대해 수행하는 동시 요청의 수입니다.Internet Explorer는 기본적으로 두 개의 동시 요청을 수행합니다.이 설정을 조정할 수도 있지만 조정하는 경우는 극히 드뭅니다.
Cs 서버의 계산 시간입니다.코드를 실행하고 데이터베이스에서 데이터를 가져오고 브라우저에 보낼 응답을 구성하는 데 소요되는 시간입니다.밀리초 단위로 측정됩니다.
Cc 클라이언트의 계산 시간입니다.브라우저에서 실제로 화면에 HTML을 렌더링하고 JavaScript를 실행하고 CSS 규칙을 구현하는 등의 작업에 소요되는 시간입니다.
 
 
 
이제 수식이 준비되었으니 각 요소를 측정하는 문제만 해결하면 됩니다.
최종 값인 응답 시간은 비교적 쉽게 측정할 수 있습니다.
전체 프로세스에 소요되는 정확한 시간을 측정하는 여러 가지 도구가 있기 때문입니다.
페이로드는 다양한 도구로 측정할 수 있으며 (대표적인 도구로 websiteoptimization.com/services/analyze가 있음), 대역폭(speedtest.net 참조), 왕복 시간(Ping 사용) 역시 마찬가지입니다.
websiteoptimization.com/services/analyze 같은 도구는 웹 페이지의 HTML, CSS, JavaScript, 이미지 등의 크기를 보고합니다.
기본적으로 동시 요청은 일정하게 유지됩니다(Internet Explorer®의 경우 기본값 2).
남은 것은 CsCc입니다.
여기에는 약간의 개발 작업이 추가로 필요합니다.
페이지 실행이 시작된 정확한 시간을 초 단위로 기록하고 그 시간을 실행이 완료된 시점의 현재 시간에서 빼는 코드를 ASP.NET 페이지에 작성하기는 비교적 쉽습니다.
클라이언트 쪽에서도 마찬가지입니다.
시간을 기록한 다음 페이지가 완료되었을 때 OnLoad 이벤트 발생 시점에서 해당 시간을 빼는 JavaScript를 HTML 페이지 위에서 실행할 수 있습니다.
사실 성능 방정식을 사용하는 웹 사이트의 디버그 모드를 구축하려는 경우 이러한 모든 요소를 코딩할 수 있습니다.
그리고 브라우저에 성능 방정식 요소를 정기적으로 렌더링할 수 있다면 성능 문제가 발생하는 지점을 쉽게 파악할 수 있으므로 이는 바람직한 방법이라 할 수 있습니다.
예를 들어 여러분의 ASP.NET 응용 프로그램을 다른 대륙의 대역폭이 낮은 사용자들이 사용한다고 가정해 봅시다.
높은 ping 시간(200ms 초과)과 낮은 대역폭(500kbps 미만) 때문에 사용자들은 응용 프로그램의 총 페이로드와 왕복 작업 수에 매우 민감할 것입니다.
이러한 사용자의 환경은 개발자의 환경과는 크게 다르므로 사용자의 입장에서 응용 프로그램을 살펴보는 것이 중요합니다.

확장 문제
응용 프로그램이 테스트 환경에서는 잘 작동하지만 실제 환경에서는 성능이 저하된다면 컨설턴트는 확장 문제를 떠올리게 됩니다.
일반적으로 두 환경의 차이는 동시 사용자의 수 뿐입니다.
응용 프로그램의 성능이 항상 나쁘다면 확장 문제가 아니라 성능 문제일 가능성이 큽니다.
확장성을 개선하기 위해 사용할 수 있는 세 가지 전략은 전문화, 최적화, 그리고 분산입니다.
전략을 적용하는 방식은 다양하지만 실제 전략 자체는 간단하고 일관적입니다.
전문화의 목적은 응용 프로그램을 작은 조각들로 나누어 문제를 격리하는 것입니다.
예를 들어 이미지, CSS, JS 파일과 같은 정적 리소스 파일을 ASP.NET 서버 외부로 옮겨야 하는 경우가 있습니다.
ASP.NET에 맞게 잘 조정된 서버라고 해서 이러한 종류의 파일을 제공하는데 특별히 더 적합한 것은 아닙니다.
따라서 리소스 파일을 처리하도록 조정된 별도의 IIS 서버 그룹을 사용하면 실행 중인 응용 프로그램의 확장성이 눈에 띄게 개선될 수 있습니다.
압축 또는 암호화(SSL용) 작업을 많이 실행하는 경우 SSL 전용 서버를 설치하면 도움이 됩니다.
압축 및 SSL 종료용으로 사용할 수 있는 전문화된 하드웨어 장치도 있습니다.
서버 계층을 분리하는 보다 일반적인 전략으로 데이터 액세스, 복잡한 계산 등에 실제 웹 페이지 생성과는 독립된 별도의 서버를 사용하는 방법이 있지만, 웹 서버 3대와 비즈니스 개체 서버 2대를 사용하기보다는 모든 작업을 수행하는 웹 서버 5대를 사용하는 것이 낫다고 생각합니다.
웹 서버와 비즈니스 개체 서버 간의 프로세스와 관련이 없는 호출들로 인해 오버헤드가 많이 발생하기 때문입니다.
전문화는 확실하게 알려진 이점을 얻는 데에만 활용해야 합니다.
게다가 가장 빠르다고 해서 항상 최고의 솔루션은 아닙니다.
확장성의 목표는 일정한 성능입니다.
부하 증가에 따른 성능 변화 범위를 좁혀야 합니다.
즉, 사용자가 한 명이든, 천 명이든 모든 사용자에 대해 페이지 렌더링에 소요되는 시간은 같아야 합니다.
결국 보다 효과적으로 확장하려면 서버 코드를 최적화해야 합니다.
서버의 계산 시간을 제외한 성능 방정식의 요소 대부분은 선형적으로 확장됩니다.
언제든지 대역폭을 추가할 수 있고(그 시점도 쉽게 알 수 있음), 클라이언트 수가 늘어나더라도 클라이언트의 계산 시간은 변하지 않습니다.
성능 방정식의 다른 요소도 확장 시에 일정하게 유지됩니다.
하지만 서버의 계산 시간은 사용자 수가 늘어남에 따라 조정해야 합니다.

코드 최적화
서버 코드를 최적화하는 방법은 테스트를 통해 실질적인 차이가 있는지 확인하는 것입니다.
프로파일링 도구를 사용하여 응용 프로그램을 분석하고 응용 프로그램에서 가장 많은 시간이 소요되는 부분을 파악해야 합니다.
전체 프로세스는 실험을 근거로 해야 합니다.
즉, 도구를 사용하여 개선할 코드를 찾고, 코드를 개선하고, 실제로 성능이 개선되었는지 테스트하는 과정을 계속 반복합니다.
규모가 매우 큰 사이트에서 이와 같은 성능 조정은 금문교에 페인트를 칠하는 작업에 비유하기도 합니다.
도색 작업을 마무리하고 보니 다시 페인트 칠을 시작해야 할 시점이라는 의미입니다.
필자는 확장이 분산에서부터 시작된다고 생각하는 사람들이 많다는 사실에 항상 놀랍니다.
이들은 "하드웨어를 추가해."라고 소리칩니다.
물론 하드웨어를 추가하면 확실히 도움이 됩니다.
하지만 전문화와 최적화 없이는 극히 미미한 효과밖에 거둘 수 없습니다.
전문화를 통해 필요에 따라 응용 프로그램의 일부분을 분리할 수 있습니다.
예를 들어 이미지 서버를 분리하면 응용 프로그램의 나머지 부분과는 독립적으로 이미지 서비스를 쉽게 확장할 수 있습니다.
최적화도 특정 작업에 필요한 작업량을 줄여 주므로 분산에 도움이 됩니다.
이는 곧 같은 사용자 수로 확장하는 데 필요한 서버의 수가 더 적어짐을 의미합니다.

부하 분산
분산을 구현하려면 서버를 추가하고 응용 프로그램을 각 서버에 복제하고 부하 분산을 구현해야 합니다.
부하 분산에는 Windows Server® 2003의 모든 버전에 포함된 서비스인 NLB(네트워크 부하 분산)를 사용할 수 있습니다.
NLB를 사용하면 모든 서버가 부하 분산 관계에서 동등한 파트너가 됩니다.
모두 동일한 알고리즘으로 분산을 수행하고, 모두 공유 가상 IP 주소를 통해 모든 트래픽을 수신합니다.
각 서버는 부하 분산 알고리즘에 기반하여 특정 요청을 어떤 서버가 담당해야 할지 파악합니다.
클러스터의 각 서버는 하트비트를 전송하여 다른 서버에 자신이 활성 상태임을 알립니다.
서버에서 오류가 발생하면 해당 서버에 대한 하트비트가 중지되고 다른 서버들이 자동으로 이 서버의 공백을 보완합니다.
NLB는 많은 수의 사용자가 서로 비슷한 요청을 보내는 경우에 효과적입니다.
그러나 일부 요청이 다른 요청에 비해 훨씬 더 큰 부하를 일으키는 시나리오에서는 보완 메커니즘이 그다지 효과가 없습니다.
다행히 이러한 상황에는 하드웨어 부하 분산 솔루션을 이용할 수 있습니다.

선호도
궁극적으로 효과적인 부하 분산의 과제는 선호도를 제거하는 데 있습니다.
예를 들어 웹 서버가 하나뿐이면 당연히 세션 데이터를 해당 서버에 저장해야겠지만 웹 서버가 두 대 이상이면 세션 정보를 어디에 저장해야 할까요?
한 가지 방법은 웹 서버에 세션 정보를 저장하고 선호도를 사용하는 것입니다.
이는 기본적으로 특정 사용자의 첫 번째 요청에 부하 분산이 적용된 후에는 해당 사용자/세션의 모든 후속 요청이 첫 번째 요청과 같은 서버로 전송됨을 의미합니다.
이 방법은 모든 부하 분산 솔루션에서 지원하는 단순한 방법이며 경우에 따라서는 효과적일 수도 있습니다.
그러나 장기적으로 선호도는 문제를 일으킬 수 있습니다.
세션 데이터를 프로세스 내에 유지하면 속도는 빠르겠지만 ASP.NET 작업자 프로세스가 재생 처리되면 해당 세션이 모두 소멸됩니다.
그리고 작업자 프로세스는 다양한 이유로 재생 처리됩니다.
부하가 높아지면 IIS는 ASP.NET의 작업자 프로세스가 중지된 것으로 판단하여 프로세스를 재생 처리합니다.
사실 IIS 6.0에서는 기본적으로 23시간마다 작업자 프로세스가 재생 처리됩니다.
이 설정을 조정할 수는 있지만 어쨌든 사용자는 세션 데이터가 프로세스 내에 있는 상태에서 이 데이터를 잃기 쉽습니다.
사이트 규모가 작다면 큰 문제가 되지 않지만 사이트가 커지고 사용자 수가 많아질수록 문제가 심각해집니다.
게다가 다른 문제도 있습니다.
IP 주소별로 부하를 분산하는 경우 AOL처럼 한 서버에서 메가프록시 문제가 발생하고 해당 서버 자체에서는 전체 부하를 처리할 수 없게 됩니다.
또한 새 버전의 응용 프로그램으로 서버를 업데이트하기가 더 어려워집니다.
이를 위해서는 사용자가 사이트 이용을 마칠 때까지 몇 시간이고 기다리거나 사용자의 불편을 감수하고 세션을 강제로 종료해야 합니다.
서버가 중단되면 수많은 세션이 손실되므로 안정성도 문제가 됩니다.
선호도 제거는 분산의 핵심적인 목표입니다.
선호도를 제거하기 위해서는 세션 상태 데이터를 프로세스 외부로 옮겨야 합니다.
즉, 확장성 증대를 위해 성능 저하를 감수해야 합니다.
세션을 프로세스 외부로 옮기면 세션 데이터는 모든 웹 서버가 액세스할 수 있는 위치(SQL Server® 또는 ASP.NET 상태 서버)에 기록됩니다.
이는 web.config에 구성됩니다.
또한 프로세스 외부 세션을 지원하기 위해서는 코딩 작업도 필요합니다.
Session 개체에 저장되는 모든 클래스에는 Serializable 특성을 표시해야 합니다.
이는 클래스의 모든 데이터는 직렬화할 수 있거나 NonSerialized로 표시되어 무시되도록 해야 함을 의미합니다.
클래스에 표시를 하지 않으면 세션 데이터를 프로세스 외부에 저장하기 위해 직렬화기를 실행할 때 오류가 발생합니다.
마지막으로, 세션을 프로세스 외부로 옮기면 세션 개체에 데이터가 지나치게 많은 경우 이를 효과적으로 확인할 수 있습니다.
이제 모든 페이지 요청에 대해 이 많은 양의 데이터를 네트워크를 통해 두 번씩 나르는(페이지 시작에서 검색을 위해 한 번, 페이지 끝에서 반환하기 위해 한 번) 대가를 치르기 때문입니다.
Session 개체 문제가 해결되면 멤버 자격역할 관리자와 같은 다른 선호도 문제를 처리해야 합니다.
여기에는 선호도 제거에 있어서 각각 고유한 문제가 있습니다.
ASP.NET 응용 프로그램을 실질적으로 확장하려면 모든 형태의 선호도를 찾아 제거해야만 합니다.
지금까지 살펴본 모든 전략은 확장할 필요가 있는 거의 모든 웹 응용 프로그램에 적용됩니다.
사실 웹 응용 프로그램뿐만 아니라 사용하는 기술에 관계없이 대부분의 응용 프로그램에 이러한 전략을 적용할 수 있습니다.
이제 ASP.NET에만 해당되는 몇 가지 기법을 살펴보도록 하겠습니다.
 
페이로드 최소화
성능 방정식을 살펴보면, 특히 대역폭이 제한적인 경우에 페이로드가 중요한 역할을 한다는 사실을 알 수 있습니다.
페이로드 크기를 줄이면 응답 시간이 향상되고 주고받는 바이트가 줄어 확장에 유리하고 대역폭 관련 투자 비용까지 절감할 수 있습니다.
페이로드 크기를 줄이는 가장 간단한 방법 중 하나는 압축을 사용하는 것입니다.
IIS 6.0에서는 정적 파일, 동적으로 생성되는 응답(예: ASP.NET 페이지) 또는 두 가지 모두를 압축하도록 지정할 수 있습니다(그림 4 참조).
 
 
 
IIS 6.0은 요청 시 정적 파일을 압축하여 지정된 압축 파일 캐시에 이를 저장합니다.
동적으로 생성되는 응답의 경우 복사본이 저장되지 않고 매번 압축됩니다.
IIS 7.0은 보다 지능적으로 압축 대상을 선택하여 자주 사용되는 파일만 압축합니다.
압축은 프로세스 주기를 소모하지만 일반적으로 전용 웹 서버에는 프로세서 용량이 충분합니다.
IIS 7.0은 프로세서 사용량이 많아지면 압축 작업을 일시 중단함으로써 최적화 수준을 더욱 높입니다.
웹 서버 자체와 독립적으로 압축을 수행하기 위한 전용 장치도 있습니다.
페이로드 줄이기에서는 ViewState도 빼놓을 수 없습니다.
개발을 진행하는 동안 ViewState 사용량이 과다해지기 싶습니다.
대부분의 웹 컨트롤은 ViewState를 사용하므로 컨트롤이 많이 사용되는 페이지에서는 ViewState가 수천 바이트까지 증가합니다.
ViewState 사용량을 줄이려면 ViewState가 필요 없는 컨트롤에 대해 이를 해제해야 합니다.
경우에 따라 개발자는 ViewState를 줄이기 위해 아예 컨트롤을 제거하기도 하지만꼭 그럴 필요는 없습니다.
오늘날 사용되는 대부분의 웹 컨트롤은 과도한 ViewState 사용에 대해 쉽게 영향을 받기 때문에 ViewState 크기를 세부적으로 제어하는 기능을 제공합니다.
또한 코드나 응용 프로그램 실행 방법을 변경할 필요 없이 자동으로 ViewState를 제거 및 대체해 주는 하드웨어 장치도 있습니다.
페이로드 크기를 줄이기 위한 가장 효과적인 기술 중 하나는 AJAX입니다.
AJAX는 실제로 페이로드 크기를 줄이는 것이 아니라 브라우저로 전송되는 총 바이트를 늘리면서 페이로드의 인지되는 크기만 줄입니다.
AJAX를 사용하면 상위 페이지가 작아져 초기 렌더링 시간이 단축됩니다.
이후 해당 페이지의 개별 요소들이 데이터를 채우기 위해 서버에 요청을 보냅니다.
결과적으로 AJAX는 시간이 지남에 따라 페이로드를 분산시키며, 페이지의 다른 부분이 로드되는 동안 사용자가 일정 부분을 볼 수 있도록 합니다.
따라서 AJAX를 사용하면 사용자 환경이 전반적으로 향상됩니다.
그러나 다시 성능 방정식을 참조하여 그에 따른 실제 비용을 측정해야 합니다.
일반적으로 AJAX는 클라이언트의 계산 시간을 성능 측면에서 수용할 수 없는 수준까지 증가시킵니다.
개별 요소를 채우기 위한 서버로의 AJAX 왕복이 전체 페이지 요청을 대체하는 경우 왕복 수는 실질적으로 감소합니다.
그러나 많은 경우 특정 사용자에 대한 총 왕복 횟수는 증가합니다.
따라서 테스트를 철저히 수행하여 AJAX가 성능을 향상시키는지 아니면 오히려 저하시키는지를 확인해야 합니다.


 

캐싱
ASP.NET 응용 프로그램 확장 전문가들은 캐싱을 자주 언급합니다.
기본적으로 캐싱데이터를 사용자에게 더 가까이 옮기기 위한 것입니다.
웬만큼의 최적화 작업을 거치지 않은 일반적인 ASP.NET 응용 프로그램에서는 사용자에게 필요한 데이터의 대부분이 데이터베이스에 저장되어 있으며 요청할 때마다 데이터베이스에서 검색됩니다.
캐싱은 이러한 방식을 바꿉니다.
ASP.NET은 페이지 캐싱(출력 캐싱이라고도 함),부분 페이지 캐싱, 프로그래밍(데이터) 캐싱 등 세 가지 형태의 캐싱을 지원합니다.
페이지 캐싱은 가장 단순한 형태의 캐싱입니다.
페이지 캐싱을 사용하려면 ASP.NET 페이지에 @OutputCache 지시문을 추가하고 만료 시점에 대한 규칙을 지정합니다.
예를 들어 60초 동안 페이지가 캐시되도록 지정할 수 있습니다.
이 지시문을 추가하면 해당 페이지의 첫 번째 요청은 데이터베이스 및 기타 페이지를 생성하는 데 필요한 리소스에 액세스하는 일반적인 방식으로 처리됩니다.
이후 웹 서버의 메모리에 페이지가 60초 동안 보관되고 이 시간 동안 전송되는 모든 요청은 메모리에서 바로 처리됩니다.
이 예는 간단하지만 실제 페이지 캐싱의 기본적인 현실을 무시하고 있습니다.
즉, 모든 요소를 일정 시간 동안 캐싱할 수 있을 만큼 정적인 ASP.NET 페이지는 없습니다.
부분 페이지 캐싱은 이러한 한계를 극복하기 위한 기술입니다.
부분 페이지 캐싱을 사용하면 ASP.NET 페이지에서 캐싱 가능한 부분을 표시하여 주기적으로 변경되는 부분만 계산되도록 할 수 있습니다.
더 복잡하지만 효과적입니다.
가장 강력하고도 복잡한 캐싱 형태는 페이지에 사용되는 개체에 중점을 둔 프로그래밍 캐싱입니다.
프로그래밍 캐싱은 데이터베이스에서 검색된 데이터를 저장하는 데 주로 사용됩니다.
데이터 캐싱에서 가장 두드러진 문제는 데이터를 캐시한 후 원본 데이터가 변경될 수 있다는 점입니다.
어떠한 형태의 캐싱을 구현하든 가장 큰 문제는 캐싱의 만료이지만 메모리도 고려해야 합니다.
사용량이 많은 ASP.NET 서버에서는 여러 가지 이유로 메모리가 매우 중요합니다.
ASP.NET 페이지가 계산될 때마다 메모리가 사용됩니다.
또한 Microsoft® .NET Framework는 메모리를 매우 빠르게 할당하도록 설정되어 있지만 그에 비해 가비지 수집을 통해 메모리를 해제하는 속도는 느립니다.
가비지 수집.NET 메모리 할당에 대한 내용은 그 자체가 하나의 기사감이며 지금까지 여러 기사에서 다루었습니다.
여기서는 사용량이 많은 웹 서버의 경우 ASP.NET 응용 프로그램에 사용 가능한 메모리 공간으로 최소한 2GB를 확보해야 한다는 정도만 알아 두면 됩니다.
이러한 메모리는 웹 페이지 계산에 사용되는 변수와 구조에 할당되기 때문에 이론적으로 메모리는 대부분 임시적으로 사용됩니다.
그러나 프로세스 내부 세션, 캐시 개체와 같은 지속 메모리 개체의 경우 메모리 사용이 훨씬 더 큰 문제가 됩니다.
물론 이러한 문제는 응용 프로그램의 사용량이 매우 많은 경우에만 드러납니다.
시나리오를 하나 예로 들겠습니다.
마케팅 행사를 새로 진행하면서 웹 사이트 방문자가 크게 늘어 수천 명의 사용자가 사이트를 방문하고 여러분은 큰 수익을 올리는 중입니다.
양호한 응답 시간을 유지하기 위해 가능한 한 페이지의 일부와 데이터 개체 그룹을 캐싱하고 있습니다.
사용자가 전송하는 각 페이지 요청은 메모리를 사용하므로 소모되는 메모리의 양이 지속적으로 증가합니다.
사용자가 늘어날수록 증가 속도도 빨라집니다.
캐시 및 세션 개체로 인한 메모리 사용 급증 현상도 발생합니다.
총 메모리 사용량이 ASP.NET 기본 캐시 메모리 한도의 90%에 육박하면 가비지 수집 이벤트가 호출됩니다.
가비지 수집기는 메모리 공간에서 작업을 진행하여 지속 메모리 개체(캐시 개체 및 세션 개체)를 버리고 더 이상 사용되지 않는 메모리(웹 페이지 계산에 사용된 메모리)를 해제합니다.
사용되지 않는 메모리는 빠르게 해제되지만 지속 개체를 버리는 속도는 느립니다.
따라서 지속 개체가 많을수록 가비지 수집기의 작업은 어려워집니다.
이 문제는 perform.exe에서 Gen 2 수집의 수가 많은 것을 통해 확인할 수 있습니다.
앞서 설명했듯이 가비지 수집을 진행하는 동안에는 ASP.NET 서버에서 페이지를 처리할 수 없기 때문에 가비지 수집 프로세스가 완료될 때까지 모든 작업이 큐에서 대기하게 됩니다.
또한 IIS도 프로세스를 감시합니다.
프로세스에 소요되는 시간이 너무 길 경우 IIS는 프로세스의 작동이 멈춘 것으로 판단하고 작업자 스레드를 재생 처리합니다.
지속 메모리 개체가 모두 제거되므로 많은 양의 메모리가 빠르게 해제되지만 일부 고객은 사이트 이용에 지장을 받게 됩니다.
메모리가 부족해지면 프로그래밍 캐시에서 개체를 자동으로 제거하는 ASP.NET용 패치가 현재 제공되고 있습니다.
얼핏 생각하면 매우 좋은 발상인 듯합니다.
시스템이 멈추는 것보다는 낫습니다.
캐시에서 무언가를 제거하더라도 결국 코드에 의해 다시 캐시된다는 사실을 명심하십시오.
무언가를 캐시하는 데는 잘못될 위험이 뒤따릅니다.
제품 데이터베이스와 이에 해당하는 주문 페이지를 예로 들어 보겠습니다.
초기 구현된 제품 페이지에서는 해당 페이지를 렌더링할 때마다 아직 재고 물량으로 남아 있는 제품 수에 대한 요청이 데이터베이스에서 수행됩니다.
이러한 요청을 분석해 보면 99%는 같은 수를 반복적으로 가져온다는 사실을 알 수 있습니다.
그렇다면 캐시하는 것이 좋겠지요?
간단한 방법은 일정 시간 동안 캐시하는 것입니다.
따라서 제품의 재고량을 1시간 동안 캐시하기로 했습니다.
이 기법의 단점은 누군가 제품을 구입한 후에 페이지로 돌아가면 똑 같은 재고량을 보게 된다는 것입니다.
따라서 이에 대한 불만이 접수됩니다.
이보다 훨씬 더 심각한 문제는 누군가 제품을 구입하기 위해 재고량을 확인할 때 실제로는 품절된 경우에도 재고가 남아 있는 것으로 나타난다는 사실입니다.
이월 주문 시스템을 구축할 수도 있지만 어쨌든 고객 불만은 감수해야 합니다.
문제는 만료 체계일지도 모릅니다.
즉, 시간만으로는 부족합니다.
재고 수량을 캐시하다가 누군가 제품을 구입한 후 캐시 개체가 만료되도록 할 수 있습니다.
이 방법은 더 논리적이지만 ASP.NET 서버가 두 대 이상이라면 어떻게 될까요?
사용자가 어떤 서버에 접속하는지에 따라 제품 재고 수량이 다르게 표시됩니다.
새로 입고되는 제품이 웹 응용 프로그램으로 전달되지 않는 경우를 생각해 보십시오.
완전히 새로운 형태의 문제가 발생하게 됩니다.
ASP.NET 서버 간에 만료를 동기화할 수도 있지만 주의를 기울여야 합니다.
캐시 개체와 웹 서버의 수가 증가함에 따라 웹 서버에서 생성되는 잡다한 항목의 수가 기하급수적으로 늘어날 수 있습니다.
캐시 만료가 성능이 미치는 영향도 신중하게 살펴야 합니다.
부하가 높은 상황에서 캐시 개체가 만료되면 많은 문제가 발생할 수 있습니다.
예를 들어 데이터베이스에서 반환되는 데 30초가 걸리는 무거운 쿼리가 있다고 가정해 보겠습니다.
부하가 높은 상황에서는 해당 페이지가 매초 한 번씩 요청되기 때문에 리소스 소모를 줄이기 위해 이 쿼리를 캐시했습니다.
캐시 개체를 처리하기 위한 코드는 상당히 간단합니다.
응용 프로그램은 필요할 때 데이터베이스에서 데이터를 가져오는 대신 먼저 캐시 개체가 채워졌는지 확인합니다.
개체가 채워져 있으면 캐시 개체의 데이터를 사용합니다.
채워져 있지 않으면 데이터베이스에서 데이터를 가져오는 코드를 실행한 후 이 데이터로 캐시 개체를 채웁니다.
이후 코드는 정상적으로 계속 실행됩니다.
문제는 30초가 소요되는 쿼리가 있고 매초 페이지를 실행한다면 캐시 항목을 채우는 시간 동안 29개의 다른 요청을 받게 되며, 이러한 모든 추가 요청은 각각 데이터베이스로 쿼리하여 캐시 항목을 채우려고 한다는 점입니다.
이 문제를 해결하려면 다른 페이지 실행에서 데이터베이스의 데이터를 요청하지 못하도록 스레드 잠금을 추가하면 됩니다.
시나리오를 다시 실행하면서 살펴보겠습니다.
첫 번째 요청이 수신됩니다.
이 요청은 캐시 항목이 채워져 있지 않음을 발견하고 코드에 잠금을 적용하고 캐시 개체를 채우기 위한 쿼리를 실행합니다.
1초 후 첫 번째 쿼리가 아직 실행 중일 때 두 번째 요청이 도착합니다.
이 요청은 캐시 개체가 채워져 있지 않음을 발견하지만 잠금이 적용되어 있기 때문에 차단됩니다.
다음 28개 요청도 동일합니다.
이후 첫 번째 요청의 처리가 끝나면 잠금이 제거되고 계속 진행됩니다.
나머지 29개 요청은 어떻게 될까요?
더 이상 차단되지 않으므로 역시 실행이 진행됩니다.
그러나 이 요청들은 이미 캐시 개체가 채워져 있는지 확인하는 검사를 실행한 상태이고, 검사 당시 채워져 있지 않았으므로 잠금을 확보하기 위해 시도합니다.
하나의 요청이 잠금에 성공하고 쿼리를 다시 실행합니다.
문제가 보이십니까?
첫 번째 요청이 캐시 개체 채우기를 완료한 후에 수신되는 다른 요청은 정상적으로 실행되지만 쿼리가 실행 중일 때 수신되는 요청은 곤란한 상황에 직면합니다.
따라서 이 문제를 해결하는 코드를 작성해야 합니다.
잠금 상태를 감지한 요청은 잠금이 해제될 때 캐시 개체가 채워져 있는지 다시 확인해야 합니다(그림 5 참조).
이때 캐시 개체가 채워져 있을 가능성이 크며, 이는 처음에 잠금이 적용된 이유입니다.
단, 그동안 다른 코드에서 캐시 개체를 다시 만료시켜 캐시 개체가 채워져 있지 않을 수도 있습니다.
 
 
// check for cached results
object cachedResults = ctx.Cache["PersonList"];
ArrayList results = new ArrayList();

if  (cachedResults == null)
{
  // lock this section of the code
  // while we populate the list
  lock(lockObject)
  {
    // only populate if list was not populated by
    // another thread while this thread was waiting
    if (cachedResults == null)
    {
      ...
    }
  }
}
 
 
효과적인 캐싱 코드는 작성하기 어렵지만 큰 이점을 얻을 수 있습니다.
그러나 캐싱은 복잡성을 높이기 때문에 신중하게 사용해야 합니다.
이러한 복잡성을 감수할 만큼 큰 효과가 있는지를 확인하고 이러한 복잡한 시나리오에 대해 항상 캐싱 코드를 테스트해야 합니다.
여러 개의 요청이 동시에 수신되면 어떤 현상이 발생하는지, 캐시 개체가 일찍 만료되면 어떻게 되는지 등을 파악해야 합니다.
그렇지 않으면 캐싱 코드로 인해 오히려 확장 문제가 더 심각해질 수도 있습니다.


 

데이터베이스 확장
일반적으로 웹 사이트는 상향으로 확장되지 않고 병렬로 확장됩니다.
이는 주로 ASP.NET 스레드와 메모리의 한계, 그리고 짧은 시간 동안만 유지되는 웹 요청의 특성 때문입니다.
데이터베이스 확장에서는 일반적으로 상향 확장이 사용됩니다.
즉, 용량이 매우 큰 한 대의 시스템 또는 클러스터로 구성된 두 대의 시스템(실질적으로는 한 번에 하나의 시스템에서만 데이터베이스가 실행됨)이 사용됩니다.
그러나 대규모 웹 응용 프로그램에서는 단일 데이터베이스로 부하를 처리할 수 없기 때문에 결국 병렬로 확장할 수밖에 없습니다.
웹 응용 프로그램 자체에 적용한 것과 같은 전략을 활용하면 병렬 확장이 가능합니다.
첫 번째 단계는 항상 전문화로, 데이터베이스를 논리적 파티션으로 나눕니다.
파티션은 데이터를 중심으로 나눌 수도 있고 지역별로 나눌 수도 있습니다.
따라서 각각 전체 데이터베이스의 일부를 포함하는 여러 개의 데이터베이스가 구성됩니다.
예를 들어 한 서버에는 동부 연안 지역의 데이터를, 다른 서버에는 서부 연안 지역의 데이터를 각각 저장할 수 있습니다.
그러나 웹 응용 프로그램의 규모가 매우 큰 경우에는 데이터베이스를 읽기 데이터베이스와 쓰기 데이터베이스로 분할합니다(그림 6 참조).
읽기 데이터베이스는 읽기 전용으로, 복제를 통해 쓰기 데이터베이스에서 데이터를 받습니다.
모든 데이터 쿼리는 데이터를 최대한 빠른 속도로 읽도록 최적화된 읽기 데이터베이스로 전달됩니다.
읽기 데이터베이스는 그 특성상 분산이 매우 용이합니다.

 

 

 

모든 데이터 쓰기 요청은 효과적인 쓰기를 위해 분할 및 조정된 쓰기 데이터베이스로 전송됩니다.
새 데이터는 복제를 통해 쓰기 데이터베이스에서 읽기 데이터베이스로 이동됩니다.
이렇게 전문화된 데이터베이스를 구축하면 지연이 발생합니다.
쓰기 작업을 여러 읽기 데이터베이스에 배포하는 데 시간이 걸리기 때문입니다.
그러나 지연을 처리할 수 있다면 확장 가능성은 막대합니다.


 

확장을 위한 끊임없는 노력
응용 프로그램의 규모가 커지면 이를 확장하기 위한 작업도 함께 증가합니다.
10,000명의 동시 사용자를 효과적으로 지원할 수 있는 ASP.NET 기법이라도 사용자가 100,000명으로 늘어나면 효율성이 떨어지고, 사용자가 100만 명으로 늘어나면 이 기준 역시 달라지게 됩니다.
물론 전적으로 응용 프로그램에 의해 성능이 좌우될 수도 있습니다.
필자는 사용자가 1,000명 이하임에도 확장 문제가 발생하는 응용 프로그램을 본 적도 있습니다.
효과적인 확장을 위해 가장 중요한 사항은 실행에 앞서 측정하는 것입니다.
테스트를 통해 필요한 곳에 시간과 노력을 투자하도록 해야 합니다.
또한 결과를 테스트하여 단순히 방식만 바뀐 것이 아니라 실제로 개선되었는지 확인해야 합니다.
확장성을 위한 최적화를 중심으로 개발 주기를 완료했더라도 어떤 부분의 속도가 가장 느린지 파악해야 합니다.
현재의 사용자를 수용하기에 충분할 만큼 속도가 빠르다면 사용자들이 앞으로 요구하게 될 부분에 투자를 집중할 수 있습니다.


 

Richard Campbell 은 Microsoft 지역 책임자이자 ASP.NET MVP이며 .NET Rocks, the Internet Audio Talkshow for .NET Developers(dotnetrocks.com)를 공동 진행하고 있습니다.그는 수년 동안 여러 회사에 ASP.NET의 성능과 확장에 대한 자문을 제공했으며 Strangeloop Networks의 공동 설립자이기도 합니다.

Kent Alstad 는 Strangeloop Networks (strangeloopnetworks.com)의 CTO이며 출원 중인 모든 Strangeloop 특허의 주 또는 보조 개발자입니다.Strangeloop 제작에 참여하기 전에는 다양한 고성능, 고확장성 ASP.NET 응용 프로그램을 구축하고 관련 자문을 제공했습니다.

 

### 출처 : MSDN Magazine / 저작권자 : Microsoft Corporation ###

Posted by Essayer

Trackback URL :: http://www.essayer7.com/trackback/136 관련글 쓰기