Microsoft MVP성태의 닷넷 이야기
.NET Framework: 353. x86 - AspCompat과 STA COM 개체가 성능에 미치는 영향 [링크 복사], [링크+제목 복사],
조회: 15776
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)

x86 - AspCompat과 STA COM 개체가 성능에 미치는 영향


테스트 환경은 다음과 같습니다.

  • .NET 4.5 x86
  • IIS 7.5




Native 시대의 COM 개체를 잘 모르더라도 ASP.NET에서 기존 COM 개체를 사용하려면 Apartment 유형에 따라 AspCompat 옵션 사용 유무를 판단해야 한다는 것은 알아야 합니다. ^^

ASP.NET 스레드 풀 내에 존재하는 모든 스레드는 기본적으로 (MTA는 아니고) MTA 성격이기 때문에 STA COM 개체를 생성하려면 특별한 마크를 해주어야 합니다. 그것이 AspCompat인데요. 이 때문에 STA COM을 사용하는 전형적인 페이지는 다음과 같이 됩니다.

// ==== default.aspx ====
<%@ Page AspCompat="true" Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeBehind="Default.aspx.cs" Inherits="WebApplication1._Default" %>

	... [HTML 생략]...

// ==== default.aspx.cs ====
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebApplication1
{
    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            WebTestHelperLib.ComAptTestClass catc = new WebTestHelperLib.ComAptTestClass();

            catc.DoMethod();
        }
    }
}

재미있는 것은, 위의 페이지를 실행하는 스레드의 흐름입니다. 이를 확인하려면 웹 페이지 코드를 다음과 같이 변경시키고,

public partial class _Default : System.Web.UI.Page
{
    int _pageTid;

    public _Default()
    {
        _pageTid = AppDomain.GetCurrentThreadId();
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        WebTestHelperLib.ComAptTestClass catc = new WebTestHelperLib.ComAptTestClass();
        int tid;
        int aptKind = catc.DoMethod3(5, out tid);

        StringBuilder sb = new StringBuilder();

        sb.AppendFormat("ctor Thread: {0}<br />", _pageTid);
        sb.AppendFormat("Page_Load Thread: {0}<br />", AppDomain.GetCurrentThreadId());
        sb.AppendFormat("Com Thread: {0}<br />", tid);
        sb.AppendFormat("Com Apt: {0}<br />", aptKind);

        Label1.Text = sb.ToString();
    }
}

COM 개체에서 제공되는 DoMethod3 메서드를 이렇게 구현해 주어야 합니다.

// sleepSecond: 메서드 실행 시 일부러 지연시킬 시간을 지정
// tid: DoMethod3을 실행하는 스레드의 Native ID가 반환됨
// aptKind: DoMethod3을 실행하는 스레드의 Apartment 유형을 반환
STDMETHODIMP CComAptTest::DoMethod3(LONG sleepSecond,LONG *tid, LONG *aptKind)
{
    ::Sleep(sleepSecond * 1000);

    *tid = ::GetCurrentThreadId();

    do 
    {
        // 아래의 코드는 https://www.sysnet.pe.kr/2/0/1351 글을 참조.
        BYTE *pTeb = (BYTE *)NtCurrentTeb();
        int *pOle = (int *)(pTeb + 0xf80);

        if (*pOle == 0)
        {
            printf("No apartment\n");
            break;
        }

        int *pNativeApt = (int *)(*pOle + 0x50);
        *aptKind = *(int *)(*pNativeApt + 0x0c);
    } while (false);

    return S_OK;
}

/*
*aptKind에 담기는 값은 다음 중의 하나입니다.

enum tagAPTKIND {
    APTKIND_NEUTRALTHREADED = 1,
    APTKIND_MULTITHREADED = 2,
    APTKIND_APARTMENTTHREADED = 4,
    APTKIND_APPLICATION_STA = 8
};
*/

자... 이렇게 하고 실행하면 다음과 같은 결과를 얻을 수 있습니다.

ctor Thread: 10992
Page_Load Thread: 5628
Com Thread: 5628
Com Apt: 4 (== APTKIND_APARTMENTTHREADED)

즉, 스레드의 실행 흐름이 다음과 같이 변경됩니다.

  1. 클라이언트의 요청을 ASP.NET Thread Pool 내의 10992 스레드가 받고,
  2. 10992 스레드가 Page 객체를 생성한 후,
  3. 10992 스레드가 AspCompat 값이 True임을 확인하고 STA COM 개체가 생성될 것임을 짐작,
  4. 따라서 별도로 생성해 둔 5628 STA 스레드로 Request 문맥을 전달,
  5. 5628은 이후 Page 개체의 나머지 실행 처리를 담당.

