Tools/FastAPI

FastAPI의 비동기 요청 처리

칼쵸쵸 2024. 11. 27. 23:46
반응형

 

비동기 요청 처리

1. 기본 개념

  • 비동기 요청 처리란?
    • 요청이 처리되는 동안 서버가 다른 요청을 처리할 수 있는 방식입니다.
    • I/O 작업(예: DB 쿼리, 외부 API 호출)이 있을 때, 해당 작업이 끝날 때까지 기다리지 않고 다른 요청을 처리합니다.
  • 비동기의 장점:
    • 네트워크 대기나 파일 I/O 같은 느린 작업에서 CPU 자원을 효율적으로 활용.
    • 동시 요청을 더 많이 처리 가능.

2. FastAPI에서 비동기 동작 확인

FastAPI는 Python의 asyncio를 기반으로 동작합니다. 비동기 처리를 확인하려면 다음과 같은 방법으로 테스트할 수 있습니다.

예제 코드

아래 코드는 비동기 처리를 테스트하는 간단한 FastAPI 애플리케이션입니다:

import asyncio
from fastapi import FastAPI

app = FastAPI()

@app.get("/sync")
def sync_endpoint():
    import time
    time.sleep(5)  # 동기적 대기
    return {"message": "동기 처리 완료"}

@app.get("/async")
async def async_endpoint():
    await asyncio.sleep(5)  # 비동기 대기
    return {"message": "비동기 처리 완료"}

 

3. 테스트 방법

비동기 요청 처리가 실제로 동작하는지 확인하려면 동시 요청을 실행하고 처리 속도를 비교해볼 수 있습니다.

1) 단일 요청 테스트

1. FastAPI 서버 실행:

uvicorn main:app --reload

 

2. 브라우저 또는 HTTP 클라이언트를 통해 요청

  • /sync: 요청이 완료될 때까지 5초 대기.
  • /async: 요청이 완료될 때까지 5초 대기.

단일 요청에서는 동기와 비동기 차이가 눈에 띄지 않을 수 있습니다.

 

2) 동시 요청 테스트

동시 요청을 보낼 때 비동기 처리의 장점이 드러납니다.

 

테스트 스크립트

아래 Python 스크립트를 사용해 두 엔드포인트에 동시에 요청을 보내보세요.

import requests
import time
import asyncio
import aiohttp

# 동기 요청 테스트
def test_sync():
    start_time = time.time()
    for _ in range(3):
        response = requests.get("http://127.0.0.1:8000/sync")
        print(response.json())
    print(f"Sync total time: {time.time() - start_time:.2f} seconds")

# 비동기 요청 테스트
async def test_async():
    start_time = time.time()
    async with aiohttp.ClientSession() as session:
        tasks = [session.get("http://127.0.0.1:8000/async") for _ in range(3)]
        responses = await asyncio.gather(*tasks)
        for response in responses:
            print(await response.json())
    print(f"Async total time: {time.time() - start_time:.2f} seconds")

# 실행
print("Testing synchronous requests...")
test_sync()

print("\nTesting asynchronous requests...")
asyncio.run(test_async())

 

결과 비교

  1. 동기 요청 결과:
    • /sync 엔드포인트는 요청 3개를 순차적으로 처리하며, 총 15초(5초 * 3) 소요됩니다.
    • 서버는 한 번에 하나의 요청만 처리하므로 동시 요청에 대해 느립니다.
  2. 비동기 요청 결과:
    • /async 엔드포인트는 요청 3개를 동시에 처리하며, 총 5초만 소요됩니다.
    • 서버가 대기 시간 동안 다른 요청을 처리하므로 비동기 방식의 장점이 나타납니다.

 

요청 별로 쓰레드를 할당해서 처리하지 않는가?

1. 스레드 기반 처리의 한계

스레드 기반 모델 (Spring Servlet 모델, 동기 처리)

  • 전통적으로, 각 요청은 하나의 스레드에 매핑됩니다.
  • 서버는 스레드 풀을 사용해 동시 요청을 처리합니다.
  • 예를 들어, 스레드 풀 크기가 100이라면 동시에 100개의 요청을 처리할 수 있습니다.

