Skip to content

Commit

Permalink
Update and rename [Distributed System] Lamport Clock.md to [CRDT] LLW…
Browse files Browse the repository at this point in the history
…Register와 Lamport Clock.md
  • Loading branch information
binary-ho committed Jun 17, 2024
1 parent f23cc86 commit 3e31cbc
Showing 1 changed file with 62 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,32 +1,82 @@
# 1. 분산 시스템과 사건 발생 시간
CRDT 기술을 공부하다가 분산 환경에서 선후관계를 어떻게 파악하는지에 대한 궁금증이 생겼고, 관련 방법들을 찾아보았다. 분산 시스템에서 여러 클라이언트가 보내오는 요청의 순서를 어떻게 결정해야 하는가? <br>
# 1. CRDT와 LLWRegister
LLWRegister는 Last Write Wins Register의 약자로 "마지막 - Last"에 쓴 내용이 반영되는 CRDT 중 하나이다. 마지막으로 쓴 내용이 이긴다고 해서(Win) Last Write Win이다. <br>

CRDT는 Conflict-free Replicated Data Type의 약자로 하나의 리소스에 여러 사용자가 접속할 때 충돌 없이 데이터를 받을 수 있도록 설계된 자료구조이다. 예를 들어 구글 독스, 노션, 피그마 등은 여러 사람이 수정하더라도 한명이 입력한 자료가 아예 사라지던지 하지 않는다. <br>

