FastAPI Throtting(요청속도 제한) 처리(feat. Redis helm chart)
2025. 4. 3. 21:54ㆍPython
API를 개발할 때 트래픽 제한(Throttling)은 필수적인 기능 중 하나입니다. 사용자가 과도한 요청을 보내는 것을 방지하여 서비스 안정성을 유지하고, 악의적인 트래픽을 차단하는 데 도움을 줍니다. FastAPI에서 사용할 수 있는 여러 Throttling 라이브러리가 있지만, 이번 글에서는 SlowAPI를 선택하여 적용하는 과정을 소개합니다
Throttling 라이브러리 비교
Throttling을 구현할 수 있는 라이브러리로 여러 가지가 있지만, 주요 라이브러리들을 비교해 본 후 SlowAPI를 선택하게 되었습니다.
라이브러리 | start | 특징 | 단점 |
fastapi-limiter | 약 1.6k | Redis 기반, FastAPI 전용 | 설정이 복잡하고 유연성이 부족 |
slowapi | 약 2.3k | Starlette/FastAPI와 호환, 단순한 설정 | 공식 문서가 부족 |
SlowAPI를 선정한 이유는 다음과 같습니다:
- FastAPI와의 높은 호환성: Starlette 기반이라 FastAPI 프로젝트와 잘 어울립니다.
- GitHub Star 수가 높음: 유지보수 가능성이 높다고 판단했습니다.
- 유연한 요구사항 반영 가능: 사용자 인증 기반의 요청 제한을 쉽게 구현할 수 있습니다.
SlowAPI를 이용한 Throttling 구현
SlowAPI를 이용하여 요청을 제한하는 기능을 추가해 보겠습니다.
from core import settings
from core.exception import TooManyRequestsHTTPException
from database.session import redis_client
from fastapi import Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_ipaddr
# 요청에서 이메일을 가져오는 함수
def get_email(request: Request):
try:
return request.state.email
except Exception:
return None
# 특정 클라이언트를 차단하는 함수
def block_client(client: str, time=3600):
try:
block_key = f"blocked:{client}"
redis_client.setex(block_key, time, "blocked")
except Exception:
pass
# 특정 클라이언트가 차단되었는지 확인하는 함수
def is_blocked(client: str) -> bool:
try:
block_key = f"blocked:{client}"
return redis_client.get(block_key) is not None
except Exception:
return False
# Rate Limit 초과 시 block 처리 핸들러
def custom_rate_limit_exceeded_handler(
request: Request,
exc: RateLimitExceeded,
):
if email := get_email(request):
block_client(email)
else:
block_client(get_ipaddr(request))
return _rate_limit_exceeded_handler(request, exc)
# IP 기반 요청 차단 확인 Depends
async def check_ip_blocked(request: Request) -> Request:
if is_blocked(get_ipaddr(request)):
raise TooManyRequestsHTTPException()
return request
# 이메일 기반 요청 차단 확인 Depends
async def check_email_blocked(request: Request) -> Request:
body = await request.json()
client = get_ipaddr(request)
if email := body.get("email", None):
client = email
request.state.email = body.get("email", None)
if is_blocked(client):
raise TooManyRequestsHTTPException()
return request
# IP 기반 제한 설정
ip_limiter = Limiter(
key_func=get_ipaddr,
storage_uri=f"redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:{settings.REDIS_PORT}",
in_memory_fallback_enabled=True, # Redis 미동작 시 제한 미처리
)
# 이메일 기반 제한 설정
email_limiter = Limiter(
key_func=get_email,
storage_uri=f"redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:{settings.REDIS_PORT}",
in_memory_fallback_enabled=True, # Redis 미동작 시 제한 미처리
)
코드 설명
- ip_limiter, check_ip_blocked를 사용해서 IP 요청 제한을 설정합니다.
- email_limiter, check_email_blocked를 사용해서 IP 요청 제한을 설정합니다.
- block을 따로 관리하는 이유는 slowAPI는 TTL 기준으로 차단이 되기 때문에 속도 제한 기준과 제한 시간을 따로 설정하기 위함입니다.
- 예를 들면 분당 100회 초과 시 1시간 제한 을 가능하게 하기 위함.
- request.state.email을 설정하는 이유:
- slowapi의 key_func 인자는 동기 함수여야 합니다.
- request.json()은 비동기 함수이므로, get_email()에서 직접 사용할 수 없습니다.
- 따라서 요청 본문에서 이메일을 추출하여 request.state.email에 저장하고 동기적으로 가져오도록 했습니다.
- in_memory_fallback_enabled=True 설정 이유:
- Redis 장애 발생 시 Throttling을 비활성화하기 위함입니다.
- API 가용성을 보장해야 하므로, Redis가 다운되더라도 API가 정상 동작하도록 구성했습니다.
SlowAPI 적용 방법
1. slowapi 라이브러리 설치
pip install slowapi
2. FastAPI 프로젝트에 적용
from fastapi import FastAPI, Request, Depends
from throttling import ip_limiter, email_limiter, check_email_blocked
app = FastAPI()
@app.post(
"/test",
summary="이메일 인증번호 검증",
dependencies=[Depends(check_email_blocked)],
)
@email_limiter.limit("10/minute")
@ip_limiter.limit("100/minute")
async def test(
request: Request,
body: schema_auths.SendEmailAuthNumberRequest,
) -> Any:
return None
4. 실행 후 테스트
- 1분에 10번 초과 요청하면 429 Too Many Requests 응답을 받게 됩니다.
curl -X POST http://127.0.0.1:8000/test -H "Content-Type: application/json" -d '{"email": "test@example.com"}'
5. 테스트 결과
k8s 환경에서 redis를 pod로 띄워 테스트하기
bitnami의 redis 를 사용하여 helm 차트를 구성하였습니다.
구성
1. values.yaml
redis:
namespaceOverride: "dev"
architecture: replication
registry: public.ecr.aws/bitnami/redis
image:
registry: "public.ecr.aws/bitnami"
repository: "redis"
tag: "7.4.2-debian-12-r6"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 6379
auth:
enabled: true
password: "redis-password"
ingress:
enabled: false
master:
count: 1
port: 6379
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "100m"
replica:
replicaCount: 2
port: 6379
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "100m"
global:
storageClass: ""
imageRegistry: ""
imagePullSecrets: []
nameOverride: "redis"
fullnameOverride: "redis"
2. Chart.yaml
apiVersion: v2
name: redis
description: A Helm chart for deploying Redis with Master-Replica setup
type: application
version: 0.1.0
appVersion: "7.4.2"
dependencies:
- name: redis
version: "20.4.0"
repository: "oci://registry-1.docker.io/bitnamicharts"
동작
helm dependency update
helm upgrade --install redis . -n dev
'Python' 카테고리의 다른 글
Python 타입 힌트 평가 지연(from __future__ import annotations)(feat. TYPE_CHECKING) (0) | 2025.04.24 |
---|---|
FastAPI에서 실제 DB를 사용하는 테스트 환경 구성하기 (0) | 2025.04.16 |
boto3를 활용한 SESManager 구축 (0) | 2025.03.20 |
lambda 를 활용한 클래스 참조 문제 해결 (0) | 2025.02.21 |
Pydantic 효과적인 사용 방법 (0) | 2025.01.10 |