무시된 데드락: 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 Awith_advisory_lockOrder #123 잠금
  • Job B가 동일 트랜잭션 내에서 Order #123 업데이트 시도
  • Job A가 다른 테이블에 쿼리 시 Job B가 이미 잠금 → 데드락 발생
  • PostgreSQLadvisory_lock을 감지하지 않음

3. 해결 방법

  • 쿼리 모니터링: pg_stat_activityLock 이벤트 확인
  • 구성 변경: postgresql.conflog_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

  1. times.map { Thread.new { Job.perform_now } }.each(&:join)

```