병렬 실행 배경 및 문제점
마이베스트는 약 1,000만 개의 상품 데이터베이스를 구축 중이며, 상품 정보 입력 및 스펙 정보 등록 작업이 대부분 수동으로 이루어져 시간이 매우 오래 걸렸습니다. 이를 개선하기 위해 AI를 활용한 상품 정보 자동 입력 프로젝트가 시작되었고, OpenAI, Gemini, Claude 등 LLM API를 여러 번 호출하는 AI 워크플로우가 구축되었습니다. 그러나 이 워크플로우는 하나의 상품 리서치에 약 2분이 소요되었고, 월 120만 개 상품 처리 목표에 비해 하루 3,500개 처리량으로는 한 달 내 완료가 불가능했습니다. 또한, 관리 화면에서 AI 워크플로우를 버튼으로 실행할 때 사이드킥 스레드 부족으로 작업이 지연되는 문제가 발생했습니다.
커넥션 고갈 문제 분석
초기 병렬 처리를 위해 parallel Gem의 멀티스레드 방식을 사용했을 때 커넥션 풀 고갈 문제가 발생했습니다. 이는 스레드 수에 비해 커넥션 풀 크기가 적절하게 설정되지 않아 checkout_timeout이 초과되었기 때문입니다. 액티브 레코드의 커넥션 풀은 데이터베이스 연결 및 해제의 높은 비용을 줄이기 위해 미리 일정 수의 연결을 확보해 재사용하는 메커니즘입니다. 하나의 스레드는 일반적으로 여러 데이터베이스 커넥션을 생성하지 않으므로, 커넥션 풀 크기는 스레드 수와 일치해야 합니다. (예: 8스레드 -> 풀 사이즈 8)
사이드킥의 경우, 새로운 프로세스를 띄워 concurrency를 10, 커넥션 풀을 10으로 설정했을 때도 고갈이 발생했습니다. 이는 사이드킥 프로세스 자체가 시작 시 하나의 커넥션을 점유하므로, 실제 사용 가능한 스레드 수에 맞춰 커넥션 풀은 concurrency + 1로 설정해야 함을 알게 되었습니다.
안전한 병렬 처리를 위한 접근 방식
1. 파라미터 설정의 비관적 예측
-
RDS Max Connections 산정: 웹 애플리케이션, 배치, 백그라운드(Sidekiq) 서비스의 예상 최대 커넥션 수를 합산하여 RDS의
MAX_CONNECTIONS를 초과하지 않도록 합니다. 배포 시 구 서비스와 신 서비스가 일시적으로 공존하여 ECS 태스크 수가 두 배가 될 수 있으므로,MAX_CONNECTIONS의 절반을 최대 허용치로 간주해야 합니다. (예: RDS 4000 -> 최대 2000). -
I/O 비율 측정 및 스레드 최적화: AI 워크플로우의 I/O 대기 비율이 90% 이상임을 측정하고, 암달의 법칙을 활용하여 최적의 스레드 수를 계산했습니다. Rake 태스크의 경우 8스레드가 가장 효율적이었으며, 이후에는 서버 인스턴스 수를 늘려 병렬 처리를 수행했습니다.
-
Sidekiq
concurrency설정: 사이드킥 공식 권장 사항에 따라concurrency는 50 이하로 설정하고, 그 이상이 필요할 경우 프로세스 수를 늘립니다. 새로운 AI 리서치용 사이드킥 프로세스를 별도로 띄우고concurrency를 50으로 설정했습니다. -
설정 확인:
ActiveRecord::Base.connection_pool.stat메서드를 사용하여 커넥션 풀의 크기 및 현재 연결 수를 모니터링하여 배포 전 설정이 올바른지 확인합니다.
2. 커넥션 고갈 방지 코드 패턴
-
DB 및 I/O 작업 분리: 애플리케이션 코드 내에서 데이터베이스 커넥션이 발생하는 읽기/쓰기 쿼리 및 트랜잭션과 외부 LLM API 통신과 같은 I/O 작업을 명확히 분리합니다.
-
ActiveRecord::Base.connection_pool.with_connection활용: 데이터베이스 커넥션이 필요한 블록에서만 이 래퍼 메서드를 사용하여 커넥션을 안전하게 빌려 쓰고 블록 종료 시 자동으로 반환하도록 합니다. 이는 멀티스레드 환경에서 커넥션 누수를 방지하고 타임아웃 발생 확률을 낮춥니다.
3. 성능 개선 (Performance Tuning)
-
장시간 트랜잭션 주의: 장시간 트랜잭션은 테이블 록,
LockWaitTimeoutError를 유발하고 커넥션을 점유하여 성능 저하를 초래할 수 있습니다. 트랜잭션 범위를 최소화하고, 외부 API 호출과 같은 무거운 작업은 트랜잭션 외부로 이동시키며, 불필요한 테이블 전체 록을 피하고 록의 세분성을 고려합니다. -
성능 프로파일링:
rack-mini-profilerGem을 사용하여 워크플로우 전체의 처리 시간을 측정하고, N+1 쿼리, 비효율적인 쿼리, 트랜잭션 내 이미지 업로드(외부 I/O)와 같은 병목 지점을 찾아 개선했습니다. 예를 들어, S3 이미지 업로드 처리와 레코드 저장 처리를 분리하고 이미지 업로드를 비동기화했습니다.