Microsoft MVP성태의 닷넷 이야기
스크립트: 37. 파이썬 - uwsgi의 --enable-threads 옵션 [링크 복사], [링크+제목 복사]
조회: 7457
글쓴 사람
정성태 (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)
13431정성태10/31/20232772스크립트: 60. 파이썬 - 비동기 FastAPI 앱을 gunicorn으로 호스팅
13430정성태10/30/20232668닷넷: 2153. C# - 사용자가 빌드한 ICU dll 파일을 사용하는 방법
13429정성태10/27/20232953닷넷: 2152. Win32 Interop - C/C++ DLL로부터 이중 포인터 버퍼를 C#으로 받는 예제파일 다운로드1
13428정성태10/25/20233016닷넷: 2151. C# 12 - ref readonly 매개변수
13427정성태10/18/20233214닷넷: 2150. C# 12 - 정적 문맥에서 인스턴스 멤버에 대한 nameof 접근 허용(Allow nameof to always access instance members from static context)
13426정성태10/13/20233379스크립트: 59. 파이썬 - 비동기 호출 함수(run_until_complete, run_in_executor, create_task, run_in_threadpool)
13425정성태10/11/20233186닷넷: 2149. C# - PLinq의 Partitioner<T>를 이용한 사용자 정의 분할파일 다운로드1
13423정성태10/6/20233166스크립트: 58. 파이썬 - async/await 기본 사용법
13422정성태10/5/20233309닷넷: 2148. C# - async 유무에 따른 awaitable 메서드의 병렬 및 예외 처리
13421정성태10/4/20233364닷넷: 2147. C# - 비동기 메서드의 async 예약어 유무에 따른 차이
13420정성태9/26/20235546스크립트: 57. 파이썬 - UnboundLocalError: cannot access local variable '...' where it is not associated with a value
13419정성태9/25/20233205스크립트: 56. 파이썬 - RuntimeError: dictionary changed size during iteration
13418정성태9/25/20233901닷넷: 2146. C# - ConcurrentDictionary 자료 구조의 동기화 방식
13417정성태9/19/20233439닷넷: 2145. C# - 제네릭의 형식 매개변수에 속한 (매개변수를 가진) 생성자를 호출하는 방법
13416정성태9/19/20233252오류 유형: 877. redis-py - MISCONF Redis is configured to save RDB snapshots, ...
13415정성태9/18/20233744닷넷: 2144. C# 12 - 컬렉션 식(Collection Expressions)
13414정성태9/16/20233501디버깅 기술: 193. Windbg - ThreadStatic 필드 값을 조사하는 방법
13413정성태9/14/20233699닷넷: 2143. C# - 시스템 Time Zone 변경 시 이벤트 알림을 받는 방법
13412정성태9/14/20236976닷넷: 2142. C# 12 - 인라인 배열(Inline Arrays) [1]
13411정성태9/12/20233477Windows: 252. 권한 상승 전/후 따로 관리되는 공유 네트워크 드라이브 정보
13410정성태9/11/20235000닷넷: 2141. C# 12 - Interceptor (컴파일 시에 메서드 호출 재작성) [1]
13409정성태9/8/20233860닷넷: 2140. C# - Win32 API를 이용한 모니터 전원 끄기
13408정성태9/5/20233819Windows: 251. 임의로 만든 EXE 파일을 포함한 ZIP 파일의 압축을 해제할 때 Windows Defender에 의해 삭제되는 경우
13407정성태9/4/20233574닷넷: 2139. C# - ParallelEnumerable을 이용한 IEnumerable에 대한 병렬 처리
13406정성태9/4/20233541VS.NET IDE: 186. Visual Studio Community 버전의 라이선스
13405정성태9/3/20233973닷넷: 2138. C# - async 메서드 호출 원칙
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...