Python FastAPI 파헤쳐보기

uchan·2025년 11월 6일

동시성은 선택이 아니라 필수다. 대규모 트래픽이 몰리는 온라인 시험 플랫폼을 운영하면서 나는 “대기 시간을 줄이는 구조”가 시스템의 응답성을 좌우한다는 것을 반복해서 확인해왔다. 이 글은 Python FastAPI를 중심으로 비동기 처리의 핵심 원리와 실무 구성을 한 번에 꿰뚫어보려는 시도다.

1. Blocking, Non-Blocking IO 에 대하여

파일 읽기/쓰기, 데이터베이스 쿼리, 외부 API 호출, 네트워크 통신처럼 커널이 처리하는 작업을 기다리는 동안 CPU는 놀게 된다. 이때 요청을 처리하는 방식은 크게 두 가지로 나뉜다. Blocking I/O와 Non-Blocking I/O.

FastAPI 문서는 햄버거 가게를 예시로 설명하고 있다.

손님이 햄버거를 주문한 이후 음식을 받을 때까지 과정은 대략 이렇다
1. 손님의 주문을 받는다.
2. 주문 내역을 보고 햄버거 조리를 시작한다.
3. 햄버거가 완료되면 손님에게 알린다.
4. 손님은 햄버거를 맛있게 먹는다.

만약 한 사람이 주문부터 조리, 알림, 서빙까지 모든 과정을 순서대로 끝낼 때까지 다음 손님을 받지 않는다면 처리량을 늘리려면 사람을 많이 투입해야 한다. 이것은 “병렬성(parallelism)”으로 처리 자원을 늘려 동시에 여러 작업을 수행하는 방식이다.

반대로, 주문을 받은 사람은 조리는 요리사에게 맡기고 다음 손님 주문을 계속 받으며, 조리가 끝나면 알림 후 서빙한다. 단일 작업자가 여러 작업을 번갈아가며 진행하여 대기 시간을 숨기는 이 접근은 “동시성(concurrency)”이다.

컴퓨터에서도 네트워크나 파일 I/O처럼 느린 작업을 기다리는 동안 CPU가 놀게 두면 비효율적이다. Non-Blocking I/O와 이벤트 루프를 활용하면 대기 중인 작업을 다른 작업으로 전환하여 처리량을 높일 수 있다. 이 구조가 현대 웹서버(예: FastAPI + Uvicorn)의 토대다.

2. 비동기 처리를 위한 구조

비동기의 핵심은 이벤트 루프, 논블로킹 I/O, 코루틴의 조합이다. 목표는 I/O 대기 시간을 다른 작업으로 전환해 처리량을 올리고 응답 지연을 줄이는 것.

  • 이벤트 루프: 하나의 루프가 작업 큐를 돌며 “지금 당장 실행 가능한” 코루틴을 조금씩 실행한다. I/O 대기 지점에서 양보(await)하면 루프는 즉시 다른 작업으로 전환한다.

  • 코루틴(async/await): 협력적 스케줄링 단위. 스레드처럼 선점되지 않고, ‎await로 스스로 제어권을 반환한다.

  • 논블로킹 I/O: 소켓, 파일, DB 드라이버 등이 호출 즉시 반환하고 완료 알림을 루프에 등록한다. 완료 이벤트가 오면 해당 코루틴을 다시 이어서 실행한다.

  • 태스크/퓨처: 코루틴 실행 상태를 추적하는 핸들. 완료/예외/취소를 이벤트 루프가 관리한다.

  • 싱글 스레드 동시성: 하나의 OS 스레드로도 수만 개 연결을 다룰 수 있다. 컨텍스트 스위칭 비용과 락 경쟁이 적어 I/O 바운드에 유리하다.

동작 흐름(요약)

  1. 요청 수신 시 코루틴 생성 → 이벤트 루프에 태스크로 등록.
  2. 외부 I/O(예: DB, HTTP) 호출에서 await → 즉시 반환하고 완료 콜백을 루프에 연결.
  3. 루프가 준비된 태스크들 사이를 공정하게 순환 실행.
  4. I/O 완료 이벤트 발생 → 해당 태스크를 재개하여 응답 작성.

3. python asyncio

Python의 asyncio는 비동기 프로그래밍을 위한 표준 라이브러리로, 코루틴(async/await), 이벤트 루프, 태스크/퓨처, 비동기 I/O API를 제공한다. 목표는 I/O 대기 시간을 협력적으로 양보하고, 하나의 스레드에서도 높은 동시성을 확보하는 것.

