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

... 61  62  63  64  65  66  67  68  [69]  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12211정성태4/27/202019261개발 환경 구성: 486. WSL에서 Makefile로 공개된 리눅스 환경의 C/C++ 소스 코드 빌드
12210정성태4/20/202020692.NET Framework: 903. .NET Framework의 Strong-named 어셈블리 바인딩 (1) - app.config을 이용한 바인딩 리디렉션 [1]파일 다운로드1
12209정성태4/13/202017405오류 유형: 614. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우 (2)
12208정성태4/12/202015965Linux: 29. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우
12207정성태4/2/202015804스크립트: 19. Windows PowerShell의 NonInteractive 모드
12206정성태4/2/202018427오류 유형: 613. 파일 잠금이 바로 안 풀린다면? - The process cannot access the file '...' because it is being used by another process.
12205정성태4/2/202015095스크립트: 18. Powershell에서는 cmd.exe의 명령어를 지원하진 않습니다.
12204정성태4/1/202015092스크립트: 17. Powershell 명령어에 ';' (semi-colon) 문자가 포함된 경우
12203정성태3/18/202017940오류 유형: 612. warning: 'C:\ProgramData/Git/config' has a dubious owner: '...'.
12202정성태3/18/202021200개발 환경 구성: 486. .NET Framework 프로젝트를 위한 GitLab CI/CD Runner 구성
12201정성태3/18/202018434오류 유형: 611. git-credential-manager.exe: Using credentials for username "Personal Access Token". [1]
12200정성태3/18/202018524VS.NET IDE: 145. NuGet + Github 라이브러리 디버깅 관련 옵션 3가지 - "Enable Just My Code" / "Enable Source Link support" / "Suppress JIT optimization on module load (Managed only)"
12199정성태3/17/202016168오류 유형: 610. C# - CodeDomProvider 사용 시 Unhandled Exception: System.IO.DirectoryNotFoundException: Could not find a part of the path '...\f2_6uod0.tmp'.
12198정성태3/17/202019528오류 유형: 609. SQL 서버 접속 시 "Cannot open user default database. Login failed."
12197정성태3/17/202018811VS.NET IDE: 144. .NET Core 콘솔 응용 프로그램을 배포(publish) 시 docker image 자동 생성 - 두 번째 이야기 [1]
12196정성태3/17/202015943오류 유형: 608. The ServicedComponent being invoked is not correctly configured (Use regsvcs to re-register).
12195정성태3/16/202018264.NET Framework: 902. C# - 프로세스의 모든 핸들을 열람 - 세 번째 이야기
12194정성태3/16/202020993오류 유형: 607. PostgreSQL - Npgsql.NpgsqlException: sorry, too many clients already
12193정성태3/16/202017904개발 환경 구성: 485. docker - SAP Adaptive Server Enterprise 컨테이너 실행 [1]
12192정성태3/14/202019928개발 환경 구성: 484. docker - Sybase Anywhere 16 컨테이너 실행
12191정성태3/14/202021042개발 환경 구성: 483. docker - OracleXE 컨테이너 실행 [1]
12190정성태3/14/202015623오류 유형: 606. Docker Desktop 업그레이드 시 "The process cannot access the file 'C:\Program Files\Docker\Docker\resources\dockerd.exe' because it is being used by another process."
12189정성태3/13/202021227개발 환경 구성: 482. Facebook OAuth 처리 시 상태 정보 전달 방법과 "유효한 OAuth 리디렉션 URI" 설정 규칙
12188정성태3/13/202026020Windows: 169. 부팅 시점에 실행되는 chkdsk 결과를 확인하는 방법
12187정성태3/12/202015581오류 유형: 605. NtpClient was unable to set a manual peer to use as a time source because of duplicate error on '...'.
12186정성태3/12/202017400오류 유형: 604. The SysVol Permissions for one or more GPOs on this domain controller and not in sync with the permissions for the GPOs on the Baseline domain controller.
... 61  62  63  64  65  66  67  68  [69]  70  71  72  73  74  75  ...