성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] VT sequences to "CONOUT$" vs. STD_O...
[정성태] NetCoreDbg is a managed code debugg...
[정성태] Evaluating tail call elimination in...
[정성태] What’s new in System.Text.Json in ....
[정성태] What's new in .NET 9: Cryptography ...
[정성태] 아... 제시해 주신 "https://akrzemi1.wordp...
[정성태] 다시 질문을 정리할 필요가 있을 것 같습니다. 제가 본문에...
[이승준] 완전히 잘못 짚었습니다. 댓글 지우고 싶네요. 검색을 해보...
[정성태] 우선 답글 감사합니다. ^^ 그런데, 사실 저 예제는 (g...
[이승준] 수정이 안되어서... byteArray는 BYTE* 타입입니다...
글쓰기
제목
이름
암호
전자우편
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'>TCP 동시 접속 (연결이 아닌) 시도를 1개로 제한한 서버</h1> <p> 고객사로부터 희한한 버그 이슈가 나왔습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 제니퍼 에이전트를 설치한 웹 사이트가 제니퍼 서버가 꺼져 있으면 자사 웹 사이트의 로그인이 느려진다. </pre> <br /> 라는 것입니다. ^^; 제니퍼 닷넷 에이전트가 고객사의 "로그인 관련 코드"를 지연할 만한 어떤 요인도 없으므로, 그럴 수가 없는데... 그렇다는 것입니다.<br /> <br /> 실제로 원격으로 접속하니... ^^; 정말 그런 현상이 나타났습니다. 아니... ^^; 어떻게 이럴 수가 있는지 황당하기만 했는데요, 또한 로그인이 지연되는 그 순간의 메모리 덤프를 받아 callstack을 조사해봤지만 (당연히) 로그인과 관련된 호출에서 제니퍼 닷넷 에이전트 측의 메서드 호출은 하나도 없었습니다.<br /> <br /> 어쨌든 이런 현상은 대부분 고객사 측이 구성한 "보안 설정"에 의한 것이 많으므로 그쪽을 살펴보라고 했습니다. 하지만 그에 대해 고객사는 아무런 문제가 없다는 회신이 있었습니다.<br /> <br /> 어쩔 수 없군요, 이런 경우는 3rd-party 업체가 어떤 식으로든 원인 규명을 해야 합니다. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 제니퍼 에이전트의 기능을 (말도 안 되지만, 혹시나 싶어) 이거저거 관련 없는 옵션들을 제거하며 테스트를 해봤지만 해당 현상은 사라지지 않았습니다.<br /> <br /> 그러다 최후로 남은 기능은, 단순히 제니퍼 서버로 TCP 소켓을 연결하는 코드에 이르렀습니다. 여기서 문제의 원인이 모습을 드러냈는데요, TCP 소켓을 제니퍼 서버로 연결하는 그 코드를 비활성화시키면 고객사의 로그인 지연 문제가 발생하지 않았던 것입니다.<br /> <br /> 재미있지 않나요? ^^<br /> <br /> 그래서 간단한 소스 코드를 다음과 같이 작성해 봤습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Collections.Generic; using System.Net.Sockets; using System.Threading; // 희한한 건, 고객사 서버에서는 보안 도구가 막고 있는 것인지 다른 버전(3.5, 4.0)의 csc.exe는 실행 시 알 수 없는 오류가 발생했습니다. // 그래서 어쩔 수 없이 유일하게 실행 가능했던 .NET Framework 2.0의 C# 컴파일러를 사용해야 했고, // 내부 구문도 C# 2.0 버전에 맞춰야 했습니다. // C:\Windows\Microsoft.NET\Framework\v2.0.50727\csc test.cs namespace ConsoleApp1 { internal class Program { static string _host; static int _port; static void Main(string[] args) { _host = args[0]; _port = int.Parse(args[1]); ParameterizedThreadStart ts = new ParameterizedThreadStart(ConnectProxy); Thread t = new Thread(ts); t.Start(); Thread.Sleep(500); Connect(_host, 80); t.Join(); } static void ConnectProxy(object arg) { Connect(_host, _port); } static void Connect(string host, int port) { using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { Console.WriteLine(DateTime.Now + " Trying to connect " + host + ":" + port); try { socket.Connect(host, port); Console.WriteLine(DateTime.Now + " Connected!"); } catch (Exception e) { Console.WriteLine(DateTime.Now + " " + e.Message); } } } } } </pre> <br /> 위의 코드는 스레드 2개에서 각각 "지정된 포트"와 80 포트로 연결을 시도합니다. 그래서 만약 "지정된 서버"는 있지만 "지정된 포트"에서 대기하고 있는 TCP 서버가 없고 80 포트 대기는 있다면 다음과 같은 식의 출력이 나타납니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > c:\temp> <span style='color: blue; font-weight: bold'>test www.microsoft.com 15000</span> 2022-11-04 오후 9:28:<span style='color: blue; font-weight: bold'>23</span> Trying to connect www.microsoft.com:15000 2022-11-04 오후 9:28:<span style='color: blue; font-weight: bold'>23</span> Trying to connect www.microsoft.com:80 2022-11-04 오후 9:28:<span style='color: blue; font-weight: bold'>23</span> Connected! 2022-11-04 오후 9:28:44 A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond 104.76.29.199:15000 </pre> <br /> 보는 바와 같이 80 포트는 23초에 접속 시작해 곧바로 "Connected"가 떴고, 반면 15000 포트로의 접속은 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12015#default_timedout'>21초의 지연 시간 후 접속할 수 없다는 오류</a>가 발생합니다.<br /> <br /> 하지만 고객사 서버에서는 다음과 같은 식의 출력이 나왔습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > c:\temp> <span style='color: blue; font-weight: bold'>test www.microsoft.com 15000</span> 2022-11-04 오후 9:28:23 Trying to connect www.microsoft.com:15000 2022-11-04 오후 9:28:<span style='color: blue; font-weight: bold'>23</span> Trying to connect www.microsoft.com:80 2022-11-04 오후 9:28:44 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다 2022-11-04 오후 9:28:<span style='color: blue; font-weight: bold'>44</span> Connected </pre> <br /> 15000 포트의 접속 시도로 서버에서는 <a target='tab' href='https://www.sysnet.pe.kr/2/0/13153'>SYN_SENT 상태의 소켓</a>이 하나 남게 됩니다. 그 상태에서는 다른 TCP 연결 시도가 막혀서 이전의 TCP 소켓 자원이 해제될 때까지 연결을 맺지 못하고 있습니다. 그러다 21초 후에 SYN_SENT 상태의 소켓이 사라지면서 곧바로 80 포트로 접속을 시도하던 연결이 맺어지게 됩니다.<br /> <br /> 정리하면, 해당 고객사의 "어떤 알 수 없는 보안 도구"가 "EXE 프로세스 당 1개"의 TCP 동시 접속 시도만 허용하도록 막고 있는 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이 문제를 고객사 상황과 연결하면 현상이 해석됩니다.<br /> <br /> 제니퍼 에이전트는 제니퍼 서버와 TCP 통신을 하는데, 이때 만약 서버가 끊기면 제니퍼 에이전트는 주기적으로 TCP 연결을 서버로 시도하게 됩니다. 물론, 서버가 없기 때문에 21초 동안 SYN_SENT 상태의 소켓이 남게 되고 그때 해당 웹 애플리케이션에서 접속하려는 로그인 서버로의 TCP 연결이 방해를 받게 된 것입니다.<br /> <br /> 그런데, 왜? 로그인 서버로의 연결만 방해를 받는 걸까요?<br /> <br /> 사실 해당 웹 사이트는 DB 호출도 사용하고, 그 외 다른 서버로의 HTTP API 호출도 사용하고 있었습니다. 하지만 이런 호출들이 방해를 받지 않은 것은 DB의 경우 Connection Pool을 유지하기 때문에 신규 연결이 아니라면 TCP 연결이 생성되지 않아 괜찮았던 것입니다. 또한 <a target='tab' href='https://www.sysnet.pe.kr/2/0/10929'>HTTP API 호출도 닷넷 내부적으로는 소켓 풀을 운영</a>하기 때문에 역시 신규 연결이 필요한 상황이 아니면 TCP 연결을 하지 않아 표면상 서비스 문제를 겪지 않았습니다.<br /> <br /> 하지만 로그인 서버로의 연결은 달랐습니다. 해당 고객사의 로그인은 DB나 다른 HTTP 호출을 이용해 확인하는 방식이 아니었습니다. 로그인 지연 현상이 발생할 때 뜬 덤프의 콜 스택을 보면 이렇게 Active Directory에 대한 호출이 있었음을 확인할 수 있었는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DomainNeutralILStubClass.IL_STUB_PInvoke(System.String, System.String, System.String, Int32, System.Guid ByRef, System.Object ByRef) <span style='color: blue; font-weight: bold'>System.DirectoryServices.Interop.UnsafeNativeMethods.ADsOpenObject(System.String, System.String, System.String, Int32, System.Guid ByRef, System.Object ByRef) System.DirectoryServices.DirectoryEntry.Bind(Boolean) System.DirectoryServices.DirectoryEntry.Bind() System.DirectoryServices.DirectoryEntry.get_AdsObject() System.DirectoryServices.DirectorySearcher.FindAll(Boolean) System.DirectoryServices.DirectorySearcher.FindOne()</span> ...[생략]... System.Web.UI.WebControls.Button.RaisePostBackEvent(System.String) System.Web.UI.Page.ProcessRequestMain(Boolean, Boolean) System.Web.UI.Page.ProcessRequest(Boolean, Boolean) System.Web.UI.Page.ProcessRequest() System.Web.UI.Page.ProcessRequest(System.Web.HttpContext) System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef) System.Web.HttpApplication+ApplicationStepManager.ResumeSteps(System.Exception) System.Web.HttpApplication.System.Web.IHttpAsyncHandler.BeginProcessRequest(System.Web.HttpContext, System.AsyncCallback, System.Object) System.Web.HttpRuntime.ProcessRequestInternal(System.Web.HttpWorkerRequest) System.Web.Hosting.ISAPIRuntime.ProcessRequest(IntPtr, Int32) DomainNeutralILStubClass.IL_STUB_COMtoCLR(Int64, Int32, IntPtr) </pre> <br /> 결국, ADsOpenObject 메서드는 호출 때마다 매번 TCP 연결을 새로 맺는 동작을 했기 때문에 저렇게 특이한 현상을 나타나는데 기여를 했던 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 위의 상황에서 가장 간단한 해결 방법은 보안 도구가 막고 있는 "TCP 동시 접속 시도" 수를 늘리는 것입니다. 하지만, 경험상 대개의 경우 고객사는 일단 설치된 서버의 설정을 바꾸는 것을 원치 않습니다.<br /> <br /> 그래서 제니퍼 에이전트 측에 제니퍼 서버로 TCP 연결을 먼저 맺지 않고 <a target='tab' href='https://www.sysnet.pe.kr/2/0/629#udp_check'>UDP로 echo 테스트를 먼저 한 다음 맺는 방식</a>을 추가함으로써 일단 이슈를 종료했습니다.<br /> <br /> 하지만, 이건 아마도 언젠가 해당 고객사의 발목을 잡을 만한 엉뚱한 보안 대책이라고 볼 수 있습니다. 왜냐하면, 고객사의 경우 HTTP 호출을 다수 하고 있기 때문에 만약 그 서버들 중 하나라도 접속이 끊기게 된다면 마찬가지 상황에 빠질 것이기 때문입니다. 게다가 이번 경우에는 제니퍼 에이전트가 TCP 연결 하나만을 일정 주기로 재시도하고 있기 때문에 크게 문제가 되진 않았지만, 만약 다수의 HTTP API 호출이 끊겼다가 새로 TCP 연결을 시도하는 SYN_SENT가 누적된다면 아예 그 사이트는 로그인 조차 거의 할 수 없는 상태로 진행될 것입니다.<br /> <br /> 또한, 그런 비정상적인 상황을 해결하기 위해 종종 하는 iisreset은 기존에 잘 연결되고 있던 DB나 살아 있는 HTTP API 서버로의 연결을 새로 맺게 만드는 과정을 요구하면서, SYN_SENT를 누적시키는 단 하나의 HTTP API 서버로 인해 더욱 심각한 장애 상황을 일으킬 것입니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
6683
(왼쪽의 숫자를 입력해야 합니다.)