Microsoft MVP성태의 닷넷 이야기
스크립트: 37. 파이썬 - uwsgi의 --enable-threads 옵션 [링크 복사], [링크+제목 복사],
조회: 14777
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 3개 있습니다.)

파이썬 - uwsgi의 --enable-threads 옵션

uwsgi는 기본적으로 요청을 처리하는 방식이, 아파치 웹 서버의 동작 방식과 굳이 비교하자면 prefork에 해당합니다. 그리고 별도의 "--threads" 옵션을 지정하면 그때부터는 아파치의 worker 방식처럼 동작을 합니다.

여기서 (IIS에 익숙한) 윈도우 개발자들이 혼동하지 말아야 할 것은, worker 방식이라고 해서 모든 요청을 스레드로 받아 하나의 프로세스에서 처리하는 것은 아니라는 점입니다. 즉, 기본적으로 fork를 해 서브 프로세스를 생성하지만 prefork 방식에서는 요청과 서브 프로세스가 1:1이었다고 하면, worker 모델에서는 서브 프로세스 당 요청을 처리할 수 있는 스레드를 지정할 수 있어 fork된 프로세스 내에서 스레드를 이용해 여러 개의 요청을 처리할 수 있다는 정도입니다.


그리고, 파이썬 환경의 경우 GIL(Global Interpreter Lock)의 특징으로 인해 uwsgi 호스팅 환경에서는 또 다른 옵션이 하나 더 존재하는데 바로 "--enable-threads"입니다.

그런데, 사실 이 옵션의 의미가 명확하지 않습니다. 즉, 저 옵션을 지정하지 않았다고 해서 스레드가 아예 동작하지 않는 것은 아니기 때문입니다. 이를 위해 지난 글에 다룬,

python - time.sleep(...) 호출 시 hang이 걸리는 듯한 문제
; https://www.sysnet.pe.kr/2/0/12875

time.sleep 함수를 이용해 테스트를 해보겠습니다.

def sleep_thread_test(request):
  from threading import Thread
  import datetime
  
  def handler():
    while True:
      print(datetime.datetime.now().time(), "time.sleep - start")
      time.sleep(2)  # sleep을 예로 들었지만, socket.recv 등의 함수를 실행해도 결과는 마찬가지입니다.
      print("time.sleep - end")
  
  t = Thread(target=handler)
  t.daemon = True
  t.start()
  
  return render(request, 'test/sleep_thread_test.html', {'result_set': "OK"})

만약 "--enable-threads" 옵션의 의미에 따라, 이를 지정하지 않았을 경우 스레드가 동작하지 않는다고 하면 화면에는 어떠한 로그도 찍히지 말아야 하지만, 위의 경우 적어도 "time.sleep - start" 함수까지는 실행이 됩니다.

게다가, 실제로 저 스레드는 요청이 처리되는 동안은 계속 살아서 동작을 합니다. 이에 대한 확인을 위해 다음과 같이 요청을 완료하기 전 일부러 time.sleep을 하나 추가해 주면,

def sleep_thread_test(request):
  # ...[생략]...
  
  t = Thread(target=handler)
  t.daemon = True
  t.start()
  
  time.sleep(3)
  
  return render(request, 'test/sleep_thread_test.html', {'result_set': "OK"})

적어도 저 시간 내에는 스레드가 계속 로그를 찍는 것을 볼 수 있습니다. 즉, 스레드가 사실 잘 동작하고 있으며, 단지 uwsgi는 요청 처리에 대한 파이프라인이 종료하는 시점이 되면 (실제로 그런 식으로 처리하는지 알 수 없지만) 그때까지 생성한 모든 리소스를 제거하는 것처럼 동작하는 식입니다. (IronPython으로 설명하면, 요청마다 Python.CreateEngine으로 독립 실행하는 것으로 보면 되겠습니다.)

그리고 이 상태에서 "--enable-threads" 옵션을 주면, 요청이 끝난 상태에서도 스레드 함수가 동작하므로 2초마다 로그가 계속 찍힌다는 차이점이 있습니다. (그런 의미에서, 사실 "--enable-threads"의 옵션명은 "--threads-go-on"이었으면 더 좋았을 것입니다. ^^;)

참고로, uwsgi의 경우 "--threads [...스레드 수...]" 옵션을 주거나 --py-autoreload 옵션을 준 경우에도 "--enable-threads" 옵션은 기본적으로 활성화됩니다.




잠시 prefork 모드에서의 uwsgi 동작을 살펴볼까요? 가령, 요청을 처리하는 코드에 os.getpid()를 함께 남겨 보면 예상으로는 fork 프로세스의 pid를 남길 것 같지만 그렇지 않고 주 프로세스의 pid가 남습니다. 예를 들어, 아래와 같이 호스팅되고 있다면,

