Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

TCP 동시 접속 (연결이 아닌) 시도를 1개로 제한한 서버

고객사로부터 희한한 버그 이슈가 나왔습니다.

제니퍼 에이전트를 설치한 웹 사이트가 제니퍼 서버가 꺼져 있으면 자사 웹 사이트의 로그인이 느려진다.

라는 것입니다. ^^; 제니퍼 닷넷 에이전트가 고객사의 "로그인 관련 코드"를 지연할 만한 어떤 요인도 없으므로, 그럴 수가 없는데... 그렇다는 것입니다.

실제로 원격으로 접속하니... ^^; 정말 그런 현상이 나타났습니다. 아니... ^^; 어떻게 이럴 수가 있는지 황당하기만 했는데요, 또한 로그인이 지연되는 그 순간의 메모리 덤프를 받아 callstack을 조사해봤지만 (당연히) 로그인과 관련된 호출에서 제니퍼 닷넷 에이전트 측의 메서드 호출은 하나도 없었습니다.

어쨌든 이런 현상은 대부분 고객사 측이 구성한 "보안 설정"에 의한 것이 많으므로 그쪽을 살펴보라고 했습니다. 하지만 그에 대해 고객사는 아무런 문제가 없다는 회신이 있었습니다.

어쩔 수 없군요, 이런 경우는 3rd-party 업체가 어떤 식으로든 원인 규명을 해야 합니다. ^^




제니퍼 에이전트의 기능을 (말도 안 되지만, 혹시나 싶어) 이거저거 관련 없는 옵션들을 제거하며 테스트를 해봤지만 해당 현상은 사라지지 않았습니다.

그러다 최후로 남은 기능은, 단순히 제니퍼 서버로 TCP 소켓을 연결하는 코드에 이르렀습니다. 여기서 문제의 원인이 모습을 드러냈는데요, TCP 소켓을 제니퍼 서버로 연결하는 그 코드를 비활성화시키면 고객사의 로그인 지연 문제가 발생하지 않았던 것입니다.

재미있지 않나요? ^^

그래서 간단한 소스 코드를 다음과 같이 작성해 봤습니다.

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);
                }
            }
        }
    }
}

위의 코드는 스레드 2개에서 각각 "지정된 포트"와 80 포트로 연결을 시도합니다. 그래서 만약 "지정된 서버"는 있지만 "지정된 포트"에서 대기하고 있는 TCP 서버가 없고 80 포트 대기는 있다면 다음과 같은 식의 출력이 나타납니다.

c:\temp> test www.microsoft.com 15000
2022-11-04 오후 9:28:23 Trying to connect www.microsoft.com:15000
2022-11-04 오후 9:28:23 Trying to connect www.microsoft.com:80
2022-11-04 오후 9:28:23 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

보는 바와 같이 80 포트는 23초에 접속 시작해 곧바로 "Connected"가 떴고, 반면 15000 포트로의 접속은 21초의 지연 시간 후 접속할 수 없다는 오류가 발생합니다.

하지만 고객사 서버에서는 다음과 같은 식의 출력이 나왔습니다.

c:\temp> test www.microsoft.com 15000
2022-11-04 오후 9:28:23 Trying to connect www.microsoft.com:15000
2022-11-04 오후 9:28:23 Trying to connect www.microsoft.com:80
2022-11-04 오후 9:28:44 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다
2022-11-04 오후 9:28:44 Connected

15000 포트의 접속 시도로 서버에서는 SYN_SENT 상태의 소켓이 하나 남게 됩니다. 그 상태에서는 다른 TCP 연결 시도가 막혀서 이전의 TCP 소켓 자원이 해제될 때까지 연결을 맺지 못하고 있습니다. 그러다 21초 후에 SYN_SENT 상태의 소켓이 사라지면서 곧바로 80 포트로 접속을 시도하던 연결이 맺어지게 됩니다.

정리하면, 해당 고객사의 "어떤 알 수 없는 보안 도구"가 "EXE 프로세스 당 1개"의 TCP 동시 접속 시도만 허용하도록 막고 있는 것입니다.