사실 요즘은 세상이 좋아져서 "아예 사라진다"가 무슨 의미인지 모를 수도 있다. 나도 자세히 기억나진 않지만, CRDT 방식이 널리 쓰이기 전 아주 예전에 어떤 협업 툴을 사용했을 때, 두 사람이 같은 텍스트를 수정하는 경우 가끔 한명이 적어 낸 변경사항 자체가 사라지곤 했었다. <br>
대략적인 CRDT가 궁금하다면 [참고하자](https://channel.io/ko/blog/crdt_vs_ot) <br> <br>


## 1.2 LLWRegister 컨셉
LLWRegister는 "마지막"을로 들어온 값을 덮어 씌우기 위해 "마지막" 판단을 위해 timestamp 값을 가지고 있다. 어떤 인덱스의 내용을 바꾸기 위해 여러가지 요청이 들어왔을 때, LLW는 마지막 요청이 무엇인지 판단해 덮어 씌운다. 이러한 "Merge" 연산을 간단하게 코드로 확인해보자. <br>
아래 어떤 좌표에 "색"을 덮어 씌울지 판단하기 위한 LLW 코드를 작성해 보았다. (코드는 요즘 진행중인 토이 프로젝트에.. -> [guestbook](https://github.com/binary-ho/guest-book))

<br> <br>


```go
type LWWRegister struct {
state *State
}

type State struct {
color rgb.RGB
timestamp Timestamp
owner user.User
}

func (register *LWWRegister) Merge(remoteState *State) {
// 만약 remote가 "이긴"다면 덮어 씌운다.
if state := *register.state; state.isWin(remoteState) {
state.SetColor(remoteState.color)
}
}

func (state *State) isWin(remoteState *State) bool {

/*
1. 더 나중에 발행된 요청인가?
2. 시간 값이 같은 경우 다른 조건으로 검사한다.
*/
... 생략
}

```

굉장히 심플하다. 더 나중에 쓰인 내용을 덮어 씌우고, 만약 동시에 쓰였다면 다른 부가적인 값들을 이용해 체크한다. <br>
예를 들어 나는 유저 등급이나 아이디 값을 통해 덮어 쓸 수 있는 우선권을 줬는데 핵심은 이게 아니다. **LLWRegister의 핵심은 그냥 "더 나중에 쓰인 내용이 이긴다"이고, 더 중요한 것은 "나중에" 쓰인 요청은 어떻게 판단할 것인가 이다. 이것이 더욱 더욱 중요하고 어려운 문제이다.** <br>
결국 우리는 코드로 시간을 확인해 요청을 보낼 것이다. "시간"은 코드 레벨에서 어떻게 판단되는가? 다양한 환경 속에서 코드를 수행한다면 "정확한 시간"은 어떻게 판단할 수 있는 걸까


# 2. 분산 시스템과 사건 발생 시간 판단의 어려움
분산 시스템에서 여러 클라이언트가 보내오는 요청의 순서를 어떻게 결정해야 하는가? <br>
서비스에 따라 다르겠지만, 현재 공부하고 있는 동시 편집 기술이나, 블록체인 쪽에서는 이러한 선후 관계 파악이 꽤나 중요한 문제로 여겨진다. 어떤 탈중앙 거래소 컨트랙트가 배포된 블록체인에서는 채굴자가 트랜잭션 순서를 다시 정렬함으로써 시간당 18.5억의 시세차익을 얻을 수도 있다. <br>
이렇게 실제 사건의 발생 순서를 따지는 것은 생각보다 중요한 일이다. <br> <br>

하지만, 분산 시스템에서 사건 발생의 제대로 된 순서를 정하는 것은 쉬운 일이 아니다. 무질서한 세상에 질서를 부여하려는 이 시도가 왜 어려운 것인지, 그리고 사건들의 순서 관계를 강제할 수 있는 Lamport Clock에 대해 알아보자.


# 2. 순서를 결정하기 위한 다양한 제안들
## 2.1 현재 시간을 사용하는 방법
# 3. 순서를 결정하기 위한 다양한 제안들
## 3.1 현재 시간을 사용하는 방법
그래서 사건들의 순서를 제대로 파악하려면 어떻게 해야 할까? <Br>
단순히 "발생 시간"을 사용할 수 있다면 참 좋겠지만, 컴퓨터는 현재 시간을 어떻게 파악할까? <br>
다양한 방법이 있겠지만 보통 클라이언트들은 자신의 OS나 위치한 Region, 혹은 설정된 서버 시간에 따라 "현재 시간"을 다르게 파악할 수 밖에 없다. <br>

**따라서, 코드에서 파악하는 "현재 시간"이란 환경에 따라 다를 수 있으므로 실제로 어떤 요청이 먼저 발생했는지 정확한 선후관계를 파악하기는 쉽지않다.** <br> <br>

## 2.2 공통된 시간 기준을 사용하기
## 3.2 공통된 시간 기준을 사용하기
공용 시계 역할을 해주는 하나의 노드가 있다면 어떨까? <Br>
그것이 서버이건, 위성이건, 방속국이건 어떤 기준이 있고 여러 클라이언트 공유해 사용한다던가, 클라이언트들의 요청을 받아내는 서버가 하나라면 선후관계를 따지기가 참 쉬울 것이다. 해당 서버의 시간을 사용하면 되니까! <br>
문제는 우리가 파악하고 싶은게 "사건 발생 시간"이라는 점이다. 클라이언트 A에서 먼저 버튼을 누르고, 클라이언트 B에서 5초 뒤 버튼을 눌렀다고 생각해보자. 그런데 네트워크에 문제가 생겨 클라이언트 A의 요청이 B의 요청보다 시계 서버에 훨씬 늦게 도착했다고 생각해보자. <br>
비록 이러한 메시지 이동 시간까지 고려해 시간을 주고 받으며 시간을 조정하는 "크리스티안 알고리즘"이나 "버클리 알고리즘"과 같은 같은 동기화 알고리즘을 사용하더라도 네트워크 소요 시간을 정확하게 반영하지는 못한다. <br> <br>

**이런 상황에서 실제로는 A가 훨씬 빠르게 어떤 행동을 했다고 해도, 네트워크 문제 등의 다른 이유들로 실제로는 늦은 요청으로 판단되서 억울하게 손해를 볼 수도 있다.**

## 2.3 논리적인 시게를 사용하기
## 3.3 논리적인 시게를 사용하기
이제까지는 물리적인 시계를 활용하는 방법을 알아보았다. 기준 시각을 가지고 있는 서버를 활용하던, 네트워크 시간을 보정해주는 알고리즘을 사용하던, 결국 분산된 장치들의 물리적인 시계는 완벽하게 동기화되지 않는다. **결국 나노초 단위의 정확한 시간을 보장하지는 못한다.** <br>
**우리는 이 문제를 절대적인 시간에 대한 집착을 버리면서 어느 정도 해결해볼 수 있다.** 결국 중요한 것인 "순서"라면 꼭 절대적인 시간이 없어도 어느 정도 지켜낼 수 있다. **이제 작업의 순서를 결정하기 위해 논리적인 시계를 활용하는 방법중 하나인 Lamport Clock에 대해 알아보자.** <br>

# 3. Lamport Clock
# 4. Lamport Clock
램포트 시계는 정확한 절대 시각을 무시하고, 분산 장치들간의 작업 선후관계를 규정한다. 어떤 작업이 먼저 일어났는지, 또는 어떤 작업이 다음에 일어났는지 등 시간 관계를 규정하는 것이 목적이다. 내 기대와 달리 정확한 인과 관계를 결정지을 수는 없었는데, 이 점에 주의해야 한다. (그래서 앞서 이야기한 시간 관계가 매우 엄밀하게 중요한 곳에선 사용할 수 없다.) <br> <br>

각 노드들은 각자의 논리적인 시간을 갖고 사건이 발생할 때마다 카운트를 1 늘린다. 예를 들어 노드 N1, N2가 있다고 하자. 둘 다 현재 시각은 0으로 초기화 되어 있다. 이후 N1에서 사건 A가 발생한다면, 사건 A는 시간 1에 발생한 것이라고 정한다. 이후 사건 B가 발생한다면 사건 B는 시간 2에 발생한 것이다. 두 사건이 발생한 이후 N1의 램포트 시간은 2일 것이다. 그리고 N2에는 아무런 사건이 발생하지 않았음으로 현재 시각은 0이다. <br>
Expand All @@ -43,7 +93,7 @@ Lamport Clock Algorithm은 이때 노드별로 부여된 id값을 활용한다.

<Br>

## 3.1 Lamport Clock Algorithm
## 4.1 Lamport Clock Algorithm
이제까지의 램포트 시계가 작동하는 과정을 알고리즘으로 나타내면 아래와 같다

1. 모든 장치가 가진 램포트 시계는 0으로 초기화 되며, 시간 값은 정수이다. 이 시간을 Ci라고 나타내자. (i가 노드 번호이다.)
Expand All @@ -54,13 +104,13 @@ Lamport Clock Algorithm은 이때 노드별로 부여된 id값을 활용한다.

<br>

## 3.2 Total Order
## 4.2 Total Order
알고리즘은 위와 같이 작동하고, 이벤트의 전체 순서 Total Order는 어떻게 결정하면 될까? 일단 시각으로 선후관계를 정한 다음, 서비스 명세에 따라 "노드의 id값" 등의 기준으로 판단하면 된다. 예를 들어 아래와 같이 진행될 수 있겠다. <Br>

1. Lamport 시각이 더 작으면 더 먼저 발생한 사건이다.
2. Lamport 시각이 같은 경우 노드의 Id값이 더 작으면 더 먼저 발생한 사건이다.

<Br>
<Br> <br>

구체적인 예시로 아래와 같은 순서로 사건들이 발생했다고 해보자
1. 사건 a가 N1에서 발생, 시각 1 -> C1(a) = 1
Expand All @@ -75,7 +125,7 @@ Lamport Clock Algorithm은 이때 노드별로 부여된 id값을 활용한다.
문제는 `사건 d`를 언제 발생했는지 판단하는 것이다. 램포트 시각이 1이므로, 사건 a와 비교되야 한다. 이때, id값이 더 작은 것이 우선 발생한 것이므로 `a -> d`를 결정할 수 있다. <br>
b와 d는 어떨까? 램포트 시각은 "d 사건"이 1로 더 작으므로, d를 먼저 발생한 사건으로 본다. 따라서 `a -> d -> b -> c`와 같은 순서로 사건이 발생했다고 결정할 수 있겠다.

# 4. 후기
# 5. 후기
CRDT 기술을 공부하다가 LLW Register의 존재를 알게 되었고, 가장 간단하게 선후관계를 논리적으로 결정하는 방법인 Lamport Clock을 알아보았다. <br>
아쉽게도 Lamport Clock은 한계가 많은 방법이였다. 결국 Lamport Clock 값이 같은 경우 보조적인 방법으로 순서를 결정하는데, 내가 궁금했던 것은 실제로 선후관계를 어떻게 파악하는지에 대한 내용이였다. <br>
아쉽게도 Lamport Clock은 한계가 많은 방법이였다. 결국 Lamport Clock 값이 같은 경우 보조적인 방법으로 순서를 결정하는데, 내가 궁금했던 것은 실제로 선후관계를 어떻게 파악할 것이고, 어떤 아이디어들이 제안 되어 왔는가에 대한 내용이였다. <br>
일단은 지금 급하게 해야할 일들이 밀렸으므로 추후 추가적으로 공부한 다음 내용을 보충해봐야겠다.

0 comments on commit 3e31cbc

Please sign in to comment.