Search

분산락

Written by
음하
음하
Date published
2024/06/28
Series
4 more properties
Introduction
회사에서 데이터 동시성 이슈가 발생하여 락을 적용하기로 계획했고 자료를 찾아보다가 분산락에 대해 알게되었다. 사내에서 레디스도 운영 중이였기 때문에 낮은 비용으로 적용이 가능하고 스프링 기반 프로젝트에서는 Redisson 이라는 라이브러리를 사용할 수 있기 때문에 구현 또한 어려움이 없었다. 하지만 기본적인 이해와 개념을 파악하기 위해서 분산락에 대한 글을 작성해보았다.

분산락(Distributed Lock)

락 (Lock)

락은 공유 자원에 대한 동시 접근을 제어하는 메커니즘이다. 프로그램에서 동시에 실행되는 여러 작업을 조율하고, 데이터의 일관성을 위해 동시성을 제어해야한다. 이때 락을 사용하면 한 번에 하나의 스레드만 해당 자원에 접근하거나 변경할 수 있다.
락을 획득한다는 것은 자원을 사용해도 된다는 의미이며, 다른 프로세스는 현재 락을 획득한 프로세스가 잠금을 건 자원에 대한 Write 접근을 할 수 없음을 의미한다.
락의 종류는 낙관적 락, 비관적 락, 스핀락, 네임드락 등이 있으면 여기서는 분산락에 대해서만 다뤄본다.

분산락이란?

분산락은 분산 환경에서 여러 대의 서버와 여러 DB 간의 동시성을 관리하는 데 사용되는 메커니즘이다. 분산 시스템에서 여러 노드가 동일한 자원에 접근하려 할 때, 일관성과 순서를 유지하면서 동시성 문제를 해결하는데 쓰인다.

Redis 분산락 필요성

분산 환경에서 하나의 공유 리소스를 서로 다른 클라이언트가 사용하는 경우가 있다. Redis는 기본적으로 싱글 스레드로 동작하기 때문에, 단일 Redis 노드를 구축해 사용해도 동시성 문제가 발생하지 않는다. 그래서 Redis를 이용해 분산 락을 구현하는 경우가 많다.
동시성 관리: 복수의 인스턴스가 동일한 데이터를 동시에 수정하려 할 때, 데이터 불일치를 방지한다.
일관성 유지: 분산 시스템 내에서 데이터의 일관성을 유지하며, 작업의 순서를 보장한다.
데드락 방지: 데드락을 감지하고 해결하는 메커니즘을 제공한다.

Redis SET NX

Redis는 2.6.12 버전 이전에는 SETNX 명령어가 제공되었지만 2.6.12 버전부터 SETNX 명령어는 deprecated하고 SET 명령어에 NX 옵션을 전달하는 방향으로 수정되었다.
SET key value NX PX 30000 # key, value를 저장하는데 not exists인 경우에만 저장하고, 30(30000ms) 동안 유지한다는 의미
Shell
복사
NX 옵션을 전달하면 SET 하려는 키가 없는 경우에만 SET이 성공한다. Redis는 기본적으로 싱글 스레드로 동작하기 때문에 여러 프로세스가 공유 자원에 접근할 때 발생하는 동시성 문제를 이 명령어로 해결할 수 있다.

프로세스

1.
먼저 접근한 스레드가 NX 옵션을 전달한 SET에 성공한다.
2.
다른 스레드들은 대기한다.
여기서 다른 스레드들이 대기하도록 while 문과 같은 루프와 Sleep 같은 함수는 개발자가 직접 제공해야 한다.
3.
처음 Lock을 획득한 스레드는 작업을 끝낸 후 키를 삭제한다.
여기서 단순히 DEL 명령어로 키를 삭제하면 락을 획득하지 않은 다른 클라이언트들도 삭제가 가능하므로 Key가 존재하고 값이 일치한 경우에만 삭제할 수 있도록 아래와 같은 Lua 스크립트를 통해 삭제할 것을 권고한다.
e.g. Lua Script
4.
1~3번을 반복한다.

문제점

여기서 한 가지 문제점은 Redis가 단일 서버로 동작한다면 단일 장애 지점(SPOF, Single Point Of Failure)이 될 수 있다. 이를 보완하기 위해 Master-Slave 복제 구조를 고려하여 Redis 서버를 구축하게 되는데, Redis의 복제는 비동기이기 때문에 아래와 같은 상황에 따라 경쟁 상태(Race Condition)가 발생할 수 있다.
1.
클라이언트 A가 Master에서 Lock을 획득한다.
2.
키에 대한 쓰기가 복제본으로 전송되기 전, Master가 다운된다.
3.
키가 쓰여지지 않은 Slave가 Master로 승격한다.
4.
클라이언트 B가 새로운 Master에서 동일한 키로 Lock을 획득한다.
이러한 문제점을 보완하기 위해 Redi는 RedLock 알고리즘을 제안한다.
경쟁 상태 (Race Condition)

RedLock