$ pstree -p | grep uwsgi
        |-init(181)---init(182)---bash(183)---uwsgi(5751)-+-uwsgi(5755)
        |                                                 |-{uwsgi}(5756)
        |                                                 |-{uwsgi}(5757)
        |                                                 `-{uwsgi}(5759)

pid == 5751의 로그가 찍히는 것을 볼 수 있습니다. 아마도 자원의 낭비를 조금이라도 막기 위해 주 프로세스에도 요청을 처리하는 fork 프로세스의 기능을 포함하고 있는 것 같고, 이후 요청이 많아지면 그 하위로 처리를 분담할 것입니다. (혹시 이에 대한 명확한 동작 방식을 아는 분이 계시다면 ^^ 덧글을 달아주시길 부탁드립니다.)

그런데, 재미있는 것은 요청을 분배하는 것의 재현이 쉽지 않다는 것입니다. 가령, 아래와 같이 CPU 소비를 3초 동안 유지하는 코드를 만들고,

def sleep_thread_test(request):

    resultset = str(os.getpid())
    old_time = datetime.datetime.now()

    while True:
        now_time = datetime.datetime.now()

        gap = now_time - old_time
        if gap.total_seconds() > 3:
            break

    resultset = str(os.getpid()) + ', ' + str(int(gap.total_seconds())) + ', ' + str(old_time.time()) + ', ' + str(datetime.datetime.now().time())

    return render(request, 'bbs/sleep_thread_test.html', {'result_set': resultset})

2개의 웹 브라우저를 띄워 동시에 요청을 보내면 각각의 브라우저 화면에는 다음과 같은 출력이 나옵니다.

[A 요청]
sleep_thread_test 5960, 3, 05:19:10.662794, 05:19:13.662828 

[B 요청]
sleep_thread_test 5960, 3, 05:19:13.674797, 05:19:16.674827

uwsgi는 A 요청으로 인해 바쁘면서도 fork 서브 프로세스에게 B 요청을 넘기지 않고 그냥 요청을 받아들이면서 순차적으로 처리를 합니다. 그런데, 더욱 재미있는 것은 A, B 요청의 결과가 모두 클라이언트 웹 브라우저의 화면에 거의 동시에 나타난다는 점입니다. (그러니까, 위의 결과에서는 A 요청의 결과가 13초에 웹 브라우저로 전송되지 않고 16초에 B 요청의 결과와 함께 전송됩니다.)

비교 대상이 되는 gunicorn의 경우, 요청에 대한 처리 결과를 보면 fork 서브 프로세스의 pid가 나옵니다. 즉, 동일하게 다음과 같은 구조라면,

$ pstree -p | grep gunicorn
        |-init(181)---init(182)---bash(183)---gunicorn(5764)-+-gunicorn(5771)-+-{gunicorn}(5772)
        |                                                    |                |-{gunicorn}(5773)
        |                                                    |                `-{gunicorn}(5774)
        |                                                    |-{gunicorn}(5768)
        |                                                    `-{gunicorn}(5769)

요청을 처리하는 코드에서 출력하는 pid 값은 5771 등의 값이 나옵니다. 그리고, 위에서 다룬 3초 처리 페이지도 gunicorn에서 호스팅하면 다음과 같은 식으로 결과가 나옵니다.

[A 요청]
sleep_thread_test 5977, 3, 05:22:34.073204, 05:22:37.073261

[B 요청]
sleep_thread_test 5977, 3, 05:22:37.240478, 05:22:40.240503 

위의 텍스트만 보면 uwsgi와 처리 결과가 같지만, 다른 점이 있다면 gunicorn은 A 요청의 처리가 완료되는 37초 시점에 웹 브라우저 화면에 결과가 나타나고, 이후 다시 3초 후에 다른 웹 브라우저의 화면에 결과가 나타난다는 점입니다.




정리해 보면, uwsgi/gunicorn의 prefork 모드라는 것들은 매 요청마다 fork 서브 프로세스에 분배를 하는 형식은 아니고 어느 정도는 하나의 프로세스에서 순차적으로 처리를 유지하면서 동작합니다. 그런 와중에 uwsgi는 상식적이지 않은 동작을 하고.

어쨌거나 경험이 아직 많이 부족하지만 파이썬은 여러모로 참 희한한 언어인 것 같습니다. 상당히 고수준 언어인 듯하면서도, 이런 면에서는 호스팅 환경에 따라 동작이 휘둘리는 의존성을 가지고 있고.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/27/2023]

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

비밀번호

댓글 작성자
 



2023-01-08 11시39분
정성태
2023-01-27 02시01분
https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html

By default the Python plugin does not initialize the GIL. This means your app-generated threads will not run. If you need threads, remember to enable them with enable-threads. Running uWSGI in multithreading mode (with the threads options) will automatically enable threading support. This “strange” default behaviour is for performance reasons, no shame in that.

기본적으로 GIL을 초기화하지 않기 때문에 이로 인해 사용자가 생성한 스레드가 실행되지 않는다고 합니다. 그러니까, 결국 --enable-threads 옵션이란 GIL에 대한 초기화를 결정하는 것입니다.
정성태

... [106]  107  108  109  110  111  112  113  114  115  116  117  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11274정성태8/22/201719369.NET Framework: 674. Thread 타입의 Suspend/Resume/Join 사용 관련 예외 처리
11273정성태8/22/201721633오류 유형: 415. 윈도우 업데이트 에러 Error 0x80070643
11272정성태8/21/201724784VS.NET IDE: 120. 비주얼 스튜디오 2017 버전 15.3.1 - C# 7.1 공개 [2]
11271정성태8/19/201719217VS.NET IDE: 119. Visual Studio 2017에서 .NET Core 2.0 프로젝트 환경 구성하는 방법
11270정성태8/17/201730703.NET Framework: 673. C#에서 enum을 boxing 없이 int로 변환하기 [2]
11269정성태8/17/201721485디버깅 기술: 93. windbg - 풀 덤프에서 .NET 스레드의 상태를 알아내는 방법
11268정성태8/14/201721050디버깅 기술: 92. windbg - C# Monitor Lock을 획득하고 있는 스레드 찾는 방법
11267정성태8/10/201725108.NET Framework: 672. 모노 개발 환경
11266정성태8/10/201724911.NET Framework: 671. C# 6.0 이상의 소스 코드를 Visual Studio 설치 없이 명령행에서 컴파일하는 방법
11265정성태8/10/201753152기타: 66. 도서: 시작하세요! C# 7.1 프로그래밍: 기본 문법부터 실전 예제까지 [11]
11264정성태8/9/201724066오류 유형: 414. UWP app을 signtool.exe로 서명 시 0x8007000b 오류 발생
11263정성태8/9/201719545오류 유형: 413. The C# project "..." is targeting ".NETFramework, Version=v4.0", which is not installed on this machine. [3]
11262정성태8/5/201718246오류 유형: 412. windbg - SOS does not support the current target architecture. [3]
11261정성태8/4/201720832디버깅 기술: 91. windbg - 풀 덤프 파일로부터 강력한 이름의 어셈블리 추출 후 사용하는 방법
11260정성태8/3/201718931.NET Framework: 670. C# - 실행 파일로부터 공개키를 추출하는 방법
11259정성태8/2/201718167.NET Framework: 669. 지연 서명된 어셈블리를 sn.exe -Vr 등록 없이 사용하는 방법
11258정성태8/1/201718960.NET Framework: 668. 지연 서명된 DLL과 서명된 DLL의 차이점파일 다운로드1
11257정성태7/31/201719157.NET Framework: 667. bypassTrustedAppStrongNames 옵션 설명파일 다운로드1
11256정성태7/25/201720649디버깅 기술: 90. windbg의 lm 명령으로 보이지 않는 .NET 4.0 ClassLibrary를 명시적으로 로드하는 방법 [1]
11255정성태7/18/201723206디버깅 기술: 89. Win32 Debug CRT Heap Internals의 0xBAADF00D 표시 재현 [1]파일 다운로드3
11254정성태7/17/201719558개발 환경 구성: 322. "Visual Studio Emulator for Android" 에뮬레이터를 "Android Studio"와 함께 쓰는 방법
11253정성태7/17/201719886Math: 21. "Coding the Matrix" 문제 2.5.1 풀이 [1]파일 다운로드1
11252정성태7/13/201718444오류 유형: 411. RTVS 또는 PTVS 실행 시 Could not load type 'Microsoft.VisualStudio.InteractiveWindow.Shell.IVsInteractiveWindowFactory2'
11251정성태7/13/201717121디버깅 기술: 88. windbg 분석 - webengine4.dll의 MgdExplicitFlush에서 발생한 System.AccessViolationException의 crash 문제 (2)
11250정성태7/13/201720690디버깅 기술: 87. windbg 분석 - webengine4.dll의 MgdExplicitFlush에서 발생한 System.AccessViolationException의 crash 문제 [1]
11249정성태7/12/201718517오류 유형: 410. LoadLibrary("[...].dll") failed - The specified procedure could not be found.
... [106]  107  108  109  110  111  112  113  114  115  116  117  118  119  120  ...