Skip to content

✍️ 필사 모드: Logical Clocks & Consistency Models 완전 가이드 2025: Lamport, Vector Clock, HLC, Linearizability, Causal Consistency

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며: 시간이 깨진 세계에서 순서를 찾다

분산 시스템의 근본 문제

당신이 세 대의 서버로 분산 채팅 서비스를 운영한다. 사용자 Alice가 "안녕"이라고 보내고, Bob이 "잘 있어"라고 답한다. 세 서버는 이 메시지들을 각자 다른 순서로 받을 수 있다:

Server 1: [Alice "안녕", Bob "잘 있어"]
Server 2: [Bob "잘 있어", Alice "안녕"]
Server 3: [Alice "안녕", Bob "잘 있어"]

누가 봐도 Server 2는 이상하다. "안녕"이 "잘 있어"보다 먼저였으므로. 그런데 어떻게 이걸 컴퓨터가 알 수 있을까?

시계를 쓰면 되지 않을까?

각 서버가 wall clock(물리 시계)으로 타임스탬프를 찍으면 되지 않을까?

안 된다. 분산 시스템에서 시계는 절대 정확히 동기화되지 않는다:

  • Clock Drift: 수정 발진기 정확도는 10^-6 ~ 10^-5 수준. 하루에 수백 ms 편차.
  • NTP 동기화: 보통 ±수 ms ~ 수십 ms 오차.
  • VM 시계 점프: 가상화 환경에서 시계가 갑자기 앞뒤로 튀는 현상.
  • Leap Second: 1초가 삽입/삭제되면서 시계가 뒤로 감.

2012년 Cloudflare는 leap second 때문에 일부 서버가 무한 루프에 빠져 대규모 장애를 겪었다. 물리 시계는 믿을 수 없다.

Leslie Lamport의 통찰

1978년 Leslie Lamport가 발표한 "Time, Clocks, and the Ordering of Events in a Distributed System" 논문은 컴퓨터 과학 역사상 가장 영향력 있는 논문 중 하나다. Lamport는 이렇게 질문했다:

"시계 없이도 '먼저 일어났다'를 정의할 수 있을까?"

답은 "예"였고, 이 아이디어에서 Lamport Clock, Vector Clock, Paxos, Causal Consistency 등 수많은 개념이 탄생했다.

이 글에서는 분산 시스템의 시간과 순서 문제를 세 가지 논리 시계다섯 가지 일관성 모델로 나눠 다룬다.


1. Happens-Before 관계

정의

Lamport가 제안한 happens-before 관계()는 세 가지 규칙으로 정의된다:

  1. 같은 프로세스 내: 이벤트 a가 b보다 먼저 발생하면 a → b.
  2. 메시지 전달: 프로세스 P가 메시지를 보내고(이벤트 a), 다른 프로세스 Q가 그 메시지를 받으면(이벤트 b), a → b.
  3. 이행성: a → b 이고 b → c이면 a → c.

이 관계로부터 정의되지 않는 것은 동시성(concurrent) 이다:

a, b가 서로 happens-before 관계가 아니면 a || b (concurrent).

물리 시계와의 차이

  • 물리 시계: a의 타임스탬프가 b보다 작으면 "먼저"라고 주장. 하지만 부정확.
  • Happens-before: 인과 관계가 있을 때만 "먼저". 인과 관계가 없으면 동시.

인과성 예시

Process P: [send M1]───────────────[receive M3]
Process Q:     [receive M1][send M2]
Process R:                   [receive M2][send M3]
  • send M1 → receive M1 (P → Q)
  • receive M1 → send M2 (Q 내부)
  • send M2 → receive M2 → send M3 → receive M3
  • 결국 send M1 → receive M3 (이행성)

하지만:

  • P에서 send M1 직후 일어난 이벤트 X와 Q에서 receive M1 직전 일어난 이벤트 Y는 concurrent. 어느 것이 "먼저"인지 말할 수 없다.

