질문: 서비스 서버 1대, 데이터베이스 1대, 운영: 사용자의 현재 잔액을 조회하고, 현재 잔액의 3%를 처리 수수료로 차감합니다
동기화 자물쇠 DB 락
질문: 서비스 서버 두 대, 데이터베이스 하나, 운영: 사용자의 현재 잔액을 조회하고, 현재 잔액의 3%를 처리 수수료로 차감합니다 분산 잠금
어떤 종류의 분산 잠금이 필요할까요? 분산 애플리케이션 클러스터에서 동일한 메서드가 한 번에 한 머신의 한 스레드에서만 실행될 수 있도록 보장합니다.
이 잠금이 재진입 잠금(교착 상태 방지)이라면,
이 자물쇠는 차단 자물쇠로 사용하는 것이 가장 좋습니다(사업 필요에 따라 이 자물쇠를 선택할지 고려해 보세요)
이 자물쇠는 공정한 자물쇠가 가장 좋습니다(사업 필요에 따라 이 자물쇠를 원할지 말지 고려하세요).
획득 및 해제 잠금 기능이 매우 유용합니다
획득 및 해제 잠금장치의 성능이 더 좋습니다
1. 데이터베이스 기반의 분산 잠금장치
테이블 기반 구현을 기반으로 한 분산 락
메서드를 잠그고 싶을 때는 다음 SQL을 실행하세요: 메서드Lock(method_name,desc) 값 ('method_name', 'desc')에 삽입합니다
method_name에 고유성 제약을 두었기 때문에, 여러 요청이 동시에 데이터베이스에 제출되면 데이터베이스는 단 하나의 연산만 성공하도록 보장하며, 성공적으로 메서드 락을 얻은 스레드가 메서드 본문 내용을 실행할 수 있다고 가정할 수 있습니다.
메서드가 실행될 때, 락을 해제하려면 다음 SQL을 실행해야 합니다: 메서드락에서 삭제 method_name ='method_name'
위의 간단한 구현에는 다음과 같은 문제들이 있습니다:
- 이 잠금은 데이터베이스의 가용성에 따라 달라지며, 데이터베이스가 중단되면 비즈니스 시스템이 사용 불가능해집니다.
- 이 잠금장치는 만료 시간이 없으며, 잠금 해제 작업이 실패하면 잠금장치 레코드가 데이터베이스에 남아 다른 스레드들은 더 이상 잠금을 얻을 수 없습니다.
- 이 잠금은 삽입 실패 시 데이터 삽입 연산이 오류를 직접 보고하기 때문에 차단 금지 기능이 없을 수 있습니다. 락을 획득하지 못한 스레드는 대기열에 들어가지 않으며, 락을 다시 얻기 위해 락 획득 작업을 다시 트리거해야 합니다.
- 자물쇠는 재진입하지 않으며, 같은 스레드는 해제되기 전까지 다시 잠금을 얻을 수 없습니다. 왜냐하면 데이터 속의 데이터는 이미 존재하기 때문입니다.
- 이 자물쇠는 불공정한 자물쇠이며, 자물쇠를 기다리고 있는 모든 실들이 운에 의지해 자물쇠를 놓고 경쟁합니다.
물론, 위 문제들은 다른 방법으로도 해결할 수 있습니다.
- 데이터베이스가 단일 지점인가요? 두 개의 데이터베이스를 구축하면 데이터가 양방향으로 동기화됩니다. 전화를 끊으면 재빨리 백업 라이브러리로 전환하세요.
- 유통기한이 없나요? 정기적으로 데이터베이스의 타임아웃 데이터를 정리하는 예약된 작업을 수행하세요.
- 차단하지 않는 방법? 인서트가 성공할 때까지 일정 시간 루프를 반복한 후 성공을 반환하세요.
- 비재진입자? 데이터베이스 테이블에 현재 잠금이 걸린 기계의 호스트 정보와 스레드 정보를 기록하는 필드를 추가하고, 다음에 잠금이 걸릴 때는 먼저 데이터베이스에 쿼리하세요. 현재 컴퓨터의 호스트 정보와 스레드 정보가 데이터베이스에서 찾을 수 있다면, 직접 잠금을 그에게 할당할 수 있습니다.
- 불공평한가요? 잠금 대기 중인 모든 스레드를 기록하는 또 다른 중간 테이블을 만들고 생성 시간에 따라 정렬하며, 처음 생성된 스레드만 잠금 해제를 받을 수 있습니다
배타적 잠금에 기반한 분산 잠금장치
데이터 테이블에서 레코드를 추가하거나 삭제하는 것 외에도, 분산 잠금은 데이터와 함께 제공되는 잠금의 도움으로 구현할 수 있습니다.
방금 만든 데이터베이스 테이블도 사용합니다. 분산 잠금은 데이터베이스에 대한 독점적 잠금을 통해 구현할 수 있습니다. MySQL 기반 InnoDB 엔진은 락업 연산을 구현하기 위해 다음과 같은 방법을 사용할 수 있습니다:
쿼리 문 후에 업데이트용을 추가하면, 쿼리 과정에서 데이터베이스 테이블에 독점적 락을 추가합니다. 독점 락이 레코드에 추가되면, 다른 스레드들은 더 이상 해당 라인의 레코드에 독점적 락을 추가할 수 없습니다.
독점적 잠금을 얻은 스레드가 분산 잠금을 얻을 수 있고, 잠금이 얻으면 메서드의 비즈니스 로직이 실행되고 다음 방법으로 잠금 해제될 수 있다고 생각할 수 있습니다:
Public Void Unlock(){ Connection.commit(); }
connection.commit(을 통해); 잠금장치를 해제하는 작업.
이 방법은 위에서 언급한 자물쇠를 해제하거나 자물쇠를 막지 못하는 문제를 효과적으로 해결할 수 있습니다.
자물쇠를 막는다고요? for update 문은 성공적으로 실행되면 즉시 반환되며, 성공할 때까지 차단된 상태로 유지됩니다.
잠금 후 서비스가 중단되고, 해제가 안 되나요? 이렇게 하면 서비스가 중단된 후 데이터베이스가 스스로 잠금을 해제합니다.
하지만 데이터베이스의 단일 지점, 재등록, 공정 잠금(Fair locking) 문제를 직접적으로 해결하지는 못합니다.
분산 잠금을 구현하기 위해 데이터베이스를 활용하는 방법을 요약하자면, 하나는 테이블 내 레코드의 존재를 통해 잠금이 있는지 여부를 판단하는 것이고, 다른 하나는 데이터베이스의 배타적 잠금을 통해 분산 잠금을 구현하는 것입니다.
데이터베이스에서 분산 잠금의 장점
데이터베이스의 도움으로 직접 이해하기 쉽습니다.
데이터베이스에서 분산 락 구현의 단점
여러 문제가 생기고, 문제를 해결하는 과정에서 전체 해결책은 점점 더 복잡해질 것입니다.
데이터베이스 운영에는 일정한 오버헤드가 필요하며, 성능 문제도 고려해야 합니다.
2. 캐시를 기반으로 한 분산 잠금
데이터베이스 기반 분산 잠금 솔루션과 비교할 때, 캐시 기반 구현이 성능 면에서 더 우수합니다.
현재 Redis, memcached 등 성숙한 캐싱 제품들이 많이 있습니다. 여기서는 Redis를 예로 들고 캐시를 활용해 분산 잠금을 구현하는 방식을 분석합니다.
인터넷에는 Redis를 기반으로 한 분산 락 구현에 관한 관련 글이 많이 있으며, 주요 구현 방법은 Jedis.setNX 메서드를 사용하는 것입니다.
위 구현에는 몇 가지 문제점도 있습니다:
1. 단일 점 문제.
2. 이 락에는 만료 시간이 없으며, 잠금 해제 작업이 실패하면 잠금 레코드가 항상 redis 상태가 되어 다른 스레드가 더 이상 잠금 장치를 얻을 수 없습니다.
3. 이 잠금은 차단하지 않을 수 있으며, 성공 여부와 상관없이 직접 돌아옵니다.
4. 이 락은 재진입하지 않으며, 스레드가 락을 획득한 후에는 잠금 해제 전에 다시 락을 얻을 수 없습니다. 왜냐하면 사용된 키가 이미 Redis에 존재하기 때문입니다. setNX 연산은 더 이상 실행할 수 없습니다.
5. 이 락은 불공평합니다. 모든 대기 스레드가 동시에 setNX 작업을 시작하고, 운 좋은 스레드가 락을 얻을 수 있습니다.
물론, 해결할 방법도 있습니다.
- 현재 주류 캐싱 서비스는 클러스터 배포를 지원하여 단일 지점 문제를 클러스터링으로 해결합니다.
- 유통기한이 없나요? redis의 setExpire 메서드는 인임 만료 시간을 지원하며, 시간이 지나면 데이터는 자동으로 삭제됩니다.
- 차단하지 않는 방법? 반복적으로 처형되면서도.
- 재입장은 가능하지 않나요? 스레드가 락을 획득한 후에는 현재 호스트 정보와 스레드 정보를 저장하고, 다음에 잠금 해제하기 전에 현재 잠금 소유자인지 확인하세요.
- 불공평한가요? 스레드가 락을 획득하기 전에 모든 대기 스레드를 큐에 넣고, 선입선출 방식으로 락을 획득합니다.
redis 클러스터의 동기화 정책은 시간이 걸리며, NX를 성공적으로 설정한 후 스레드 A가 잠금을 받을 수 있지만, 이 값이 스레드 B가 setNX를 실행하는 서버로 업데이트되지 않아 동시성 문제를 일으킬 수 있습니다.
redis의 저자인 Salvatore Sanfilippo는 단일 노드보다 더 안전하고 신뢰할 수 있는 분산 잠금 관리(DLM)를 구현한 Redlock 알고리즘을 제안했습니다.
레드록 알고리즘은 서로 독립적인 N개의 레디스 노드가 있다고 가정하며, 일반적으로 N=5로 설정되어 있고, 이 N개의 노드는 물리적 독립성을 유지하기 위해 서로 다른 컴퓨터에서 실행됩니다.
알고리즘의 단계는 다음과 같습니다:
1. 클라이언트는 현재 시간을 밀리초 단위로 얻습니다. 2. 클라이언트는 N개의 노드의 락을 얻으려 시도하며, (각 노드는 앞서 언급한 캐시 락과 동일한 방식으로 락을 얻습니다), N개의 노드가 동일한 키와 값을 가진 락을 얻습니다. 클라이언트는 인터페이스 접근 타임아웃을 설정해야 하며, 인터페이스 타임아웃 시간은 잠금 타임아웃보다 훨씬 짧아야 합니다. 예를 들어, 락은 자동으로 해제 시간이 10초이고, 인터페이스 타임아웃은 약 5-50ms로 설정됩니다. 이렇게 하면 redis 노드가 다운된 후 가능한 빨리 타임아웃할 수 있고, 일반적인 잠금 사용 횟수를 줄일 수 있습니다. 3. 클라이언트는 1단계에서 얻은 시간을 현재 시간으로 빼어 락을 얻는 데 걸리는 시간을 계산합니다. 클라이언트가 락의 노드가 3개 이상을 얻고, 락을 획득하는 데 걸리는 시간이 잠금의 타임아웃 시간보다 짧을 때만 분산 락을 얻습니다. 4. 클라이언트가 잠금장치를 획득하는 시간은 설정된 잠금장치 타임아웃 시간에서 3단계에서 계산된 잠금장치를 획득하는 데 걸린 시간을 뺀 값입니다. 5. 클라이언트가 잠금을 얻지 못하면, 모든 잠금장치를 차례로 삭제합니다. Redlock 알고리즘을 사용하면 최대 2개의 노드를 행할 때도 분산 잠금 서비스가 작동할 수 있음을 보장할 수 있어, 이전의 데이터베이스 잠금 및 캐시 잠금에 비해 가용성이 크게 향상됩니다.
하지만 한 분산 전문가가 "분산 잠금을 하는 방법"이라는 글을 작성해 Redlock의 정확성에 의문을 제기했습니다.
전문가는 분산 잠금을 고려할 때 두 가지 측면이 있다고 언급했습니다: 성능과 정확성입니다.
고성능 분산 잠금을 사용하고 올바른 상태가 필요하지 않다면 캐시 잠금만 사용하는 것으로 충분합니다.
신뢰할 수 있는 분산 잠금장치를 사용한다면 엄격한 신뢰성 문제를 고려해야 합니다. 반면 레드락은 그 정확성에 부합하지 않습니다. 왜 안 돼요? 전문가들은 여러 가지 측면을 나열합니다.
요즘 많은 프로그래밍 언어들이 GC 함수를 가진 가상 머신을 사용하는데, Full GC에서는 프로그램이 GC 처리를 중단하고, 때로는 시간이 오래 걸리며, 심지어 프로그램도 몇 분간 지연이 있습니다. 기사에서는 HBase, 때로는 GC가 몇 분 동안 사용되어 임대 타임아웃이 발생할 수 있습니다. 예를 들어, 아래 그림에서 클라이언트 1은 잠금을 받고 공유 자원을 처리하려 하며, 처리 시 잠금 해제가 만료될 때까지 Full GC가 발생합니다. 이렇게 해서 클라이언트 2는 다시 잠금을 얻고 공유 자원 작업을 시작합니다. 클라이언트 2가 처리 중일 때, 클라이언트 1은 전체 GC를 완료하고 공유 자원 처리를 시작하여 두 클라이언트가 공유 자원을 처리하게 됩니다.
전문가들은 아래 그림과 같이 해결책을 제시했습니다. MVCC처럼 보입니다. 토큰을 락에 가져오는 토큰은 버전 개념입니다. 연산 락이 완료될 때마다 토큰이 추가됩니다. 1, 공유 자원을 처리할 때 토큰을 가져오며, 지정된 버전의 토큰만이 공유 자원을 처리할 수 있습니다.
그리고 전문가는 알고리즘이 지역 시간에 의존하고, Redis가 키 만료 처리를 할 때 단조 시계 대신 getTimeOfDay 메서드를 사용해 시간을 산다고 했는데, 이로 인해 시간 부정확성이 발생한다고 했습니다. 예를 들어, 두 클라이언트 1과 클라이언트 2가 5개의 REDIS(A, B, C, D, E)를 가지고 있습니다.
1. 클라이언트 1이 A, B, C로부터 락을 성공적으로 획득하고, D와 E로부터 락 네트워크 타임아웃을 얻습니다. 2. 노드 C의 클럭이 부정확하여 잠금 타임아웃을 유발합니다. 3. 클라이언트 2가 C, D, E로부터 잠금을 성공적으로 획득하고, A와 B로부터 잠금 네트워크 타임아웃을 얻습니다. 4. 이렇게 해서 클라이언트 1과 클라이언트 2 모두 잠금을 얻습니다. 레드록의 부재에 대해 전문가들이 말하는 두 가지 점을 요약하자면:
1. GC 및 기타 시나리오는 언제든지 발생할 수 있으며, 클라이언트가 락을 얻게 하고, 처리 타임아웃이 다른 클라이언트가 락을 얻게 만듭니다. 전문가들은 자기 증가 토큰 사용법도 제시했습니다. 2. 알고리즘은 로컬 시간에 의존하며, 클럭이 부정확하여 두 클라이언트가 동시에 잠금을 받게 됩니다. 따라서 전문가들의 결론은 Redlock이 정상적으로 정상적으로 작동할 수 있는 네트워크 지연, 프로그램 중단, 클럭 오류 범위 내에서만 가능하다는 것이지만, 이 세 가지 시나리오의 경계는 확인되지 않아 전문가들은 Redlock 사용을 권장하지 않는다는 것입니다. 높은 정확성 요구가 필요한 시나리오에서는 전문가들이 Zookeeper를 추천하며, 이는 이후 Zookeeper를 분산 잠금장치로 사용해 논의할 예정입니다.
Redis의 저자의 답변
Redis 저자는 전문가의 글을 보고 블로그를 작성하며 대응했습니다. 저자는 정중하게 전문가에게 감사를 표한 뒤, 전문가의 견해에 동의하지 않는다고 표현했습니다.
REDIS 저자의 토큰 사용에 대한 논의는 잠금(lock) 타임아웃 문제를 해결하는 데 관한 내용입니다:
첫 번째, 분산 잠금장치의 사용은 일반적으로 공유 자원을 제어할 다른 방법이 없기 때문입니다. 전문가들은 토큰을 사용해 공유 자원 처리를 보장하므로 분산 잠금이 필요 없습니다. 포인트 2: 토큰 생성의 경우, 서로 다른 클라이언트가 획득한 토큰의 신뢰성을 보장하기 위해 토큰을 생성하는 서비스는 여전히 분산 락이 필요합니다. 3번째 포인트, 전문가들이 자기 증가 토큰을 말하는 방식에 대해 redis 저자는 전혀 불필요하다고 생각합니다. 각 클라이언트는 고유한 uuid를 토큰으로 생성할 수 있고, 공유 자원을 uuid를 가진 클라이언트만 처리할 수 있는 상태로 설정할 수 있습니다. 즉, 자물쇠를 획득한 클라이언트가 잠금 해제를 할 때까지 다른 클라이언트가 공유 자원을 처리할 수 없게 합니다. 위 그림에서 보듯, 토큰 34의 클라이언트가 쓰기 과정에서 GC를 보내고 잠금이 타임아웃되면, 다른 클라이언트가 토큰 35의 잠금을 받아 다시 쓰기를 시작할 수 있으며, 이로 인해 잠금이 충돌할 수 있습니다. 따라서 토큰의 순서는 공유 자원과 결합할 수 없습니다. 5번째 지점, redis 저자는 대부분의 시나리오에서 분산 잠금이 비트랜잭션 시나리오의 업데이트 문제를 처리하는 데 사용된다고 믿습니다. 작성자는 토큰을 결합해 공유 자원을 처리하기 어려운 상황이 있어, 자원을 잠그고 처리하기 위해 잠금에 의존해야 한다는 점을 말씀드릴 것입니다. 전문가들이 언급하는 또 다른 시계 문제로, Redis 저자들도 설명을 제공합니다. 잠금 획득 시간이 너무 길고 기본 타임아웃 시간을 초과하면 클라이언트는 현재 잠금 장치를 얻을 수 없으며, 전문가들이 제시한 예시는 없습니다.
개인적인 감정
제가 요약한 첫 번째 문제는 클라이언트가 분산 락을 획득한 후, 클라이언트가 처리 중인 타임아웃 후에 잠금이 해제될 수 있다는 점입니다. 이전에 데이터베이스 잠금이 설정한 2분 타임아웃에 대해 이야기할 때, 만약 한 작업이 주문 잠금을 2분 이상 차지한다면, 다른 거래 센터가 이 주문 잠금을 획득하여 두 거래 센터가 동시에 같은 주문을 처리할 수 있게 했습니다. 일반적인 상황에서는 작업이 몇 초 만에 처리되지만, RPC 요청 참여로 설정된 타임아웃이 너무 길거나 작업 내에 여러 개의 타임아웃 요청이 있을 경우 자동 잠금 해제 시간이 초과될 가능성이 큽니다. 자바로 작성하면 중간에 Full GC가 있을 수 있어서, 잠금 타임아웃 후 잠금이 해제되면 클라이언트가 인지할 수 없게 됩니다. 이는 매우 심각한 문제입니다. 이 문제는 락 자체의 문제는 아니라고 생각합니다. 위에서 언급한 어떤 분산 락이든 타임아웃 해제 특성을 가진 한 이런 문제는 발생할 수 있습니다. 잠금 타임아웃 기능을 사용하면 클라이언트가 공유 자원을 계속 처리하는 대신 잠금 타임아웃을 설정하고 그에 따라 행동해야 합니다. 레드록 알고리즘은 클라이언트가 락을 획득한 후 머무를 수 있는 잠금 시간을 반환하며, 클라이언트는 그 시간 이후에 작업을 중단하기 위해 이 시간을 처리해야 합니다.
두 번째 문제는 분산 전문가들이 Redlock을 이해하지 못한다는 점입니다. Redlock의 핵심 기능 중 하나는 잠금 획득 시간이 잠금 해제 기본값으로 전환된 총 시간에서 잠금 획득 시간에서 뺀 시간으로, 클라이언트가 처리하는 데 걸리는 시간은 현지 시간과 관계없이 상대 시간이 됩니다.
이 관점에서 Redlock의 정확성은 충분히 보장할 수 있습니다. Redlock을 신중히 분석한 결과, 노드의 redis와 비교했을 때, Redlock이 제공하는 주요 특징은 더 높은 신뢰성으로, 이는 일부 상황에서 중요한 기능입니다. 하지만 레드록이 신뢰성을 달성하기 위해 너무 많은 돈을 썼다고 생각합니다.
- 첫째, Redlock을 더 신뢰성 있게 만들기 위해 5개의 노드를 배치해야 합니다.
- 그 다음 5개의 노드를 요청해 락을 받아야 하고, Future 메서드를 통해 먼저 5개의 노드에 동시에 요청한 뒤 응답 결과를 함께 얻을 수 있는데, 이는 응답 시간을 단축할 수 있지만 단일 노드 REDIS 락보다 시간이 더 걸립니다.
- 따라서 5개 노드 중 3개 이상을 확보해야 하므로 잠금 충돌이 발생할 수 있습니다. 즉, 모두가 1-2개의 잠금을 획득한 경우, 그 결과 아무도 잠금을 얻을 수 없습니다. 이 문제는 Redis 작성자가 래프트 알고리즘의 핵심을 차용하여 무작위 시점의 충돌을 통해 충돌 시간을 크게 줄일 수 있지만, 특히 잠금을 처음 획득할 때 이 문제를 잘 피하기 어렵기 때문에 잠금 획득에 드는 시간 비용이 증가합니다.
- 5개 노드 중 2개가 다운되면 락의 가용성이 크게 줄어듭니다. 우선 이 두 노드의 결과가 타임아웃될 때까지 기다려야 하며, 노드는 3개뿐이고, 클라이언트는 3개 노드 모두의 락을 확보해야 잠금을 걸어야 합니다. 이 역시 더 어렵습니다.
- 네트워크 파티션이 존재한다면, 클라이언트가 잠금을 절대 얻을 수 없는 상황이 발생할 수 있습니다.
여러 이유를 분석한 결과, Redlock 문제의 가장 중요한 점은 클라이언트가 쓰기의 일관성을 보장해야 하고, 백엔드 5개 노드는 완전히 독립적이며 모든 클라이언트가 이 5개 노드를 운영해야 한다는 점입니다. 5개의 노드 중에 리더가 있다면, 클라이언트가 리더로부터 잠금을 받는 한 리더의 데이터를 동기화할 수 있어 분할, 타임아웃, 충돌 등의 문제가 없습니다. 따라서 분산 잠금의 정확성을 보장하기 위해서는 강한 일관성을 가진 분산 조정 서비스를 사용하는 것이 문제를 더 잘 해결할 수 있다고 생각합니다.
다시 질문이 생깁니다. 만료 기간을 얼마나 설정해야 할까요? 무효화 시간 설정이 너무 짧고, 잠금 장치가 자동으로 해제되어 메서드가 실행되기 전에 동시성 문제가 발생할 수 있습니다. 너무 오래 걸리면, 잠금을 받는 다른 스레드들이 오래 기다려야 할 수도 있습니다.
이 문제는 분산 잠금을 구현하기 위해 데이터베이스를 사용할 때도 존재합니다.
현재 주류 접근법은 각 잠금 획득에 대해 짧은 타임아웃 시간을 설정하고, 타임아웃에 도달할 때마다 스레드를 시작해 잠금 타임아웃 시간을 갱신하는 것입니다. 자물쇠를 해제하는 동시에 이 스레드를 종료하세요. 예를 들어, Redis의 공식 분산 락 구성 요소인 redisson은 이 솔루션을 사용합니다.
분산 락을 구현하기 위해 캐싱을 사용하는 장점 좋은 성과였다.
분산 락을 구현할 때 캐싱을 사용할 때의 단점 실행은 너무 책임감 있고, 고려해야 할 요소가 너무 많습니다.
Zookeeper 구현을 기반으로 한 분산 잠금장치
Zookeeper의 임시 순서 노드를 기반으로 한 분산 잠금장치.
일반적인 아이디어는 각 클라이언트가 메서드를 잠그면, 지정된 노드의 디렉터리에서 zookeeper의 메서드에 대응하는 고유한 즉각적인 순서 노드가 생성된다는 것입니다. 잠금 여부를 결정하는 방법은 간단합니다. 순서가 있는 노드에서 가장 작은 일련번호를 가진 자물쇠만 결정하면 됩니다. 잠금이 해제되면 즉시 노드를 삭제하면 됩니다. 동시에, 해제할 수 없는 서비스 다운타임으로 인한 교착 상태를 피할 수 있습니다.
Zookeeper가 앞서 언급한 문제들을 해결할 수 있을지 한번 봅시다.
- 잠금장치가 풀리지 않나요? Zookeeper를 사용하면 잠금이 해제되지 않는 문제를 효과적으로 해결할 수 있습니다. 잠금을 생성할 때 클라이언트가 ZK에 임시 노드를 생성하고, 클라이언트가 잠금을 획득해 갑자기 중단(세션 연결이 끊어짐)하면 임시 노드가 자동으로 삭제되기 때문입니다. 다른 고객들은 다시 자물쇠를 얻을 수 있습니다.
- 차단하지 않는 자물쇠? 노드가 변경되면 Zookeeper가 클라이언트에게 알림을 보내고, 클라이언트는 자신이 만든 노드가 모든 노드 중 가장 작은 순서수인지 확인할 수 있습니다.
- 재입장이 안 돼? 클라이언트가 노드를 만들 때, 현재 클라이언트의 호스트 정보와 스레드 정보를 직접 노드에 기록하고, 다음에 락을 얻고 싶을 때 현재 가장 작은 노드의 데이터와 비교할 수 있습니다. 정보가 자신의 것과 같다면 직접 잠금을 얻을 수 있고, 다르면 임시 순차 노드를 만들어 대기열에 참여할 수 있습니다.
다시 한 번 질문이 생깁니다. Zookeeper가 클러스터에 배포되어야 한다는 것을 알고 있는데, Redis 클러스터처럼 데이터 동기화 문제가 생길까요?
Zookeeper는 약한 일관성을 보장하는 분산 구성 요소로, 즉 궁극적인 일관성을 보장합니다.
Zookeeper는 쿼럼 기반 프로토콜(Quorum Based Protocol)이라는 데이터 동기화 프로토콜을 사용합니다. Zookeeper 클러스터에 N개의 Zookeeper 서버가 있다면(N은 보통 홀수이며, 3개는 데이터 신뢰성을 충족하고 높은 읽기 및 쓰기 성능을 가지며, 5개는 데이터 신뢰성과 읽기 및 쓰기 성능의 균형이 가장 좋습니다), 사용자의 쓰기 작업이 먼저 N/2 + 1 서버에 동기화된 후 사용자에게 반환되어 사용자가 성공적으로 쓰도록 안내합니다. 쿼럼 기반 프로토콜에 기반한 데이터 동기화 프로토콜은 Zookeeper가 지원할 수 있는 강도의 일관성을 결정합니다.
분산 환경에서는 강한 일관성을 충족하는 데이터 저장이 사실상 존재하지 않으며, 한 노드의 데이터를 업데이트할 때 모든 노드가 동기적으로 업데이트되어야 합니다. 이 동기화 전략은 마스터-슬레이브 동기식 복제 데이터베이스에 나타납니다. 하지만 이 동기화 전략은 쓰기 성능에 너무 큰 영향을 미치며 실제로는 거의 볼 수 없습니다. Zookeeper는 N/2+1개의 노드를 동기적으로 쓰고, N/2개의 노드는 동기적으로 업데이트되지 않기 때문에 Zookeeper는 일관성이 강하지 않습니다.
사용자의 데이터 업데이트 연산은 이후 읽기가 업데이트된 값을 반드시 읽는다는 보장은 없지만, 결국 일관성을 보여준다. 일관성을 희생한다고 해서 데이터의 일관성을 완전히 무시하는 것은 아닙니다. 그렇지 않으면 데이터가 혼란스러워지므로, 시스템 가용성이 아무리 높고, 분포가 아무리 좋아도 아무런 가치가 없습니다. 일관성을 희생한다는 것은 관계형 데이터베이스에서 강한 일관성이 더 이상 필요하지 않다는 뜻이지만, 시스템이 궁극적으로 일관성을 달성할 수 있다면 된다는 뜻입니다.
단일 포인트 질문인가요? Zookeeper를 사용하면 단일 지점 문제를 효과적으로 해결할 수 있습니다. ZK는 클러스터로 배포되며, 클러스터 내 기계의 절반 이상이 살아남으면 외부 세계에 서비스를 제공할 수 있습니다.
공정성 문제인가요? Zookeeper를 사용하면 공정한 잠금 문제를 해결할 수 있습니다. 클라이언트가 ZK에서 생성한 임시 노드는 질서 정돈되어 있고, 잠금 해제 시 ZK는 가장 작은 노드에게 잠금 해제를 알릴 수 있어 공정성을 보장합니다.
다시 한 번 질문이 생깁니다. Zookeeper가 클러스터에 배포되어야 한다는 것을 알고 있는데, Redis 클러스터처럼 데이터 동기화 문제가 생길까요?
Zookeeper는 약한 일관성을 보장하는 분산 구성 요소로, 즉 궁극적인 일관성을 보장합니다.
Zookeeper는 쿼럼 기반 프로토콜(Quorum Based Protocol)이라는 데이터 동기화 프로토콜을 사용합니다. Zookeeper 클러스터에 N개의 Zookeeper 서버가 있다면(N은 보통 홀수이며, 3개는 데이터 신뢰성을 충족하고 높은 읽기 및 쓰기 성능을 가지며, 5개는 데이터 신뢰성과 읽기 및 쓰기 성능의 균형이 가장 좋습니다), 사용자의 쓰기 작업이 먼저 N/2 + 1 서버에 동기화된 후 사용자에게 반환되어 사용자가 성공적으로 쓰도록 안내합니다. 쿼럼 기반 프로토콜에 기반한 데이터 동기화 프로토콜은 Zookeeper가 지원할 수 있는 강도의 일관성을 결정합니다.
분산 환경에서는 강한 일관성을 충족하는 데이터 저장이 사실상 존재하지 않으며, 한 노드의 데이터를 업데이트할 때 모든 노드가 동기적으로 업데이트되어야 합니다. 이 동기화 전략은 마스터-슬레이브 동기식 복제 데이터베이스에 나타납니다. 하지만 이 동기화 전략은 쓰기 성능에 너무 큰 영향을 미치며 실제로는 거의 볼 수 없습니다. Zookeeper는 N/2+1개의 노드를 동기적으로 쓰고, N/2개의 노드는 동기적으로 업데이트되지 않기 때문에 Zookeeper는 일관성이 강하지 않습니다.
사용자의 데이터 업데이트 연산은 이후 읽기가 업데이트된 값을 반드시 읽는다는 보장은 없지만, 결국 일관성을 보여준다. 일관성을 희생한다고 해서 데이터의 일관성을 완전히 무시하는 것은 아닙니다. 그렇지 않으면 데이터가 혼란스러워지므로, 시스템 가용성이 아무리 높고, 분포가 아무리 좋아도 아무런 가치가 없습니다. 일관성을 희생한다는 것은 관계형 데이터베이스에서 강한 일관성이 더 이상 필요하지 않다는 뜻이지만, 시스템이 궁극적으로 일관성을 달성할 수 있다면 된다는 뜻입니다.
Zookeeper가 인과적 일관성을 충족하는지는 클라이언트가 어떻게 프로그래밍되었는지에 달려 있습니다.
인과적 일관성을 만족시키지 않는 관행
- 프로세스 A는 Zookeeper의 /z에 데이터를 쓰고 성공적으로 반환합니다
- 프로세스 A는 프로세스 B가 /z의 데이터를 수정했다고 알립니다
- B는 Zookeeper의 /z 데이터를 읽습니다
- B에 연결된 Zookeeper의 서버가 A의 기록 데이터로 업데이트되지 않았을 수 있으므로, B는 A의 작성 데이터를 읽을 수 없습니다
인과적 일관성을 충족하는 실천
- 프로세스 B는 Zookeeper의 /z 데이터 변경 사항을 듣습니다
- 프로세스 A는 Zookeeper의 /z에 데이터를 쓰고, 성공적으로 반환되기 전에 Zookeeper가 /z에 등록된 리스너를 호출해야 하며, 리더가 데이터 변경 사실을 B에게 알립니다
- 프로세스 B의 이벤트 응답 메서드가 응답한 후, 변경된 데이터를 받으므로 B는 반드시 변경된 값을 얻을 수 있습니다
- 여기서 인과 일관성은 리더와 B 간의 인과적 일관성을 의미하며, 즉 리더가 변화를 데이터에 알리는 것을 의미합니다
두 번째 이벤트 청취 메커니즘은 Zookeeper를 올바르게 프로그래밍하는 방법이기도 하며, 따라서 Zookeeper는 인과적 일관성을 충족해야 합니다
따라서 Zookeeper를 기반으로 분산 잠금을 구현할 때는 인과적 일관성을 만족하는 방식을 사용해야 합니다. 즉, 잠금을 기다리는 스레드는 Zookeeper의 락 변경 사항을 듣고, 잠금이 해제되면 Zookeeper가 공정한 잠금 조건을 충족하는 대기 스레드에 알림을 보냅니다.
Zookeeper 서드파티 라이브러리 클라이언트를 직접 사용할 수 있는데, 이 클라이언트는 재진입 잠금 서비스를 캡슐화합니다.
ZK로 구현된 분산 락은 이 글 초반에 기대했던 분산 잠금에 딱 맞는 것 같습니다. 하지만 실제로는 그렇지 않으며, Zookeeper가 구현한 분산 락은 오히려 단점이 있는데, 즉 성능이 캐싱 서비스만큼 높지 않을 수 있다는 점입니다. 잠금 기능을 구현하기 위해서는 즉각적인 노드가 동적으로 생성되고 파괴되어야 하기 때문입니다. ZK에서 노드를 생성하고 삭제하는 것은 리더 서버를 통해서만 가능하며, 그 데이터는 모든 팔로워 머신과 공유됩니다.
Zookeeper를 사용해 분산 잠금장치를 구현할 때의 장점 단일 지점 문제, 재진입 불가, 차단 방지 문제, 잠금 해제 실패 문제를 효과적으로 해결합니다. 구현은 비교적 간단합니다.
분산 락을 구현하기 위해 Zookeeper를 사용할 때의 단점 성능은 분산 잠금을 구현하기 위해 캐시를 사용하는 것만큼 좋지 않습니다. ZK의 원리에 대한 이해가 필요합니다.
세 가지 선택지의 비교
이해의 용이성(낮은 수준에서 높은 수준까지) 관점에서 데이터베이스 > 캐시 > Zookeeper
구현 복잡성 관점에서 (낮은 것부터 높은 것까지) Zookeeper > 캐시 > 데이터베이스
성능 관점에서 (높은 것부터 낮은 것까지) 캐싱 > Zookeeper >= 데이터베이스
신뢰성 관점에서 (높은 것부터 낮은 것까지) Zookeeper > 캐시 > 데이터베이스
|