한계점:

  1. 스레드의 메모리 비용:
    • 각 스레드는 일정한 메모리 스택(1MB 내외)을 차지합니다.
    • 많은 요청이 들어오면 스레드가 부족하거나 메모리가 부족해질 수 있습니다.
  2. 블로킹 문제:
    • 스레드 기반 처리에서 **I/O 작업(예: DB 조회, 외부 API 호출)**이 발생하면, 해당 스레드는 작업이 끝날 때까지 대기 상태에 놓입니다.
    • 즉, 요청 처리에 필요한 실제 작업이 없어도 스레드가 점유된 상태로 남습니다.
  3. 스케일링 비용:
    • 요청이 많아질수록 더 많은 스레드를 생성해야 하며, 이는 CPU 컨텍스트 스위칭 비용과 메모리 사용량 증가로 이어집니다.

2. 비동기 처리가 필요한 이유

비동기 처리는 스레드의 블로킹 문제를 해결하고 자원을 효율적으로 사용할 수 있는 방법을 제공합니다.

비동기 처리의 특징:

  • I/O 대기 시간 동안 스레드를 점유하지 않음:
    • 비동기 작업은 I/O 대기(예: 파일 읽기, DB 쿼리, API 호출)가 발생하면 해당 작업을 다른 이벤트 루프로 넘기고, 스레드는 즉시 반환됩니다.
    • 이로 인해 적은 수의 스레드로도 많은 요청을 처리할 수 있습니다.

어떤 상황에서 유리한가?

  • I/O 바운드 작업:
    • 데이터베이스 조회, API 호출, 파일 읽기 등 네트워크 대기 시간이 많은 작업.
    • 이 경우, 비동기 처리로 스레드를 효율적으로 사용할 수 있습니다.
  • 동시 요청이 많은 경우:
    • 예를 들어, 10,000개의 요청이 들어오더라도 비동기 처리로 스레드 수를 제한하면서도 높은 처리량을 유지할 수 있습니다.

비교 예시:

스레드 풀 크기가 100인 경우:

  • 동기 모델:
    • 100개의 요청이 I/O 작업(5초 대기)에 들어가면, 5초 동안 추가 요청을 처리할 수 없습니다.
  • 비동기 모델:
    • 100개의 요청이 I/O 작업에 들어가더라도 스레드는 반환되어 다른 요청을 처리할 수 있습니다.
    • 결과적으로 훨씬 더 많은 요청을 처리할 수 있습니다.

3. 비동기 vs 스레드 풀의 차이

비동기 처리는 "스레드 기반 처리의 한계"를 극복하려는 접근 방식입니다.

특징스레드 기반 동기 처리비동기 처리

자원 사용 요청당 스레드 점유 이벤트 루프 기반으로 적은 스레드 사용
I/O 대기 처리 스레드가 대기 상태로 점유됨 대기 작업을 이벤트 루프로 넘김
스케일링 비용 스레드 메모리, 컨텍스트 스위칭 비용 증가 효율적인 자원 사용으로 비용 감소
적합한 작업 CPU 바운드 작업 I/O 바운드 작업

4. 실제 예제: 동기 vs 비동기

동기 처리의 문제 (스레드 점유)

import time
from fastapi import FastAPI

app = FastAPI()

@app.get("/sync")
def sync_endpoint():
    time.sleep(5)  # 동기적으로 5초 대기
    return {"message": "동기 처리 완료"}
  • 이 경우, 요청 하나가 처리되는 동안 스레드가 5초 동안 점유됩니다.
  • 스레드 풀이 가득 차면 더 이상의 요청은 대기해야 합니다.

비동기 처리로 효율 개선

import asyncio
from fastapi import FastAPI

app = FastAPI()

@app.get("/async")
async def async_endpoint():
    await asyncio.sleep(5)  # 비동기로 5초 대기
    return {"message": "비동기 처리 완료"}
  • await asyncio.sleep(5)는 대기 중에도 스레드를 반환하므로 더 많은 요청을 처리할 수 있습니다.
  • 결과적으로, I/O 바운드 작업이 많은 환경에서 더 높은 처리량을 보입니다.

반응형

'Tools > FastAPI' 카테고리의 다른 글

FastAPI의 구조  (0) 2024.11.13