
0. 들어가며
분산 시스템을 운영하다 보면 여러 서버가 동시에 동일한 자원(데이터베이스, 캐시, 파일 등)에 접근해야 하는 상황이 자주 발생합니다. 이때 동시성 문제를 해결하지 않으면 데이터 불일치나 충돌이 발생할 수 있습니다.
이를 방지하기 위해 우리는 락(Lock)을 사용합니다. 분산 환경에서는 Redis를 이용한 분산락을 사용할 수 있는데요. 이때 주의할 점들이 있습니다. 예를 들어, 하나의 Redis 인스턴스를 이용해 락을 관리한다고 가정해 봅시다. 만약 이 Redis 서버가 다운되면 어떻게 될까요? 현재 락이 유지되고 있는지 확인할 수 없게 되고, 여러 개의 클라이언트가 동시에 동일한 자원에 접근하거나 모두가 접근하지 못할 위험이 생깁니다.
이 문제를 해결하기 위해 Redis 기반의 Redlock 알고리즘이 등장했습니다. Redlock은 다수의 독립적인 Redis 인스턴스를 활용하여 보다 안전한 분산 락을 제공하는 알고리즘입니다. 하지만, Redlock도 완벽한 해결책은 아닙니다. 클럭 드리프트(Clock Drift), 네트워크 지연 등의 문제를 어떻게 해결할 것인지 고민해야하죠.
📌 이 글에서는
- Redis 기반의 락 구현 방식과 그 한계를 살펴보고
- Redlock 알고리즘의 원리를 이해해보며
- 실무에서 발생할 수 있는 문제와 해결책을 알아봅니다.
1. Redis 기반 락의 기본 개념과 단일 인스턴스 방식의 한계
1.1 단일 Redis 인스턴스 기반 락 구현 방식
Redis는 빠른 성능과 단순한 사용법 덕분에 분산락(Distributed Lock)을 구현하는 데 널리 사용됩니다. 기본적인 락을 단일 Redis 인스턴스를 활용해 구현하는 방법은 다음과 같습니다.
SET my_lock <random_value> NX PX 3000
각 옵션의 의미
- SET my_lock <random_value> → 락을 설정할 키와 임의의 랜덤 값을 저장
- NX → 키가 존재하지 않을 때만 값을 설정 (이미 존재하면 무시)
- PX 3000 → TTL(Time To Live)을 3초(3,000ms)로 설정
동작 원리
- 클라이언트가 my_lock이라는 키로 락을 설정하려고 요청
- 만약 my_lock이 존재하지 않으면 락 획득 성공
- TTL(3초)이 지나면 자동으로 락 해제 (클라이언트가 해제하지 않더라도)
이 방법은 매우 간단하고 Redis의 빠른 응답 속도를 활용할 수 있는 장점이 있습니다. 하지만 단일 인스턴스 방식에는 치명적인 단점이 존재합니다.
1.2 단일 인스턴스 방식의 한계
1) 단일 장애점 (Single Point of Failure, SPOF)
- Redis 서버가 다운되면 기존에 설정된 락이 유효한지 확인할 방법이 없습니다.
- 여러 클라이언트가 동일한 자원에 동시에 접근할 수 있어 데이터 충돌 발생
2) 네트워크 장애로 인해 락이 손실될 가능성
- 네트워크 단절로 인해 Redis가 정상적인 TTL(락 만료 시간) 관리를 하지 못할 수 있음
- 락이 의도한 시간보다 빨리 해제되거나 유지됨
3) 클라이언트 장애 시 락이 해제되지 않을 가능성
- 클라이언트가 락을 해제하기 전에 예기치 않게 종료될 경우
- TTL이 만료되기 전까지 다른 클라이언트가 락을 획득할 수 없음 (데드락 위험)
4) 마스터-슬레이브 복제 문제
- Redis는 마스터-슬레이브 복제를 지원하지만, 데이터 동기화가 지연될 수 있음
- 락이 슬레이브 노드에서 존재하지 않는 것으로 인식될 가능성
1.3 결론 - 단일 Redis 락은 안전하지 않다!
단순한 Redis 락 방식은 작은 규모에서는 유용할 수 있지만, 좀 더 신뢰할 수 있는 신뢰할 수 있는 락 시스템이 필요할 수 있습니다. 이를 해결하기 위한 방법이 바로 Redlock 알고리즘입니다.
2. Redlock 알고리즘 개요 및 동작 원리
Redlock 알고리즘은 Redis의 단일 인스턴스 락 방식이 가진 한계를 극복하기 위해 고안된 분산 락 메커니즘입니다. 단일 Redis 인스턴스를 사용하는 방식은 간단하지만, 장애 발생 시 락이 정상적으로 유지되지 않을 수 있다는 치명적인 문제가 있습니다.
이를 해결하기 위해 Redlock은 여러 개의 독립적인 Redis 인스턴스를 활용하여 과반수(Majority) 이상의 인스턴스에서 락을 획득해야만 유효하다고 판단하는 방식을 사용합니다.
이 알고리즘은 기본적으로 랜덤 값(Random Value), TTL(Time To Live), 과반수 개념을 중심으로 동작합니다.
- 랜덤 값: 각 락 요청마다 고유한 랜덤 값을 생성하여, 락을 획득한 클라이언트만 해당 락을 해제할 수 있도록 합니다.
- TTL: 락이 일정 시간이 지나면 자동으로 해제되도록 설정하여 데드락을 방지합니다. 예를 들어 TTL을 3초로 설정하면, 클라이언트가 락을 해제하지 못하더라도 3초 후에 자동으로 만료돼요.
- 과반수 원칙: 5개의 Redis 인스턴스 중 3개 이상에서 락을 획득해야 유효하다고 판단합니다.
이러한 방식을 사용하면 하나의 Redis 서버가 다운되더라도 다른 서버에서 락의 상태를 유지할 수 있기 때문에 보다 견고한 분산 락 시스템을 구축할 수 있습니다.
2.1 락 획득 과정
Redlock 알고리즘에서 클라이언트가 락을 획득하는 과정은 다음과 같은 단계로 진행됩니다.
1. 현재 시간 기록
클라이언트는 락 요청을 시작하기 전에 자신의 로컬 시계로 현재 시간을 밀리초 단위로 기록합니다. 이 값은 이후 락의 유효 시간을 계산하는 데 사용됩니다.
2. 각 Redis 인스턴스에 락 요청
클라이언트는 사전에 생성한 고유한 랜덤 값을 사용하여, 다섯 개의 Redis 인스턴스에 락을 요청합니다. 락을 요청하는 방식은 다음과 같습니다.
SET my_lock <random_value> NX PX 3000
여기서 TTL은 3초(3,000ms)이며, NX 옵션을 통해 이미 동일한 키가 존재하면 설정하지 않도록 제한합니다.
각 Redis 인스턴스는 이 요청에 대해 다음 두 가지 응답을 반환할 수 있습니다.
- 성공: 해당 인스턴스가 락을 설정하는 데 성공함
- 실패: 이미 존재하는 락이 있거나, 기타 네트워크 지연 등의 이유로 요청이 실패함
중요한 점은 각 요청이 일정한 타임아웃을 가지도록 설정해야 한다는 것이에요. 특정 Redis 인스턴스의 응답이 너무 느리다면, 클라이언트는 해당 인스턴스에서의 응답을 기다리지 않고 다음 단계를 진행해야 합니다.
3. 과반수 획득 여부 판단
모든 Redis 인스턴스에 락 요청을 보낸 후, 성공적으로 락을 획득한 인스턴스의 수를 확인합니다. 일반적으로 다섯 개의 인스턴스 중 3개 이상에서 락을 성공적으로 획득해야 락이 유효한 것으로 판단합니다.
또한, 전체 락 획득에 걸린 시간이 TTL보다 짧아야 합니다. 만약 시간이 너무 오래 걸리면, 락의 유효성이 보장되지 않기 때문에 이미 획득한 락을 해제하고 재시도하는 것이 바람직합니다.
4. 락 유효 시간 계산
락을 획득하는 과정에서 걸린 시간과 예상되는 클럭 드리프트(서버 간 시계 차이)를 고려하여 실제로 사용할 수 있는 락의 유효 시간을 계산해야 합니다.
예를 들어 TTL이 3초이고, 락을 획득하는 데 50밀리초가 소요되었으며, 서버 간 클럭 차이를 20밀리초로 예상한다고 가정하면, 안전한 유효 시간은 다음과 같이 계산됩니다.
TTL - 락 획득 시간 - 예상 클럭 드리프트 = 3000ms - 50ms - 20ms = 2930ms
이렇게 하면 락이 예상보다 일찍 해제되는 문제를 방지할 수 있습니다.
5. 실패 시 정리 작업
과반수 이상의 인스턴스에서 락을 획득하지 못하거나, 계산된 유효 시간이 너무 짧다면 클라이언트는 이미 획득한 락을 해제하고 다시 시도해야 합니다. 이를 통해 이후에 다른 클라이언트가 해당 락을 정상적으로 획득할 수 있도록 합니다.
2.2 락 해제 과정
락을 해제할 때는 안전한 방식으로 처리해야 합니다. 단순히 DEL my_lock 명령어를 실행하는 것은 위험합니다. 다른 클라이언트가 동일한 키를 설정한 경우, 실수로 다른 클라이언트의 락을 해제하는 문제가 발생할 수 있기 때문입니다.
이를 방지하기 위해, Redlock 알고리즘은 클라이언트가 설정한 랜덤 값을 검증한 후 락을 해제하는 방식을 사용합니다. 이를 Lua 스크립트를 활용해 구현하면 다음과 같습니다.
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
이 스크립트는 현재 저장된 값이 클라이언트가 생성한 랜덤 값과 동일한 경우에만 락을 삭제합니다. 이를 통해 다른 클라이언트의 락을 실수로 해제하는 문제를 방지할 수 있습니다.
Redlock 알고리즘은 단일 Redis 인스턴스를 활용한 락 방식보다 훨씬 안정적이지만, 몇 가지 문제점을 가지고 있습니다. 클럭 드리프트(Clock Drift)와 네트워크 지연으로 인해 락이 예상보다 오래 지속되거나 빨리 해제될 수 있다는 점이 대표적입니다.
다음 섹션에서는 이러한 문제를 어떻게 해결할 수 있는지, 그리고 **펜싱 토큰(Fencing Token)**을 활용하여 Redlock을 보다 신뢰성 있게 운영하는 방법을 살펴보겠습니다.
3. Redlock 알고리즘의 한계와 보완 방법
Redlock 알고리즘은 단일 Redis 인스턴스를 활용한 락 방식보다 훨씬 안정적이지만, 몇 가지 근본적인 한계를 가지고 있습니다. 특히 클럭 드리프트(Clock Drift), 네트워크 지연, 예상치 못한 장애 상황에서 락이 안전하게 유지되지 않을 가능성이 존재합니다.
이 섹션에서는 이러한 문제점들을 분석하고, 보다 신뢰성 높은 락 시스템을 구축하기 위한 해결책을 살펴보겠습니다.
3.1 클럭 드리프트(Clock Drift) 문제
클럭 드리프트란?
분산 환경에서 각 서버의 시계는 완벽하게 동기화되지 않습니다. 서버 간의 시간 차이가 발생하는 현상을 클럭 드리프트라고 합니다.
Redlock 알고리즘은 클라이언트가 여러 Redis 인스턴스에 락을 요청하는 방식으로 동작하는데, 만약 각 Redis 인스턴스가 서로 다른 시스템 시간을 가지고 있다면, 락의 만료 시점이 다르게 판단될 수 있습니다.
예를 들어, 클라이언트가 3개의 Redis 인스턴스에서 락을 획득했다고 가정합니다. 그러나 이들 중 하나의 서버 시계가 100ms 정도 뒤처져 있다면, 해당 서버에서는 락이 아직 유효하다고 판단하는 반면, 다른 서버에서는 이미 만료되었을 수 있습니다. 이로 인해 일부 클라이언트가 만료된 락을 여전히 사용할 수 있는 문제가 발생합니다.
클럭 드리프트 문제 해결 방법
TTL 보정
실제 TTL = 설정된 TTL - 락 획득 시간 - 예상 클럭 드리프트
락 획득 시 걸린 시간과 예상되는 최대 클럭 드리프트 값을 TTL에서 감산하여 실질적인 유효 시간을 조정합니다.
NTP(Network Time Protocol) 활용
Redis 인스턴스를 운영하는 서버들의 시스템 시간을 최대한 동기화하기 위해 NTP(Network Time Protocol)를 사용합니다.
락 만료 후에도 일정 시간 동안 재사용 방지
락이 만료되었더라도, 일정 시간 동안 같은 클라이언트가 락을 재사용하지 못하도록 추가적인 메커니즘을 도입할 수 있습니다.
3.2 펜싱 토큰(Fencing Token)의 필요성
펜싱 토큰이란?
펜싱 토큰은 락이 재획득될 때마다 증가하는 숫자 값입니다. 이 숫자는 락을 획득한 클라이언트가 다른 클라이언트보다 최신 상태임을 보장하는 역할을 합니다.
왜 필요한가?
Redlock은 TTL을 기반으로 자동으로 락을 해제하지만, 모종의 이유로 인해 락이 만료된 후에도 클라이언트가 여전히 락을 가지고 있다고 착각할 가능성이 있습니다.
예를 들어, 클라이언트 A가 락을 획득한 후 GC로 인한 Stop The World(STW)가 발생했다고 가정합니다. STW 시간 동안 TTL이 지나고 다른 클라이언트 B가 락을 획득했지만, A는 여전히 자신이 락을 가지고 있다고 생각할 수 있습니다. 이 경우 A와 B가 동시에 동일한 자원에 접근하는 문제가 발생할 수 있습니다.
펜싱 토큰을 활용하면 낮은 값의 토큰을 가진 요청을 거부할 수 있기 때문에 이런 문제를 방지할 수 있습니다.
펜싱 토큰 적용 방법
- 락을 획득할 때마다 펜싱 토큰 값을 증가시킵니다.
- 클라이언트는 락을 사용할 때 현재 락의 펜싱 토큰을 함께 전달합니다.
- 자원을 보호하는 시스템(DB, 파일 시스템 등)은 이전보다 작은 값의 토큰을 가진 요청을 거부합니다.
이 방식은 클럭 드리프트로 인해 발생할 수 있는 동기화 문제를 해결하는 데 매우 효과적입니다.
4. 결론
Redlock 알고리즘은 단일 Redis 인스턴스를 활용한 락 방식보다 훨씬 안정적인 분산 락 시스템을 제공합니다. 하지만 클럭 드리프트, 네트워크 지연 등의 문제로 인해 완벽한 해결책은 아닙니다.
이러한 한계를 극복하기 위해 펜싱 토큰을 활용한 안전한 락 검증, NTP 기반의 시간 동기화, 적절한 타임아웃 및 백오프 전략을 적용해야 합니다.
분산 환경에서의 락은 단순해 보이지만, 세부적인 요소들을 신경 쓰지 않으면 데이터 정합성 문제로 이어질 수 있습니다. Redlock을 사용할 때는 그 한계를 명확히 이해하고, 보완책을 함께 적용하는 것이 중요합니다.
'Dev.' 카테고리의 다른 글
[Dev.] SPF, DKIM, DMARC: 이메일 보안을 위한 개념 정리 (0) | 2025.03.02 |
---|---|
[Git] Merge 전략에 대하여. (1) | 2022.12.27 |