핵심 구성요소

  • 코루틴: async def로 정의된 함수. 내부에서 I/O나 기다림이 발생하면 await로 제어권을 루프에 반환한다.
  • 이벤트 루프: 준비된 작업을 실행하고, 네트워크/파일/타이머 완료 이벤트를 받아 해당 코루틴을 재개한다.
  • 태스크/퓨처: 코루틴 실행을 래핑해 상태와 결과를 추적하는 객체. 취소/예외/완료를 관리한다.
  • 비동기 API: 소켓/서브프로세스/락·세마포어/큐 등 협력적 동작을 위한 프리미티브 제공.

기본 패턴

  • 단일 요청을 처리할 때: 코루틴 내부에서 외부 호출을 await로 연결하고, CPU 바운드는 피한다.
  • 동시 실행: asyncio.create_task()로 다수 작업을 스케줄링하고 await asyncio.gather(...)로 결과를 모은다.
  • 타임아웃/취소: asyncio.wait_for(coro, timeout)task.cancel()로 리소스 보호와 응답성 유지.

한가지 예시로 외부 API 를 이용하여 유저의 정보를 가져오는 코드를 살펴보자.

import asyncio
import httpx

# 비동기 함수로 선언
# 유저의 정보를 api.example.com 으로부터 가져온다
async def fetch_user(uid):
    async with httpx.AsyncClient(timeout=5) as client:
        r = await client.get(f"https://api.example.com/users/{uid}")
        r.raise_for_status()
        return r.json()

# gather 를 이용하여 유저 가져오는 작업을 여러 개 동시에 호출하고 기다린다.
async def fetch_all(uids):
    tasks = [asyncio.create_task(fetch_user(uid)) for uid in uids]
    try:
        return await asyncio.gather(*tasks) # 동시에 api.example.com 에 api 호출하고 모두 응답완료될 때까지 기다린다.
    except Exception as e:
        for t in tasks:
            t.cancel()
        raise

async def main():
    data = await asyncio.wait_for(fetch_all([1,2,3,4]), timeout=3)
    print(data)

if __name__ == "__main__":
    asyncio.run(main())

GIL 이라고 들어봤던거 같은데?

GIL 은 간단히 말하자면 python 에서 제공하는 기본 락이다. 멀티 스레드 구조로 설계하고 처리할 때 특정 데이터에 race condition 이 발생할 수 있다. 파이썬에서는 GIL 를 통해 임의의 시점에는 오직 단 하나의 스레드만 처리할 수 있도록 한다.

즉, GIL 은 멀티 스레드 방식에서 안정성을 높여주지만 성능 저하를 일으킬 수 있다. 하지만 위에서 언급한 asyncio 는 I/O 바운드에 초점을 맞춘 것으로, 멀티 프로세스 및 멀티 스레드와 같은 CPU 바운드를 위한 구조 설계와 달리 GIL 의 영향이 적다.

다음 챕터에서 Uvicorn이 asyncio 이벤트 루프를 어떻게 구동하고 ASGI를 통해 요청을 코루틴으로 전달하는지 연결해 설명하겠다.

4. Uvicorn

Uvicorn는 Python의 ASGI(Asynchronous Server Gateway Interface) 구현체이자 서버다. 요청/응답을 네트워크 소켓에서 받아 asyncio 이벤트 루프로 전달하고, 애플리케이션(FastAPI)의 코루틴 핸들러를 실행해 응답을 다시 소켓으로 내보낸다. 목표는 최소 오버헤드로 높은 동시성과 낮은 지연을 제공하는 것이다.

FastAPI 가 Uvicorn 과 쓰이는 이유

FastAPI는 ASGI 웹 프레임워크로 설계되었다. 즉, FastAPI는 비동기 처리를 위해 Uvicorn과 같은 ASGI 서버가 필요하다. 쿠버네티스에 어플리케이션을 배포했다 가정하였을 때 다음과 같이 Uvicorn 에 FastAPI 를 띄운 형태로 들어간다.


ref: https://yscho03.tistory.com/328

WSGI 이라고 들어봤던거 같은데?

WSGI(Web Server Gateway Interface)는 동기 인터페이스로 설계되었다. 이를 구현한 서버는 Gunicorn 이며, 보통 django 웹 프레임워크를 띄울 때 함께 띄워 관리한다. 동기처리를 하기 때문에 여러 프로세스(워커) 수를 조절해가며 처리량을 제어한다.

