- Redis 기반 Redlock은 장애 허용 분산 락을 목표로 하지만, 정확성이 걸린 작업에는 안전성이 부족하고 효율 최적화 용도로는 지나치게 복잡함
- 분산 락은 중복 작업을 줄이는 효율성 목적과 공유 상태를 보호하는 정확성 목적을 먼저 구분해야 하며, 실패 시 비용 증가인지 데이터 손상인지가 판단 기준임
- 완벽한 락 서비스가 있어도 긴 GC 정지, 프로세스 일시 중단, 네트워크 지연 때문에 lease 만료 뒤 오래된 쓰기가 실행될 수 있어 fencing token이 필요함
- Redlock은 락 획득마다 단조 증가하는 토큰을 만들 수 없고, Redis 키 만료가
gettimeofday기반 시스템 시계에 의존해 시계 점프나 지연 상황에서 안전성이 깨질 수 있음 - 정확성이 필요한 락에는 ZooKeeper 같은 합의 시스템과 fencing token 검사를 쓰고, Redis 단일 노드 락은 근사적·비핵심 용도로 제한해야 함
Redlock을 검토한 출발점
- Redlock은 Redis 위에서 장애 허용 분산 락, 더 정확히는 lease를 구현하는 알고리듬임
- 이미 10개가 넘는 독립 구현이 존재하고 누가 이 알고리듬에 의존하는지 알 수 없어 공개 검토할 가치가 있음
- Redis 자체는 서버 간 일시적이고 근사적이며 빠르게 변하는 데이터를 공유하는 용도에 잘 맞음
- 예: IP 주소별 요청 카운터, 사용자 ID별 고유 IP 집합
- 우려되는 지점은 Redis가 더 강한 일관성과 내구성을 기대하는 데이터 관리 영역으로 점차 쓰이는 흐름이며, 분산 락도 그런 영역 중 하나임
락의 목적: 효율성인가 정확성인가
- 분산 애플리케이션에서 락은 여러 노드가 같은 작업을 시도할 때 한 번에 하나만 수행하도록 하는 장치임
- 락 사용 이유는 크게 두 가지로 나뉨
- 효율성: 같은 비싼 계산을 두 번 하지 않기 위한 최적화이며, 실패해도 AWS 비용이 조금 늘거나 같은 이메일 알림이 두 번 가는 정도에 그침
- 정확성: 동시 프로세스가 같은 상태를 망가뜨리지 않게 막기 위한 장치이며, 실패하면 파일 손상, 데이터 손실, 영구 불일치, 잘못된 약물 투여 같은 심각한 문제가 생길 수 있음
- 효율성 목적의 락에는 5개 Redis 서버와 다수결 확인을 사용하는 Redlock의 비용과 복잡성이 불필요함
- 단일 Redis 인스턴스와 필요 시 비동기 복제를 쓰는 편이 더 적합함
- 이 경우 전원 장애나 Redis 노드 문제로 일부 락을 잃을 수 있지만, 비핵심 최적화라면 감수 가능한 실패임
- Redlock은 5개 복제본과 다수결 때문에 정확성이 중요한 락에 맞아 보이지만, 실제로는 그 목적에 부적합함
lease만으로는 리소스를 안전하게 보호할 수 없음
- 분산 시스템의 락은 멀티스레드 애플리케이션의 mutex와 다르며, 노드와 네트워크가 서로 독립적으로 실패할 수 있어 더 복잡함
- 공유 스토리지의 파일을 갱신하는 전형적인 흐름은 락 획득, 파일 읽기, 수정, 다시 쓰기, 락 해제임
- 락은 두 클라이언트가 read-modify-write를 동시에 수행해 업데이트를 잃는 일을 막기 위한 것임
- 클라이언트가 락을 가진 채 오래 멈추면 lease가 만료될 수 있음
- GC가 개입해 클라이언트가 긴 시간 정지할 수 있음
- lease는 크래시한 클라이언트가 락을 영원히 잡지 않도록 하는 좋은 설계지만, 정지 시간이 만료 시간보다 길면 클라이언트는 만료 사실을 모른 채 위험한 쓰기를 실행할 수 있음
- 이 문제는 이론적 사례가 아니며 HBase에도 과거 유사 문제가 있었음
- “stop-the-world” GC 정지는 몇 분 동안 지속된 사례가 있음
- HotSpot JVM의 CMS 같은 “concurrent” GC도 때때로 애플리케이션을 멈춰야 함
- 쓰기 직전에 락 만료 여부를 확인하는 방식으로는 해결되지 않음
- GC는 마지막 확인과 쓰기 작업 사이를 포함해 어떤 지점에서도 실행 중인 스레드를 멈출 수 있음
프로세스 정지와 네트워크 지연은 일반적인 위협 모델임
- 긴 GC 정지가 없는 런타임을 쓰더라도 프로세스는 여러 이유로 멈출 수 있음
- 메모리에 없는 주소를 읽어 page fault가 발생할 수 있음
- 디스크가 EBS라면 변수 읽기가 Amazon 네트워크를 통한 동기 요청으로 바뀔 수 있음
- CPU 경합, 스케줄러 지연, 실수로 보낸
SIGSTOP도 프로세스를 멈출 수 있음
- 네트워크 지연도 같은 문제를 만듦
- 애플리케이션이 쓰기 요청을 보냈지만, 패킷이 지연되어 lease 만료 뒤 스토리지 서버에 도착할 수 있음
- GitHub의 한 장애에서는 네트워크 패킷이 약 90초 지연됨
- Ethernet과 IP 같은 패킷 네트워크는 패킷을 임의로 지연시킬 수 있고 실제로 그런 일이 발생함
- 따라서 잘 관리된 네트워크에서도 타이밍을 가정할 수 없으며, 단순한 lease 기반 코드는 어떤 락 서비스를 쓰더라도 근본적으로 안전하지 않음
fencing token으로 오래된 쓰기를 차단해야 함
- 해결책은 모든 스토리지 쓰기 요청에 fencing token을 포함하는 것임
- fencing token은 클라이언트가 락을 획득할 때마다 증가하는 숫자임
- 예: 클라이언트 1이 token 33으로 lease를 얻은 뒤 오래 멈추고 lease가 만료됨
- 클라이언트 2가 token 34로 새 lease를 얻고 스토리지에 쓰기 요청을 보냄
- 나중에 클라이언트 1이 깨어나 token 33으로 쓰기를 보내면, 스토리지는 이미 더 높은 token 34를 처리했으므로 token 33 요청을 거부함
- 스토리지 서버가 토큰을 적극적으로 검사하고, 토큰 값이 뒤로 간 쓰기를 거부해야 안전함
- 락 서비스가 엄격하게 단조 증가하는 토큰을 생성하면 락을 안전하게 만들 수 있음
- ZooKeeper를 락 서비스로 쓰는 경우
zxid나 znode 버전 번호를 fencing token으로 사용할 수 있음
- ZooKeeper를 락 서비스로 쓰는 경우
- Redlock의 큰 문제는 fencing token 생성 기능이 없음임
- Redlock의 고유한 난수 값은 필요한 단조 증가성을 제공하지 않음
- 단일 Redis 노드의 카운터는 그 노드가 실패할 수 있어 충분하지 않음
- 여러 노드의 카운터는 서로 어긋날 수 있음
- fencing token 생성을 위해서도 합의 알고리듬이 필요할 가능성이 있음
Redlock은 시간 가정에 안전성을 의존함
- 분산 알고리듬에서 실용적인 모델은 비동기 모델과 불신뢰 실패 감지기임
- 프로세스는 임의 길이로 멈출 수 있음
- 패킷은 네트워크에서 임의로 지연될 수 있음
- 시계는 임의로 틀릴 수 있음
- 그래도 알고리듬은 올바른 결정을 해야 함
- 시계는 노드가 다운됐을 때 영원히 기다리지 않기 위한 timeout 생성에만 쓰일 수 있음
- timeout은 정확할 필요가 없으며, 요청이 timeout됐다고 상대 노드가 반드시 다운된 것은 아님
- 네트워크 지연이나 로컬 시계 오류일 수도 있음
- Redis는 키 만료를 결정할 때 monotonic clock이 아니라
gettimeofday를 사용함gettimeofday는 시스템 시간이 불연속적으로 점프할 수 있음- NTP가 시계를 조정하거나 관리자가 수동으로 시간을 바꾸면 Redis 키 만료가 예상보다 훨씬 빠르거나 느려질 수 있음
- 비동기 모델의 알고리듬은 일반적으로 타이밍 가정 없이 안전성을 유지하고, timeout 같은 실패 감지기는 활성에만 영향을 줌
- 타이밍이 엉망이면 성능은 나빠질 수 있지만 잘못된 결정을 내리지는 않아야 함
- Redlock은 이와 다르게 안전성이 여러 타이밍 가정에 의존함
- 모든 Redis 노드가 대략 올바른 시간 동안 키를 유지해야 함
- 네트워크 지연이 만료 시간보다 충분히 작아야 함
- 프로세스 정지가 만료 시간보다 훨씬 짧아야 함
나쁜 타이밍에서 Redlock이 깨지는 사례
- 5개 Redis 노드 A, B, C, D, E와 클라이언트 1, 2가 있을 때, 한 노드의 시계가 앞으로 점프하면 두 클라이언트가 모두 락을 가졌다고 믿을 수 있음
- 클라이언트 1이 A, B, C에서 락을 얻고 네트워크 문제로 D, E에는 닿지 못함
- C의 시계가 앞으로 점프해 락이 만료됨
- 클라이언트 2가 C, D, E에서 락을 얻고 네트워크 문제로 A, B에는 닿지 못함
- 결과적으로 클라이언트 1과 2가 모두 락 보유자라고 판단함
- C가 락을 디스크에 지속화하기 전에 크래시하고 즉시 재시작해도 비슷한 문제가 생길 수 있음
- Redlock 문서는 크래시한 노드의 재시작을 가장 긴 락 TTL 이상 지연하라고 권장함
- 이 재시작 지연도 합리적으로 정확한 시간 측정에 의존하며, 시계가 점프하면 실패할 수 있음
- 클라이언트 프로세스 정지도 Redlock을 깨뜨릴 수 있음
- 클라이언트 1이 A, B, C, D, E에 락을 요청함
- 응답이 이동 중일 때 클라이언트 1이 stop-the-world GC에 들어감
- 모든 Redis 노드의 락이 만료됨
- 클라이언트 2가 A, B, C, D, E에서 락을 얻음
- 클라이언트 1이 GC를 끝내고 커널 네트워크 버퍼에 머물던 성공 응답을 받음
- 두 클라이언트가 모두 락을 보유한다고 믿음
- Redis가 C로 작성되어 GC가 없다는 점은 도움이 되지 않음
- 문제는 클라이언트가 GC 정지를 겪을 수 있는 시스템에서 발생함
- fencing token 같은 방식으로 클라이언트 2가 락을 얻은 뒤 클라이언트 1의 작업을 막아야 안전함
- 긴 네트워크 지연도 프로세스 정지와 같은 효과를 낼 수 있음
- TCP user timeout을 Redis TTL보다 훨씬 짧게 설정하면 지연 패킷이 무시될 가능성이 있지만, 구체적인 TCP 구현을 봐야 확신할 수 있음
- 이 경우에도 다시 시간 측정의 정확성 문제로 돌아감
Redlock이 요구하는 동기 시스템 가정
- Redlock은 다음 속성을 가진 동기 시스템 모델에서만 올바르게 동작함
- 네트워크 지연의 상한이 보장됨
- 프로세스 정지 시간이 제한됨
- 시계 오차가 제한됨
- 동기 모델은 시계가 정확히 동기화된다는 뜻이 아니라, 네트워크 지연·정지·시계 drift에 대해 알려진 고정 상한이 있다는 뜻임
- Redlock은 지연, 정지, drift가 모두 락 TTL에 비해 작다고 가정함
- 타이밍 문제가 TTL만큼 커지면 알고리듬이 실패함
- 일반적인 데이터센터 환경에서는 이런 타이밍 가정이 대부분의 시간에 만족될 수 있으며, 이를 부분 동기 시스템이라고 부름
- 정확성이 락에 의존한다면 “대부분의 시간”은 충분하지 않음
- 타이밍 가정이 깨지는 순간 Redlock은 한 클라이언트의 lease가 만료되기 전에 다른 클라이언트에 lease를 부여하는 등 안전성을 위반할 수 있음
- GitHub의 90초 패킷 지연 사례는 실제 환경에서 동기 시스템 모델을 가정하기 어렵다는 근거임
- Raft, Viewstamped Replication, Zab, Paxos는 부분 동기 시스템 모델 또는 실패 감지기가 있는 비동기 모델을 위해 설계된 합의 알고리듬 범주에 속함
- 이런 알고리듬은 타이밍 가정을 버려야 하며, 분산 시스템의 네트워크·프로세스·시계가 실제보다 신뢰할 만하다고 가정하지 않도록 주의해야 함
결론과 권장 선택지
- Redlock은 효율 최적화용 락에는 불필요하게 무겁고 비싸며, 정확성이 걸린 락에는 충분히 안전하지 않음
- 특히 네트워크 지연과 연산 실행 시간에 상한이 있는 동기 시스템을 사실상 가정하고, 그 가정이 깨지면 안전성을 위반할 수 있음
- 긴 네트워크 지연이나 멈춘 프로세스로부터 시스템을 보호하는 fencing token 생성 기능도 없음
- 최선 노력 기반의 효율성 최적화 락이 필요하다면 Redis의 단일 노드 락 알고리듬을 쓰는 편이 나음
- 조건부 set-if-not-exists로 락을 얻음
- 값이 일치할 때만 원자적으로 삭제해 락을 해제함
- 코드에 락이 근사적이며 가끔 실패할 수 있음을 명확히 문서화해야 함
- 5개 Redis 노드 클러스터를 구성할 필요는 없음
- 정확성이 필요한 락에는 Redlock을 쓰지 말고 ZooKeeper 같은 합의 시스템을 사용해야 함
- 가능하면 락을 구현한 Curator recipes를 사용할 수 있음
- 최소한 합리적인 트랜잭션 보장을 제공하는 PostgreSQL 같은 데이터베이스를 사용할 수 있음
- 락 아래의 모든 리소스 접근에는 fencing token 검사를 강제해야 함
- Redis는 의도한 용도에 맞게 쓰면 유용한 도구이며, 모든 도구에는 한계가 있으므로 그 한계를 알고 계획해야 함
- 2016년 2월 9일 업데이트에서 Redlock 원저자인 Salvatore가 반박 글을 게시했지만, 결론은 유지됨