이 철학적 입장이 분산 시스템의 가장 정직한 시간 정의다.


2. Lamport Clock: 최소한의 논리 시계

알고리즘

각 프로세스 P는 정수 L_P를 유지한다:

  1. 이벤트 발생 시: L_P := L_P + 1
  2. 메시지 송신 시: L_P := L_P + 1, 메시지에 L_P 첨부.
  3. 메시지 수신 시: L_P := max(L_P, 메시지의 timestamp) + 1

구현

class LamportClock:
    def __init__(self):
        self.time = 0

    def local_event(self):
        self.time += 1
        return self.time

    def send_event(self):
        self.time += 1
        return self.time  # 메시지에 첨부

    def receive_event(self, msg_timestamp):
        self.time = max(self.time, msg_timestamp) + 1
        return self.time

# 사용 예시
p = LamportClock()
q = LamportClock()

t1 = p.send_event()    # P time = 1
t2 = q.receive_event(t1)  # Q time = 2
t3 = q.local_event()   # Q time = 3

Lamport Clock의 보장

핵심 성질: a → b 이면 L(a) < L(b).

즉, a가 b보다 happens-before라면, Lamport timestamp도 반드시 더 작다.

중요한 한계

역이 성립하지 않는다: L(a) < L(b) 라고 해서 a → b 인 것은 아니다. 그냥 동시성(concurrent)일 수도 있다.

예시:

P: [a, L=1] [b, L=2]
Q: [c, L=1] [d, L=2]

P와 Q 사이에 메시지가 없으면 a와 c는 동시성이지만 둘 다 L=1이다. 그리고 b(L=2) vs c(L=1)는 Lamport상 b가 커 보이지만 사실은 concurrent다.

결론: Lamport Clock의 쓸모

Lamport Clock은:

  • 전체 순서 부여: 타임스탬프가 같을 때 프로세스 ID로 tie-break하면 모든 이벤트에 고유 전체 순서 가능.
  • 인과 관계 감지는 불가: 두 이벤트가 인과 관계인지 concurrent인지 구분 못함.
  • 용도: 분산 뮤텍스, 순서가 있는 큐 구현.

인과 관계를 알고 싶으면 다음 단계, Vector Clock이 필요하다.


3. Vector Clock: 인과 관계를 정확히 추적

알고리즘

N개 프로세스가 있으면, 각 프로세스는 길이 N의 벡터를 유지한다:

  1. 초기: V_P = [0, 0, ..., 0]
  2. 이벤트 발생 시: V_P[P] := V_P[P] + 1
  3. 메시지 송신 시: V_P[P] := V_P[P] + 1, 메시지에 V_P 첨부.
  4. 메시지 수신 시:
    • V_P[i] := max(V_P[i], 받은 V[i]) (모든 i)
    • V_P[P] := V_P[P] + 1

비교 규칙

두 벡터 V1, V2에 대해:

  • V1 < V2: 모든 i에 대해 V1[i] <= V2[i], 그리고 최소 하나의 i에 대해 V1[i] < V2[i].
  • V1 = V2: 모든 i에 대해 V1[i] = V2[i].
  • V1 || V2 (concurrent): 위 두 경우가 아닐 때.

그리고 핵심 성질:

a → b ⟺ V(a) < V(b)

즉, vector clock은 인과 관계를 완벽하게 표현한다. iff(양방향)이다.

구현

class VectorClock:
    def __init__(self, process_id, num_processes):
        self.id = process_id
        self.vector = [0] * num_processes

    def local_event(self):
        self.vector[self.id] += 1
        return list(self.vector)

    def send_event(self):
        self.vector[self.id] += 1
        return list(self.vector)

    def receive_event(self, msg_vector):
        for i in range(len(self.vector)):
            self.vector[i] = max(self.vector[i], msg_vector[i])
        self.vector[self.id] += 1
        return list(self.vector)

