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

비밀번호

댓글 작성자
 




... 46  47  48  49  50  51  [52]  53  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12335정성태9/21/202022407Linux: 31. 우분투 20.04 초기 설정 - 고정 IP 및 SSH 설치
12334정성태9/21/20207856오류 유형: 652. windbg - !py 확장 명령어 실행 시 "failed to find python interpreter"
12333정성태9/20/20208289.NET Framework: 941. C# - 전위/후위 증감 연산자에 대한 오버로딩 구현 (2)
12332정성태9/18/202010329.NET Framework: 940. C# - Windows Forms ListView와 DataGridView의 예제 코드파일 다운로드1
12331정성태9/18/20209492오류 유형: 651. repadmin /syncall - 0x80090322 The target principal name is incorrect.
12330정성태9/18/202010556.NET Framework: 939. C# - 전위/후위 증감 연산자에 대한 오버로딩 구현 [2]파일 다운로드1
12329정성태9/16/202012476오류 유형: 650. ASUS 메인보드 관련 소프트웨어 설치 후 ArmouryCrate.UserSessionHelper.exe 프로세스 무한 종료 현상
12328정성태9/16/202012690VS.NET IDE: 150. TFS의 이력에서 "Get This Version"과 같은 기능을 Git으로 처리한다면?
12327정성태9/12/202010262.NET Framework: 938. C# - ICS(Internet Connection Sharing) 제어파일 다운로드1
12326정성태9/12/20209760개발 환경 구성: 516. Azure VM의 Network Adapter를 실수로 비활성화한 경우
12325정성태9/12/20209292개발 환경 구성: 515. OpenVPN - 재부팅 후 ICS(Internet Connection Sharing) 기능이 동작 안하는 문제
12324정성태9/11/202010552개발 환경 구성: 514. smigdeploy.exe를 이용한 Windows Server 2016에서 2019로 마이그레이션 방법
12323정성태9/11/20209412오류 유형: 649. Copy Database Wizard - The job failed. Check the event log on the destination server for details.
12322정성태9/11/202010521개발 환경 구성: 513. Azure VM의 RDP 접속 위치 제한 [1]
12321정성태9/11/20208772오류 유형: 648. netsh http add urlacl - Error: 183 Cannot create a file when that file already exists.
12320정성태9/11/20209994개발 환경 구성: 512. RDP(원격 데스크톱) 접속 시 비밀 번호를 한 번 더 입력해야 하는 경우
12319정성태9/10/20209727오류 유형: 647. smigdeploy.exe를 Windows Server 2016에서 실행할 때 .NET Framework 미설치 오류 발생
12318정성태9/9/20209217오류 유형: 646. OpenVPN - "TAP-Windows Adapter V9" 어댑터의 "Network cable unplugged" 현상
12317정성태9/9/202011541개발 환경 구성: 511. Beats용 Kibana 기본 대시 보드 구성 방법
12316정성태9/8/20209965디버깅 기술: 170. WinDbg Preview 버전부터 닷넷 코어 3.0 이후의 메모리 덤프에 대해 sos.dll 자동 로드
12315정성태9/7/202012259개발 환경 구성: 510. Logstash - FileBeat을 이용한 IIS 로그 처리 [2]
12314정성태9/7/202010830오류 유형: 645. IIS HTTPERR - Timer_MinBytesPerSecond, Timer_ConnectionIdle 로그
12313정성태9/6/202011999개발 환경 구성: 509. Logstash - 사용자 정의 grok 패턴 추가를 이용한 IIS 로그 처리
12312정성태9/5/202015902개발 환경 구성: 508. Logstash 기본 사용법 [2]
12311정성태9/4/202011087.NET Framework: 937. C# - 간단하게 만들어 보는 리눅스의 nc(netcat), json_pp 프로그램 [1]
12310정성태9/3/202010338오류 유형: 644. Windows could not start the Elasticsearch 7.9.0 (elasticsearch-service-x64) service on Local Computer.
... 46  47  48  49  50  51  [52]  53  54  55  56  57  58  59  60  ...