무시된 데드락: Rails, Sidekiq, PostgreSQL의 충돌
카테고리
프로그래밍/소프트웨어 개발
서브카테고리
DevOps
대상자
- 대상자: Rails, Sidekiq, PostgreSQL를 사용하는 백엔드 개발자
- 난이도: 중급 이상 (데이터베이스 데드락, 트랜잭션, 동시성 문제 이해 필요)
핵심 요약
- advisory_lock은 트랜잭션 내부에서 사용 시 데드락 발생 가능 →
with_advisory_lock
을 트랜잭션 외부로 이동 - PostgreSQL의
log_lock_waits = on
설정을 통해 데드락 감지 → 로그 미활성화는 최대 실수 - Idempotent한 작업 설계가 필수 → 재시도 시 작업 중복 방지
섹션별 세부 요약
1. 문제 현상
- Sidekiq 워커가 트랜잭션 중간에 멈춤 (에러/타임아웃 없음)
- PostgreSQL 로그에서 데드락 기록 없음 (
log_lock_waits
비활성화) - 간단한 쿼리 (예:
UPDATE orders SET status = 'processed'
)로 인해 데드락 발생
2. 데드락 원인 분석
- Job A가
with_advisory_lock
로Order #123
잠금 - Job B가 동일 트랜잭션 내에서
Order #123
업데이트 시도 - Job A가 다른 테이블에 쿼리 시 Job B가 이미 잠금 → 데드락 발생
- PostgreSQL은
advisory_lock
을 감지하지 않음
3. 해결 방법
- 쿼리 모니터링:
pg_stat_activity
로Lock
이벤트 확인 - 구성 변경:
postgresql.conf
에log_lock_waits = on
,deadlock_timeout = 1s
활성화 - 코드 리팩토링:
```ruby
# 잘못된 예
Order.transaction do
with_advisory_lock("order_#{id}") do
end
end
# 올바른 예
with_advisory_lock("order_#{id}") do
Order.transaction do
end
end
```
4. 최적화 전략
- 비상요건 작업 (예: 로깅)은 트랜잭션 외부로 이동
- 메가-job 분할: idempotent한 마이크로-job으로 분리
- 동일한 행 경쟁 시:
```ruby
Order.lock("FOR UPDATE SKIP LOCKED").where(status: "pending").first
Order.transaction do
Order.lock!("FOR UPDATE NOWAIT") # 잠금 시 즉시 실패
rescue ActiveRecord::LockWaitTimeout
retry_later
end
```
결론
advisory_lock
사용 시 트랜잭션 외부로 이동- PostgreSQL의
log_lock_waits
설정 미활성화는 최대 실수 → 프로덕션에서 반드시 활성화 - idempotency를 고려한 작업 설계로 재시도 시 중복 처리 방지
- 로컬 환경에서 동시성 테스트:
```ruby
- times.map { Thread.new { Job.perform_now } }.each(&:join)
```