WAL, 분산 시스템 데이터 일관성과 장애 복구 패턴
WAL(Write-Ahead Log) 기반의 복제 프로토콜의 핵심 메커니즘들을 알아보겠다.
WAL #
Write Ahead Log는 데이터베이스에 실제 값을 적용하기 전에, 내가 무엇을 할 것인지 디스크에 먼저 기록해두는 파일이다.
내구성 #
데이터베이스는 성능(속도) 안전(보존) 사이에 딜레마가 존재한다 WAL은 이 두마리를 다 잡기 위해 사용하는데
데이터를 디스크에 바로바로 정리해서 저장하려면 너무 느리다 random io
그래서 보통 RAM에서 먼저 처리하는데, 이때 전원이 나가면 메모리 데이터는 다 날아간다.
그리고 분산 시스템에서 리더의 데이터 쓰기를 처리하고 레플리케이션에게 이를 동기화 하기 이전에
리더에서 장애가 나게 된다면 어떻게 이 쓰기를 기억할까 바로 WAL로그에 기록해둔다면 그걸 사용하면 된다.
즉 쓰기 전에 나 이거 쓸게 하고 WAL 파일 맨 끝에 빠르게 append한다. 컴퓨터가 꺼지거나 데이터가 유실되었다면 이 WAL을 다시 읽어 replay하여 메모리 상태를 복구한다.
성능 (랜덤 쓰기를 순차 쓰기로 바꿈) #
데이터베이스의 실제 저장 구조 B-Tree등을 찾아서 수정하는건 random io라 디스크헤드가 왔다갔다 해야해서 느리다.
반면 WAL은 파일 끝에 append하는 구조라 sequential io라서 속도가 엄청나게 빠르다.
즉, 빠른 기록을 먼저해서 응답 속도를 높이고 실제 저장은 나중에 천천히 하는 전략이다.
Flow #
- client 요청: a를 +100하는 작업
- WAL기록: 디스크 로그 파일 끝에 set a.score = score + 100 적음 이게 성공해야 진짜 성공
- 메모리 변경: RAM에 있는 유저 a 데이터를 수정함
- ACK: 클라이언트에게 성공 응답.
- Flush: 메모리에 쌓인 데이터를 실제 데이터 파일로 옮겨적음(나중에일어남)
분산 시스템에서 로그 매칭과 하이 워터마크등의 메커니즘으로 리더와 복제간에 동기화 역할을 하는데, 여기서 사용하는 로그가 WAL이다.
리더는 요청을 받으면 WAL을 로컬에쓰고 복제는 팔로워들에게 똑같이 복사하여 전달, 일관성적으로 모든 노드가 똑같은 순서의 WAL을 가지고 있다면 결과데이터도 똑같을것이라는 믿음 state machine replication이 핵심
데이터 일관성과 장애 복구 패턴 #
시간과 순서 #
분산 환경에서는 물리적인 시간 NTP를 신뢰할 수 없으므로 논리적인 순서가 중요하다.
논리시계 & 램포트 시계 #
노드 간 시스템 시간이 미묘하게 달라 이벤트의 선후 관계를 물리적인 시간으로 확정할 수 없다.
사건의 인과관계 happened before relationship만을 정의하는 카운터다.
- 각 노더는 로컬 카운터를 가진다
- 이벤트를 발생시키면 카운터 + 1
- 메시지를 보낼 때 자신의 카운터를 포함
- 메시지를 받은 노드는 Max(자신의 카운터, 받은카운터) + 1로 갱신한다.
단순히 순서만 알 수 있는 램포트 시계의 한계를 넘어 동시성까지 파악하려면 벡터시계를 사용해야하지만 일단 알고만 있자.
합의와 복제 #
데이터를 여러 노드에 복사할때 성공의 기준을 정하는 방법이다.
Quorum 정족수 #
분산 시스템에서 읽기/쓰기 작업의 유효성을 보장하기 위해 승인받아야하는 최소 노드 수다.
핵심 공식은 인데 N=총복제본수 W=쓰기 성공 응답을 간주한 최소응답수 R=읽기시 조회해야할 최소 노드 수 이다.
쓰기 집합과 읽기 집합이 반드시 하나 이상의 노드에서 겹치도록(overlap) 설계하여 항상 최신 데이터를 읽을 수 있게 보장한다.
하이 워터 마크 hwm #
로그 파일 내에서 모든 팔로워(혹은 정족수 이상)에게 복제가 완료되어 commit이 확정된 마지막 오프셋을 의미한다.
가시성을 챙길수있는데 클라이언트는 hwm 이하의 데이터만 읽을 수 있다. (복제중인 불안정한 데이터 조회 방지)
복구 기준점도 챙길수있다 리더가 죽었을 때, 데이터를 어디까지 살리고 어디부터 버릴지 결정하는 기준선이다.
LEO(Log End Offset): 현재 기록된 로그의 마지막 끝부분은 항상 LEO >= HW 이다.
리더 선출, Uncommitted 데이터 처리 #
리더가 장애로 죽고나서 새로운 리더가 선출될 때, 일관성이 어떻게 유지되는지 알아보겠다.
기존 리더가 클라이언트 요청 A1를 받아 WAL로그에 썼지만, 팔로워들에게 아직 다 전파하지 못한 상황에서 (hwm갱신전) 죽어버렸다면 어떻게 될까
일단 처리과정은 리더 선출, 살아있는 노드 중 가장 최신 로그(높은 epoch/term + leo)를 가진 노드가 리더로 선출된다. 이는 데이터 유실을 최소화하기 위함이다.
그 이후 epoch를 증가시킨다, 새 리더는 자신이 새로운 세대의 리더임을 알리기위해 해당 번호를 증가시켜 이전 리더가 다시 되살아나도 팔로워들이 무시하게 만들수있도록 해주는 작업이다.
로그 매칭 및 truncation을 하는데 이 과정에서 uncommitted 데이터의 운명이 결정된다.
- Case A(dirty read 방지): 새 리더의 로그보다 더 긴 로그(하지만 커밋되지 않은)를 가진 팔로워가 있다면, 해당 팔로워는 새 리더와 일치하는 지점 이후의 로그를 잘라내서 버린다.
- Case B(데이터 복구): 새 리더가 가진 로그가 팔로워에게 없다면, 팔로워는 리더의 로그를 받아 채워넣는다.
그 이후에 하이 워터마트를 갱신한다 트렁케이션 및 동기화가 끝나고 현재 자신의 로그를 정족수 이상에게 전파했다는 확인을 새 리더가 받으면 그제야 hwm을 전진시킨다. 이때부터 클라이언트가 정상적으로 데이터를 읽을 수 있다.
이 과정 때문에, 분산시스템에서는 커밋되지 않은 데이터는 유실될 수 있다는 것을 전제로 설계해야한다.
데이터 보장 의미론 #
일관성 모델 #
일관성 모델의 모델들이 존재하는데 하나씩 알아보겠다.
- Strong Consistency: 어떤 노드에 접근하든 항상 최신 hw 기준의 데이터를 반환한다. 속도가 느리고 정족수가 필요하다.
- Eventually Consistency: 일시적으로 데이터가 다를 수 있지만, 시간이 지나면 모든 노드가 같아진다. 속도가 빠르다.
- Linearizability: 시스템이 마치 하나의 복사본만 있는 것 처럼 동작하도록 보장하는 최상위 일관성이다.
멱등성 Idempotency #
클라이언트가 요청을 보냈는데 타임아웃이 발생해서 서버가 처리하고 응답을 못준건지, 아예 처리를 못한건지 모른다 그래서 클라이언트는 retry를 해 시스템 상태가 망가지지 않게 해줄 수 있는데.
여기서 문제는 이 콜이 멱등하게 동작하지않는다면 중복된 데이터가 쌓이거나 상태가 오히려 더 깨질수있기 때문에 몇번을 시도하든 한번을 시도한것과 결과가 같은 상태로 있는것 즉 멱등성있게 동작하도록 해야한다.
구현 패턴은 unique key, Request ID 등으로 클라이언트가 요청시 uuid를 생성해서 보내고 서버가 이 id를 기반으로 처리했는지를 확인한다. 했으면 바로 return 안했으면 retry
분산 시스템에서 정확히 한 번 전송 Exactly-once는 불가능하므로 최소 한번 전송 + 멱등성을 결합하여 사실상의 Exactly once효과를 낸다.
정리 #
- 논리 시계로 이벤트 순서를 잡고 세대(epoch)를 구분한다.
- 리더는 정족수 충족 여부를 확인해 하이 워터마크를 올린다.
- 장애 발생시 새 리더는 하이 워터마크와 epoch를 기준으로 로그 트렁케이션을 수행해 일관성을 맞춘다.
- 이 모든 과정중 네트워크 중복/재시도 문제는 멱등성 키를 통해 해결한다.