이 문제를 고객사 상황과 연결하면 현상이 해석됩니다.

제니퍼 에이전트는 제니퍼 서버와 TCP 통신을 하는데, 이때 만약 서버가 끊기면 제니퍼 에이전트는 주기적으로 TCP 연결을 서버로 시도하게 됩니다. 물론, 서버가 없기 때문에 21초 동안 SYN_SENT 상태의 소켓이 남게 되고 그때 해당 웹 애플리케이션에서 접속하려는 로그인 서버로의 TCP 연결이 방해를 받게 된 것입니다.

그런데, 왜? 로그인 서버로의 연결만 방해를 받는 걸까요?

사실 해당 웹 사이트는 DB 호출도 사용하고, 그 외 다른 서버로의 HTTP API 호출도 사용하고 있었습니다. 하지만 이런 호출들이 방해를 받지 않은 것은 DB의 경우 Connection Pool을 유지하기 때문에 신규 연결이 아니라면 TCP 연결이 생성되지 않아 괜찮았던 것입니다. 또한 HTTP API 호출도 닷넷 내부적으로는 소켓 풀을 운영하기 때문에 역시 신규 연결이 필요한 상황이 아니면 TCP 연결을 하지 않아 표면상 서비스 문제를 겪지 않았습니다.

하지만 로그인 서버로의 연결은 달랐습니다. 해당 고객사의 로그인은 DB나 다른 HTTP 호출을 이용해 확인하는 방식이 아니었습니다. 로그인 지연 현상이 발생할 때 뜬 덤프의 콜 스택을 보면 이렇게 Active Directory에 대한 호출이 있었음을 확인할 수 있었는데요,

DomainNeutralILStubClass.IL_STUB_PInvoke(System.String, System.String, System.String, Int32, System.Guid ByRef, System.Object ByRef)
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()
...[생략]...
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)

결국, ADsOpenObject 메서드는 호출 때마다 매번 TCP 연결을 새로 맺는 동작을 했기 때문에 저렇게 특이한 현상을 나타나는데 기여를 했던 것입니다.




위의 상황에서 가장 간단한 해결 방법은 보안 도구가 막고 있는 "TCP 동시 접속 시도" 수를 늘리는 것입니다. 하지만, 경험상 대개의 경우 고객사는 일단 설치된 서버의 설정을 바꾸는 것을 원치 않습니다.

그래서 제니퍼 에이전트 측에 제니퍼 서버로 TCP 연결을 먼저 맺지 않고 UDP로 echo 테스트를 먼저 한 다음 맺는 방식을 추가함으로써 일단 이슈를 종료했습니다.

하지만, 이건 아마도 언젠가 해당 고객사의 발목을 잡을 만한 엉뚱한 보안 대책이라고 볼 수 있습니다. 왜냐하면, 고객사의 경우 HTTP 호출을 다수 하고 있기 때문에 만약 그 서버들 중 하나라도 접속이 끊기게 된다면 마찬가지 상황에 빠질 것이기 때문입니다. 게다가 이번 경우에는 제니퍼 에이전트가 TCP 연결 하나만을 일정 주기로 재시도하고 있기 때문에 크게 문제가 되진 않았지만, 만약 다수의 HTTP API 호출이 끊겼다가 새로 TCP 연결을 시도하는 SYN_SENT가 누적된다면 아예 그 사이트는 로그인 조차 거의 할 수 없는 상태로 진행될 것입니다.

