Microsoft MVP성태의 닷넷 이야기
스크립트: 37. 파이썬 - uwsgi의 --enable-threads 옵션 [링크 복사], [링크+제목 복사],
조회: 7697
글쓴 사람
정성태 (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에 대한 초기화를 결정하는 것입니다.
정성태

... 31  32  33  34  35  [36]  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12746정성태7/31/20219217개발 환경 구성: 588. 네트워크 장비 환경을 시뮬레이션하는 Packet Tracer 프로그램 소개
12745정성태7/31/20217053개발 환경 구성: 587. Azure Active Directory - tenant의 관리자 계정 로그인 방법
12744정성태7/30/20217672개발 환경 구성: 586. Azure Active Directory에 연결된 App 목록을 확인하는 방법?
12743정성태7/30/20218385.NET Framework: 1083. Azure Active Directory - 외부 Token Cache 저장소를 사용하는 방법파일 다운로드1
12742정성태7/30/20217584개발 환경 구성: 585. Azure AD 인증을 위한 사용자 인증 유형
12741정성태7/29/20218797.NET Framework: 1082. Azure Active Directory - Microsoft Graph API 호출 방법파일 다운로드1
12740정성태7/29/20217437오류 유형: 747. SharePoint - InvalidOperationException 0x80131509
12739정성태7/28/20217402오류 유형: 746. Azure Active Directory - IDW10106: The 'ClientId' option must be provided.
12738정성태7/28/20218031오류 유형: 745. Azure Active Directory - Client credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI).
12737정성태7/28/20216964오류 유형: 744. Azure Active Directory - The resource principal named api://...[client_id]... was not found in the tenant
12736정성태7/28/20217523오류 유형: 743. Active Azure Directory에서 "API permissions"의 권한 설정이 "Not granted for ..."로 나오는 문제
12735정성태7/27/20218066.NET Framework: 1081. C# - Azure AD 인증을 지원하는 데스크톱 애플리케이션 예제(Windows Forms) [2]파일 다운로드1
12734정성태7/26/202124075스크립트: 20. 특정 단어로 시작하거나/끝나는 문자열을 포함/제외하는 정규 표현식 - Look-around
12733정성태7/23/202111348.NET Framework: 1081. Self-Contained/SingleFile 유형의 .NET Core/5+ 실행 파일을 임베딩한다면? [1]파일 다운로드2
12732정성태7/23/20216630오류 유형: 742. SharePoint - The super user account utilized by the cache is not configured.
12731정성태7/23/20217834개발 환경 구성: 584. Add Internal URLs 화면에서 "Save" 버튼이 비활성화 된 경우
12730정성태7/23/20219350개발 환경 구성: 583. Visual Studio Code - Go 코드에서 입력을 받는 경우
12729정성태7/22/20218309.NET Framework: 1080. xUnit 단위 테스트에 메서드/클래스 수준의 문맥 제공 - Fixture
12728정성태7/22/20217719.NET Framework: 1079. MSTestv2 단위 테스트에 메서드/클래스/어셈블리 수준의 문맥 제공
12727정성태7/21/20218767.NET Framework: 1078. C# 단위 테스트 - MSTestv2/NUnit의 Assert.Inconclusive 사용법(?) [1]
12726정성태7/21/20218598VS.NET IDE: 169. 비주얼 스튜디오 - 단위 테스트 선택 시 MSTestv2 외의 xUnit, NUnit 사용법 [1]
12725정성태7/21/20217261오류 유형: 741. Failed to find the "go" binary in either GOROOT() or PATH
12724정성태7/21/202110034개발 환경 구성: 582. 윈도우 환경에서 Visual Studio Code + Go (Zip) 개발 환경 [1]
12723정성태7/21/20217650오류 유형: 740. SharePoint - Alternate access mappings have not been configured 경고
12722정성태7/20/20217491오류 유형: 739. MSVCR110.dll이 없어 exe 실행이 안 되는 경우
12721정성태7/20/20218075오류 유형: 738. The trust relationship between this workstation and the primary domain failed. - 세 번째 이야기
... 31  32  33  34  35  [36]  37  38  39  40  41  42  43  44  45  ...