반응형
비동기 요청 처리
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())
결과 비교
- 동기 요청 결과:
- /sync 엔드포인트는 요청 3개를 순차적으로 처리하며, 총 15초(5초 * 3) 소요됩니다.
- 서버는 한 번에 하나의 요청만 처리하므로 동시 요청에 대해 느립니다.
- 비동기 요청 결과:
- /async 엔드포인트는 요청 3개를 동시에 처리하며, 총 5초만 소요됩니다.
- 서버가 대기 시간 동안 다른 요청을 처리하므로 비동기 방식의 장점이 나타납니다.
요청 별로 쓰레드를 할당해서 처리하지 않는가?
1. 스레드 기반 처리의 한계
스레드 기반 모델 (Spring Servlet 모델, 동기 처리)
- 전통적으로, 각 요청은 하나의 스레드에 매핑됩니다.
- 서버는 스레드 풀을 사용해 동시 요청을 처리합니다.
- 예를 들어, 스레드 풀 크기가 100이라면 동시에 100개의 요청을 처리할 수 있습니다.
한계점:
- 스레드의 메모리 비용:
- 각 스레드는 일정한 메모리 스택(1MB 내외)을 차지합니다.
- 많은 요청이 들어오면 스레드가 부족하거나 메모리가 부족해질 수 있습니다.
- 블로킹 문제:
- 스레드 기반 처리에서 **I/O 작업(예: DB 조회, 외부 API 호출)**이 발생하면, 해당 스레드는 작업이 끝날 때까지 대기 상태에 놓입니다.
- 즉, 요청 처리에 필요한 실제 작업이 없어도 스레드가 점유된 상태로 남습니다.
- 스케일링 비용:
- 요청이 많아질수록 더 많은 스레드를 생성해야 하며, 이는 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 |
---|