파일 하나를 변경하여 31.8배 속도 향상: AWS Bedrock에서 비동기 임베딩 (Async Embedding) 호출하기
요약
AWS Bedrock을 이용한 RAG 인제스션 파이프라인에서 순차적 HTTP 호출을 비동기 방식으로 전환하여 처리 속도를 31.8배 향상시킨 사례를 소개합니다. 네트워크 대기 시간을 줄이기 위해 asyncio와 aiohttp를 활용한 병렬 요청 방식의 효과를 분석합니다.
핵심 포인트
- 순차적 블로킹 호출을 비동기 워크플로우로 변경하여 처리 시간 49.61초에서 1.56초로 단축
- 네트워크 I/O 대기 시간 동안 CPU 유휴 상태를 방지하기 위해 병렬 요청 활용
- aiohttp와 asyncio.gather를 사용한 비동기 구현 방법 제시
- boto3 사용 시 aioboto3 또는 asyncio.to_thread() 활용 권장
RAG (Retrieval-Augmented Generation) 인제스션 (Ingestion) 파이프라인을 테스트하던 중 고통스러운 점을 발견했습니다. 문서 하나를 인제스션하는 데 거의 50초가 걸렸습니다.
메트릭 (Metrics)을 확인해 보니 CPU는 거의 아무것도 하지 않고 있었습니다. 앱은 연산 제한 (Compute-bound) 상태가 아니었습니다. 단순히 순차적인 HTTP 네트워크 왕복 (Round-trips) 응답을 기다리며 유휴 (Idle) 상태로 머물러 있었을 뿐입니다.
블로킹 (Blocking) 임베딩 모듈을 비동기 (Asynchronous) 워크플로우로 전환함으로써, 인프라 변경 없이 처리 시간이 49.61초에서 1.56초로 단축되었습니다.
벤치마크 설정 (Benchmark Setup)
- 모델 (Model): Amazon Titan Text Embeddings V2 (AWS Bedrock)
- 데이터셋 (Dataset): 단일 문서에서 추출한 33개의 텍스트 청크 (Chunks)
- 리전 (Region):
us-east-1 - 테스트 (Test): 순차적 블로킹 요청 (Sequential blocking requests) vs 병렬 비동기 요청 (Concurrent asynchronous requests)
결과 (Results)
| 방식 | 시간 | 차이 |
|---|---|---|
순차적 (Sequential) (requests 블로킹 루프) | 49.61s | 기준점 (Baseline) |
병렬적 (Concurrent) (aiohttp + asyncio.gather) | 1.56s | 31.8배 더 빠름 |
격차가 왜 이렇게 컸는가
순차적 (Sequential, Blocking I/O)
각 요청은 다음 요청이 시작되기 전에 반드시 완료되어야 합니다.
청크 1 ──> 요청 ──> ⏳ 대기 (~1.5s) ──> 응답
청크 2 ──> 요청 ──> ⏳ 대기 (~1.5s) ──> 응답
청크 3 ──> 요청 ──> ⏳ 대기 (~1.5s) ──> 응답
...
네트워크 지연 시간 (Latency)이 실행 시간의 대부분을 차지하기 때문에, 이벤트 루프 (Event loop)가 전혀 사용되지 않은 채 방치됩니다.
병렬적 (Concurrent, Non-blocking Async)
요청들이 동시에 발송됩니다. 전체 시간은 가장 느린 단일 요청의 지속 시간 수준으로 급감합니다.
청크 1 ──> 요청 ──────────────> 응답
청크 2 ──> 요청 ──────────────> 응답
청크 3 ──> 요청 ──────────────> 응답
...
코드 변경 사항
변경 전: 순차적 실행 (Sequential Execution)
import requests
def generate_embeddings(texts: list[str]) -> list[list[float]]:
...
변경 후: 병렬적 실행 (Concurrent Execution)
import asyncio
import aiohttp
...
참고: 만약 직접적인 HTTP 요청 대신 boto3를 사용하고 있다면, aioboto3를 살펴보거나 asyncio.to_thread()를 사용하여 블로킹 (Blocking) SDK 호출을 오프로드 (Offload)함으로써 유사한 비차단 (Non-blocking) 동작을 얻을 수 있습니다.
확장성 (How It Scales)
| 청크 (Chunks) | 순차적 실행 (Sequential, 추정치) | 병렬적 실행 (Concurrent, 추정치) |
|---|---|---|
| 31 | 49.61s | 1.56s |
| ... |
더 많은 청크를 수집할수록 병렬적 접근 방식의 이점이 커집니다. 30분씩 걸리던 배치 수집 (Batch ingestion) 작업이 이제는 몇 초 만에 완료됩니다.
핵심 요약 (Key Takeaways)
- 먼저 유휴 대기 (Idle waiting) 상태를 확인하세요. 인프라를 확장하거나 복잡한 워커 큐 (Worker queues)를 추가하기 전에, 파이프라인이 단순히 I/O 작업에서 차단 (Blocked)되어 있는 것은 아닌지 확인하십시오.
- 서비스 할당량 (Service quotas)에 유의하세요. Bedrock에는 리전별 요청 속도 제한 (Request-rate limits)이 있습니다. 청크 수가 수천 개로 늘어남에 따라, 제한에 도달하기 전에 병렬성 (Concurrency)을 제한하십시오 (예:
asyncio.Semaphore). - 적은 노력으로 큰 효과를 얻으세요. 단 하나의 파일(
embedder.py)을 변경하는 것만으로도 파이프라인에서 임베딩 (Embedding) 단계가 병목 현상 (Bottleneck)이 되는 것을 제거했습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기