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

1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13482정성태12/13/20232903닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/20232285개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/20232658개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/20232337개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/20232528닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
13476정성태12/8/20232266닷넷: 2181. C# - .NET 8 JsonStringEnumConverter의 AOT를 위한 개선파일 다운로드1
13475정성태12/7/20232329닷넷: 2180. .NET 8 - 함수 포인터에 대한 Reflection 정보 조회파일 다운로드1
13474정성태12/6/20232177개발 환경 구성: 690. 닷넷 코어/5+ 버전의 ilasm/ildasm 실행 파일 구하는 방법 - 두 번째 이야기
13473정성태12/5/20232384닷넷: 2179. C# - 값 형식(Blittable)을 메모리 복사를 이용해 바이트 배열로 직렬화/역직렬화파일 다운로드1
13472정성태12/4/20232196C/C++: 164. Visual C++ - InterlockedCompareExchange128 사용 방법
13471정성태12/4/20232276Copilot - To enable GitHub Copilot, authorize this extension using GitHub's device flow
13470정성태12/2/20232566닷넷: 2178. C# - .NET 8부터 COM Interop에 대한 자동 소스 코드 생성 도입파일 다운로드1
13469정성태12/1/20232286닷넷: 2177. C# - (Interop DLL 없이) CoClass를 이용한 COM 개체 생성 방법파일 다운로드1
13468정성태12/1/20232232닷넷: 2176. C# - .NET Core/5+부터 달라진 RCW(Runtime Callable Wrapper) 대응 방식파일 다운로드1
13467정성태11/30/20232321오류 유형: 882. C# - Unhandled exception. System.Runtime.InteropServices.COMException (0x800080A5)파일 다운로드1
13466정성태11/29/20232491닷넷: 2175. C# - DllImport 메서드의 AOT 지원을 위한 LibraryImport 옵션
13465정성태11/28/20232241개발 환경 구성: 689. MSBuild - CopyToOutputDirectory가 "dotnet publish" 시에는 적용되지 않는 문제파일 다운로드1
13464정성태11/28/20232379닷넷: 2174. C# - .NET 7부터 UnmanagedCallersOnly 함수 export 기능을 AOT 빌드에 통합파일 다운로드1
13463정성태11/27/20232302오류 유형: 881. Visual Studio - NU1605: Warning As Error: Detected package downgrade
13462정성태11/27/20232343오류 유형: 880. Visual Studio - error CS0246: The type or namespace name '...' could not be found
13461정성태11/26/20232378닷넷: 2173. .NET Core 3/5+ 기반의 COM Server를 registry 등록 없이 사용하는 방법파일 다운로드1
13460정성태11/26/20232330닷넷: 2172. .NET 6+ 기반의 COM Server 내에 Type Library를 내장하는 방법파일 다운로드1
13459정성태11/26/20232305닷넷: 2171. .NET Core 3/5+ 기반의 COM Server를 기존의 regasm처럼 등록하는 방법파일 다운로드1
13458정성태11/26/20232315닷넷: 2170. .NET Core/5+ 기반의 COM Server를 tlb 파일을 생성하는 방법(tlbexp)
13457정성태11/25/20232258VS.NET IDE: 187. Visual Studio - 16.9 버전부터 추가된 "Display inline type hints" 옵션
13456정성태11/25/20232557닷넷: 2169. C# - OpenAI를 사용해 PDF 데이터를 대상으로 OpenAI 챗봇 작성 [1]파일 다운로드1
1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...