여기서 문제는 5628 STA 스레드가 모든 COM 개체의 사용에 대응된다는 것입니다. 따라서, 해당 COM 개체를 사용하는 모든 웹 페이지의 실행은 5628 스레드로 직렬화 되어 성능 저하가 심각해 집니다. 예를 들어, 위의 test.aspx.cs에서 COM 개체의 DoMethod3 메서드에 5초의 지연시간을 갖도록 지정해 보겠습니다. 2명의 사용자가 동시에 test.aspx 웹 페이지를 방문하면 다음과 같은 현상이 발생합니다.

  1. A와 B사용자가 동시에 test.aspx 웹 페이지 요청
  2. A 요청을 10992 스레드가 받아서 5628 STA 스레드에 전달
  3. B 요청은 10330 스레드가 받아서 5628 STA 스레드에 전달
  4. 5628 스레드는 먼저 전달된 A 요청을 실행 - 5초 걸림
  5. 5초 후 5628 스레드는 두 번째로 전달된 B 요청을 실행 - 5초 걸림

결국, 이런 직렬화 처리로 인해 B 요청은 A 요청과 거의 비슷한 시기에 전달되었음에도 불구하고 10초 후에나 응답을 받게 됩니다.

이 문제를 해결하려면, STA COM 개체를 MTA COM 개체로 만들어야 합니다.




그런데, 만약 AspCompat=True 값을 설정하지 않았다면 어떻게 될까요? 역시 같은 방법으로 2개의 요청을 동시에 보내보면 다음과 같은 결과를 얻을 수 있습니다.

=== A 요청 ====
ctor Thread: 34968
Page_Load Thread: 34968
Com Thread: 5628
Com Apt: 4

=== B 요청 ====
ctor Thread: 13432
Page_Load Thread: 13432
Com Thread: 5628
Com Apt: 4

Page_Load까지도 ASP.NET 스레드 풀 내의 스레드가 처리를 담당하다가 정작 STA COM 개체의 메서드 호출에는 별도로 생성되어 있는 STA 스레드에 맡겨지고 있습니다. 이 결과와 AspCompat=True였을 때의 상황과는 어떤 차이가 있을까요?

이에 대한 해답은 AspCompat=False이고, STA COM 개체를 썼을 때 2개의 동시 요청을 어떤 식으로 처리하는지 파악해 보면 알 수 있습니다.

  1. A와 B사용자가 동시에 test.aspx 웹 페이지 요청
  2. A 요청을 34968 스레드가 받아서 5628 STA 스레드에 전달하고 대기
  3. B 요청은 13432 스레드가 받아서 5628 STA 스레드에 전달하고 대기
  4. 5628 스레드는 먼저 전달한 A 요청을 실행(5초 걸림)하고 34968 스레드로 실행 반환
  5. 5628 스레드는 두 번째로 전달한 B 요청을 실행(5초 걸림)하고 13432 스레드로 실행 반환

차이를 아시겠어요? AspCompat=True일 때는 ASP.NET의 요청을 처리하는 스레드 풀의 스레드가 자유로웠지만, False일 때는 그 스레드가 STA 스레드의 실행이 끝날 때까지 대기한다는 문제가 발생합니다.

이 때문에, 스레드 풀의 스레드 수가 500개이면 위의 페이지가 500개의 요청을 받는 경우 모든 스레드가 바닥난다는 의미가 됩니다. 더 이상 다음의 요청을 받아서 처리할 수 없다는 것이지요.




AspCompat=True인 경우, STA COM 개체를 Page_Load가 아닌 생성자나 멤버 변수 정의시점에 만들면 상황이 더 재미있어집니다.

public partial class _Default : System.Web.UI.Page
{
    WebTestHelperLib.ComAptTestClass catc; // 여기서 new를 하거나!

    public _Default()
    {
        catc = new WebTestHelperLib.ComAptTestClass(); // 여기서 new를 한 경우!
    }

    protected void Page_Load(object sender, EventArgs e)
    {
    }
}

AspCompat 속성은 _Default 페이지 개체를 생성하는 순간에는 그 값을 알 수 없습니다. 따라서 ASP.NET이 AspCompat 속성을 보고 어떠한 조치를 취하기에는 너무 빠르다는 것인데요.

이 때문에 이런 요청을 A, B 두 사용자가 동시에 보낸다면 결과가 다음과 같이 나옵니다.

ctor Thread: 24344
Page_Load Thread: 23712
Com Thread: 5628
Com Apt: 4

ctor Thread: 348
Page_Load Thread: 23712
Com Thread: 5628
Com Apt: 4

차이가 눈에 보이시나요? ctor를 실행한 스레드는 ASP.NET의 스레드 풀 중의 하나입니다. 그리곤 뒤늦게 AspCompat=True 속성을 인식하고는 23712 스레드로 Page_Load를 실행하도록 변경하긴 했으나 이미 해당 COM 개체는 별도의 STA 스레드에 묶인 상태입니다.

이런 경우에는 스레드 풀의 모든 요청이 Page_Load 스레드에서 미리 직렬화됩니다. 23712 스레드가 24344 요청을 처리하고 COM 메서드를 실행한 다음 사용자에게 결과를 반환하고, 다시 348 요청을 COM 메서드를 실행한 다음 결과를 반환합니다.

AspCompat=True에서의 이런 문제점은 MSDN의 도움말에 다음과 같이 씌여 있습니다.

COM Component Compatibility
; https://docs.microsoft.com/en-us/previous-versions/aspnet/zwk9h2kb(v=vs.100)