def compare(v1, v2):
    less = all(v1[i] <= v2[i] for i in range(len(v1)))
    greater = all(v1[i] >= v2[i] for i in range(len(v1)))
    if less and greater:
        return "equal"
    elif less:
        return "before"
    elif greater:
        return "after"
    else:
        return "concurrent"

# 사용 예시
p = VectorClock(0, 3)
q = VectorClock(1, 3)
r = VectorClock(2, 3)

va = p.local_event()    # P: [1,0,0]
vb = p.send_event()     # P: [2,0,0]
vc = q.receive_event(vb)  # Q: [2,1,0]
vd = r.local_event()    # R: [0,0,1]

print(compare(va, vc))  # "before" (a → c)
print(compare(va, vd))  # "concurrent"

Vector Clock의 실전: DynamoDB와 Riak

Amazon Dynamo 논문(2007)은 vector clock을 conflict 감지에 사용했다:

  1. 클라이언트가 키를 쓸 때 vector clock 첨부.
  2. 여러 replica에 복제 → 네트워크 파티션으로 분기.
  3. 다른 클라이언트가 두 버전을 읽을 때 vector clock을 비교.
  4. V1 < V2 → V2가 최신. V1은 폐기.
  5. V1 || V2충돌. 애플리케이션이 병합해야 함.

Riak도 이 방식을 계승했다. 실전에선 vector clock의 크기가 문제다: 수십 개 replica면 벡터가 매우 커진다. Dynamo는 pruning으로 오래된 엔트리를 정리한다.

Version Vector

Vector clock과 비슷하지만 미묘하게 다른 Version Vector도 있다. 데이터 아이템별로 유지하며, 복제 간 버전 관계를 추적하는 데 쓰인다. Dynamo, CRDT 등에서 사용.


4. Hybrid Logical Clock (HLC): 이론과 실전의 결혼

동기

Vector clock은 크기가 N(프로세스 수)에 비례한다. 수천 개 노드에서는 부담스럽다. 반대로 wall clock은 작지만 부정확하다.

2014년 Kulkarni 등이 발표한 Hybrid Logical Clock (HLC) 은 이 둘을 절묘하게 결합한다:

"Wall clock을 기반으로 하되, happens-before를 보장하는 논리 시계."

HLC 알고리즘

각 프로세스는 두 필드를 가진 타임스탬프를 관리한다:

  • l (logical component): wall clock의 최대값 저장.
  • c (counter): 같은 l에서 순서 부여.
import time

class HybridLogicalClock:
    def __init__(self):
        self.l = 0
        self.c = 0

    def now(self):
        # 물리 시계 (마이크로초)
        return int(time.time() * 1_000_000)

    def local_event(self):
        pt = self.now()
        l_old = self.l
        self.l = max(l_old, pt)
        if self.l == l_old:
            self.c += 1
        else:
            self.c = 0
        return (self.l, self.c)

    def send_event(self):
        return self.local_event()

    def receive_event(self, msg_l, msg_c):
        pt = self.now()
        l_old = self.l
        self.l = max(l_old, msg_l, pt)
        if self.l == l_old == msg_l:
            self.c = max(self.c, msg_c) + 1
        elif self.l == l_old:
            self.c += 1
        elif self.l == msg_l:
            self.c = msg_c + 1
        else:
            self.c = 0
        return (self.l, self.c)

HLC의 특성

  1. 공간: O(1). 프로세스 수와 무관하게 (l, c) 두 개.
  2. 근사 wall clock: l 필드는 실제 wall clock과 크게 벗어나지 않는다.
  3. Happens-before 보장: a → b 이면 HLC(a) < HLC(b).
  4. 역은 성립 안 함: Lamport clock과 같은 한계.

HLC의 매력

HLC는 **"대략적인 실제 시간"**을 제공하면서도 인과성을 보장한다. 그래서:

  • 로그 정렬이 편하다: 사람이 읽을 때 시간이 그럴듯하게 흐른다.
  • TTL 관리 가능: 실제 시간과 연결되니 expiration 구현 가능.
  • Wall clock이 부정확해도 안전: 그냥 논리 시계로 퇴화할 뿐.

