일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- asyncio
- leetcode
- K8S
- dockerfile
- elasticsearch
- EKS
- ansible
- Service
- intervals
- AWS
- DevOps
- event loop
- IAM
- YAML
- ebs
- Python
- POD
- Django
- docker
- IAC
- 자바스크립트
- terraform
- Deployment
- github
- FastAPI
- EC2
- Kubernetes
- 쿠버네티스
- asgi
- WSGI
- Today
- Total
궁금한게 많은 개발자 노트
[ FastAPI ] Streaming large size file using StreamingResponse 본문
[ FastAPI ] Streaming large size file using StreamingResponse
궁금한게 많은 개발자 2024. 4. 22. 14:40서버로 부터 이미지나 동영상 파일을 다운로드 받을 때, base64로 encode된 PlainTextResponse를 사용해도 되지만,
좀 더 나은 성능을 보장하며 Async방식으로 다운 받을 수 있는 StreamingResponse 사용 및 사용 시 주의 점에 대해 알아보고자 합니다.
https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse
서버는 보통 클라이언트에서 요청한 S3에 저장된 이미지를 가져오고, 해당 이미지를 StreamingResponse로 내려줍니다.
이 때, 작은 사이즈의 파일들은 별다른 처리 없이 boto3 library를 사용하여 S3에서 받은 result의 body를 StreamingResponse에 넣어주면 이미지 파일이 받아집니다.
async def download_image_file(
request: Request,
) -> StreamingResponse:
try:
async with S3Client() as s3_client:
result = await s3_client.download_file(key=key)
except:
...
return StreamingResponse(
result["Body"], media_type=settings.media_type
)
하지만, 큰 사이즈의 이미지 파일은 위 형태로 반환 시 이미지 파일을 chunk size로 나눈 첫번 째 chunk만 포함됩니다.
이는 StreamingResponse의 내부 구현과 관련이 있는데, 위 코드에서는 return을 통해 반환되어 함수가 끝나기 때문에 아래 stream_response에서 더 이상 context가 유지되지 않기에 나머지 chunk가 전달되지 않습니다.
async def stream_response(self, send: Send) -> None:
await send(
{
"type": "http.response.start",
"status": self.status_code,
"headers": self.raw_headers,
}
)
async for chunk in self.body_iterator:
if not isinstance(chunk, bytes):
chunk = chunk.encode(self.charset)
await send({"type": "http.response.body", "body": chunk, "more_body": True})
await send({"type": "http.response.body", "body": b"", "more_body": False})
이에 FastAPI의 공식 문서에서 가이드 하듯, StreamingResponse의 content인자에 async generator를 전달해주어, context를 유지하여 yield와 반복문을 통해 chunk를 순회하며 streaming하는 방식으로 전달해야 합니다.
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def fake_video_streamer():
for i in range(10):
yield b"some fake video bytes"
@app.get("/")
async def main():
return StreamingResponse(fake_video_streamer())
이렇게 되면 return을 하더라도 StreamingResponse를 반환한 곳에서 context가 유지되고 stream_response함수에서 지속적으로 await send()를 통해 남은 chunk들을 순서대로 보낼 수 있게 됩니다. 아래는, 위 download_image_file함수를 변환한 것으로, S3에서 받은 큰 사이즈의 이미지를 StreamingResponse로 반환하는 예시입니다.
async def download_image_file(
request: Request,
) -> StreamingResponse:
try:
async def download_chunks() -> AsyncGenerator[bytes, None]:
async with S3Client() as s3_client:
result = await s3_client.download_file(key=key)
async for chunk in result["Body"].iter_chunks():
yield chunk
return StreamingResponse(
content=download_chunks(), media_type=settings.media_type
)
except:
...
물론 서버에서 Streaming이 필요없다면, 메모리에 이미지를 저장하고 한번에 내려줄 수도 있지만 스트리밍을 통해 성능 상의 이점을 가지고 싶다면 위 방식대로 구현해보는 것도 좋을 것 같습니다. 감사합니다 😊🙌
'Back End' 카테고리의 다른 글
API 디자인 (REST, GraphQL, gRPC, WebSocket) (4) | 2024.09.17 |
---|---|
[ Fluent Bit ] Backpressure 해결책 Buffering (0) | 2024.05.28 |
[ Python] aiobotocore를 사용한 AWS S3와 연동 (0) | 2024.02.08 |
[ FastAPI ] 비동기 메커니즘 (0) | 2023.12.11 |
[ Database ] Elasticsearch vs RDBMS (0) | 2023.07.22 |