
LLM 배치를 돌리는 과정에서 Azure Open AI를 쓰는 과정에 502 Gateway Timeout 에러를 발생하는 문제를 겪었다.
InternalServerError: <html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>openresty</center>
</body>
</html>
코드를 비동기로 짰음에도 불구하고 왜 갑자기 타임아웃이 나는걸까?
문제 원인은 의외로 간단했다.
비동기 처리를 쓴다고 해서 안전하게 병렬처리가 되는게 아니였다.
오히려 동시에 너무 많은 요청을 날리는 바람에 서버나 프록시가 과부하가 걸린 것이였다.
당시 구조는 다음과 같았다.
tasks = [process(x) for x in df]
for task in asyncio.as_completed(tasks):
final = await task
tasks에서 코루틴(중간에 멈췄다가 다시 이어서 실행할 수 있는 함수 / await 지점에서 멈추고 그다음 작업 이어서 실행 가능) 이 실행되면서 500개의 tasks를 만들었고, 한번에 API 요청이 동시에 시작된 코드다.
찾아보니 Azure Embedding API는 초당 처리량 제한(Throughput Limit)이 있어서, 초과 시 502 또는 429 에러가 난다고 한다.
1. Azure OpenAI API는 초당 요청 제한(Rate Limit)이 있고, 프록시 서버나 네트워크 소켓 자원에도 한계가 있다.
결국 연결 과부하 → 응답 지연 → 타임아웃(502) 로 이어졌다
처음엔 비동기니까 한꺼번에 돌려도 괜찮겠지라고 생각했다. 하지만 비동기 = 무제한 동시 실행을 의미하는게 아니였다.
제어 없이 동시에 500개를 날리면 네트워크는 순식간에 포화 상태가 되는 것이였다.
2. 또 process 함수 내부에서 asyncio.run()으로 비동기 함수를 짜놓고, 메인 루프에서 또 await을 섞었기 때문에 이벤트 루프가 충돌되는 부분도 있었다.
이 문제를 해결한 방법은 세마포어였다.
세마포어는 동시에 실행할 수 있는 작업 개수를 제한하는 도구이다.
예를 들어 asyncio.Semaphore(5)를 사용하면,
항상 최대 5개의 작업만 동시에 실행되고 나머지는 대기하게 된다.
대기열에 작업들을 대기시켰다가 5개를 실행하고 그 다음 5개가 실행되는 구조였다.
import asyncio
semaphore = asyncio.Semaphore(5)
async def process(input: str) -> List[int]:
async with semaphore:
return await process_final(input)
async def process_result(df):
tasks = [process(x) for x in df]
results = []
for task in asyncio.as_completed(tasks):
result = await task
results.append(result)
return results
이렇게 바꾼 후에는 요청이 일정한 개수로 서버에 전달되었고, 타임아웃은 0건이며 안전하게 비동기 코드가 잘 처리가 되었다.
비동기는 속도를 높이는 기술이지, 서버 부하를 제어하는 기술이 아니라는 것을 알았고,
대량 API 호출 시 동시성 제어는 필수라고 느꼈다.
속도보다 지속적인 처리량이 더 중요하다는 것을 깨달았고, 이벤트 루프가 꼬이는 경우를 잘 확인해봐야 겠다고 다시 한번 느꼈다.
만약, Azure Open api 사용 시 비동기 처리로 호출한다면, 세마포어를 통해 동시 요청 개수를 제어하는 습관을 들여보자
그 한 줄 차이가 502 타임아웃 지옥에서 천국으로 데려다 줄수 있을 것 같다....
그것도 모르고 코드를 계속 뜯어고친 나 자신 반성해야겠다 ㅠㅠ
비동기 처리 시 이해가 안갔던 함수 부분들을 정리해 둔 부분이다.
Asyncio.run()
비동기 함수를 실행할때 새 이벤트 루프를 만들어서 돌린 후 종료하는 함수
- 특징: 기존에 실행 중인 이벤트 루프가 없어야 함. (이미 실행 중이면 RuntimeError 발생)
- 한 번 실행하면 새로운 루프를 열고, 작업이 끝나면 루프를 닫음.
nest_asyncio
이미 실행 중인 이벤트 루프를 중첩 실행 가능하게 패치해주는 라이브러리
- 특징: 이미 루프가 돌고 있는 환경에서도 await나 asyncio 코드를 돌릴 수 있게 해줌
- 이미 실행 중인 루프 위에서 또 비동기 함수를 실행해야 할 때
| 구분 | Asyncio.run() | nest_asyncio |
| 이벤트 루프 | 새로 생성 후 종료 | 기존 루프 재사용 |
| 사용 환경 | Python 스크립트 | Notebook, Databricks 등 이미 루프 실행 중 |
| 목적 | 한 번만 실행 | 중첩 실행 가능 |
| 제약 | 실행 중 루프 있으면 에러 (동기) | 중첩 실행 가능(비동기) |