위와 같이 프로세스 수를 조절한다는 점에서 Gunicorn + Uvicorn 을 함께 사용하는 경우가 있다. Uvicorn 은 오직 하나의 워커에서만 처리되기 때문에 Gunicorn 을 이용하여 워커 수를 조절하여 처리량을 제어하는 것이다.

                  ┌───────────────────────────┐
                  │ Reverse Proxy / Ingress   │
                  │ Nginx / Envoy / Istio     │
                  └─────────────┬─────────────┘
                                │
                  ┌─────────────┴─────────────┐
                  │   gunicorn master         │
                  │  (프로세스 관리/재시작)    │
                  └─────────────┬─────────────┘
                        ┌───────┴───────┐
                        │               │
          ┌─────────────▼─────────────┐ ┌─────────────▼─────────────┐
          │ UvicornWorker (worker 1)  │ │ UvicornWorker (worker N)  │
          │ ASGI + asyncio/uvloop     │ │ ASGI + asyncio/uvloop     │
          └─────────────┬─────────────┘ └─────────────┬─────────────┘
                        │                               │
               ┌────────▼────────┐              ┌───────▼─────────┐
               │ FastAPI (ASGI)  │              │ FastAPI (ASGI)  │
               └─────────────────┘              └─────────────────┘

5. FastAPI, 그리고 서버 구성

위에서 말했듯이 FastAPI는 Python의 ASGI 위에서 동작하는 웹 프레임워크다. async/await 기반의 비동기 처리로 높은 동시성을 제공할 뿐더러 타입 힌트를 활용하여 쉽게 자동 검증·문서화까지 가능하다.

# app.py
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserIn(BaseModel):
    name: str
    age: int

@app.get("/health")
async def health():
    return {"status": "ok"}

@app.post("/users")
async def create_user(payload: UserIn):
    return {"id": 1, "name": payload.name, "age": payload.age}
# 개발 실행 (핫 리로드)
uvicorn app:app --reload

# 프로덕션 예시 (단일 프로세스)
uvicorn app:app --workers 4 --loop uvloop --http httptools

# 프로덕션 예시 (멀티 프로세스: gunicorn 관리)
gunicorn -k uvicorn.workers.UvicornWorker -w 4 app:app

6. FastAPI 에서 async def 와 def 의 차이

흔히 FastAPI 관련하여 구글링을 하다보면 FastAPI 에 API 액션 함수를 정의할 때 누군가는 async def 를 사용하고 다른 누군가는 def 를 사용한다. 이 두 개의 차이는 무엇일까?

FastAPI 문서를 보면 FastAPI 의 성능을 최대한 끌어올리려면 이 둘의 차이를 분명히 알아야한다.

둘의 차이를 명확하기 확인하기 위해 time.sleepasyncio.sleep 을 이용하여 간단하게 3가지 시나리오를 통해 async defdef 의 차이를 알아보자.

3가지 시나리오에 대해서 공통 조건은 다음과 같다.
a. API 액션 함수 로직은 10초 동안 처리(슬립)하고 응답하는 걸로 동일하다
b. uvicorn 서버를 이용해 FastAPI 앱을 띄우고 동시에 2개의 요청을 날린다.

1. async def + time.sleep

API 함수 정의할 때 async 를 붙이고 처리 과정에는 time.sleep 을 사용한다.

from fastapi import FastAPI
import uuid
import time
import asyncio

app = FastAPI()

@app.get("/")
async def index():
    request_id = uuid.uuid4()

    for i in range(10):
        time.sleep(1)
        print(f"{request_id}: 처리 중... {i + 1}/10")

    print('처리 완료')
    return {"status": "ok"}