COM components that are created at construction time run before the request is scheduled to the STA thread pool and consequently run on a multithreaded apartment (MTA) thread. This has a substantial negative performance impact and should be avoided. If you use AspCompat with STA components, you should create COM components only from the Page_Load event or later in the execution chain and not at page construction time.





결론을 내려 보면, STA COM 개체를 사용한다면 AspCompat=True로 설정하고 Page_Load 이후에 COM 개체를 생성하는 것이 좋습니다.

그리고, 만약 가능하다면 COM 개체를 만든 업체 측에 MTA로 해달라고 요청하는 것이 성능을 위해 가장 좋습니다.

참고 삼아 AspCompat=True인 경우의 콜 스택을 보면 다음과 같이 나옵니다.

WebApplication1.dll!WebApplication1.WebForm1.Page_Load(object sender, System.EventArgs e) Line 16	C#
System.Web.dll!System.Web.UI.Control.LoadRecursive() + 0x74 bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) + 0xb5e bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequest(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) + 0xb9 bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequest() + 0x77 bytes	
System.Web.dll!System.Web.HttpApplication.ExecuteStep(System.Web.HttpApplication.IExecutionStep step, ref bool completedSynchronously) + 0xa5 bytes	
System.Web.dll!System.Web.Util.AspCompatApplicationStep.ExecuteAspCompatCode() + 0x83 bytes	
System.Web.dll!System.Web.Util.AspCompatApplicationStep.OnAspCompatExecution() + 0x79 bytes	

ExecuteAspCompatCode, OnAspCompatExecution 2개의 메서드가 눈에 띕니다. 또한 HttpContext.Current가 유효한 단계는 System.Web.UI.Page.ProcessRequest 호출이므로 그 이후의 호출 스택에서만 HTTP 문맥 개체를 사용할 수 있습니다.

비교를 위해 다음은 일반적인 웹 페이지의 콜 스택을 보여주고 있습니다.

WebApplication1.dll!WebApplication1.WebForm1.Page_Load(object sender, System.EventArgs e) Line 16	C#
System.Web.dll!System.Web.UI.Control.LoadRecursive() + 0x74 bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) + 0xb5e bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequest(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) + 0xb9 bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequest() + 0x77 bytes	
System.Web.dll!System.Web.UI.Page.ProcessRequest(System.Web.HttpContext context) + 0xa3 bytes	
App_Web_cx5dgpsp.dll!ASP.default_aspx.ProcessRequest(System.Web.HttpContext context) + 0x33 bytes	C#
System.Web.dll!System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() + 0x445 bytes	
System.Web.dll!System.Web.HttpApplication.ExecuteStep(System.Web.HttpApplication.IExecutionStep step, ref bool completedSynchronously) + 0xa5 bytes	
System.Web.dll!System.Web.HttpApplication.PipelineStepManager.ResumeSteps(System.Exception error) + 0x4de bytes	
System.Web.dll!System.Web.HttpApplication.BeginProcessRequestNotification(System.Web.HttpContext context, System.AsyncCallback cb) + 0x85 bytes	
System.Web.dll!System.Web.HttpRuntime.ProcessRequestNotificationPrivate(System.Web.Hosting.IIS7WorkerRequest wr, System.Web.HttpContext context) + 0x255 bytes	
System.Web.dll!System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(System.IntPtr rootedObjectsPointer, System.IntPtr nativeRequestContext, System.IntPtr moduleData, int flags) + 0x47e bytes	
System.Web.dll!System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(System.IntPtr rootedObjectsPointer, System.IntPtr nativeRequestContext, System.IntPtr moduleData, int flags) + 0x22 bytes	
[Native to Managed Transition]	
[Managed to Native Transition]	
System.Web.dll!System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(System.IntPtr rootedObjectsPointer, System.IntPtr nativeRequestContext, System.IntPtr moduleData, int flags) + 0x632 bytes	
System.Web.dll!System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(System.IntPtr rootedObjectsPointer, System.IntPtr nativeRequestContext, System.IntPtr moduleData, int flags) + 0x22 bytes	
[Appdomain Transition]	




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/17/2021]

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)
13508정성태1/3/20242431오류 유형: 886. ORA-28000: the account is locked
13507정성태1/2/20243227닷넷: 2191. C# - IPGlobalProperties를 이용해 netstat처럼 사용 중인 Socket 목록 구하는 방법파일 다운로드1
13506정성태12/29/20232751닷넷: 2190. C# - 닷넷 코어/5+에서 달라지는 System.Text.Encoding 지원
13505정성태12/27/20233369닷넷: 2189. C# - WebSocket 클라이언트를 닷넷으로 구현하는 예제 (System.Net.WebSockets)파일 다운로드1
13504정성태12/27/20232948닷넷: 2188. C# - ASP.NET Core SignalR로 구현하는 채팅 서비스 예제파일 다운로드1
13503정성태12/27/20232806Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/20232873닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/20232630개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/20232799디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20233610닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/20232740오류 유형: 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/20232963Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232899Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20233049Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20233203닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232832개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232544Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232690개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232469개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20232411오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232730개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232542개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20232404오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/20232617개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/20232790닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/20233542닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...