이전 글에서 Write-Ahead-Log, Leader and Follower, Version Control, Majority Quorum, Generation Clock에 대해 언급 했다. 이번 글에서는 그 이후의 것을 알아본다.
이 글은 “Patterns of Distributed Systems” (Unmesh Joshi)의 책을 기초로 쓰였다.
Write-Ahead-Log(WAL): 모든 단위 작업을 디스크에 기록한다. 디스크에 기록이 완료된 작업만이 완료(Commit) 처리 될 수 있다. 시스템에 장애가 발생해도 Log를 Replay하여 복구 할 수 있다.
Leader and Follower: 작업을 실제 처리·승인해 주는 객체와 Leader에 의해 확정된 데이터를 복사 받는 객체를 나눠서 생각한다.
Version Control: 모든 저장된 값(상태)에 대해 버전을 매긴다. eventually consistency 상황에서는 순간적으로 여러 버전이 공존할 수 있다.
Majority Quorum: “정족수 과반” 정도로 번역된다. 구성요소의 과반수 이상이 승인해야 값이 최종 승인된다. (구성하기에 따라 구성요소의 과반수 이상에 값을 복제해야~로 할 수도 있다.)
일부 구성 요소와 연결이 끊겨도 남은 구성 요소들이 상태를 유지할 수 있게 하기 위해 필요하다.
Generation Clock
복잡한 장애 상황을 고려해 보자. 여기서는 편의를 위해 단일 Leader인 상황을 가정한다. 그리고 현재 Leader는 A이다.
A, B, C 라는 서버가 있다. A 서버가 최초에 Leader였다. A가 [id123=567]
이라는 수정 요청을 받았다. 슬프게도, 이 값을 B, C에 뿌리기 전에 A가 죽어버렸다. 이제 B가 Leader로 선출됐다. B에서도 [id123=888]
이라는 값이 저장됐다. B도 정보를 뿌리기 전에 죽어버렸다.
몇 분 후에 A와 B 모두 살아났다. 복구 과정속에서 각자 Leader일 때 commit된 정보를 Follow에게 뿌리게 된다. 이때, [id123=567]
와 [id123=888]
라는 상태가 공존하게 된다. B가 먼저 살아나서 [id123=888]
이 적용 됐으나, A가 살아남에 따라 이전 상태인 [id123=567]
로 데이터가 씌어질 수도 있다.
이 문제를 해결하기 위해서는 “Leader가 바뀔 때 마다” “세대(Generation) 번호“를 같이 기록해야 한다. 최초 A가 Leader일 때는 0세대이다. 그리고 B가 Leader로 변하자 1세대 라고 기록한다. 추후 복구 과정속에서 데이터가 겹친다면(Conflict) “더 높은 세대의 데이터“를 선택한다.
사실 정석적인 방법은 Majority Quorum에서 허가 받아야만 Commit(승인)되고, 그렇지 않으면 아예 트랜잭션이 취소되게 하는 것이다.
여기서는 Majority Quorum에 승인 받지 못한 트랜잭션을 Uncommitted Transaction이라고 하고, 시스템은 최대한 Uncommitted Transaction을 복구하려는 상황이다.
Generation Clock의 추가 효과
Generation Clock을 가지는 것은 또 다른 문제점을 해결할 수도 있다. A 서버가 죽은게 아니라 일시적으로 네트워크가 끊겼을 수도 있다. 이때, A 스스로는 Leader가 바뀌었다는것을 알아채지 못 할 수도 있다.
Generation Clock이 있으면 세대가 바뀌었음(=Leader가 바뀌었음)을 알아채고 스스로 Follower Mode로 전환할 수 있다.
Majority Quorum에서 데이터를 허가해야만 승인(commit)하는 시스템에서도 Generation Clock이 분산 시스템의 잠재적인 문제를 해결 할 수 있다.
High-Water Mark
High-Water Mark는 “최고점” 정도로 번역되는듯 하다. 그러나 분산 시스템에서는 “최대 어느 지점까지 처리했는지” 추적하는데 쓰인다. High-Water Mark를 통해 모든 구성 요소들이 어디까지 처리됐는지를 알 수 있다. 아래 예시를 통해서 단어의 느낌을 알아보자.
이전 단락에서 Majority Quorum(정족수 과반)에 의해 허가(accept) 받아야지 데이터가 승인(commit) 될 수 있게 하는 것이 정석이라 언급했다. 이 방법을 사용할 때도 고려할 것들이 있다.
기존에는 3개의 서버만 존재하고, 1개의 Leader만 있는 상황을 고려했다. 하지만 Majority Quorum에서는 이것이 단순한 문제가 아니게 된다. 전체 의사회(Quorum)의 반 이상이 승인(Accept)해야 하기 때문이다. A, B, C 3개 서버중, 본인을 포함해서 최소 2개 서버에서 승인을 받아야 된다.
아직까지는 1개가 Leader인 상태이다. Leader가 Commit 하기 전에 위원회(Quorum)에 허가를 받아야 한다.
예시 시나리오
편의를 위해 A, B, C 전체가 의사회(Quorum)이라고 하자. A가 [id333=666]
이라는 요청을 받았다. 그리고 B와 C에 [id333=666]
쓰기를 요청했다. A는 당연히 “동의”를 한 셈이다. B와 C중 한 곳에서 “허가”를 해 주면 된다.
B가 먼저 “허가”를 해 줬다고 하자. 그러면 과반 이상의 정족수가(Majority Quorum) 허가한 상태가 되어, 자동으로 “승인” 상태가 된다. 승인이 되면 이 상태를 각 구성원에 알려야 한다.
Quorum의 구성요소에게 “처리가 어디까지 됐는지” (처리된 가장 마지막 지점) 알림
각 구성원에게 어디까지 됐는지 알려주기 위해서는 요청에 번호를 붙여야만 한다. 다시 처음으로 돌아가서 요청에 번호를 붙여보자.
A는 14: [id333=666]
처럼 번호를 붙여서 B, C에게 데이터를 보낸다. B, C중 하나가 허가를 내주면 A는 최고점(High-Water Mark)=14
라고 기록하고 전파한다. High-Water Mark를 통해서 어디까지 처리가 됐는지를 각 서버들에게 알릴 수 있게 된다.
순서대로 번호를 붙여서 예시를 보자.
- A가
id555=1111]
,[id333=666]
,[id123=444]
라는 요청을 수신 - A가 들어온 순서대로
13: [id555=1111]
,14: [id333=666]
,15: [id123=444]
로 번호 붙이고, B와 C에게 허가 요청을 보냄 13번
을 C가 허가(응답)해줌.- (정족수가 넘었으므로, 작업이 어디까지 됐는지 기록하는
High-Water Mark = 13
으로 수정) 13번
을 B도 허가해 줌- (이미 High-Water Mark가 13이므로 별다른 처리 없음 = 이미 13번 까지는 처리됐다고 알고 있음)
High-Water Mark = 13
이라는 상태를 각 구성요소에게 전파- ~~~~~ 계속해서 작업 진행 ~~~~~
14번도 마찬가지이다. 14번은 B가 먼저 응답한다. B가 응답하자 마자 현재 처리의 최고점=14
라는 상태를 가진다. C가 뒤늦게 14번 허가함
을 보내도 이미 최고점이기 때문에 추가적인 반응이 없다.
High-Water Mark 전파는 실시간일 필요는 없음
한쪽에서 먼저 허가해주면 최고점이 올라간다.
여기서는 B가 허가하자 마자 High-Water Mark가 올라간다. 이후 C가 허가해 줘도 High-Water Mark는 오르지 않는다.
최대값에 따라서만 작동한다는 점에서 최고점 (High-Water Mark) 전략이라고 할 수 있다.
Commit 처리의 순서: 사용자가 요청 생성 → 각 구성요소에 요청 보냄 → 정족수 만큼의 허가를 받음 → 처리 최고점 갱신 → Commit 실행 → 각 구성요소에 High-Water mark 전파 → 사용자에게 요청 완료 알림
High-Water Mark를 알리는 신호는 시간 단위로 (100ms 마다, 1초마다 등등) 보내도 되고, 매 Accept마다 보낼 수도 있다. 책에서는 서버가 정상 작동중인지 확인하는 Heart Beat에 포함할 것을 언급한다.
최고점을 전파하는 방식이기에, 매번 번호를 보낼 필요는 없다. 한번에 “n번까지 승인했어요~”라고 알릴 수 있다.
High-Water Mark를 전파하기 전에 Leader가 죽음
15번 요청을 B나 C에게서 요청을 허가받고, High-Water Mark를 올린 직후에 네트워크가 끊긴 상황을 가정해 보자. A는 아직 최고점=15
를 B나 C에게 전파하지 못했다. 하지만 A 내부적으로는 15번 요청을 B와 C에게 허가를 받았기에 Commit 처리 된 상태이다.
B, C 입장에서는 Leader인 A와 연결이 끊겼다고 판단하고, 자기들끼리 처리를 이어 나간다. B가 다음 Leader가 됐고, 복구의 책임을 가졌다고 하자. B는 B, C의 Uncommitted 로그를 전부 확인한다. 그러면 15번 작업이 Quorum에 의해 정족수 이상의 승인을 받았음을 확인 할 수 있다. 이것을 통해 데이터를 최고점=15
라는 정보를 확보 할 수 있다.
5개가 구성요소 일 때, 3개가 정족수이다. 망이 분리됐을때 과반인 쪽에 반드시 1개 이상의 데이터가 있다. (정족수 만큼 데이터를 저장하기 때문) 이 데이터를 이용해서 복구를 할 수 있는 것이 Majority Quorum 기법이다.
(다른 이야기) 추후 A가 살아난다면 A에 들어왔던 요청이 복구 될 수도 있음.
A에서만 요청을 받고 Quorum에 보내진 않은 데이터가 있을 수 있다. A가 살아나면 WAL에는 있지만 Quorum에서 허가(Accept)되진 않은 요청을 새로운 Leader에게 보낼 수 있다. 이때 Leader가 Uncommitted Transaction으로 보고, 충돌(conflict)이 없다면 복구 과정을 진행 할 수 있다. 만약 충돌이 있다면 Generation Clock에 의해 “더 늦게 쓰여진 데이터”가 남게 된다.
Idempotent Operation (멱등성 처리)
멱등성 처리는 일반 DBMS에서도 중요하다. 멱등성을 가진다는 말은 같은 작업을 수차례 반복해도 동일한 값을 가진다는 의미이다. 예를 들어, 첫번째 코드는 멱등성을 가지지만 두번째는 그렇지 않다. 첫번째 줄은 100번을 실행해도 id333에 10이 저장된다. 그러나 두번째 코드는 100번 실행하면 2000이라는 숫자가 저장될 것이다. 한번 더 저장하면 2020이 될 것이다.
[id333] = 10
[id333] = [id333] + 20
MySQL에서는 멱등성을 위해 Binary Log에 Statement 대신 ROW 타입 선택을 권장한다.
멱등성이 보장되지 않는 Statement 타입 선택시, 복구 과정에서 저장된 값이 달라질 수 있다.
분산 처리를 하다보면 “재시도”가 발생할 수 있다. 이유야 다양할 것이다. Dead Lock이 감지되어 rollback 후 재시도를 할 수도 있다. 잠시 서버가 죽은 후 복구되며 WAL이 replay될 수도 있다. 작업이 재시도 될 수 있다면 반드시 멱등성을 가져야 한다.
작업 결과 응답을 위해서도 필요한 개념
조금 복잡한 시나리오를 생각해 보자. 여기서도 A, B, C 라는 구성요소 3개가 등장한다. 마찬가지로 A가 Leader이다.
ESM이라는 사람이 A에게 “10이라는 데이터를 미국에 보내줘” 라고 요청한다. 슬프게도 요청 중간에 A가 죽어버렸다. ESM은 처리 결과를 알지 못한다. 하지만 이 시스템은 발송 여부를 반드시 알려줘야 한다. 발송 여부를 알 수 없으면 안된다는 제약이 있다.
필자가 생각하기에 ‘이렇게 까지 해야하나’ 싶긴 하다. 그러나, 상황에 따라 필요한 기능일 수 있음은 분명하다.
이 문제를 해결하기 위해서 각 요청에 ID를 발급하고 알려준다. 이제 ESM은 A에 REQ-10: 10이라는 데이터를 미국에 전송
이라고 보낸다. A는 결과 정보를 저장해 둔다. 결과 목록에 REQ-10
을 항목을 만들고, 그곳에 “성공” 또는 “실패”를 저장한다.
만약 ESM이 요청을 보내고 TCP-ACK를 받기 전에 네트워크가 손상됐다고 하자. 네트워크가 복구되자 마자 ESM은 이 요청의 결과를 알고 싶어한다. 그러면 REQ-10: 10이라는 데이터를 미국에 전송
이라는 요청을 한번 더 보내면 된다. 이미 정상 접수된 항목이라면 결과 목록의 결과가 즉시 응답될 것이다. 접수가 안됐다면 처리가 이루어 질 것이다.
TCP에서 데이터를 보냈는데 ACK가 오기 전에 네트워크가 끊길 수도 있다. 이 경우, 요청은 정상 접수 된다.
정확한 순서: A에 Unique Request ID 요청 → 실제 요청에 Request ID 포함 → 응답을 받는데 실패하면 다른 노드에 Request ID로 상태 조회
결과 정보를 공유하면 다른 구성요소에게도 결과를 물을 수 있음
결과 정보를 여러 서버에 복제하면 효과가 더 크다. Quorum에 요청을 보낼때 요청 ID도 같이 보낸다고 하자. Leader인 A가 정족수 과반의 허가를 받았다. 처리를 다 하고 나서 Quorum에 결과를 공유하자 마자 서버가 죽었다. 그렇다면 ESM은 옆의 구성요소에 REQ-10
의 작업 결과를 물어보면 된다.
허가를 받자마자 A가 죽어버렸다 해도 된다. B가 다음 Leader가 되어 복구를 담당한다고 하자. B는 Quorum의 데이터를 모아서 Uncommitted Transaction을 복구한다. (멱등성을 통해) 로그를 Replay를 하고, 이 결과를 Quorum에 공유하면 ESM은 다른 구성요소에서라도 결과를 알아 낼 수 있다. REQ-10
이 어디에서도 발견되지 않았다면 짜피 Accept 되지 않은 요청이므로 실행하면 된다.
작업 자체가 멱등성을 가지지 않았다면 Replay로 인해 문제가 생길 수 있다.
여기서는 [유저-처리 서버] 구간 내에서 응답 결과 반환을 보장받는데 의의가 있다.
특히 Load Balancer 같은 것이 전면에 있다면 사용자는 뒷동네의 상황을 알 필요 없어진다. 클라이언트 입장에서는 [600초 내에 응답이 없으면 Retry를 한다~] 와 같이 처리하면 된다.
Lamport Clock
분산 시스템에서는 “일의 순서”가 중요하다. 1개의 서버에서만 동작한다면 쉽게 문제를 해결 할 수 있다. 모든 요청마다 번호를 붙이면 된다. 그러면 Total Order가 가능하다.
Total Order = 전 순서 = 모든 항목(원소)는 다른 원소와 비교를 할 수 있다.
그러나 여러 개의 서버가 등장하면 순서 보장이 어려워 진다. 현재 시간으로 동기화 하는 것은 불가능 하다. 모든 서버가 완전히 정확한 시간을 동기화 하는 것은 이상 세계에서나 가능하기 때문이다. 10ms라도 차이나면 일의 순서가 바뀔 수 있다.
Lamport Clock은 가상의 논리적 시계로서, 시스템 시간과 상관 없이 독립적인 순서 보장을 가능하게 한다.
Happened-before 문제라고도 한다. 실제 서버는 수십 초에서 수 분의 시간 차이가 발생하기도 한다.
그냥 시간 값을 사용 할 수 없음을 보여주는 예시
선착순 1명을 뽑는 프로그램이 있다. 이 시스템은 요청이 들어온 순서가 매우, 엄격하게 중요하다. 부하 분산을 위해 n개의 분산 서버를 뒀다. A → B 순서로 신호가 들어왔다. 하지만 A의 요청을 받은 서버가 표준 시간보다 2ms 더 느렸다. 이 경우, 시간 값으로만 처리하면 B가 1등으로 착각 할 수 있다.
Version Control에도 이 문제를 고려해야 한다. A, B, C 라는 서버가 있다고 하자. A는 1분 빠르고 (현재 시각 = 05:29) B는 1분 늦다 (현재 시각 = 05:31). 다행히 C는 정상이다. C에서 05:30에 [id111=333]
, [id555=777]
요청이 들어와서 처리하고 A와 B에 뿌렸다. A와 B도 비슷한 시간에 요청이 들어왔다. 이때, 어느것이 최신 값인지를 구별 할 수 있어야 한다. (최신 값 만이 시스템에 남는다.)
A가 50초 뒤에 요청을 받았음에도 (A - 05:29:50) [id111=333]
이 (C - 05:30:00) [id111=333]
보다 늦었다고 판정되어 최신 값이 아니게 됐다. B는 30초 이전에 요청을 받았음에도 (C - 05:30:00) [id555=777]
가 아닌 (B - 05:30:30) [id555=777]
가 최신값이 됐다. 그러므로, 단순히 시간 값으로 “이것이 최신 값이다” 를 보장할 수 없다.
Lamport Clock의 구조
각 구성 요소는 각자의 카운터를 가진다. 그리고 단위 작업이 있을 때 마다 각자의 숫자를 +1
한다. 이 숫자는 다른 서버에 요청을 보낼 때 같이 보낸다. 그러면 해당 숫자를 받은쪽에서 본인의 카운터와 상대방에게서 받은 숫자
중 더 큰 숫자 + 1
Counter에 저장한다
구성 요소간 통신을 할 수도 있지만, 사용자가 통신하면서도 사용할 수 있다. 아까 예시를 다시 보자. [id111=333]
, [id555=777]
상태에서 ESM이 A: [id111=333]
→ B: [id555=777]
요청을 했다. 이때, 단순히 시간 값을 사용하면 순서가 엉킨다. 하지만 사용자를 통해서 A의 Lamport Clock을 B에게 넘긴다면 최소한 B는 A보다 큰 clock을 가지게 된다. (순서가 유지된다)
Lamport Clock에 대해서는 https://great-park.tistory.com/90 에서 가장 잘 정리한 것 같다.
my_clock = max(상대방_숫자, my_clock) + 1
어떻게 보면 High-Water Mark의 약한 제약 버전 느낌이다.
그러나 Lamport Clock가 제대로 동작하기 위해서는 “연관된 작업”이어야 한다. 만약 A와 B가 계속 독립적으로 운영된다면 각자의 숫자를 가지게 될 것이다. 그리고 그 동안에는 숫자가 더 크다고 해서 “최신 상태”이다는 보장을 할 수 없다. A가 요청을 더 많이 받았다면 A의 Lamport Clock이 당연히 클 것이다. 그러므로 연관된 작업 순서 사이에서만 순서가 보장된다. Lamport Clock을 이용한 버전 값 자체는 Partial Order이다.
연관된 작업이 a → b라면 L(a) < L(b) 이다.
그러나 연관된 작업이 아니라면 보장이 안되며, L(a) < L(b) 일 때도 a → b를 보장할 수도 없다.
Lamport Clock은 연관되지 않은 작업끼리는 순서 보장 불가
잠시 여러 서버에 샤딩된 DB를 생각해 보자. 규칙없이 여러 서버에 데이터가 나뉘어져 있다면 최신 값이 무엇인지 알 수 없다. 각 서버마다 별개의 Clock을 가지므로 순서를 매길 수 없기 때문이다. 이 경우 담당 서버 = ROW_ID % 3
와 같이 특정 데이터의 처리 요소는 한 곳로 제한해야 한다. 그렇다면 최신 값
은 당연히 그 서버에 마지막으로 저장된 값 일 것이다.
Hybrid Clock
Lamport Clock은 순서를 알려주기에는 좋다. 그러나 처리 했을때의 시간을 알 수는 없다. 일반 사용자는 실제 처리된 시간을 알기 원한다. 또한, 조금은 틀리더라도 전체적인 순서 보장이 필요하다. Timestamp가 들어가면 조금의 오차는 있더라도 전체적인 순서를 알 수 있다. 그러므로 서버의 Timestamp를 섞어 쓰는 방법이 필요하다. 이게 Hybrid Clock이다.
Hybrid Clock = (Server Timestamp, Lamport Clock)
이다. 각 구성요소간 메세지를 보낼때 Hybrid Clock을 포함해서 보낸다. 메세지를 수신 하는 서버는 Hybrid Clock = (Max(메세지 내의 Timestamp, 내 서버의 Timestamp), Max(메세지 내의 Lamport Clock, 내 서버의 Lamport Clock) + 1)
을 사용한다. 시간 값이 있으므로 “동일한 시간 값의 Lamport Clock”을 사용하면 된다.:
A가 B에게 (2024-12-08 06:50
, 0)을 보낸다. 만약 B의 시간이 저것보다 이전이라면 서버 시간이 조정된다.
분이 바뀔 때 마다 Lamport Clock 항은 0으로 초기화 하면 된다. 그러면 Lamport Clock 항은 max(현재 시각의 Lamport Clock, 메세지 상의 Lamport Clock) + 1
이 된다.
시간 차이는 계속해서 저장되진 않는다. 대신, Timestamp의 최대값을 저장한다. “+5분을 해야한다” 처럼 보정 값을 가지는 대신에 “현재 알려진 최대의 Timestamp”를 저장하고, max(시스템 Timestamp, 알려진 최대 Timestamp)
를 사용한다. 덕분에 서버 시간이 미래일 경우 그냥 과거로 돌리면 된다. 그러면 서버의 Timestamp가 아까의 미래 시간이 될 때 까지 알려진 최대 Timestamp
가 쓰일 것이며, Lamport Clock의 숫자가 늘 것이다.
만약 시간 차이를 계속 저장된다면 순간 오류로 1시간의 시간 차이가 발생 했을때 해당 시스템은 계속해서 1시간 차이를 가지게 될 것이다.
Clock-Bound Wait
Hybrid Time과 같이 시간 기반으로 순서를 처리하다 보면 마찬가지로 순서가 맞지 않을 수 있다. 또한, 저장된 데이터의 시간 값이 현재 서버의 시간보다 미래일 수도 있다. 로직에 의해서 미래의 값은 예약 같은 효과로 데이터가 보이지 않을 수 있다. 트랜잭션을 위해 멀티버전을 사용해야 할 경우 read 메서드가 read(Key, 시간(Clock)값)
모양일 수도 있다. 이 경우, 미래의 시간 값이 저장되어 있으면 사용자는 계속해서 과거값만 보게 된다.
이때 서버간 시간 차이가 (clock skew) 보장되면 문제를 파훼할 수 있다. 각 서버간의 시간 차이가 최대 5초라고 하자. 그러면 요청을 처리하고 Commit 할 때 5초를 기다리면 된다. 그러면 “미래의 값”이 보이진 않는다.
시간이 가장 느린 서버가 15초이고, 가장 빠른곳이 20초라고 하자. 20초인 서버에서 “20초”가 찍힌 데이터를 저장하려 한다. 저장시 무조건 5초를 기다리므로 가장 빠른 곳은 25초이고 가장 느린곳도 20초이다. 그러므로 최소한 미래의 시간이 보이진 않는다.
쓰기 처리량 향상을 위해 Read시 대기를 할 수도 있음
그러나, 이 경우 “최대 시간 차이”만큼 기다려야 한다는 문제가 생긴다. 20초에 Update요청을 넣었어도 25초 까지는 적용되지 않는다. 다른 사용자들이 22초, 24초에 읽기 시도를 해도 과거 값이 보인다. 또한, 쓰기 작업도 대기가 발생한다. 그로 인해 쓰기 처리량도 줄어든다.
때문에, 일반적으로 200ms ~ 500ms의 시간 차이를 가정하고 분산 프로그램을 만든다고 한다. 이 숫자는 여러 데이터센터 속의 서버 시간을 비교했을 때, 주기적으로 NTP 시간 동기화를 하는 서버들 간의 최대 시간 차이라고 한다.
구글은 TrueTime 이라는 자체적인 시간 API를 만들어서 대부분 1ms의 시간 차이를 유지하고, 최악일 경우에도 7ms만 차이나게 유지한다.
아마존 또한 Clock Bound라는 라이브러리와 AWS Time Sync 서비스를 통해 시간 차이를 최소화 한다고 한다.
그럼에도 불구하고 매 쓰기 요청마다 200ms를 기다리는 것은 성능상 좋지 못하다. 어짜피 CockroachDB, YugabyteDB와 같은 상용 분산 DB는 Multi-Version 구조로 인해 시간 값이 필요하다. 그래서 쓰기시 200ms를 대기하는 대신에, 읽기 시에 read(key, 현재시간 + 200ms(~ 500ms))
를 요청한다고 한다.
만약 미래의 시간이 수신된다면, Client 측에서 해당 시간차이 만큼을 대기하여 Clock을 일치 시킨다. 미래의 값이 존재한다면 논리적 오류가 발생할 수 있기 때문이다. Client에서 절대 미래의 값을 다루진 않기 위해, Client에서 sleep(해당 레코드의 최신 시간 - 현재 시간)
을 한 뒤에 다시 읽기 시도를 한다고 한다.
읽은 데이터가 03초의 것인데, 쓰기시 timestamp = 02초
로 저장한다면.. 논리적 순서가 깨진다. 그래서 잠재적인 논리 오류 방지를 위해 Clock 미래의 것은 건들지 않는다.
여기 까지…
여기까지 데이터를 적절하게 저장·처리 하기 위해 필요한 것을 다뤘다. 서버 장애 발생후 복구하는 동안 충돌(Conflict)이 나지 않게 Generation을 도입했다. Majority Quorum을 통해 Leader가 죽어도 Uncommitted Transaction을 복구할 수 있게 했다. Majority Quorum을 쓰기 위해 어디까지 처리했는지 전파하도록 High-Water Mark를 가져왔다.
Leader가 처리중에 죽어도 지속적인 처리 및 결과 반환이 가능하도록 멱등성을 가져왔고, Multi-Version 처리 및 다양한 곳에 활용 할 수 있는 Clock 개념(Lamport, Hybrid) 을 들고왔다. Clock을 적용하며 발생할 수 있는 시간차에 대해 잠시 다뤘다.
앞으로 남은건 데이터 파티셔닝(샤딩)과 코어 시스템 관리 영역이다. 파티셔닝시 데이터 정합성을 위한 Two-phase commit과 gossip Protocol 같은게 남은 셈이다.
답글 남기기