동시성 실험 플랫폼에서 발견한 분산 락의 숨겨진 함정들
Redis 백오프 버그 수정과 비관적 락의 커넥션 블리딩 현상을 실험으로 검증하며 얻은 실무 인사이트
백오프 알고리즘의 미묘한 버그
LockBench 프로젝트에서 Redis 분산 락 성능을 측정하던 중 흥미로운 버그를 발견했다. RedisDistributedLockStrategy의 exponential backoff 구현이 표준과 달랐던 것이다.
// 버그가 있던 코드
long baseDelay = Math.min(maxBackoffMillis, exponential);
long jitter = ThreadLocalRandom.current().nextLong(baseDelay + 1);
long sleepMillis = baseDelay + jitter; // [baseDelay, 2×baseDelay]
// 수정된 코드
long cap = Math.min(maxBackoffMillis, exponential);
long sleepMillis = ThreadLocalRandom.current().nextLong(cap + 1); // [0, cap]
기존 구현은 [baseDelay, 2×baseDelay] 범위에서 대기 시간을 선택했지만, 표준 full-jitter는 [0, cap] 범위를 사용한다. 작은 차이처럼 보이지만 성능에 미치는 영향은 상당했다.
버그 수정 후 Redis 분산 락의 p95 지연시간이 60% 감소했다. PLATFORM 스레드에서 1358ms → 540ms, VIRTUAL 스레드에서 2103ms → 819ms로 대폭 개선됐다. 처리량 역시 2배 이상 향상되어 백오프 알고리즘의 중요성을 다시 한번 깨달았다.
비관적 락이 시스템을 마비시키는 순간
더 흥미로운 실험은 락 블리드(Lock Bleed) 테스트였다. 비관적 락이 DB 커넥션을 장기간 점유할 때, 락과 무관한 읽기 API까지 차단되는지 검증하는 실험이었다.
실험 설정은 단순했다. HikariCP 기본 풀 크기 10개, 200개 동시 스레드가 SELECT FOR UPDATE + 100ms sleep으로 커넥션을 점유하는 동안, 별도의 읽기 요청을 초당 10개씩 보내며 응답 시간을 측정했다.
@Transactional
public OrderResult placeOrder(Long productId, int quantity, int optimisticRetries, long holdMillis) {
boolean updated = stockAccessPort.decreaseWithPessimisticLock(productId, quantity);
if (updated) {
if (holdMillis > 0) {
Thread.sleep(holdMillis); // 커넥션 + 행 락 유지 상태에서 대기
}
return OrderResult.ok();
}
// ...
}
결과는 충격적이었다. PESSIMISTIC_LOCK은 읽기 API의 p95를 30초까지 끌어올리고 20%의 요청을 실패시켰다. 반면 다른 전략들은 모두 15ms 이하의 지연시간과 0% 실패율을 기록했다.
커넥션 풀 고갈의 연쇄 반응
원인 분석 결과 커넥션 풀 고갈로 인한 연쇄 반응이었다. 200개 스레드가 SELECT FOR UPDATE로 순차 직렬화되면서 약 20초간 처리가 지연되고, 그 동안 HikariCP 풀 10개가 모두 소진된다. 무관한 읽기 요청들이 커넥션을 얻지 못해 대기 큐에 쌓이다가 30초 타임아웃에 걸려 실패하는 것이다.
반면 REDIS_DISTRIBUTED_LOCK은 Redis 키만 점유하고 DB 커넥션은 쿼리 후 즉시 반환하므로 읽기 API에 전혀 영향을 주지 않았다. NO_LOCK과 OPTIMISTIC_LOCK도 마찬가지로 비즈니스 로직 처리 시에는 커넥션을 반환한 상태였다.
실험 자동화와 측정의 중요성
k6 멀티 시나리오 테스트로 이런 복잡한 상황을 정량적으로 측정할 수 있었다. tags: { type: "read" } 필터링으로 쓰기 부하 중 읽기 성능을 분리 측정하고, PowerShell 스크립트로 4가지 락 전략을 자동 실행하며 결과를 비교했다.
// k6 읽기 프로브 시나리오
readProbe: {
executor: 'constant-arrival-rate',
rate: READ_RATE, // 초당 10req
timeUnit: '1s',
duration: '90s',
preAllocatedVUs: 20,
startTime: '3s', // 쓰기 실험 시작 3초 후
exec: 'readStock',
}
자동화된 측정 없이는 이런 미묘한 성능 차이나 사이드 이펙트를 놓치기 쉽다. 특히 분산 락처럼 복잡한 동시성 제어에서는 정량적 성능 측정이 필수다.
실무에서 얻은 교훈
첫째, PESSIMISTIC_LOCK은 락 범위를 최소화해야 한다. 외부 API 호출이나 이벤트 발행 같은 비즈니스 로직을 트랜잭션 내에서 실행하면 무관한 읽기 API까지 차단될 수 있다.
둘째, HikariCP 풀 크기를 과소평가하면 안 된다. 기본값 10은 고부하 환경에서 즉시 한계에 부딪힌다. 예상 동시 트랜잭션 수를 고려한 적절한 설정이 필요하다.
셋째, Redis 분산 락의 숨겨진 장점을 발견했다. 단순히 여러 서버 간 동기화뿐만 아니라, DB 커넥션을 조기 반환함으로써 쓰기 부하가 읽기를 차단하는 것을 방지한다.
마지막으로 백오프 알고리즘의 정확한 구현이 성능에 미치는 영향은 생각보다 크다. 표준을 벗어난 구현은 예상치 못한 성능 저하를 일으킬 수 있다.
동시성 제어는 미묘한 영역이다. 이론적 지식만으로는 부족하고, 실제 부하 상황에서의 정량적 측정과 검증이 필수다. LockBench 같은 실험 플랫폼을 통해 다양한 시나리오를 체계적으로 검증하는 것이 안정적인 시스템 구축의 열쇠라고 생각한다.