0c9693fe-4f0c-4ac1-be1f-d4e5c928e54c: 처리 중... 1/10
0c9693fe-4f0c-4ac1-be1f-d4e5c928e54c: 처리 중... 2/10
0c9693fe-4f0c-4ac1-be1f-d4e5c928e54c: 처리 중... 3/10
0c9693fe-4f0c-4ac1-be1f-d4e5c928e54c: 처리 중... 4/10
0c9693fe-4f0c-4ac1-be1f-d4e5c928e54c: 처리 중... 5/10
0c9693fe-4f0c-4ac1-be1f-d4e5c928e54c: 처리 중... 6/10
0c9693fe-4f0c-4ac1-be1f-d4e5c928e54c: 처리 중... 7/10
0c9693fe-4f0c-4ac1-be1f-d4e5c928e54c: 처리 중... 8/10
0c9693fe-4f0c-4ac1-be1f-d4e5c928e54c: 처리 중... 9/10
0c9693fe-4f0c-4ac1-be1f-d4e5c928e54c: 처리 중... 10/10
처리 완료
INFO:     127.0.0.1:54602 - "GET / HTTP/1.1" 200 OK
20a96cd4-76e4-4f54-9551-2745d2f4d785: 처리 중... 1/10
20a96cd4-76e4-4f54-9551-2745d2f4d785: 처리 중... 2/10
20a96cd4-76e4-4f54-9551-2745d2f4d785: 처리 중... 3/10
20a96cd4-76e4-4f54-9551-2745d2f4d785: 처리 중... 4/10
20a96cd4-76e4-4f54-9551-2745d2f4d785: 처리 중... 5/10
20a96cd4-76e4-4f54-9551-2745d2f4d785: 처리 중... 6/10
20a96cd4-76e4-4f54-9551-2745d2f4d785: 처리 중... 7/10
20a96cd4-76e4-4f54-9551-2745d2f4d785: 처리 중... 8/10
20a96cd4-76e4-4f54-9551-2745d2f4d785: 처리 중... 9/10
20a96cd4-76e4-4f54-9551-2745d2f4d785: 처리 중... 10/10
처리 완료
INFO:     127.0.0.1:54603 - "GET / HTTP/1.1" 200 OK

동시에 2개의 요청을 보냈지만 처리 과정은 마치 하나의 요청이 끝난 후 다음 요청을 처리하는 것과 같다. 즉, 동시에 처리하는게 아닌 하나씩 처리하는 것이다.

2. async def + asyncio.sleep

이번에는 API 함수 정의할 때 async 를 붙이고 처리 과정에는 1번과 다르게 asyncio.sleep 을 사용한다.

from fastapi import FastAPI
import uuid
import time
import asyncio

app = FastAPI()

@app.get("/")
async def index():
    request_id = uuid.uuid4()

    for i in range(10):
        await asyncio.sleep(1)
        print(f"{request_id}: 처리 중... {i + 1}/10")

    print('처리 완료')
    return {"status": "ok"}
f35346da-c0e4-4e90-bb9a-7ec4c95a673b: 처리 중... 1/10
5d9355c1-2912-4703-bb91-04e61928ee7d: 처리 중... 1/10
f35346da-c0e4-4e90-bb9a-7ec4c95a673b: 처리 중... 2/10
5d9355c1-2912-4703-bb91-04e61928ee7d: 처리 중... 2/10
f35346da-c0e4-4e90-bb9a-7ec4c95a673b: 처리 중... 3/10
5d9355c1-2912-4703-bb91-04e61928ee7d: 처리 중... 3/10
f35346da-c0e4-4e90-bb9a-7ec4c95a673b: 처리 중... 4/10
5d9355c1-2912-4703-bb91-04e61928ee7d: 처리 중... 4/10
f35346da-c0e4-4e90-bb9a-7ec4c95a673b: 처리 중... 5/10
5d9355c1-2912-4703-bb91-04e61928ee7d: 처리 중... 5/10
f35346da-c0e4-4e90-bb9a-7ec4c95a673b: 처리 중... 6/10
5d9355c1-2912-4703-bb91-04e61928ee7d: 처리 중... 6/10
f35346da-c0e4-4e90-bb9a-7ec4c95a673b: 처리 중... 7/10
5d9355c1-2912-4703-bb91-04e61928ee7d: 처리 중... 7/10
f35346da-c0e4-4e90-bb9a-7ec4c95a673b: 처리 중... 8/10
5d9355c1-2912-4703-bb91-04e61928ee7d: 처리 중... 8/10
f35346da-c0e4-4e90-bb9a-7ec4c95a673b: 처리 중... 9/10
5d9355c1-2912-4703-bb91-04e61928ee7d: 처리 중... 9/10
f35346da-c0e4-4e90-bb9a-7ec4c95a673b: 처리 중... 10/10
처리 완료
INFO:     127.0.0.1:54594 - "GET / HTTP/1.1" 200 OK
5d9355c1-2912-4703-bb91-04e61928ee7d: 처리 중... 10/10
처리 완료
INFO:     127.0.0.1:54595 - "GET / HTTP/1.1" 200 OK

두 번째 시나리오에서는 2개의 요청이 동시에 처리되는 것과 같다. 1번과 asyncio.sleep 만 다를 뿐인데 말이다.