실전: CockroachDB와 MongoDB

CockroachDB는 HLC를 핵심에 둔다:

  • 모든 트랜잭션에 HLC 타임스탬프 부여.
  • Snapshot isolation의 스냅샷 시점으로 사용.
  • 실제 wall clock 오차가 max_offset (기본 500ms) 이하라고 가정.
  • 오차가 이를 초과하면 트랜잭션 재시도 또는 서버 종료.

**MongoDB 3.6+**도 HLC를 쓴다. clusterTime이 바로 HLC다. 다중 문서 트랜잭션과 change stream의 순서 보장에 사용.

YugabyteDB, TiDB 등 대부분의 현대 NewSQL이 HLC를 채택했다. 사실상의 표준이 되었다.


5. 극단의 정밀함: Google Spanner의 TrueTime

왜 TrueTime인가

HLC는 훌륭하지만 외부 일관성(external consistency) 을 완전히 보장하진 않는다. 외부 일관성이란:

"실제 시간상 T2가 T1 이후에 시작되면, 시스템에서도 T2는 T1 이후로 보여야 한다."

HLC는 wall clock을 근사할 뿐 정확하지 않아서, 이 보장을 제공하지 못한다.

Google Spanner는 이를 위해 TrueTime API를 도입했다. 원리:

  1. GPS + 원자시계를 데이터센터마다 설치.
  2. 여러 개를 voting으로 검증.
  3. TrueTime API는 TT.now() 대신 TT.now(): [earliest, latest] 를 반환. 즉, "지금은 이 구간 어딘가"라는 불확실성 구간을 명시.
  4. 보통 구간 크기 ε는 1~7ms.

Commit-wait

Spanner는 트랜잭션 커밋 전에 commit-wait를 수행한다:

1. 트랜잭션 T가 commit timestamp s를 정함.
2. TT.now() 의 latest가 s를 넘을 때까지 대기.
3. 이 시점에서 s는 "진짜로 과거"가 되었음이 보장됨.
4. 이제 commit을 완료하고 클라이언트에 응답.

이 대기 덕분에, 다른 노드들은 s 이후의 타임스탬프로 트랜잭션을 시작하면 확실히 T 이후로 보인다.

대가

TrueTime은 정확도만큼의 지연을 트랜잭션에 추가한다. ε = 7ms이면 평균 3.5ms의 대기. 이는 Google이 하드웨어에 투자할 가치가 있다고 판단한 비용이다.

일반 시스템은?

Google처럼 원자시계를 못 살 뿐이라면, HLC가 가장 현실적인 타협이다. AWS는 Time Sync Service로 NTP를 고정밀 수준(~1ms)으로 제공한다. Amazon은 이를 위해 PTP + 원자시계 조합의 하드웨어를 배포했다.


6. 일관성 모델 개관

지금까지는 "시간과 순서"를 다뤘다. 이제 그 위에서 정의되는 일관성 모델을 살펴보자.

일관성 모델의 계층

강함  ───────────────────────────────────  약함
┌────────────────────────────────────────────┐
Strict ConsistencyLinearizabilitySequential ConsistencyCausal ConsistencyEventual ConsistencyWeak / FIFO / PRAM└────────────────────────────────────────────┘

각 모델은 "클라이언트가 관찰할 수 있는 동작" 을 정의한다. 강할수록 프로그래밍 쉽고 성능 나쁘다.


7. Linearizability: "진짜 원자성"

정의

Linearizability는 가장 직관적인 강한 일관성이다:

"모든 연산이 즉시, 원자적으로, 어떤 순간에 발생한 것처럼 보인다."

보다 형식적으로:

  1. 각 연산이 호출(invocation)과 응답(response) 사이의 어떤 선형화 시점(linearization point) 에 발생한 것으로 간주.
  2. 실제 시간상 T1이 T2보다 먼저 끝났다면, 선형화 순서도 T1이 T2보다 먼저.
  3. 이 순서가 마치 단일 스레드가 실행한 것처럼 일관되어야 한다.

