분산 시스템에서 같은 데이터에 여러 노드가 동시에 쓰면 어떻게 될까?
Amazon Dynamo는 2007년에 발표된 분산 key-value 저장소다. “항상 쓰기 가능(Always Writeable)“이라는 원칙 아래, 장애나 네트워크 파티션 상황에서도 쓰기를 거부하지 않는다. 대신 서로 다른 노드에 서로 다른 값이 써질 수 있고, 나중에 이 충돌을 해결해야 한다.
이 글에서는 Dynamo가 충돌을 감지하는 방법(벡터 클럭)과 해결하는 방법(클라이언트 중재)을 다룬다.
먼저 생각해볼 것
장바구니에 두 명의 사용자가 동시에 아이템을 추가한다고 해보자. 한 명은 사과를, 다른 한 명은 오렌지를 넣는다. 이 두 변경 사항을 어떻게 처리해야 할까?
하나만 남기고 다른 하나를 버려야 할까? 아니면 둘 다 살려야 할까? 그리고 그 판단은 누가 해야 할까?
이것이 분산 시스템에서의 동시 쓰기(concurrent write) 문제다. Dynamo는 이를 벡터 클럭으로 감지하고, 애플리케이션에게 해결을 맡긴다.
벡터 클럭이란
분산 시스템에서 이벤트의 인과 관계(causality)를 추적하는 메커니즘이다.
각 노드가 자신만의 논리적 카운터를 유지하고, 이벤트가 발생할 때마다 카운터를 증가시킨다. 노드가 3개(A, B, C)일 때 벡터 클럭은 [A:0, B:0, C:0] 형태다.
서버는 이 벡터 클럭을 비교해서 판단한다.
- 이게 단순한 연속 업데이트인지 (한 쪽이 다른 쪽의 후속)
- 아니면 병렬 업데이트인지 (동시에 일어난 충돌)
비교 규칙
두 벡터 클럭 A, B가 있을 때:
| 조건 | 결과 | 처리 |
|---|---|---|
| A의 모든 카운터 <= B의 카운터 | A는 과거 버전 | 자동 삭제 가능 |
| 그 외 (어떤 건 A가 크고 어떤 건 B가 큼) | 충돌 | 중재 필요 |
예를 들어보자.
V1: [A:2, B:1, C:0]
V2: [A:2, B:2, C:0]
V1의 모든 카운터가 V2 이하다. V1은 과거 버전이므로 자동으로 정리된다.
V1: [A:2, B:1, C:0]
V2: [A:1, B:0, C:1]
A 카운터는 V1이 크고, C 카운터는 V2가 크다. 어느 쪽이 “나중"인지 판단할 수 없으므로 충돌이다.
Dynamo 장바구니 예제
Dynamo는 데이터를 N개 노드에 복제한다. 예를 들어 N=3이면 키 하나의 데이터가 3개 노드에 저장된다. 이 노드 목록을 선호 목록(Preference List)이라 한다. 평소에는 이 3개 노드가 동기화되지만, 네트워크가 끊기면 각자 다른 값을 가질 수 있다.
네트워크 파티션이 발생한 상황을 보자.
네트워크 파티션 발생:
노드 A, B (연결됨) 노드 C (고립)
초기 장바구니는 비어 있다.
A쪽에서 사용자1: add apple → {apple}
C쪽에서 사용자2: add orange → {orange}
이제 서로 다른 두 버전이 존재한다.
V1 = {apple}
V2 = {orange}
Dynamo에서는 둘 다 저장한다. add 동작은 비즈니스적으로 절대 누락되면 안 되기 때문이다.
이후 읽기를 시도하면 여러 버전이 함께 반환된다.
get(cart#123) →
[
{apple}, context1,
{orange}, context2
]
중재(Reconciliation)
누가 합치는가? 클라이언트(애플리케이션)가 한다.
장바구니라면 합집합(union)이 자연스럽다.
{apple} ∪ {orange} → {apple, orange}
병합한 결과를 다시 put하면 된다. 이때 벡터 클럭은 두 버전을 모두 포함하도록 갱신된다.
신택틱 중재 vs 시맨틱 중재
신택틱 중재 (자동 정리)
인과 관계가 명확한 경우다.
V1: [A:1]
V2: [A:2]
V2가 V1을 포함한다. V1은 자동으로 삭제된다. 애플리케이션이 개입할 필요가 없다.
시맨틱 중재 (의미 기반 병합)
인과 관계가 없는 경우다.
V1: [A:2, C:0]
V2: [A:0, C:1]
비교 불가능하므로 충돌이다. 애플리케이션이 비즈니스 로직에 따라 직접 병합해야 한다.
remove가 위험한 이유
초기: {apple}
A: add banana → {apple, banana}
C: remove apple → {}
네트워크 복구 후 병합하면?
→ {banana} 또는 {apple, banana}?
정답이 명확하지 않다. 논문에서도 “remove된 아이템이 다시 나타날 수 있다"고 언급한다.
Dynamo의 합집합 기반 병합에서는 add는 절대 누락되지 않지만, remove는 되살아날 수 있다. 이것은 “항상 쓰기 가능(Always Writeable)“이라는 설계 철학이 낳은 의도된 트레이드오프다.
W+R > N의 의미
Dynamo에서는 서비스마다 N, R, W 파라미터를 조정할 수 있다.
- N — 데이터를 복제할 노드 수
- W — 쓰기 성공에 필요한 최소 응답 노드 수
- R — 읽기 성공에 필요한 최소 응답 노드 수
이 값들의 관계가 일관성과 가용성의 균형을 결정한다.
R + W > N이면 읽기 노드 집합과 쓰기 노드 집합이 반드시 겹친다.
N=3, W=2, R=2
쓰기 시: 노드 A, B에 저장 (2개)
읽기 시: 노드 B, C에서 읽음 (2개)
→ B가 겹침 → 최신 값을 읽을 수 있음
W=1, R=1로 설정하면 어떻게 될까?
N=3, W=1, R=1
1 + 1 = 2 < 3
→ 읽기와 쓰기 노드가 안 겹칠 수 있음
→ 가용성은 최대 (아무 노드 1개만 살아있어도 OK)
→ 일관성은 최저 (stale 데이터를 읽을 확률이 높음)
일반적으로 (3, 2, 2) 설정을 사용한다. N은 내구성을, W와 R의 조합은 가용성과 일관성 사이의 트레이드오프를 결정한다.
| 설정 | W+R vs N | 일관성 | 가용성 | 용도 |
|---|---|---|---|---|
| N=3, R=2, W=2 | 4 > 3 | 강함 | 보통 | 일반적 사용 |
| N=3, R=1, W=N | 4 > 3 | 강함 | 쓰기 낮음 | 읽기 위주 (상품 카탈로그) |
| N=3, R=1, W=1 | 2 < 3 | 약함 | 최대 | 가용성 극대화 |
참고자료
- Dynamo: Amazon’s Highly Available Key-value Store — 원문 Section 4.4 (Data Versioning)
- Dynamo 한글 번역본 (parksb)
- Vector Clocks - Wikipedia