3. def + time.sleep

이번엔 1번에서 async def 대신에 def 로 바꾸고 실행시켜보자.

from fastapi import FastAPI
import uuid
import time
import asyncio

app = FastAPI()

@app.get("/")
def index():
    request_id = uuid.uuid4()

    for i in range(10):
        time.sleep(1)
        print(f"{request_id}: 처리 중... {i + 1}/10")

    print('처리 완료')
    return {"status": "ok"}
40f55724-f453-428c-84e1-72610cbb10c4: 처리 중... 1/10
34601578-5cbf-410c-bd9b-80073da58cfe: 처리 중... 1/10
34601578-5cbf-410c-bd9b-80073da58cfe: 처리 중... 2/10
40f55724-f453-428c-84e1-72610cbb10c4: 처리 중... 2/10
34601578-5cbf-410c-bd9b-80073da58cfe: 처리 중... 3/10
40f55724-f453-428c-84e1-72610cbb10c4: 처리 중... 3/10
40f55724-f453-428c-84e1-72610cbb10c4: 처리 중... 4/10
34601578-5cbf-410c-bd9b-80073da58cfe: 처리 중... 4/10
40f55724-f453-428c-84e1-72610cbb10c4: 처리 중... 5/10
34601578-5cbf-410c-bd9b-80073da58cfe: 처리 중... 5/10
40f55724-f453-428c-84e1-72610cbb10c4: 처리 중... 6/10
34601578-5cbf-410c-bd9b-80073da58cfe: 처리 중... 6/10
34601578-5cbf-410c-bd9b-80073da58cfe: 처리 중... 7/10
40f55724-f453-428c-84e1-72610cbb10c4: 처리 중... 7/10
40f55724-f453-428c-84e1-72610cbb10c4: 처리 중... 8/10
34601578-5cbf-410c-bd9b-80073da58cfe: 처리 중... 8/10
40f55724-f453-428c-84e1-72610cbb10c4: 처리 중... 9/10
34601578-5cbf-410c-bd9b-80073da58cfe: 처리 중... 9/10
40f55724-f453-428c-84e1-72610cbb10c4: 처리 중... 10/10
처리 완료
INFO:     127.0.0.1:54617 - "GET / HTTP/1.1" 200 OK
34601578-5cbf-410c-bd9b-80073da58cfe: 처리 중... 10/10
처리 완료
INFO:     127.0.0.1:54618 - "GET / HTTP/1.1" 200 OK

이번에는 1번과 달리 async def 에서 async 만 빼줬을 뿐인데 마치 2번과 동일하게 처리된 거처럼 보인다.

도대체 위 3개의 시나리오는 어떻게 다른걸까?

FastAPI 는 알아서 잘 처리해준다?

FastAPI 문서 중...

Note: You can mix def and async def in your path operation functions as much as you need and define each one using the best option for you. FastAPI will do the right thing with them.

Anyway, in any of the cases above, FastAPI will still work asynchronously and be extremely fast.

But by following the steps above, it will be able to do some performance optimizations.

위 시나리오 3개에 대해서 설명하기 위해 앞서 비동기 얘기를 길~게 했던 것이다. 각 시나리오 별 FastAPI 가 어떻게 처리했는지 2, 1, 3번 순으로 얘기를 해보겠다.

2번 시나리오의 경우

API 내부 코드를 보면 asyncio.sleep(1)await 하고 있다. 이 과정에서 FastAPI 는 메인스레드에서 asyncio.sleep 작업을 이벤트 루프로 넘겨버린다. 그럼 이벤트 루프에서 해당 작업이 완료되면 다시 갖고와서 처리한다. 즉, 메인스레드에서 직접 처리하기보다는 이벤트 루프를 통해 별도로 처리하는 것이다.
따라서 hang 이 걸리지 않고 바로 다음 요청을 처리할 수 있는것이다. (두 번째 요청 마찬가지로 asyncio.sleep 작업이 이벤트 루프에 등록될 것이다)

1번 시나리오의 경우

1번 시나리오에서는 time.sleep(1) 을 이용하여 대기를 하고 있다. 요 sleep 은 따로 비동기적으로 처리되지 않고 동기적으로 처리된다. 즉, 메인스레드에서 별도 테스크로 이벤트 루프에 등록하는게 아니라 메인 스레드 자체에서 처리되는 것이다.
따라서 메인스레드는 sleep 작업을 처리하느라 다른 요청을 받을 수 없기 때문에 두 번째 요청은 잠시 hang 걸린것이다.

