CS Insights

스플릿 브레인(Split-Brain) 붕괴를 막는 분산 락(Distributed Lock) 시스템과 펜싱(Fencing) 토큰의 도입

스플릿 브레인(Split-Brain) 붕괴를 막는 분산 락(Distributed Lock) 시스템과 펜싱(Fencing) 토큰의 도입
결제 중복 출금이라는 치명적인 CS 인입 메일이 터진 날, 저희 팀의 분산 락(Distributed Lock) 서버 코드가 완벽히 무력화되었음을 인지하고 등골이 서늘해졌습니다. 원인은 서버 시계의 오차도, 네트워크 패킷의 증발도 아닌 JVM의 가비지 컬렉션(GC) 일시 정지 현상이었습니다. 다수의 마이크로서비스 인스턴스가 동일한 사용자 잔액을 변경하려 할 때, 우리는 Redis나 ZooKeeper를 통해 락(Lock)을 임대하여 상호 배제를 적용합니다. 만약 A 서버가 불의의 사고로 다운되어 락을 영원히 놓지 않는 데드락을 방지하기 위해 이 분산 락에는 필연적으로 파기 기한(Lease Time/TTL)이 설정됩니다. 그러나 이 TTL에 의존하는 설계에는 끔찍한 치명타, 이른바 시간 정지 버그가 도사리고 있습니다. A 서버가 성공적으로 락을 획득하여 결제 시작 직전, JVM이 거대한 힙 공간을 청소하며 Stop-the-World 현상에 빠져 서버 프로세스 전체가 10초간 완전히 정지해 버렸습니다. 시간은 흘러 분산 락 서버 내에서 락의 TTL 기한은 만료되어 소멸했고, 이때 B 서버가 새롭게 접속해 다른 변경 트랜잭션을 잡고 무사히 잔고를 다 빼갔습니다. 잠시 후 GC 청소가 끝나 깨어난 A 서버는 락을 여전히 소유하고 있다고 착각한 채 뒤이어 같은 잔고 차감을 날려버린 무시무시한 스플릿 브레인(Split-Brain) 참사가 발발한 것입니다. 이 원천적인 시스템의 닫힌 시계 세계를 방어하기 위해 도입한 개념이 바로 펜싱 토큰(Fencing Token) 기법입니다. 락을 발급하는 ZooKeeper 트랜잭션은 단조 증가하는 절대적인 고유 번호(예: Token #33)를 락에 덧붙여 발급합니다. 쓰기 요청을 받는 백엔드 데이터베이스 엔티티 구조에서, 자신이 마지막으로 반영한 토큰보다 적은 번호(예: 뒤늦게 깬 A서버의 Token #33 시도와 달리 현재 DB의 락인 Token #34가 진행된 상태)의 쓰기는 모두 롤백 및 거부 처리하도록 설계 원칙을 바꾸었습니다. 인프라의 상태 공간은 언제나 무너질 수 있음을 인정하고, 제일 밑단 스토리지 계층에서 펜싱 경호벽을 치는 다중 겹 구조의 데이터 엔지니어링 미학이었습니다.

Related Posts