Redis가 공식적으로 권장하고 있는 분산락 알고리즘인 RedLock은 경쟁 상황이 발생할때, 하나의 공유 자원 접근할때 데이터에 결함이 발생하지 않도록 원자성(Atomic)을 보장하는 기법이다. Spring에서 Redis-Client는 Redisson 인터페이스를 지원한다.

RedLock에서 보장해야하는 3가지 특성

1.
오직 한 순간에 하나의 작업자만이 락(Lock)을 걸 수 있다.
2.
락 이후, 어떠한 문제로 인해 락을 풀지 못하고, 종료된 경우라도 다른 작업자가 락을 획득할 수 있어야한다.
3.
Redis 노드가 작동하는 한, 모든 작업자는 락을 걸고 해체할 수 있어야한다.

Lock 획득 조건

RedLock은 N개의 단일 Redis 노드들을 이용하여, Quorum 이상의 노드에서 잠금을 획득하면 분산락을 획득한 것으로 판단한다. (= 과반 수 이상의 Redis 노드에서 Lock을 획득했다면 Lock을 획득한 것으로 간주)

알고리즘 절차

1.
현재 시간을 ms 단위로 구한다.
2.
순차적으로 N대의 Redis 서버에 잠금을 요청한다. 이 때 timeout은 Lock의 유효시간 보다 훨씬 작은 시간을 사용한다. 만약 Lock의 유효시간이 10초라면 각 Redis 서버에 잠금을 획득하기 위한 timeout은 5~50ms 이다. 이렇게 짧은 timeout을 사용해 장애가 발생한 Redis 서버와 통신에 많은 시간을 사용하지 않도록 방지 할 수 있다.
3.
Redis 서버가 5대라고 가정하고 과반수(3대) 이상의 서버로부터 Lock을 획득했고 Lock을 획득하기 위해 사용한 시간이 Lock의 유효시간보다 작았다면 Lock을 획득했다고 간주한다. (Lock의 유효시간이 10초인데 Lock을 얻기 위해 11초가 걸렸다면 실패한 걸로 간주)
4.
Lock을 획득 한 후 유효시간은 처음 Lock의 유효시간 - Lock을 얻기 위해 걸린 시간 이다. 예를 들어 Lock의 유효시간이 10초인데 획득에 3초가 걸렸다면 얻은 후부터 7초 뒤에 만료된다.
5.
Lock을 얻지 못했다면 모든 Redis 서버에게 Lock 해제 요청을 보낸다. 예를 들어 5대의 Redis 서버 중 한 대의 서버에게만 Lock 획득을 성공했다고 가정한다. 과반 수 이상의 Lock을 획득하지 못했으므로 Lock 획득에 실패한 것이고 모든 Redis 서버에 Lock 해제 요청을 보낸다.

RedLock 알고리즘의 한계

공식 문서에 업급된 Martin Kleppmann이 분석한 문서에 따르면 RedLock 알고리즘에도 문제가 발생할 수 있다. 문서에서 언급한 것과 같이 GC, 네트워크 지연, Timing 이슈에 따라 RedLock이 깨질 수 있음을 알고 있어야 한다.
Stop-the-World GC
일반적으로 GC는 매우 빠르게 수행되지만 Stop-the-World GC는 드물게 잠금이 만료될 정도로 지속될 수 있다. 아래는 발생할 수 있는 문제를 시각화 한 다이어그램과 절차이다.
1.
클라이언트1이 Master에서 잠금을 획득한다.
2.
클라이언트1에서 Stop-the-World GC로 인한 애플리케이션 코드 중지가 발생하고 그 사이에 잠금이 만료된다.
3.
클라이언트2가 분산락을 획득하고 파일에 데이터를 쓴다.
4.
클라이언트1이 GC가 끝난 후 파일에 데이터를 쓰면서 동시성 문제가 발생한다.
Clock Drift
RedLock 알고리즘은 노드들 간에 동기화된 시계(synchronized clock)는 없지만, 로컬 시간이 거의 동일한 속도로 갱신된다는 가정에 의존한다. 하지만 현실에서는 시간이 정확한 속도로 동작하지 않는 Clock Drift 현상으로 인해 RedLock 알고리즘이 깨질 수 있다.
예를 들어 시스템에 Redis 노드 5개(A, B, C, D, E)와 클라이언트 2개(1, 2)가 있다고 가정한다. 이때 Clock Drift 현상이 발생한다면 알고리즘은 깨질 수 있다.
1.
클라이언트 1이 노드 A, B, C에서 잠금을 획득하지만, 네트워크 문제로 인해 D, E에서는 잠금 획득에 실패한다.
2.
이때 노드 C의 시계가 오작동하여 시간이 앞으로 점프하게 된다. 결과로 클라이언트 1이 설정한 락이 만료된다.
3.
클라이언트 2가 노드 C, D, E에서 잠금을 획득하지만, 네트워크 문제로 인해 A와 B에서는 잠금 획득에 실패한다.
4.
이제 클라이언트 1과 2는 모두 자신의 잠금을 획득했다고 믿게되며 이는 경쟁 상태를 초래하게 된다.
Search