예시

Client A: W(x=1) [t=10]──────[t=20] 
Client B:                         R(x) [t=15]──────[t=25]

Linearizable이면: B의 읽기는 반드시 x=1을 반환해야 한다. 왜? B의 읽기 호출이 t=15이고 A의 쓰기가 t=20에 완료되었는데, B는 그 뒤까지(t=25) 기다리므로 t=20~25 사이에 A의 쓰기가 완료됨을 관찰할 수 있다.

단, B의 읽기가 A의 쓰기 완료 전에 끝났다면 (예: B의 응답 t=18), B는 0 또는 1 어느 것이나 반환 가능.

Strict Consistency와의 차이

Strict Consistency는 "쓰기가 즉시 모든 곳에서 보여야 한다"로, 물리적으로 불가능하다 (빛의 속도). 그래서 이론적 이상형이고, Linearizability가 실질적 최강이다.

구현 방법

Linearizability는 다음 중 하나로 달성된다:

  1. 단일 Leader + 동기식 복제: 모든 쓰기가 Leader를 거치고, 읽기도 Leader에서 (또는 확인된 replica에서).
  2. Consensus 기반: Raft, Paxos로 로그를 전역적으로 정렬.
  3. Quorum + Strict Read: R + W > N + 모든 replica의 동기화.

etcd, ZooKeeper, Spanner는 linearizable하다.

비용

  • 네트워크 왕복 비용
  • CAP에서 CP: 파티션 발생 시 가용성 포기.
  • Latency가 높음.

8. Sequential Consistency: 약간의 양보

정의

Leslie Lamport가 1979년 정의한 Sequential Consistency는 Linearizability보다 약간 약하다:

"모든 연산이 어떤 순차 순서로 실행된 것처럼 보인다. 이 순서는 각 프로세스의 프로그램 순서를 존중한다."

핵심 차이: 실제 시간 순서는 무시. 각 프로세스 내부의 순서만 유지하면 된다.

예시

Client A: W(x=1) [t=10, 완료 t=20]
Client B:                           R(x) [t=25, 완료 t=30]

Sequential consistency에서 B가 x=0을 반환하는 것이 허용될 수 있다. 왜? 전체 순서가 [B의 R(x=0), A의 W(x=1)] 이라고 해석될 수 있기 때문이다. 실제 시간상 A가 먼저 끝났지만, 이 사실은 무시된다.

언제 쓰이나

  • 멀티코어 CPU 메모리 모델: 대부분의 현대 CPU(ARM, POWER)는 sequential consistency보다 약한 모델을 쓰지만, x86은 거의 sequential에 가깝다.
  • 분산 캐시의 기본 모드: 약한 쪽으로 양보해서 성능 추구.

Linearizability와의 차이 요약

항목LinearizabilitySequential
프로세스 내부 순서유지유지
실제 시간 순서유지무시
성능낮음약간 높음
예시 시스템etcd, SpannerZooKeeper local read

9. Causal Consistency: 인과성만 보장

정의

Causal Consistency는 happens-before 관계를 존중한다:

"인과 관계가 있는 연산들은 모든 노드에서 같은 순서로 보인다. 인과 관계가 없는 연산들은 다르게 보일 수 있다."

즉, a → b 이면 모든 노드가 a를 먼저 본 후 b를 봐야 한다. 하지만 a || b (concurrent)이면 노드마다 순서가 다를 수 있다.

예시: 소셜 미디어

Alice: "오늘 프로포즈 받았어!" (t=10)
Bob:   [Alice의 글 본 후] "축하해요!" (t=11)
Carol: [Alice의 글 본 적 없음] "날씨 좋네요" (t=10.5)