또한, 그런 비정상적인 상황을 해결하기 위해 종종 하는 iisreset은 기존에 잘 연결되고 있던 DB나 살아 있는 HTTP API 서버로의 연결을 새로 맺게 만드는 과정을 요구하면서, SYN_SENT를 누적시키는 단 하나의 HTTP API 서버로 인해 더욱 심각한 장애 상황을 일으킬 것입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 3/15/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 31  32  33  34  35  36  [37]  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12721정성태7/20/20218073오류 유형: 738. The trust relationship between this workstation and the primary domain failed. - 세 번째 이야기
12720정성태7/19/20217425Linux: 43. .NET Core/5+ 응용 프로그램의 Ubuntu (Debian) 패키지 준비
12719정성태7/19/20216615오류 유형: 737. SharePoint 설치 시 "0x800710D8 The object identifier does not represent a valid object." 오류 발생
12718정성태7/19/20217202개발 환경 구성: 581. Windows에서 WSL로 파일 복사 시 root 소유권으로 적용되는 문제파일 다운로드1
12717정성태7/18/20217136Windows: 195. robocopy에서 파일의 ADS(Alternate Data Stream) 정보 복사를 제외하는 방법
12716정성태7/17/20218022개발 환경 구성: 580. msbuild의 Exec Task에 robocopy를 사용하는 방법파일 다운로드1
12715정성태7/17/20219578오류 유형: 736. Windows - MySQL zip 파일 버전의 "mysqld --skip-grant-tables" 실행 시 비정상 종료 [1]
12714정성태7/16/20218376오류 유형: 735. VCRUNTIME140.dll, MSVCP140.dll, VCRUNTIME140.dll, VCRUNTIME140_1.dll이 없어 exe 실행이 안 되는 경우
12713정성태7/16/20218912.NET Framework: 1077. C# - 동기 방식이면서 비동기 규약을 따르게 만드는 Task.FromResult파일 다운로드1
12712정성태7/15/20218337개발 환경 구성: 579. Azure - 리눅스 호스팅의 Site Extension 제작 방법
12711정성태7/15/20218704개발 환경 구성: 578. Azure - Java Web App Service를 위한 Site Extension 제작 방법
12710정성태7/15/202110485개발 환경 구성: 577. MQTT - emqx.io 서비스 소개
12709정성태7/14/20217027Linux: 42. 실행 중인 docker 컨테이너에 대한 구동 시점의 docker run 명령어를 확인하는 방법
12708정성태7/14/202110480Linux: 41. 리눅스 환경에서 디스크 용량 부족 시 원인 분석 방법
12707정성태7/14/202177761오류 유형: 734. MySQL - Authentication method 'caching_sha2_password' not supported by any of the available plugins.
12706정성태7/14/20218899.NET Framework: 1076. C# - AsyncLocal 기능을 CallContext만으로 구현하는 방법 [2]파일 다운로드1
12705정성태7/13/20219074VS.NET IDE: 168. x64 DLL 프로젝트의 컨트롤이 Visual Studio의 Designer에서 보이지 않는 문제 - 두 번째 이야기
12704정성태7/12/20218223개발 환경 구성: 576. Azure VM의 서비스를 Azure Web App Service에서만 접근하도록 NSG 설정을 제한하는 방법
12703정성태7/11/202113908개발 환경 구성: 575. Azure VM에 (ICMP) ping을 허용하는 방법
12702정성태7/11/20219023오류 유형: 733. TaskScheduler에 등록된 wacs.exe의 Let's Encrypt 인증서 업데이트 문제
12701정성태7/9/20218703.NET Framework: 1075. C# - ThreadPool의 스레드는 반환 시 ThreadStatic과 AsyncLocal 값이 초기화 될까요?파일 다운로드1
12700정성태7/8/20219065.NET Framework: 1074. RuntimeType의 메모리 누수? [1]
12699정성태7/8/20217875VS.NET IDE: 167. Visual Studio 디버깅 중 GC Heap 상태를 보여주는 "Show Diagnostic Tools" 메뉴 사용법
12698정성태7/7/202111837오류 유형: 732. Windows 11 업데이트 시 3% 또는 0%에서 다운로드가 멈춘 경우
12697정성태7/7/20217684개발 환경 구성: 574. Windows 11 (Insider Preview) 설치하는 방법
12696정성태7/6/20218309VC++: 146. 운영체제의 스레드 문맥 교환(Context Switch)을 유사하게 구현하는 방법파일 다운로드2
... 31  32  33  34  35  36  [37]  38  39  40  41  42  43  44  45  ...