웹 서비스를 호출할 때, 성능이 예상 보다 너무 안 나올 때가 종종 있습니다.
코드를 어떻게 작성했냐를 떠나서 아주 간단한 웹 서비스 호출임에도 불구하고 성능이 안 나오는 경우가 종종 있지요.
이 때는 항상 네트워크를 의심해 봐야 합니다. 결론만 쨉싸게 먼저 말하면 HttpWebRequest를 사용하는 코드는
디폴트로 한 서버에 2개를 초과하는 TCP 연결을 맺지 않도록 되어 있습니다. 따라서 여러 쓰레드가 동시에 하나의 웹
서비스 서버에 요청을 하는 경우, 2개를 제외한 나머지 쓰레드는 운 좋은 그 두 놈이 종료하기를 기다려야 합니다.
이러한 제한에 대해 상세히 알아보고, 이 제한을 없애는 설정에 대해서도 살펴보겠습니다. 사실 예전부터 이 내용을 다루어야지
하다가... 모 게시판에 어떤 분이 질문하셔서 이제서야 글을 쓴다는... 아웅...
Web Service Bottleneck
필자가 예전에 스트레스 테스트를 할 때였다. 웹 어플리케이션이 다른 어플리케이션 서버의 비즈니스 로직을 호출하는데,
이 때 사용된 방법이 웹 서비스였다. 테스트 방법은 비주얼 스튜디오에 포함된 ACT(Application Center
Test)로 웹 어플리케이션에 졸라 스트레스를 주고 그 결과를 살펴보는 것 이였다. 그런데... 성능이 예상 보다 너무 안
나오는 것 이였다. 하도 이상해서 ACT가 직접 어플리케이션 서버로 스트레스를 주도록 바꾸어 보았더니 엄청난 성능이 나오는
것이 아닌가? 쓰바 이게 뭔 일이냐...
Problem
닷넷에서 웹 서비스를 호출하고자 하면, 일반적으로 웹 서비스 프록시를 만들게 된다. 비주얼 스튜디오에서 "웹 참조"를
하거나 wsdl.exe 유틸리티를 수행시키면 웹 서비스에 대한 클라이언트 프록시가 이뿌게 만들어지고, 웹 서비스를 호출하는
것은 이제 프록시를 호출하는 것에 지나지 않을 정도로 간단해 진다.
웹 서비스 프록시는 System.Web.Protocol 네임스페이스의 HttpSoapClientProtocol
클래스를 상속 받아 구현된다. 사실 웹 서비스를 호출하는 중요한 작업은 모두 이 클래스에서 수행한다고 알면 되는데... 이
클래스가 웹 서비스 서버를 호출할 때, System.Net 네임스페이스의
HttpWebRequest 클래스를 사용한다는 것이다. 이 클래스야 닷넷에서 HTTP 프로토콜이 필요할
때 많이 애용되는 클래스 이므로 대충은 어떤 클래스인지 알고 있을 것이다.
그런데... 이 클래스에는 기본적으로 제약 사항을 갖고 있다.
HttpWebRequest 클래스는 동일 호스트에 대해 최대 2개까지의 TCP 연결 만을 맺도록 기본 설정되어 있다.
대개의 경우 이러한 제약은 문제가 되지 않는다. 사실 바꿀 수 있으므로 제약이라고 하기도 좀 뭐한 것이 되겠다. IE가
HTTP 액세스 할 때 사용되는 WinInet 역시 동일한 제약을 갖고 있으며, 이것이 불편하게 느껴진 적이 별로 없을
것이다.
그러나, 웹 어플리케이션이 또 다른 웹 서비스 서버를 호출하는 경우에는
문제가 달라진다. 웹 서비스 서버에 2개의 TCP 연결만을 허용한다면 한번에 웹 서비스를 호출할 수
있는 쓰레드는 기껏해야 둘이다. 하지만 웹 어플리케이션에 접속하여 사용하는 사용자가 많고 동시에 수십, 수백명이 웹 서비스
호출이 요구되는 페이지를 액세스 한다면? 완전히 조뙤는 것이 되겠다. 거의 순차적으로 요청이 처리될 것이고 사용자는 느린
반응 속도에 불평할 것이 틀림이 없다.
구체적으로 테스트를 해보자. 먼저 웹 서비스 코드(리스트1)는 다음과 같이
간단하게 작성한다. 웹 메쏘드 DoWebMethod() 는 단순히 1초 동안의 딜레이를 주도록 되어 있다.
1 [WebMethod]
2 public
void DoWebMethod()
3 {
4
// 1초 동안 딜레이를 준다.
5
System.Threading.Thread.Sleep(1000);
6 }
리스트1. 테스트에 사용할 웹 서비스의 웹 메쏘드
이제 테스트용 클라이언트 코드를 살펴보자. 리스트2에서 보인 것 처럼 10 개의 쓰레드를
만들어 이 쓰레드들이 동시에 웹 서비스를 호출한다. 그리고 10개의 쓰레드들이 모두 웹 서비스 호출을 마칠 때까지
기다린다. 그리고, 이들 10개의 쓰레드를 만들어 수행하고 이들이 모두 종료될 때까지의 시간을 Win32 API인
GetTickCount를 통해 구한다. 리스트2의 코드는 웹 서비스의 클라이언트이므로 앞서 말한 웹 어플리케이션
시나리오라면 ASP.NET의 코드 정도로 생각해야 할 것이다.
1 //
간단한 웹 서비스 테스트 클라이언트
2 class
TestClientApp
3 {
4
// Win32 API의 GetTickCount를 호출하여 시간을
잰다.
5
[System.Runtime.InteropServices.DllImport("kernel32")]
6
private
static
extern
int GetTickCount();
7
8
[STAThread]
9
static
void Main(string[]
args)
10
{
11
int nrThreads = 10;
12
Thread[] threads = new
Thread[nrThreads];
13
int start, end;
14
15
Console.WriteLine("Begin Invoke...");
16
// 쓰레드를 생성하여 동시에 웹 서비스를 호출하도록
한다.
17
start = GetTickCount();
18
for(int
i=0; i < nrThreads; i++) {
19
threads[i] = new
Thread(new
ThreadStart(WorkerMain));
20
threads[i].Start();
21
}
22
// 쓰레드들이 종료되기를 기다린다.
23
foreach(Thread thread
in threads) {
24
thread.Join();
25
}
26
// 소요된 시간을 계산 하여 나타낸다.
27
end = GetTickCount();
28
Console.WriteLine("End Invoke... Elapsed time = {0:0.000}
sec", (end-start) / 1000.0);
29
}
30
31
static
void WorkerMain()
32
{
33
TestService proxy = new
TestService();
34
proxy.DoWebMethod();
35
}
36 }
리스트2. 테스트용 웹 서비스 클라이언트
리스트2를 컴파일 하고 수행하면 어떤 결과가 나올까? 10개의 쓰레드가 1초가 소요되는 웹 서비스
메쏘드를 동시에(concurrently) 호출했다면, 수행결과는 1초 정도 소요되어야 옳을 것이다. 쓰레드 생성 및 종료에
소요되는 오버헤드를 계산하더라도 2초 이내로 결과가 나와야 상식이라 할 수 있다. 하지만
수행 결과는 쌩뚱맞게 5.xx 초 정도 소요된다.
10.xxx 초도 아니고 5.xx 초?
여기서 호스트 당 연결이 2개로 제한되어 있음을 알 수 있다. 10개의 쓰레드 중 2개는 호출이 수행될 것이고 나머지
8개의 쓰레드는 앞서 간 2 쓰레드의 호출이 끝나기를 기다리게 되며, 이런 식으로 매번 동시에 호출 가능한 쓰레드는 2개로
제한된다. 따라서 결과가 5초정도 소요되게 되는 것이다.
Solution
해결하는 방법은 의외로 매우 간단하다. Configuration 파일의
<system.net> 섹션에 <connectionManagement> 요소를 추가하고 Url 혹은 호스트 별로 최대 연결
개수를 명시해 주면 된다. 기본 설정은 machine.config 에 다음과 같이 되어 있다. 웹
어플리케이션에게는 졸라 엿 같은 설정이라 할 수 있겠다.
<system.net>
<!-- 다른 부분 생략... -->
<connectionManagement>
<add address="*" maxconnection="2" />
</connectionManagement>
</system.net>
닷넷 프레임워크 2.0에는 이러한 설정이 없다. 즉 2개의 연결 제한이
기본적으로 없다는 얘기이다. 어찌 되었건 이러한 제한을 없앨려면 클라이언트의 configuration
설정을 다음과 같이 바꾼다. 웹 서비스를 호출하는 클라이언트가 웹 어플리케이션(ASP.NET)이라면 web.config 일
것이고 그냥 콘솔 혹은 윈도우 프로그램이라면 app.config 가 될 것이다.
<system.net>
<!-- skip... -->
<connectionManagement>
<add address="*" maxconnection="100" />
</connectionManagement>
</system.net>
MSDN을 뒤져보면 <add> 태그에 address 속성에 호스트 이름으로 주어 각 호스트 별로 서로 다른 최대 연결
개수를 지정할 수 있는 것처럼 되어 있다. 하지만 닷넷 프레임워크 1.1에서는
아무리 address 속성을 이렇게 저렇게 넣어보아도 효과가 없었다. 다만 address를 * 로 주는
경우에만 효과가 있었다. 1.1 버전의 버그인 듯하다. 닷넷 프레임워크 2.0 에서는 address 속성에
http://hostname 형태로 프로토콜까지 지정해 주면 설정이 효과를 나타내었다.
Conclusion
결론은 닷넷 프레임워크 1.1에서 HttpWebRequest를 사용하는 경우, 기본적으로 2개의 TCP 연결만이
허용되는 제한을 갖고 있다는 것이고 이 제한을 없애기 위해서는 configuration 파일에
connectionManagement 설정을 해주어야 한다.
HttpWebRequest를 사용하는 웹 서비스, 그리고 닷넷 리모팅에서 HTTP 채널을 사용하는 경우에는 모두 이러한
제약에 걸릴 수 있음에 유의해야 한다. 그리고 이러한 제약은 닷넷 프레임워크 2.0에서는 없어진
듯하다.
1.1 버전을 사용할 때 항상 connectionManagement 설정을 해 주어야 할까? 그렇지는 않다. 단순한
C/S 클라이언트는 대개, 여러 쓰레드를 사용하여 서버를 동시에 호출하는 경우가 없기 때문에 굳이 이 설정을 해주지 않아도
된다. 하지만 웹 어플리케이션은 동시에 여러 웹 서비스 호출이 발생할 수 있으므로 반드시
connectionManagement 설정을 해주어야만 한다. 그렇지 않으면 느린 응답속도에 괴로워하며 현업들의 등살에
몸부림 치게 될 것이다. (너무 무섭나? -_-)