FastAPI로 전환하며 겪은 응답 시간 지연 문제와 해결 과정
Flask에서 FastAPI로 전환하면서 겪었던 주요 문제는 Flask의 동기 처리 방식 때문이었습니다. Flask는 요청을 처리하는 동안 다른 요청을 처리할 수 없어서 응답 시간이 길어지곤 했습니다. 하지만 FastAPI는 비동기 처리를 지원하여 이 문제를 해결할 수 있었습니다.
문제 발생
비동기 지원을 하는 FastAPI로 전환한 초기에는 기존의 동기 방식 코드와 데이터베이스 처리 로직 때문에 응답 시간이 늘어났습니다. 특히 데이터베이스 쿼리와 외부 API 호출 시 병목 현상이 발생했습니다.
해결 과정
첫 번째로, I/O 바인딩된 코드를 비동기 방식으로 리팩토링했습니다. Python의 asyncio
와 aiohttp
라이브러리를 사용하여 HTTP 호출을 비동기로 처리했습니다.
import aiohttp
import asyncio
async def fetch_data(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
asyncio.run(fetch_data('https://api.example.com/data'))
데이터베이스 처리를 위해 databases
라이브러리를 사용하여 비동기 지원을 추가했습니다.
from databases import Database
database = Database('sqlite:///test.db')
async def fetch_db_data(query):
await database.connect()
result = await database.fetch_all(query=query)
await database.disconnect()
return result
query = "SELECT * FROM users"
asyncio.run(fetch_db_data(query))
추가 최적화 과정
데이터베이스 쿼리 최적화 과정
데이터베이스 쿼리 최적화를 위해 PostgreSQL을 사용하였습니다. PostgreSQL의 EXPLAIN 명령은 쿼리 실행 계획을 보여주며, 이를 통해 어떤 쿼리가 성능 저하의 원인이 되는지 파악할 수 있습니다. 예를 들어, 쿼리가 비효율적으로 인덱스를 사용하지 않는 경우가 드러나면, 적절한 인덱스를 추가하여 처리 속도를 개선하였습니다.
EXPLAIN SELECT * FROM users WHERE user_id = 101;
ANALYZE 명령은 실제 쿼리를 실행하고 실행 시간과 행 처리 통계를 수집하여, EXPLAIN 결과와 함께 분석하였습니다. 이 정보를 바탕으로 쿼리 성능을 꼼꼼히 검토했으며, 필요한 쿼리 재구성 또는 인덱스 조정을 통해 성능을 최적화하였습니다.
EXPLAIN ANALYZE SELECT * FROM users WHERE user_id = 101;
ORM과 N+1 쿼리 문제
ORM(Object-Relational Mapping)은 데이터베이스의 테이블을 객체로 매핑하여 프로그래밍 언어에서 쉽게 사용할 수 있게 해 주는 도구입니다. ORM을 사용하면 복잡한 SQL 쿼리 없이도 데이터베이스 작업을 수행할 수 있지만, 종종 N+1 쿼리 문제가 발생합니다. 이 문제는 한 번의 작업으로 여러 개의 관련 객체를 로드할 때, 각 객체를 위해 별도의 쿼리가 발생하여 성능이 저하되는 현상을 말합니다.
N+1 쿼리 문제를 해결하기 위해 eager loading 기법을 적용했습니다. 이는 필요한 모든 관련 데이터를 처음의 쿼리에서 함께 로드함으로써 추가 쿼리의 수를 줄이고 전체적인 응답 시간을 단축합니다. 예를 들어, 사용자와 그들의 주문을 함께 조회하는 경우, eager loading을 사용하여 한 번의 쿼리로 모든 정보를 로드할 수 있습니다.
SELECT users.*, orders.* FROM users JOIN orders ON users.id = orders.user_id WHERE users.id = 101;
Uvicorn 워커 프로세스 수 설정 과정
Uvicorn은 비동기 ASGI 서버로, 워커 프로세스 수는 서버의 성능에 직접적인 영향을 미칩니다. 이 설정은 서버의 CPU 코어 수와 애플리케이션의 특성을 고려하여 결정합니다. 초기에는 일반적인 권장사항에 따라 CPU 코어 수의 2배로 설정하였으나, 이는 단순한 시작점일 뿐입니다.
워커 수의 최적화를 위해 실제 부하 테스트를 진행했습니다. 사용한 도구는 Apache JMeter로, 이를 통해 애플리케이션에 대한 다양한 부하 시나리오를 시뮬레이션하였습니다. 부하 테스트는 서버의 CPU 사용률, 메모리 사용량, 네트워크 I/O 등을 포함한 여러 성능 지표를 실시간으로 모니터링하는 과정을 포함합니다.
모니터링 도구로는 Prometheus를 사용하여, 서버의 응답 시간과 처리량 등의 중요 지표를 관찰하였습니다. 이 데이터를 분석하여, CPU 코어 수의 3배에 해당하는 워커 수에서 응답 시간과 처리량이 가장 최적화된 결과를 보였기 때문에, 최종적으로 워커 수를 그 수준으로 설정하였습니다.
# Uvicorn 서버 시작 명령 예시
uvicorn app:app --workers 12 --host 0.0.0.0 --port 8000
결과
이러한 변경을 통해 FastAPI 기반의 서비스 응답 시간을 크게 개선할 수 있었습니다. 추가로 데이터베이스 쿼리 최적화와 Uvicorn 웹 서버의 워커 프로세스 수 조정을 통해 더욱 효율적인 서비스를 제공할 수 있게 되었습니다.
'FastAPI' 카테고리의 다른 글
Uvicorn 워커 수 조절과 Python GIL의 관계 (0) | 2024.04.18 |
---|