ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python Backend] 파이썬에서의 Thread와 Process & uvicorn, gunicorn의 관점에서
    WEB 2025. 6. 14. 16:45
    728x90

     

    [Python Backend] 파이썬에서의 Thread와 Process & uvicorn, gunicorn의 관점에서

     

     

    * 이전 포스트에서 이어집니다 (https://asidefine.tistory.com/332

    * 다음 포스트에서 이어집니다 (https://asidefine.tistory.com/335)

     

     

     

     

    * 이전 포스트에서 이어집니다 (https://asidefine.tistory.com/332) 

     

    * 다음 포스트에서 이어집니다

     

     

    python backend를 공부하는 중이었는데, uvicorn이랑 gunicorn이 뭐가 다른 거냐, 왜 같이 써야 되나 ... 싶어서 공부하다가 여기까지 왔다 

    작년에 이 내용을 복습/정리한 적 있었는데, 뭔가 큰 틀에서만 기억나고 나머지를 까먹음 (ㅎㅎ..)

    일단 학부때도 C언어로 배웠기 때문에 파이썬 관점에서 살펴보고자 한다 (언어마다 다른 프로세스나 스레드 처리가 다르니까)

     

    https://asidefine.tistory.com/255

     

    2024 운영체제 정리 - 2. Process & Thread

    2024 운영체제 정리 - 2. Process & Thread 1. 운영체제란 2. Process & Thread 3. IPC 4. CPU Scheduling 5. Synchronization 6. Virtual Memory Process Process란 ? 프로그램이 실행되기 위해 memory에 적재되어 CPU에 의해 실행되는

    asidefine.tistory.com

     

    개념 복습 1: Process와 Thread란 ?

     

    • Process란 ? 
      • 프로그램이 실행되기 위해 memory에 적재되어 CPU에 의해 실행되는 작업의 단위
      • 프로세스는 크게 1) Logical Control Flow, 2) Private Address Space라는 특징을 가지고 있다. 
        • 1) Logical Control Flow   
          • 마치 CPU를 혼자 쓰는 것처럼 보인다는 특징과 
        • 2) Private Address Space 
          • 한 프로세스당 하나의 메모리 영역를 할당 받는 것을 의미한다 
      • 위의 특징은 전 포스팅에서 설명한 메모리의 1) 커널 영역과 2) 사용자 영역으로 연결하여 설명할 수 있다 
        • 1) 커널 영역
          • 각 프로세스는 커널 영역에 저장되는 PCB (Process Control Block)이라는 것을 통해, 해당 프로세스에 대한 정보를 저장하고 구분한다
        • 2) 사용자 영역
          • 각 프로세스는 사용자 영역에서 하나의 메모리 영역을 할당 받는데, 이는 1) 코드 영역, 2) 데이터 영역, 3) 힙 영역, 4) 스택 영역으로 구성되어 있다 

     

    • PCB (Process Control Block)과 Context Switching 
      • 운영체제는 한정된 CPU 자원을 나눠서 사용하기 위해 여러 프로세스를 번갈아가면서 실행한다 => "Context Switching"
        • 한 번에 하나의 프로세스만 수행된다면, 다른 프로세스들은 그 프로세스가 끝날 때까지 기다려야 하기 때문
      • Context Switching는 하나의 프로세스가 실행되는 중에 인터럽트하고 그 정보를 나중에 재개하기 위해 PCB에 저장하고, 다른 프로세스를 실행한다 
      • 따라서 PCB는 다시 프로세스가 실행될 때 필요한 정보들을 담고 있다 -> 다시 재개할 때 커널 영역에 저장된 이 PCB에 접근한다  

     


    Q. 파이썬에서는 메모리 관리가 어떻게 될까?

    C나 JAVA의 경우 malloc을 통해 동적할당을 할 수 있지만 파이썬은 자동으로 동적할당을 해주는 언어이기 때문에 사용자는 신경 쓸 부분이 없다.
    (python memory manager라는 기능이 heap영역 메모리를 관리해준다고 한다)


    현대 프로그램 언어에서 자료는 크게 두 가지로 분류된다. 
    1) 기본 자료형 : 숫자, 문자열, 불 등 작고 간단한 자료
    2) 복합 자료형 : 리스트, 딕셔너리, 객체 등 크고 무거운 자료

    파이썬에서는 다음과 같이 숫자, bool, string 자료형은 기본자료형, List나 dictionary, 함수와 같은 것들은 복합자료형에 해당이 된다.

    기본 자료형은 크기가 작고 고정되어 있어 상자(스택)에 넣어 차곡차곡 보관하고,
    복합 자료형은 크기가 정해져 있지 않은 복잡한 자료로 다른 창고(힙)에 넣고 보관하고 창고 위치를 스택에 보관한다


    함수는 복합 자료형이기 때문에 힙(heap)에 저장된다.
    함수를 호출할 때마다 스택(stack)이 새로 생성되고, 함수 호출이 완료되면 스택(stack)은 사라진다. 


    아래의 링크에서 구체적으로 파이썬이 어떻게 실행되는지 확인할 수 있다
    https://pythontutor.com/python-compiler.html#mode=edit

     

    • Multi Process 
      • 위에서는 하나의 프로세스를 중점적으로 살펴봤다. 그렇다면 이런 프로세스가 한 번에 여러 개 실행될 순 없을까? 
      • "동시성"과 "병렬성"으로 위를 설명할 수 있다 
        • 동시성 : 하나의 CPU에서는 하나의 프로세스만 수행할 수 있다. 다만 Context Switching을 통해 빠르게 바뀌면서 마치 하나처럼 수행되는 것처럼 보인다 
        • 병렬성 : 최근의 컴퓨터는 멀티 코어, 즉 여러 개의 CPU 연산장치를 탑재하고 있기에, 각 CPU당 하나의 프로세스를 실행하게 되면서, Process가 동시에 실행되는 것 

     

     


    • Thread란? 
      • 하나의 프로세스를 구성하는 여러 실행 흐름의 단위 
      • Thread도 process처럼 동시에 수행되기 위해서 Context Switching이 일어난다!
        • 하나의 프로세스 내에서도 여러 작업을 병렬적으로 수행하기 위해서 multi thread로 나누게 된다 
      • Thread는 한 프로세스 내에서 Program Counter와 Stack 영역을 제외한 나머지 메모리 영역(code, data, heap)을 공유한다 
        • 왜 Program Counter와 Stack 영역은 독립적으로 가지냐?
          • Program Counter의 경우, 한 프로세스 내에서도 각각의 thread끼리 context switching이 일어나기 때문에, 다음 명령어를 가리키기 위해선 독립적으로 필요한다 
          • Stack 영역의 경우, 명령어/함수 실행 시의 지역 변수나  매개 변수 전달을 위해서 독립적인 공간이 필요하다 

     

     

    개념 복습 2: Multi-Process와 Multi-Thread의 차이는 ? 

     

    • Multi Process와 Multi Thread의 차이
      • 위에서 Process도 Thread도 여러 개로 나뉘어 수행될 수 있다고 말했다. 그럼 두 방식은 어떤 상황에서 좋을까? 
      • Multi Process의 장단점  
        •  장점 
          • 프로그램 안정성 : 하나의 프로세스가 죽어도 다른 프로세스에 영향을 주지 않아 안정적임
        • 단점 
          • Context Switching Overhead: Multi process는 Multi thread에 비해서 Context Switching을 할 때 Overhead 발생한다 
            • CPU는 다음 프로세스의 정보를 불러오기 위해 (System Call을 통해서 Kernel 모드에 접근해서 interrupt 해야 되고 ~) 메모리를 검색하고, CPU 캐시 메모리를 초기화하며, 프로세스 상태를 저장하고, 불러올 데이터를 준비해야 하기 때문에 ㅇㅇ
          • 자원 비효율성 : 한 프로세스당 하나의 영역을 가지기 때문에 더 많은 메모리 공간과 CPU 공간을 차지하게 됨 
            • 프로세스 간 자원을 공유하기 위해선 IPC(Inter Process Communication)를 사용해야 한다 (다음 포스트에서 다룬다)
      • Multi Thread의 장단점 
        • 장점 
          • 자원 효율성: 한 프로세스 내에서 Thread끼리 (통신없이) 자원을 공유할 수 있어 효율적이다 
          • Context Switching 비용 감소 
            • Thread는 스위칭할 때 스레드 간에 공유하는 자원을 제외한 스레드 정보(stack, register)만을 교체하면 되므로 프로세스 컨텍스트 스위칭 비용보다 상대적으로 낮다  
        • 단점 
          • 안정성 문제 : 한 Thread가 같은 프로세스 내의 다른 Thread들에게도 영향을 줄 수 있다 

     

    파이썬에서의 Multi-Process와 Multi-Thread는?

     

      • 위에서 잠깐 언급한 "동시성"과 "병렬성"을 다시 한 번 짚고 넘어가자
        • 동시성 : 하나의 CPU에서는 하나의 프로세스만 수행할 수 있다. 다만 Context Switching을 통해 빠르게 바뀌면서 마치 하나처럼 수행되는 것처럼 보인다 
        • 병렬성 : 최근의 컴퓨터는 멀티 코어, 즉 여러 개의 CPU 연산장치를 탑재하고 있기에, 각 CPU당 하나의 프로세스를 실행하게 되면서, Process가 동시에 실행되는 것 
      • 파이썬에서는 GIL라는 것 때문에 Threaad에서 병렬 처리는 불가능하지만, 동시성 처리는 가능하다 
        • GIL (Global Interpreter Lock) 이란 ? 
          • Python 인터프리터(CPython)가 한 번에 오직 하나의 스레드만 Python 바이트코드를 실행하도록 제한하는 전역 락(Lock)
          • 즉, 멀티 스레드를 만든다 해도 실제로 동시에 python 코드를 실행하는 건 1개의 스레드 뿐이라는 것
          • ✔👀 왜 이게 존재하는 건데 ? 
            • python은 내부적으로 객체 관리에 쓰는 메모리가 단일 쓰레드 기반임 → 그래서 간단한 언어 구현 대신 성능을 희생한 것
          • ✔👀  그럼 왜 파이썬에서 멀티스레딩이 실제 병렬 처리가 안 되는데도 threading 모듈이 존재해 ? 
            • 병렬은 안 되지만, 동시성(concurrency)은 된다
            • GIL 때문에 CPU-bound 작업에선 병렬 처리는 안 되지만, I/O-bound 작업의 동시성엔 효과가 있기 때문
              • I/O-bound 작업은 GIL를 잠깐 놓기 때문 

     

    CPU-bound, GPU-bound, I/O-bound, 뭐가 다르냐? 


    1. 🔥 CPU-bound (CPU가 병목인 상태)

    프로그램이 계산을 너무 많이 해서 CPU가 꽉 차 있는 상태
    예: 수치 계산, 이미지 처리, LLM 추론 멀티스레딩 X → multiprocessing, Gunicorn -w, 또는 Cython 등 최적화 필요 GIL이 병목이라 threading으론 성능 향상 불가

    🧠 대표 작업

    GPT 모델 추론 대용량 파일 압축/암호화 데이터 정렬, 벡터 계산 등



    2. 💾 I/O-bound (입출력이 병목인 상태)

    프로그램이 파일, 네트워크, DB 등과 통신하느라 기다리는 시간이 대부분인 상태
    CPU는 한가한데 계속 기다리는 중이라 처리 속도가 느림 async/await이나 threading으로 동시성 처리가 잘 됨

    🧠 대표 작업
    웹 크롤링 DB 쿼리, 파일 읽기/쓰기 API 요청 (예: 외부 서버에서 응답 기다림)



    3. 🎮 GPU-bound (GPU 계산이 병목인 상태)

    프로그램이 GPU 연산 능력을 다 써버려서 더 이상 속도 향상이 안 되는 상태
    주로 딥러닝 추론/학습 시 발생 GPU 코어 수, VRAM, Tensor Cores 등이 부족하면 병목 PyTorch, TensorFlow에서 이 상태를 자주 만남

    🧠 대표 작업

    LLM 모델 추론 (ex. GPT, Stable Diffusion) 이미지 생성, 음성 합성 배치 추론, Fine-tuning
    CPU-bound 계산 멀티프로세싱, 코드 최적화 multiprocessing, Gunicorn
    I/O-bound 대기 비동기 처리 asyncio, FastAPI async def
    GPU-bound 연산 병목 모델 경량화, GPU 확장 PyTorch/TensorFlow 최적화

     

     

    그럼 웹 서버(uvicorn, gunicorn)의 관점에서 멀티 스레드, 멀티 프로세스는 ? 

     

    https://asidefine.tistory.com/332

     

    [Python Backend] FastAPI를 이해하기 위한 비동기 처리 개념 정리

    [python backend] FastAPI를 이해하기 위한 비동기 처리 개념 정리 그렇다 FastAPI가 빠른 이유는 비동기 지원으로 인한 것 그럼 당최 비동기란 뭐고 그거 어떻게 하는 건데? * 이전 포스트에서 이어집니

    asidefine.tistory.com

     

    위 포스트에서는 웹 프레임워크인 FastAPI와, 그를 실행하는 ASGI 서버인 웹 서버 uvicorn에 대해서만 살펴보았다 

    여기에서도 uvicorn 특징이 비동기를 지원하기 때문에, I/O bound 작업을 빠르게 처리할 수 있다는 점이 특징 & 또 비동기 함수를 실행함으로써 별도의 Thread 생성 없이 동시성 구현한다는 점을 특징을 배웠다

     

    그래서 아래에서부터는 이 포스팅의 목적인 gunicorn과 uvicorn과의 차이점과 왜 같이 써야 되는지에 대해 살펴본다

      • Uvicorn만 쓸 수 없는 이유?
        • Uvicorn은 ASGI 기반 고성능 웹 서버 (FastAPI 앱을 실행시키는 역할) → 비동기 I/O 처리에 최적화되어 빠름
        • 그러나, 
          • 하지만 기본적으로 싱글 프로세스    멀티프로세싱 기능이 약함 (멀티스레드는 어느 정도 가능하지만)
          • 프로세스 충돌, 오류 복구, 로그 관리 등 운영 레벨 안정성이 부족
        • 그래서 Uvicorn만 쓰는 경우는 아무래도 개발 단계로 한정됨 ! 
          • CPU는 하나, 처리 요청도 하나씩 → 100명 이상 유저가 동시 요청하면 병목 발생
        • gunicorn만 쓸 수 없는 이유?
          • Gunicorn은 WSGI 기반 서버즉, 비동기 프레임워크(FastAPI, Starlette, etc.)는 제대로 못 돌림 (Gunicorn 혼자선 FastAPI를 비동기 성능으로 못 활용)
          • 그대신 프로세스 관리 잘함   uvicorn.workers.UvicornWorker를 붙이면 ASGI 서버도 병렬 처리 가능 (멀티프로세싱을 잘 다루는 프로세스 매니저) 
            • 로드 밸런서 (Load Balancer) 역할 함 : 로드 밸런서란, 여러 대의 서버 중에서 부하가 덜 걸린 곳에 요청을 분산해주는 트래픽 분배기

     

        • uvicorn + gunicorn
          • Gunicorn은 프로세스 관리 잘함 → 멀티프로세스 띄우고 충돌 방지, 로깅 등 담당
          • Uvicorn은 FastAPI 실행 및 비동기 처리 잘함 → 웹 요청 빠르게 처리

     

    그래서 실제 서비스에서는 아래와 같은 구조를 많이 사용한다고 함 

     

     

    [Client]
       ↓
    [Nginx (optional, reverse proxy)]
       ↓
    [Gunicorn] → 여러 프로세스를 관리
       ↓
    [UvicornWorker] → FastAPI 앱 비동기 실행
       ↓
    [FastAPI app]
    
    
    ----------------------------
    
          ┌────────────┐
          │  Client    │
          └────┬───────┘
               ↓
          ┌────────────┐
          │   Nginx    │   ← HTTPS, CORS, 정적 파일 처리 ← ❗ 외부 요청 분산 (로드 밸런서)
          └────┬───────┘
               ↓
     ┌────────────────────┐
     │ Gunicorn (Process) │ ← ❗ 내부 멀티프로세싱 실행 (Worker 분산)
     └────┬────┬────┬─────┘
          ↓    ↓    ↓
     Uvicorn Uvicorn Uvicorn (Worker)

     

    gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app
    • -k: Gunicorn에게 "Uvicorn 워커(worker)를 사용해라"라고 지정
    • -w 4: 워커 4개 (즉, FastAPI 서버 4개가 병렬로 돌아감)
    • UvicornWorker: Gunicorn이 FastAPI 앱을 실행할 수 있게 해주는 ASGI-compatible worker

     

     

     

    +) nginx는 또 뭔데 ? 

     

    이건 다음 포스트에서 정리하겠다 .... 

    https://asidefine.tistory.com/335

     

    Nginx 가이드 (FastAPI 백엔드 기준)

    🚀 Nginx 가이드 (FastAPI 백엔드 기준) * 이전 포스트에서 이어집니다 (https://asidefine.tistory.com/334) 타고 타고 가다가 nginx까지 나왔다 그래서 그게 뭔데✅ 0. Nginx란?Nginx는 고성능 웹 서버이자 리버스

    asidefine.tistory.com

     

     

     

    공부한 뒤 생각이 든 점 

     

    • 내가 작업하고 있는 AI agent와 같은 어플리케이션에서는 그럼 
    1. 유저 요청 수신 (FastAPI) HTTP 요청 처리 I/O-bound
    2. LangChain → LLM 호출 (ex. OpenAI, Mistral, LLama.cpp) 외부 API/로컬 추론 실행 보통은 I/O-bound, 가끔 GPU/CPU-bound
    3. 응답 받아 스트리밍 chunk 단위로 메시지 전송 I/O-bound
    4. 유저에게 스트리밍 응답 반환 yield, await 등으로 전송 I/O-bound

     

    • 위의 표에서도 알 수 있듯이 LangChain 통해 LLM 추론 + 스트리밍하는 과정은 일반적으로 I/O-bound 작업이기 때문에 async/await를 적용하는 것을 알 수 있다 (FastAPI + 비동기 구조에 최적)
    • 만약 모델이 CPU/GPU-Bound라면, 이때는 Multiprocessing이므로, gunicorn 워커나 분산 처리 고려해야 한다
      • 그래서 나의 경우엔 vllm으로  모델 추론을 api 형식으로 받아오고 있음 - vllm 자체는 multi-processing 구조를 쓰고 있다 (왜냐하면 얘는 GPU-bound 작업이기 때문에, 단순한 병렬처리가 아니라서! ) (그래서 실제로 vllm 켜면 multiprocessing으로 돌아가는 거 확인할 수 있었구나 )
        • 내부적으로 CUDA 기반 GPU 연산을 관리하면서, 서버 스케일에 맞는 비동기/병렬 처리를 하기 위해서 

     

    [Client] 
       ↓
    [FastAPI or OpenAPI 서버 (REST API)]  ← async 요청 수신
       ↓
    [Multiprocessing 기반 vLLM Engine]  ← GPU로 작업 전달
       ↓
    [CUDA backend] → 텐서 연산 수행 → 응답 스트리밍

     

     

    Reference 

    https://monkey3199.github.io/develop/python/2018/12/04/python-pararrel.html

     

    Nathan's Blog

    The blog to learn more.

    monkey3199.github.io

    https://jiyeonseo.github.io/2022/10/24/uvicorn-gunicorn-fastapi-docker/

     

    가장 빠르게 FastAPI를 돌려보자 - uvicorn-gunicorn-fastapi-docker | Daily Log

    가장 빠르게 FastAPI를 돌려보자 - uvicorn-gunicorn-fastapi-docker FastAPI 관련 라이브러리나 예제 코드들을 살펴보다보면 실행 Dockerfile 파일이 아래와 같이 시작하는 파일들을 많이 만나볼 수 있다. FastAPI

    jiyeonseo.github.io

    https://wikidocs.net/177269

     

    4-09 Gunicorn 사용하기

    운영 환경에서 FastAPI 서버를 구동하기 위해서는 AWS 터미널에서 다음과 같은 명령을 실행해야 한다. ```no-highlight $ uvicorn main:app --r…

    wikidocs.net

     

     

     

     

    728x90
Designed by Tistory.