Causal consistency는:

  • Alice의 글과 Bob의 댓글은 모든 사람이 이 순서로 봐야 한다 (인과 관계).
  • Alice의 글과 Carol의 글은 어떤 순서로도 보일 수 있다 (인과 관계 없음).

이는 Sequential보다 훨씬 약하지만 실용적으로 충분한 경우가 많다.

구현

Vector Clock이 직접적인 구현 도구다. Riak, COPS 등의 시스템이 사용.

Causal+ Consistency

Causal Consistency는 concurrent 연산들의 순서가 노드마다 달라도 된다고 한다. 하지만 이는 이상하게 느껴질 수 있다. Causal+ Consistency는 추가로 convergence를 요구한다:

"모든 concurrent 연산은 모든 노드에서 동일한 (임의의) 순서로 결정된다."

이는 CRDT와 함께 자주 등장한다.

장점

  • 파티션 견딤: CAP에서 AP 선택 가능.
  • 성능: 동기화 최소화.
  • 직관적: 대부분의 개발자가 "인과성"은 이해한다.

10. Eventual Consistency: 결국은 수렴한다

정의

가장 약한(하지만 가장 실용적인) 모델:

"업데이트가 더 이상 없다면, 결국 모든 노드가 같은 값을 갖는다."

형식적 보장은 이게 전부다. 중간 상태에 대한 제약이 없다.

예시와 주의점

Client writes x=1 on replica A.
Client reads x from replica B (복제 전)0 반환.
Client reads x from replica B (복제 후)1 반환.

일시적으로 stale한 값이 보이지만, 결국엔 일관된다. "결국"이 언제인지는 보장되지 않는다.

Read-your-writes 위반

Eventual consistency의 악명 높은 문제:

A가 프로필 업데이트 (W to replica X).
A가 즉시 자기 프로필 조회 (R from replica Y).
Replica Y는 아직 복제 받지 못함 → 옛 값 반환.
A: "어? 내가 방금 업데이트 했는데?"

이를 막으려면 Session Guarantees:

  • Read-your-writes: 자신의 쓰기는 항상 보임.
  • Monotonic reads: 한 번 본 것보다 최신 값만 봄.
  • Monotonic writes: 자신의 쓰기들은 순서대로.
  • Writes-follow-reads: 읽은 것에 기반한 쓰기는 순서대로.

이 네 개를 합치면 Session Consistency가 된다. 실전 시스템(DynamoDB, MongoDB)이 제공하는 중간 수준이다.

어디에 쓰이나

  • DNS: 변경이 전 세계 전파되는 데 시간 걸림.
  • CDN: 캐시 무효화는 eventual.
  • DynamoDB: 기본 모드.
  • Cassandra: 기본 모드.
  • Git: 여러 클론의 변경이 결국 merge.

트레이드오프

  • 장점: 고가용성, 낮은 지연, 파티션 내성.
  • 단점: 프로그래머가 중간 상태를 처리해야 함. 충돌 해결 로직 필요.

11. 실전 시스템 비교

시스템일관성 모델시계
etcdLinearizableRaft 로그
ZooKeeperSequential (Linearizable writes)Zab
SpannerExternal Consistency (Linearizable+)TrueTime
CockroachDBSerializable SnapshotHLC
YugabyteDBSerializableHLC
TiDBSnapshot / SerializableTSO (중앙 타임스탬프)
MongoDBLinearizable (writeConcern majority)HLC (clusterTime)
DynamoDBEventual or StrongVector clock (legacy)
CassandraTunable (QUORUM/ONE/ALL)Timestamp (client)
RiakEventualVector clock

클라이언트의 선택권

많은 시스템이 연산별로 일관성 수준을 선택할 수 있게 한다:

# Cassandra
session.execute(query, consistency_level=ConsistencyLevel.QUORUM)

# DynamoDB
client.get_item(Key=..., ConsistentRead=True)

# MongoDB
db.collection.find(...).readConcern({ level: "linearizable" })

이는 "비싼 일관성은 필요할 때만 지불" 하는 실용적 접근이다.


