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

비밀번호

댓글 작성자
 




1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13503정성태12/27/20232191Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/20232298닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/20232098개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/20232179디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20232856닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/20232296오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232311Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232322Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20232502Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20232562닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232265개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232233Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232349개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232136개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20232069오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232381개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232195개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20232088오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/20232162개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/20232301닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/20232877닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/20232271개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/20232611개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/20232308개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/20232484닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
13476정성태12/8/20232211닷넷: 2181. C# - .NET 8 JsonStringEnumConverter의 AOT를 위한 개선파일 다운로드1
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...