3번 시나리오의 경우

그럼 3번도 time.sleep 으로 동기적으로 처리되어야할텐데 왜 이건 동시에 처리된걸까? 사실 동시성 있게 처리된게 아니라 병렬적으로 처리된 것이다. 위 "1.Blocking, Non-Blocking IO" 에서 다음 얘기를 했었다.

만약 한 사람이 주문부터 조리, 알림, 서빙까지 모든 과정을 순서대로 끝낼 때까지 다음 손님을 받지 않는다면 처리량을 늘리려면 사람을 많이 투입해야 한다. 이것은 “병렬성(parallelism)”으로 처리 자원을 늘려 동시에 여러 작업을 수행하는 방식이다.

3번의 시나리오의 경우, FastAPI 에서는 메인 스레드에서 관리하고 있는 별도 스레드 풀에 요청을 처리한다. 즉, 여러 개의 스레드로 해당 작업을 처리한 것이다. 따라서 출력 결과도 동시에 처리된걸로 보이는 것이다.

7. FastAPI 프로덕션 Best Practice

그럼 FastAPI 를 어떻게 하면 잘 사용할 수 있을까? 아래 글은 해당 유튜브 영상을 참고하여 작성하였다.

  1. async def 엔드포인트에 블로킹 작업을 수행해선 안된다.

  2. 최상의 퍼포먼스를 위해 되도록이면 엔드포인트를 async def로 유지한다.

  3. 무거운 연산(이미지/영상 처리나 큰 ML 추론)과 같은 CPU 바운드 작업은 엔드포인트에서 직접 돌리지 말고 워커로 분리한다.

  4. FastAPI dependencies 에서도 위 1, 2, 3 원칙을 지킨다.

  5. 백그라운드 작업: 응답을 지연시키지 않는 소규모 작업은 BackgroundTasks로 처리하되, 보장/재시도/장기 작업에는 큐+워커(Celery 등)를 사용한다.

  6. Swagger/ReDoc/OpenAPI URL을 프로덕션에서 꺼서 노출을 방지한다.

  7. 커스텀 Pydantic BaseModel 을 만들어서 전역 설정(예: snake_case↔camelCase alias, datetime/Decimal/ObjectId 인코딩)을 한곳에서 관리한다.

  8. 엔드포인트에서 response_model을 지정했다면 딕셔너리 그대로 반환하고 검증/JSON 인코딩은 FastAPI에 맡긴다. (ex: return User(name='이유찬') => return {"name": "이유찬"})

  9. 필드 검증은 엔드포인트가 아닌 pydantic 으로 정의한 모델에서 하여 일관된 에러와 문서화를 확보한다.

  10. DB 기반 검증(유저 권한 확인 등)은 엔드포인트가 아닌 Depend 로 분리하여 재사용한다.

  11. 각 endpoint에서 새 DB 연결을 만들지 말고 풀을 만들고 의존성으로 연결을 빌려 쓰고 반납한다.

  12. DB/캐시 클라이언트 등 초기화·정리를 lifespan 컨텍스트로 통합하고 실패 시에도 안전한 정리를 보장한다.

  13. .env와 .env.example, Settings(BaseSettings)로 설정을 중앙화하고 os.environ 직접 접근을 코드 전반에서 남발하지 않는다.

  14. print 대신 logging/Loguru/Structlog로 레벨, 타임스탬프, 요청/사용자 ID 등 컨텍스트를 포함하고 JSON 로그를 중앙집중화한다.

  15. 프로덕션은 Gunicorn + UvicornWorker + uvloop로 실행하고 워커 수는 CPU 코어 × 2 + 1을 시작점으로 벤치마크, 필요하면 Docker로 컨테이너화한다.

왜 워커 수는 CPU 코어 × 2 + 1 이 적합할까?

워커 수가 많다는 건 그만큼 컨텍스트 스위치가 빈번하게 일어나는 것이다.

CPU 는 1개인데 워커 수를 100,000,000 개 만들었다 가정하자. 1개의 CPU 가 특정 워커에서 다음 워커로 이동하는 과정이 100,000,000 번 이동하면서 처리를 해야되기 때문에 그만큼 비용도 발생한다.

1번 워커 -> <이동 > -> 2번 워커 -> <이동 > -> 3번 워커 -> ...

따라서 CPU 코어 수에 맞춰 적당한 워커 수로 지정하는 것이 좋다.

0개의 댓글