12. CAP와 PACELC

CAP 정리

Eric Brewer의 CAP 정리 (2000):

Consistency, Availability, Partition tolerance 중 2개만 택할 수 있다.

정확히는 "파티션이 발생했을 때 C와 A 중 하나를 포기해야 한다" 이다. 파티션은 선택이 아니라 현실이므로 실제 선택은 CP vs AP:

  • CP: 파티션 중 일관성 유지, 일부 요청 실패. (etcd, ZooKeeper, HBase)
  • AP: 파티션 중 가용성 유지, 일관성 타협. (Cassandra, DynamoDB, Riak)

PACELC: 더 완전한 그림

Daniel Abadi의 PACELC:

Partitioned → A vs C. Else → Latency vs Consistency.

즉, 파티션이 없을 때도 선택이 있다: 강한 일관성을 위해 latency를 지불하거나, 빠른 응답을 위해 약한 일관성을 택하거나.

시스템PACELC
SpannerPCEC (지연 감수)
CassandraPAEL (빠름)
MongoDBPCEL (기본) / EC (majority)
DynamoDBPAEL (기본) / EC (strong read)

13. 실전 팁: 어떤 일관성을 언제?

강한 일관성이 꼭 필요한 경우

  • 금융 거래: 잔고 업데이트.
  • 재고 관리: 판매 가능 수량.
  • 경매/예약: 단일 승자.
  • 인증/권한: 로그인 세션.
  • 락/리더 선출: Consensus 필수.

Causal이면 충분한 경우

  • 댓글/답글: 원글 순서만 유지.
  • 메시징: 한 대화 내 순서.
  • 뉴스피드: 인과성만 유지하면 OK.

Eventual로 충분한 경우

  • 조회수 카운터: 정확성 낮아도 됨.
  • 좋아요 수: 약간의 오차 허용.
  • 추천 점수: 실시간이 아님.
  • 로그/메트릭: 약간 늦게 도착해도 됨.

혼합 전략

현실에선 한 시스템 안에서도 연산마다 다른 일관성을 쓴다:

  • 돈 계산 → linearizable
  • 사용자 이름 조회 → eventual
  • 프로필 수정 후 조회 → read-your-writes

모든 것을 강한 일관성으로 만드는 건 비싸고 느리다. 꼭 필요한 곳만.


퀴즈로 복습하기

Q1. Lamport Clock으로 두 이벤트가 concurrent인지 알 수 있는가?

A. 알 수 없다. Lamport clock은 a → b 이면 L(a) < L(b) 를 보장하지만, 역은 성립하지 않는다. 즉, L(a) < L(b) 라도 a와 b가 concurrent일 수 있다. Vector clock만이 a → b ⟺ V(a) < V(b)양방향 관계를 제공한다. 인과 관계 감지가 필요하면 vector clock을 써야 한다.

Q2. Vector Clock의 가장 큰 실전 문제와 해결책은?

A. 벡터 크기가 프로세스 수에 비례한다는 점이다. 수천 개 노드에서는 메시지 오버헤드가 커진다. 해결책: (1) Pruning: 오래된 엔트리 제거. (2) Version Vector: 복제본(한정된 수)에만 대응. (3) Dotted Version Vector: 클라이언트별이 아닌 서버별 카운터. (4) HLC: 완전히 다른 접근으로 O(1) 공간에 대략적인 인과성 제공.

Q3. Linearizability와 Serializability의 차이는?

A. Linearizability단일 객체에 대한 동시 연산의 순서를 다룬다. "실제 시간 순서를 존중하는 원자적 원소 연산"이 핵심. Serializability트랜잭션(여러 연산의 그룹)의 동시성을 다룬다. "어떤 순차 실행과 동일한 결과"가 핵심. 둘은 독립적이라 조합 가능하다: Strict Serializability = Linearizable + Serializable. 예시: PostgreSQL의 SERIALIZABLE은 linearizable하지 않을 수 있다 (단일 노드라 주로 문제 안 됨). Spanner의 external consistency는 strict serializable이다.

