Python 비동기 HTTP 요청 처리 시 타임아웃 이슈 해결

문제 상황

레거시 동기 HTTP 클라이언트를 aiohttp 기반 비동기로 전환하는 작업을 진행했다. 기존에는 requests 라이브러리로 순차 처리했던 100여 개의 외부 API 호출을 동시에 처리하도록 변경했다.

배포 후 특정 요청들이 간헐적으로 응답을 받지 못하고 무한 대기하는 현상이 발생했다. 모니터링 결과 전체 요청 중 5% 정도가 타임아웃 없이 계속 pending 상태로 남아있었다.

초기 접근

timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session:
    async with session.get(url) as response:
        return await response.json()

ClientTimeout을 설정했지만 문제는 계속 발생했다. 로그를 확인해보니 connection pool에서 연결을 재사용하는 과정에서 keep-alive timeout과 충돌하는 케이스가 있었다.

해결 방법

개별 요청마다 명시적 타임아웃을 설정하고, TCPConnector의 옵션을 조정했다.

connector = aiohttp.TCPConnector(
    limit=100,
    ttl_dns_cache=300,
    force_close=True  # 연결 재사용 비활성화
)

timeout = aiohttp.ClientTimeout(
    total=30,
    connect=5,
    sock_read=10
)

async with aiohttp.ClientSession(
    connector=connector,
    timeout=timeout
) as session:
    try:
        async with session.get(url, timeout=timeout) as response:
            return await response.json()
    except asyncio.TimeoutError:
        logger.warning(f"Timeout for {url}")
        return None

force_close=True로 연결 재사용을 비활성화하니 문제가 해결됐다. 성능은 약간 감소했지만 안정성이 더 중요했다.

결론

aiohttp의 connection pooling은 성능에 유리하지만, 외부 API처럼 제어 불가능한 엔드포인트를 다룰 때는 신중해야 한다. 필요에 따라 연결 재사용을 포기하는 것도 합리적 선택이다.