성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그냥 RSS Reader 기능과 약간의 UI 편의성 때문에 사용...
[이종효] 오래된 소프트웨어는 보안 위협이 되기도 합니다. 혹시 어떤 기능...
[정성태] @Keystroke IEEE의 문서를 소개해 주시다니... +_...
[손민수 (Keystroke)] 괜히 듀얼채널 구성할 때 한번에 같은 제품 사라고 하는 것이 아...
[정성태] 전각(Full-width)/반각(Half-width) 기능을 토...
[정성태] Vector에 대한 내용은 없습니다. Vector가 닷넷 BCL...
[orion] 글 읽고 찾아보니 디자인 타임에는 InitializeCompon...
[orion] 연휴 전에 재현 프로젝트 올리자 생각해 놓고 여의치 않아서 못 ...
[정성태] 아래의 글에 정리했으니 참고하세요. C# - Typed D...
[정성태] 간단한 재현 프로젝트라도 있을까요? 저런 식으로 설명만 해...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>모바일용 웹 사이트에서 발생하는 응답 시간 지연 현상</h1> <p> 제니퍼 닷넷으로 웹 사이트를 모니터링하는 경우, 시각적으로 가장 유용한 것이 바로 X-View입니다.<br /> <br /> <img alt='jennifer_connection_pooling_demo_1.png' src='/SysWebRes/bbs/jennifer_connection_pooling_demo_1.png' /><br /> <br /> 위의 화면을 보면, Y축이 응답시간을 나타내기 때문에 점이 위에 분포하는 것들은 그만큼 응답시간이 느렸다는 것을 의미합니다. Y축 단위가 ms(밀리세컨드)이기 때문에 이 경우에는 6~7초씩 처리 시간이 걸렸음을 알 수 있습니다.<br /> <br /> 이렇게 처리 시간이 오래 걸리는 요인은 일반적으로 DB나 외부 호출에 의해 병목현상이 발생한 것입니다. 그래서 X-View 상세보기를 통해 어떤 연동 작업에서 느려졌는지를 다음과 같이 확인할 수 있습니다.<br /> <br /> <img alt='wcf_recycle_2.png' src='/SysWebRes/bbs/wcf_recycle_2.png' /><br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 최근에 고객사로부터 재미있는 현상이 하나 보고되었습니다. 해당 웹 사이트는 "모바일 폰"에서만 접근하는 용도로 만들어졌는데 간헐적으로 응답 시간이 50초 이상씩 걸리는 현상이 발생한 것입니다.<br /> <br /> 문제는, X-View 상세 보기 화면에는 그 원인이 프로파일링 되지 않았다는 점입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ------------------------------------------------------------------------------------------------- [ NO ][ START_TIME ][ GAP][CPU_T] ------------------------------------------------------------------------------------------------- [0000][<span style='color: blue; font-weight: bold'>10:51:18 068</span>][ 0][ 0] START [0000][<span style='color: blue; font-weight: bold'>10:52:07 037</span>][ 0][ 0] [native:1680], 69 byte(s) [0001][10:52:07 037][ 0][ 0] AKL.Framework.ControllerBase.Initialize [0002][10:52:07 037][ 0][ 0] AKL.Framework.Core.GetUri [0003][10:52:07 037][ 0][ 0] Feelanet.Dev2008.Request [31 ms] [0004][10:52:07 068][ 31][ 0] Feelanet.Dev2008.get_Name [0005][10:52:07 068][ 0][ 0] Feelanet.Dev2008.get_Age [0006][10:52:07 068][ 0][ 0] Feelanet.Dev2008.get_Email [0007][10:52:07 068][ 0][ 0] Feelanet.Dev2008.Dispose [0008][10:52:07 068][ 0][ 0] END ------------------------------------------------------------------------------------------------- TOTAL[<span style='color: blue; font-weight: bold'>49,000</span>][ 0] </pre> <br /> 즉, 개발자가 작성한 코드에 포함된 원격 호출(WCF, .NET Remoting, HTTP 호출, DB 호출... 등)과는 무관한데다 사용자 코드 진입의 초기 시점에 문제가 발생한 것입니다.<br /> <br /> 다행히 제니퍼 닷넷의 옵션 중에는 응답시간이 지정된 시간 이상 걸리면 StackTrace를 남기는 기능이 있어서 이를 통해 해당 현상이 발생할 때마다 어떤 함수에서 오래 걸리는지 확인할 수 있었는데요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>at System.Web.Hosting.UnsafeIISMethods.MgdSyncReadRequest</span>(IntPtr pHandler, Byte[] pBuffer, Int32 offset, Int32 cbBuffer, Int32& pBytesRead) at System.Web.Hosting.IIS7WorkerRequest.ReadEntityCoreSync(Byte[] buffer, Int32 offset, Int32 size) at System.Web.HttpRequest.GetEntireRawContent() at System.Web.HttpRequest.FillInFormCollection() <span style='color: blue; font-weight: bold'>at System.Web.HttpRequest.get_Form</span>() at System.Web.HttpRequestWrapper.get_Form() at AKL.Framework.UI.MVC.Security.CrossSiteScriptFilter.OnActionExecuting(ActionExecutingContext filterContext) ...[생략]... at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`1.End() at System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult) at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) at System.Web.HttpApplication.PipelineStepManager.ResumeSteps(Exception error) ...[생략]... </pre> <br /> 콜스택을 봤을 때 AKL.Framework 내부에서 HttpRequest.Form 공용 속성을 접근하는데 이로 인해 System.Web.HttpRequest.GetEntireRawContent가 불려진 것입니다. 이름에서 유추할 수 있듯이 이 메소드는 HTTP payload 전체를 읽어들이는데요. 마지막으로 불리는 System.Web.Hosting.UnsafeIISMethods.MgdSyncReadRequest 메소드를 .NET Reflector로 확인해 보면 Win32 DLL에 구현된 API인 것을 알 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [DllImport("webengine4.dll", CharSet=CharSet.Unicode)] internal static extern int MgdReadEntityBody(IntPtr pHandler, byte[] pBuffer, int dwOffset, int dwBytesToRead, bool fAsync, out int pBytesRead, out IntPtr ppAsyncReceiveBuffer); </pre> <br /> 애석하게도, '관리' 영역을 벗어나 버리니 더 해볼 것이 없군요. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이쯤 되면 해답을 추정하기 위해 최대한 노력을 해봐야 합니다. 이를 위해 우선 GetEntireRawContent 수행 시간이 50초를 넘는 경우 HTTP Header/Body를 덤프로 남기는 코드를 넣어봤습니다.<br /> <br /> 결과는, 매우 재미있게도 해당 현상이 발생하는 경우엔 언제나 Content-Length에 지정된 길이의 HTTP Body 내용이 없다는 것이었습니다. (참고로, GetEntireRawContent 메소드는 HTTP Post인 경우에만 호출됩니다.)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>POST</span> /GetMenuList HTTP/1.1 Connection: keep-alive <span style='color: blue; font-weight: bold'>Content-Length: 23 </span> Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Accept: application/json, text/javascript, */*; q=0.01 Accept-Charset: utf-8, iso-8859-1, utf-16, *;q=0.7 Accept-Encoding: gzip,deflate Accept-Language: ko-KR, en-US Cookie: ASP.NET_SessionId=...[생략]...]; NFMPT.reqCnt=0 Host: m.abnkorea.co.kr Referer: http://m.test.co.kr/MapDetail User-Agent: Mozilla/5.0 (Linux; U; Android 4.1.2; ko-kr; SHV-E220S Build/JZO54K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30 Origin: http://m.test.co.kr <span style='color: blue; font-weight: bold'>X-Requested-With: XMLHttpRequest</span> </pre> <br /> 물론, 정상적으로 수행된 /GetMenuList의 경우에는 HTTP Body가 포함되어 있었습니다.<br /> <br /> 확신할 수는 없지만, 이 현상을 설명하는 한 가지 시나리오는 모바일 클라이언트에서 HTTP Header까지는 정상적으로 보냈으나 그다음 Body를 보내는 Socket.Send 단계에서 통신 불가 상태로 빠져 버린 것입니다. 어쨌든 3G 네트워크는 유선망과 비교해 안정성은 낮으므로 그나마 그렇게라도 해석 가능합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 3G 모바일 네트워크의 이해 ; <a target='tab' href='http://helloworld.naver.com/helloworld/111111'>http://helloworld.naver.com/helloworld/111111</a> </pre> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 개인적으로 궁금함이 생겼습니다. 위의 과정에서 보면 2가지 상황이 유추될 수 있습니다.<br /> <br /> <ol> <li>IIS 서버는 HTTP Body를 읽지 못해도 예외를 발생시키지 않는다.</li> <li>IIS 서버는 클라이언트로부터 HTTP 헤더만 읽어도 요청 처리를 시작한다.</li> </ol> <br /> 무엇보다 첫 번째 문제가 더 궁금했습니다. 왜냐하면, IIS 측에서 Body를 읽는 시도에서 성공하지 못했다면 오히려 Time-out 같은 예외를 냈을 것 같기 때문입니다.<br /> <br /> 이를 확인하기 위해 TCP Socket을 만들어 다음과 같이 클라이언트 코드를 만들어 보았습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static void Main(string[] args) { Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); EndPoint serverEP = new IPEndPoint(IPAddress.Loopback, 8010); socket.Connect(serverEP); string request = "POST / HTTP/1.1\nHost: 127.0.0.1\n"; request += "Content-Length: 5\n"; request += "Content-Type: application/x-www-form-urlencoded; charset=UTF-8\n\n"; byte[] sendBuffer = Encoding.UTF8.GetBytes(request); <span style='color: blue; font-weight: bold'> // HTTP 헤더를 먼저 보내고, socket.Send(sendBuffer); // 60초 대기 Thread.Sleep(1000 * 60);</span> // 그냥 종료 } </pre> <br /> 그런 다음 ASP.NET 웹 사이트의 global.asax.cs에 다음과 같은 코드를 넣었습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public class Global : System.Web.HttpApplication { void Application_Start(object sender, EventArgs e) { // Code that runs on application startup } void Application_BeginRequest(object sender, EventArgs e) { System.Diagnostics.Trace.WriteLine("BeginRequest ================================"); } // ... [생략] ... } </pre> <br /> 클라이언트 측에서 HTTP 헤더만을 보낸 시점에 디버깅 화면으로 "BeginRequest =====..." 로그가 출력되는 것을 확인할 수 있었습니다. 또한 결국에 HTTP Body를 못 받았음에도 불구하고 어떠한 예외도 발생하지 않았습니다.<br /> <br /> 이로써 2가지 모두 확인이 되었군요. ^^<br /> <br /> <hr style='width: 50%' /><br /> <a name='header_body'></a> <br /> 그런데, 아직도 궁금한 것이 하나 남았습니다. 정말로 모바일 브라우저 측에서 XMLHttpRequest가 그런 식으로 Header와 Body를 나눠서 전송하느냐는 것인데요.<br /> <br /> 이를 재현하기 위해 간단하게 TCP 웹 서버를 다음과 같이 만들고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static void Main(string[] args) { using (Socket srvSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { Console.WriteLine("http://localhost:8000으로 방문해 보세요."); IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 8000); srvSocket.Bind(endPoint); srvSocket.Listen(10); while (true) { Socket clntSocket = srvSocket.Accept(); ThreadPool.QueueUserWorkItem(httpProcessFunc, clntSocket); } } } private static void httpProcessFunc(object state) { Socket socket = state as Socket; byte[] reqBuf = new byte[4096]; int nRecv = socket.Receive(reqBuf); string input = Encoding.UTF8.GetString(reqBuf, 0, nRecv); <span style='color: blue; font-weight: bold'> // HTTP Body까지 한 번에 읽었는지 확인 bool hasBody = HasBody(input); Console.WriteLine("1: " + input); if (hasBody == false) { // HTTP Body가 이전에 안 읽혔으면 마저 읽어들인다. nRecv = socket.Receive(reqBuf); input = Encoding.UTF8.GetString(reqBuf, 0, nRecv); Console.WriteLine("2: " + input); }</span> string header = "HTTP/1.0 200 OK\nContent-Type: text/html; charset=UTF-8\n\n"; string body = "<html><body><mark>테스트 HTML</mark> 웹 페이지입니다.</body></html>"; byte[] respBuf = Encoding.UTF8.GetBytes(header + body); socket.Send(respBuf); socket.Close(); } private static bool HasBody(string input) { string splitter = "\r\n\r\n"; int pos = input.IndexOf(splitter); if (pos + splitter.Length == input.Length) { return false; } return true; } </pre> <br /> 그다음, 로컬 PC의 폴더에 다음과 같은 HTML 페이지를 만들었습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <html> <head> <script language="javascript" type="text/javascript"> function btn_Click() { var xmlObj = new XMLHttpRequest(); xmlObj.open("POST", "http://localhost:8000/", false); xmlObj.send("test"); } </script> </head> <body> <span onclick="btn_Click()">Click!</span> </body> </html> </pre> <br /> 준비가 되었으니 테스트를 시작해야죠. ^^ IE 웹 브라우저를 실행해서 HTML 페이지를 로드한 후 Click 텍스트를 마우스로 눌러보았는데요. 결과가 재미있습니다. ^^<br /> <br /> 한 번은 Header + Body가 모두 전송되어오고, 그다음 한 번은 정확히 Header만 먼저 전송된 후 Body가 나뉘어져 Receive에 읽혀졌습니다.<br /> <br /> 물론, 이것은 IE 웹 브라우저를 대상으로 테스트가 되었기 때문에 안드로이드 모바일 환경에서는 다를 수 있지만 그래도 브라우저들 간에 이런 식으로 구현되어졌음을 유추해 볼 수는 있습니다.<br /> <br /> 참고로, 이런 현상에 대한 Q&A 글도 있군요. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > When does a browser send HTTP payload separately from the HTTP request? ; <a target='tab' href='http://serverfault.com/questions/337949/when-does-a-browser-send-http-payload-separately-from-the-http-request'>http://serverfault.com/questions/337949/when-does-a-browser-send-http-payload-separately-from-the-http-request</a> </pre> <br /> 위의 글에서는 XMLHTTPRequest가 언제나 Header와 Body를 나눠서 보낸다고 되어 있는데요. <strike>IE의 경우에는 2번에 한 번만 그런 식으로 동작했습니다.</strike> TCP Stream의 특성상 Header, Body가 나뉘어 전송돼도 한 번에 받는 것이 가능하므로 위의 현상이 그런 의미에서 설명이 가능합니다.<br /> <br /> 암튼, 실제로 이런 식으로 동작하고 있으니 모바일 환경에서의 특성과 조화(?)만 이룬다면 고객사에서 겪었던 문제가 발생할 수 있는 근거는 마련된 것 같습니다.<br /> <br /> 좀 더 확실하게 테스트하려면, 모바일 폰을 가지고 다니면서 XMLHTTPRequest.send의 결과 값이 예외가 발생하는 순간과 서버 측에 기록된 GetEntireRawContent의 수행 시간이 맞아 떨어지는 현상이 정말 발생하는지 확인해 보면 됩니다. 음... 이 부분은 나중에 심심해지면 한번 시도해보겠습니다. ^^<br /> <br /> (<a target='tab' href='http://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=769&boardid=331301885'>첨부한 파일은 위의 테스트 코드를 포함</a>하고 있습니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1017
(왼쪽의 숫자를 입력해야 합니다.)