Q4. HLC가 wall clock과 Lamport clock의 장점을 어떻게 결합하는가?

A. HLC는 (l, c) 쌍을 사용한다. l은 wall clock을 따라가며 실제 시간과 근사한 값을 유지한다. c는 같은 l 내에서 순서를 부여하는 논리 카운터다. 메시지 수신 시 l = max(local_wall, local_l, msg_l)로 갱신해서 happens-before를 보장한다. 결과: (1) O(1) 공간, (2) 실제 시간과 가까운 값 (로그 정렬 가능), (3) 인과성 유지, (4) wall clock 오차가 커도 논리 시계로 안전하게 퇴화. CockroachDB, MongoDB, YugabyteDB가 이 방식을 쓴다.

Q5. Read-your-writes를 eventual consistency 위에서 어떻게 달성할 수 있는가?

A. 여러 방법이 있다: (1) Sticky session: 사용자를 항상 같은 replica로 라우팅. (2) Timestamp tracking: 클라이언트가 마지막으로 본 타임스탬프를 기억하고, 읽을 때 그 이상을 요구. (3) Read from primary: 자신의 최근 쓰기는 primary(또는 master) replica에서 읽기. (4) Versioned reads: 쓰기 후 받은 version을 세션에 저장, 이후 읽기에 해당 version 이상 요구. DynamoDB의 ConsistentRead=True나 MongoDB의 readConcern: "majority"가 이런 접근이다. 단, 이는 대부분 성능 비용을 동반한다.


마치며: 시간과 순서, 그리고 겸손함

핵심 정리

  1. 물리 시계는 믿을 수 없다. 분산 시스템에서 "언제"는 근본적으로 불분명하다.
  2. Lamport clock: 전체 순서는 주지만, concurrent 여부는 알 수 없다.
  3. Vector clock: 인과 관계를 완벽히 표현. 대신 공간 비용.
  4. HLC: 실전의 타협. O(1) + 근사 wall clock + 인과성.
  5. TrueTime: Google이 하드웨어로 푼 궁극의 답.
  6. 일관성 모델: 강하면 프로그래밍 쉽고 성능 나쁘다.
  7. Linearizability: 가장 직관적인 강한 보장.
  8. Causal Consistency: 인과성만 유지하는 실용적 중간.
  9. Eventual Consistency: 고가용성의 대가.

시스템 설계자에게

분산 시스템을 설계할 때 스스로에게 물어야 할 질문들:

  1. 정말 strong consistency가 필요한가? 대부분의 답은 "아니오"다.
  2. 인과성만으로 충분한가? 대부분의 소셜/협업 앱에서 충분하다.
  3. 어떤 데이터는 linearizable이고 어떤 데이터는 eventual일 수 있는가? 정답은 거의 항상 "다르다".
  4. 시계 오차가 얼마나 허용되는가? HLC의 max_offset을 결정한다.
  5. 파티션 중에 읽기/쓰기 중 어느 것을 포기할 것인가? CAP 선택.

겸손함

Leslie Lamport는 2013년 튜링상 수상 연설에서 이렇게 말했다:

"Distributed systems are weird. Very weird."

30년 넘게 이 분야를 만들어 온 사람이 한 말이다. 우리가 분산 시스템을 설계할 때 느끼는 혼란은 당연한 것이다. 이론을 알면 적어도 무엇이 불가능한지를 알 수 있다. 그것만으로도 많은 버그를 막을 수 있다.


참고 자료

현재 단락 (1/358)

당신이 세 대의 서버로 분산 채팅 서비스를 운영한다. 사용자 Alice가 "안녕"이라고 보내고, Bob이 "잘 있어"라고 답한다. 세 서버는 이 메시지들을 각자 다른 순서로 받을 ...

작성 글자: 0원문 글자: 15,106작성 단락: 0/358