-
Notifications
You must be signed in to change notification settings - Fork 2
fix: AB테스트 데드락 문제 해결 #2113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: AB테스트 데드락 문제 해결 #2113
Conversation
BaeJinho4028
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사실상 2줄 PR이지만, 많은 고민이 담겨있네요.
잘 봤습니다 🙇
Soundbar91
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
작성해주신 내용 잘 확인했습니다 유익했습니다 !
한 가지 사례 공유 코멘트 남깁니답
최근에 발생한 데드락 에러 로그입니다. 요청이 한 번 왔는데 500 에러가 발생해서 문제 상황과 불일치한 거 같습니다.

의문이 들어서 로그를 볼 수 있나 찾아보던 중, MySQL에서 데드락 로그를 볼 수 있다고해서 가져왔습니다.
동일한 access_history에 대해 서로 다른 device들이 업데이트를 치는 과정에서 데드락이 발생한 것으로 해석했습니다. 해당 로그에서 확인이 되는 device_id 중 하나는 DB 상에 남아있지 않고, device 테이블에 가보면 동시간에 동일한 user_id에 대해서 device가 2개가 생성됨을 확인할 수 있었습니다.
device가 두 개 생기는 것을 막는다면, 락 문제도 해결되지 않을까 ? 라는 생각이 들어서 의견 남겨봅니다..!
틀린 부분이 있다면 피드백 주시면 감사하겠습니다 (__)
|
@Soundbar91
데이터독은 샘플링이 적용되어 성공한 요청에 대해서는 일정 비율만큼만 제공하고, 실패한 요청만 전부 제공한다고 합니다. 실제로 애플리케이션 로그를 확인해보니 동시에 들어온 요청을 확인할 수 있었습니다. 제공해주신 DB 로그에서도 트랜잭션이 2개임을 알 수 있구요. 말씀해주신 시나리오는 적절하고, 실제로 제가 제시한 문제 상황과 일치합니다. 데드락 발생 조건은 다음과 같습니다.(본문과 동일)
맞습니다. (조건 1번)AB테스트 참가 이력이 있는 사용자라면, (조건 4번)동일한 access_history에 2번 이상의 요청이 동시에 수행됩니다.
이것도 맞습니다. 로그를 확인해보면, 트랜잭션 1, 2가 동시에 공유락을 쥔 상태로 배타락 승격을 대기하는데요, 여기서 DB는 교착 상태를 감지하고 트랜잭션 2를 강제 롤백시켰습니다. 그럼 트랜잭션 1만 커밋이 될텐데, 각 트랜잭션이 device를 insert하고 accesshistory를 update하려다 실패한 것이기 때문에, device insert 기록은 남아있습니다. 하지만 실제로 영속화된 device는 트랜잭션 1의 device뿐입니다.(해당 로그에서 확인이 되는 device_id 중 하나는 DB 상에 남아있지 않습니다)
맞습니다. 하지만 device가 2개 생기는 것을 막기 위해서는 결국 원자적 삽입을 위한 동시성 제어 매커니즘이 필요하고, 이는 또다른 락이나 복잡한 로직으로 연결됩니다. 단일 access_history에 대해 여러 요청이 동시에 들어오지 않는 현재 상황에서는 간단하게 access_history에 배타락을 걸어 해결하는 것이 가장 효율적으로 보입니다. |
🔍 개요
매우 간헐적으로 발생하던 AB테스트 데드락 문제의 근본 원인을 해결합니다.
문제 상황
같은 사용자가 동시에 2번 이상 AB테스트 실험군 조회 api를 호출할 경우 간헐적으로 데드락이 발생한다.
발생 조건
위 4가지 조건이 모두 참일 때 발생한다.
근본 원인
여러 스레드가 공유락을 쥔 상태에서 배타락으로 승격하기 위해 서로의 공유락 해제를 대기하는 현상이 발생했다.
원인
문제 발생 시나리오
코드 상의 원인
assignOrGetVariable()메서드에서createDeviceIfNotExists이후accessHistory.addAbtestVariable를 진행한다.createDeviceIfNotExists는 device를 save하고 accessHistory에 연결하며,accessHistory.addAbtestVariable는 accessHistoryAbtestVariable을 save한다.@GeneratedValue(IDENTITY)에 의해 device insert → flush → accessHistoryAbtestVariable insert → flush 가 일어난다.1번
createDeviceIfNotExists에서 device를 insert하면서 device_id를 accessHistory에 적용했는데, 이 update는 jpa sql 쓰기지연 때문에 바로 날아가지 않는다. 그리고 2번 accessHistoryAbtestVariable을 insert할 때 flush가 발생하면서 기존 쓰기지연 sql 저장소에 들어있던 accessHistory UPDATE 쿼리도 함께 날아간다. 그런데 이 때 JPA 내부 최적화에 의해 쿼리 발송 순서가 insert 이후 update로 변경된다.결국 accessHistory에 공유락이 걸린 후 device_id 컬럼을 수정하고자 해서 배타락 취득이 필요해졌고, 동시에 같은 accessHistoryId를 가진 요청이 들어올 경우 서로가 가진 공유락 해제를 기다리다가 데드락이 발생한다.
해결 방안
최종 선택: 명시적 배타락으로 조회하기
왜 명시적 배타락으로 해소했는가?
리소스가 가장 적게 들기 때문. 동일 accessHistoryId로 동시성 요청이 꾸준히 발생하는 환경이라면 배타락이 성능 상 안좋을 수 있다. 하지만 지금 환경은 해당 상황이 거의 연출되지 않기 때문에 큰 문제가 없다고 판단함.
검증용 테스트코드
제가 세운 데드락 발생 가설을 검증하기 위해 테스트를 진행했습니다. 다만 가설 검증일 뿐, 커밋은 불필요한 내용이기에 커밋하지 않고 PR 본문에 명시합니다.
테스트 코드
테스트 결과
테스트: 데드락 재현 (Lock 없음) -> 성공: 1, 데드락/충돌: 1
기존의 API 분리로 인해 데드락 발생 빈도가 줄어든 이유
API가 분리되며 하나의 API에서 수행하는 작업량이 줄어들었다. 비록 데드락이 발생하는 공유락과 배타락을 분리하지는 못했지만, API 작업량이 줄어들면서 트랜잭션의 활성시간도 함께 줄어들어 경합이 발생할 확률이 줄어들었다.
조건 중에 “로그인 후 실험군 조회 api 최초 요청”이 있는데, 이게 시간이 지날수록 발생할 확률을 낮추는 요인이라고 볼 수도 있다.
💬 참고 사항
✅ Checklist (완료 조건)