5. Replication

복제는 네트워크로 연결된 복수의 기계에 같은 데이터의 사본을 유지하는 것이다. 이는 데이터를 사용자의 지정학적 근처에 놓아 접근 지연시간을 줄이고, 시스템의 일부 부분이 실패해도 시스템이 동작을 계속하도록 하고, 쿼리를 읽을 수 있는 기기의 수를 확장하는 목적이 있다. 이에 대해 알아보자. 복제할 데이터가 시간에 따라 변화하지 않으면 복제는 쉽지만 그렇지 않으면 복제는 어려워진다. 복제에는 여러 트레이드오프가 존재한다. 데이터베이스의 복제 이론은 처음 나온 이후 크게 변하지는 않았다.

Leaders and Followers

각 노드는 데이터베이스의 복제라는 사본을 가진다. 데이터베이스에 대한 모든 쓰기는 모든 복제에 수행되어야 한다. 이는 하나의 리더(마스터)와 이를 제외한 팔로워(슬레이브)를 둬서 리더 기반 복제를 하는 것으로 해결할 수 있다. 이는 많은 관계형 데이터베이스의 특성이다.

Synchronous Versus Asynchronous Replication

복제된 시스템의 중요한 디테일은 복제가 동기적인가 비동기적인가 하는 것이다. 어떤 시점에는 리더가 팔로워에게 업데이트를 전달해야 하기 때문이다. 이것이 성공적이면 리더는 클라이언트에게 이를 보낸다. 동기와 비동기의 차이는 팔로워의 응답을 기다리는지의 여부이다. 일반적으로 복제는 매우 빠르지만 항상 그렇다는 보장은 없다. 동기적 복제의 장점은 데이터 일관성이 보장된다는 것이다. 모든 팔로워에 대해 동기화를 유지하는 것이 아니라 하나만 유지한다. 이는 대개 반동기적이라고 이야기하고는 한다. 리더 기반 복제는 완전 비동기적일 수도 있다. 이는 일관성을 보장하지 않지만 많이 쓰인다.

Setting Up New Followers

새 팔로워가 세팅되어야 한다면 어떻게 할까? 데이터를 한 노드에서 다른 노드로 복사하는 것만으로는 충분치 않다. 데이터 일관성을 위해 락을 걸 수 있지만 그러면 가용성이 낮아진다. 이에 대처하기 위해서는 리더 데이터베이스에 대한 스냅샷을 어떤 시점에 확보한 후 그 스냅샷을 새 팔로워 노드에 복사하고, 팔로워는 리더에게 스냅샷 이후의 모든 데이터 변화 내역을 요구한 뒤 이를 업데이트한다. 세부 구현은 데이터베이스마다 달라질 수 있다.

Handling Node Outages

노드의 실패에 대해서는 어떻게 대처해야 할까?

Follower failure: Catch-up recovery

팔로워가 크래시되면 리더의 크래시 시점 이후 변경 사항을 전달받아 업데이트해 복구하면 된다.

Leader failuer: Failover

리더가 크래시되면 팔로워 중 하나를 새 리더로 만들고 쓰기에 대한 요청을 새 리더로 전달해야 한다. 이는 수동으로 될수도 있고 자동으로 될수도 있다. 자동으로 하려면 리더가 실패했는지를 감지한 뒤 새 리더를 선택하고, 시스템이 새 리더를 사용하도록 한다. 비동기적 복제가 사용되면 대개 이전 리더의 복제되지 않은 쓰기를 버린다. 하지만 이는 데이터베이스의 내용과 다른 저장소 시스템이 연관되어 있다면 위험하다. 리더 노드를 두 개 두는 방법도 있다. 리더의 고장을 감지하는 타임아웃은 적절한 수치가 좋다. 이 문제들에 대한 쉬운 해법은 없다.

Implementation of Replication Logs

복제 로그는 어떻게 구현할까?

Statement-based replication

리더 로그가 모든 쓰기 요청을 기록할 수도 있다. 그러나 단점이 있는데, NOW()나 RAND() 같은 것을 부르는 요청에 대해 단순히 쿼리만 기록하면 안 되고, 명령문이 자동 증가되는 열을 가지는 등 부가효과가 있을 경우에 대처하지 못한다는 단점이 있다. 이에 대한 해결책도 존재한다. 이는 MySQL 5.1 이전까진 쓰였으나 이후엔 행 기반 복제로 변경되었다.

Write-ahead log (WAL) shipping

로그 구조 저장소나 B-트리 구조 저장소나 로그는 덧붙이기만 가능하다. 이의 단점은 로그 메커니즘이 저수준이므로 저장소 유형의 세부 구현에 크게 의존한다는 것이다. 이의 효과는 크다.

Logical (row-based) log replication

다른 방식은 복제에 대해 다른 로그 형식을 쓰고 행 기반 로그 복제를 수행하는 것이다. 관계형 데이터베이스의 논리적 로그는 데이터베이스 테이블에 대한 쓰기를 행 단위로 묘사한다. 삽입된 행에 대해서는 모든 열의 새 값을, 삭제된 행에 대해서는 이를 알리는 정보를, 업데이트된 행에 대해서는 업데이트된 값과 그 열을 식별하는 정보를 기록한다. 여러 행을 수정하는 트랜잭션은 여러 행에 대한 각각의 정보를 기록한다. 이는 저장소 엔진 내부 구현과 로그를 분리시킨다. 또 외부 애플리케이션에서 파싱하기도 쉽다.

Trigger-based replication

더 유연성이 필요할 수도 있다. 이는 트리거 기반으로 수행된다. 트리거는 데이터베이스에 데이터가 변경될 때 자동적으로 실행되는 커스텀 애플리케이션 코드를 등록 가능케 한다. 이는 유연성이 더 좋지만 오버헤드가 더 크고 버그에 더 취약하다는 단점이 있다.

Problems with Replication Lag

노드 실패에 대해 대처하는 것은 복제의 한 이유일 뿐이다. 읽기 중심으로 부하가 걸린다면 많은 팔로워를 두고 읽기 요청을 분산시키는 것이 낫다. 이는 비동기적 복제에 대해서만 잘 작동한다. 그러므로 데이터 일관성이 보장되지 않을 수도 있다. 이런 복제 지연은 대개는 큰 문제가 아니지만 가끔은 문제가 될 수도 있다. 이 경우 데이터 일관성은 유의미한 문제가 된다.

Reading Your Own Writes

많은 애플리케이션에서는 쓴 데이터를 사용자가 직접 읽을 수 있다. 비동기적 복제라면 쓴 시점에 바로 반영이 되지 않을 수 있으므로 문제가 될 수 있다. 이 경우에는 쓰기 이후 읽기 일관성이 요구된다. 사용자의 수정 사항에 대해서는 읽기를 리더에서만 시킬 수도 있지만 애플리케이션의 많은 부분이 수정 가능하면 이는 적용 가능하지 않다. 사용자가 쓴 타임스탬프를 기억해 시스템이 그 타임스탬프까지 반영된 복제들에 대해서만 읽기를 수행시킬 수도 있다. 복제가 복수의 데이터 센터에 분포되어 있다면 더 복잡해진다. 또한, 같은 사용자가 여러 기기에서 서비스에 접근하는 것도 생각해야 한다. 이 경우 타임스탬프 정보는 중앙화되어야 한다. 또한 다른 기기에서의 라우팅도 같은 데이터 센터로 연결시켜야 한다.

Monotonic Reads

한 유저가 여러 복제에서 읽을 때에는 읽은 사본의 시점이 거꾸로 될 수도 있다. 단조 읽기는 이를 방지하는데, 이를 얻는 한 방법은 한 유저는 한 복제에서만 읽도록 하는 것이다.

Consistent Prefix Reads

어떤 쓰기 연산들간에 인과관계가 있을 경우 이 일관성을 보존해야 한다. 이런 연산들은 항상 같은 파티션에 적용시킬 수도 있지만 이는 확장 가능하기 어렵다. 이런 인과 의존성을 추적하는 알고리즘이 존재한다.

Solutions for Replication Lag

복제 지연이 문제가 될 경우를 염두해야 한다. 애플리케이션에서 이를 보장할 수도 있지만 쉽지는 않다. 그래서 데이터베이스 차원에서 트랜잭션이 존재한다. 하지만 분산형 데이터베이스가 발전되면서 단일 노드 트랜잭션은 사장되었다.

Multi-Leader Replication

리더를 단일로 두지 않을 수도 있다. 리더가 실패할 경우에 대비해야 하기 때문이다. 이럴 때는 복수의 리더를 둔다.

Use Cases for Multi-Leader Replication

단일 데이터 센터에 복수의 리더를 두는 경우도 드물지만 존재는 한다.

Multi-datacenter operation

리더 기반 복제에서 리더는 데이터 센터 중 하나에 있어야 한다. 복수의 리더가 있으면 데이터 센터 각각에 리더를 둔다. 이는 지연 시간을 줄일 수 있다. 또한, 리더의 실패에 대비할 수도 있다. 네트워크 문제에 대해서도 대처할 수 있다. 어떤 데이터베이스는 이들을 구현한다. 이는 단점도 있는데, 쓰기 충돌을 해결해야 한다. 또한 키를 자동 증가시키는 것이나 통합성 유지 등은 더 까다로워진다.

Clients with offline operation

복수의 리더를 두는 경우가 적절할 때는 인터넷이 끊긴 상태에서도 애플리케이션이 동작해야 할 때이다. 오프라인에서 한 변경도 다시 온라인이 되면 서버에 반영되어야 하기 때문이다. 이 경우 각 기기가 리더로서 동작하는 로컬 데이터베이스가 된다. 이는 복수 리더식 접근의 극단적 예로 볼 수도 있다. 이에 특화된 데이터베이스들도 존재한다.

Collaborative ending

실시간 협업 수정 애플리케이션은 여러 사람이 문서를 동시에 수정할 수 있도록 한다. 이는 오프라인 수정과 많은 고려 사항을 공유한다. 쓰기 각각에 대해 락을 걸 수도 있지만 락을 걸지 않는 것이 더 빠르다. 이 경우엔 쓰기 충돌을 해결해야 한다.

Handling Write Conflicts

복수 리더 복제의 문제는 쓰기 충돌이 발생할 수 있다는 것이다. 비동기적 복제에서 이는 문제가 된다.

Synchronous versus asynchronous conflict detection

복수 리더 데이터베이스에서 비동기 복제에서 쓰기 충돌은 문제가 될 수 있다. 충돌 감지를 동기적으로 만들면 복수 리더의 이점이 없어진다.

Conflict avoidance

가장 간단한 방법은 충돌을 피하는 것이다. 같은 사용자로부터의 요청은 항상 같은 데이터센터로만 돌리는 것이다. 하지만 사용자에 배정된 리더를 바꿔야 할 때는 문제가 된다.

Converging toward a consistent state

단일 리더 데이터베이스는 순차적 쓰기를 한다. 복수 리더 설정에서는 그런 순서가 없다. 그래서 쓰기 충돌 해결을 통해 일관성을 보장해야 한다. 이에는 각 쓰기에 고유 ID와 무작위 UUID를 부여하거나, 각 복제에 유일한 ID를 부여하고 쓰기에 ID에 따른 우선도를 부여하거나, 값을 병합하거나, 충돌을 기록한 뒤 나중에 해결하는 방법 등이 있다.

Custom conflict resolution logic

커스텀 충돌 해결 로직이 있을 수 있다. 이는 쓰기 시에 수행될 수도 있고, 읽기 시에 수행될 수도 있다. 충돌 해결은 전체 트랜잭션이 아니라 각 행이나 문서 단위로 적용되어야 한다.

What is a conflict?

어떤 종류의 충돌은 직관적이다. 그러나 어떤 경우엔 이것이 충돌인지 판단하기 어려운 것도 있다.

Multi-Leader Replication Topologies

복제 위상은 쓰기가 한 노드에서 다른 노드로 전파되는 통신 경로를 이야기한다. 가장 일반적인 것은 전체-전체이다. 원환/별모양 위상도 존재한다. 이들의 단점은 한 노드가 실패하면 정보 흐름이 실패한다는 것이다. 전체-전체 위상도 단점이 있는데, 네트워크간 속도 차이로 인해 어떤 복제 메시지가 다른 메시지를 앞지를 수도 있다는 것이다. 이것은 이전에 다룬 인과성의 문제이다. 이벤트들을 올바르게 순서화하려면 버전 벡터 기법을 써야 한다. 복수 리더 복제를 쓴다면 이런 문제들을 알아둘 필요가 있다.

Leaderless Replication

복제의 리더는 쓰기가 수행될 순서를 결정한다. 어떤 데이터 저장소들은 리더를 두지 않기도 한다. 이 경우 클라이언트는 쓰기를 여러 복제에 직접 전달한다.

Writing to the Database When a Node Is Down

복제 중 하나가 사용 불가능하면 어떻게 할까? 리더가 없는 설정에서는 이 경우 실패가 없다. 읽기 요청도 여러 노드에 병렬로 보내지기 때문이다.

Read repair and anti-entropy

사용 불가능한 노드가 복구되면 이뤄지지 못한 쓰기를 어떻게 따라잡을까? 읽기 요청을 받은 시에 다른 노드와 비교하는 방법이 있다. 아니면 백그라운드에서 복제간 데이터 차이를 주기적으로 검사해 반영하는 방법도 있다. 모든 시스템이 이들을 구현하는 것은 아니다.

Quorums for reading and writing

쓰기 성공을 위해서는 최소 몇 개의 노드에 쓰기가 성공해야 할까? n개의 복제가 있을 때 w개 이상의 노드에 쓰기가 성공해야 하고 각 읽기마다 r개의 노드에 요청을 보낸다면, w + r > n이면 충분하다. 이 값들은 설정 가능하다. 읽기/쓰기 시 성공된 노드가 이 값을 넘지 않는지를 체크해 오류를 감지할 수 있다.

Limitations of Quorum Consistency

w + r > n이면 일반적으로 모든 읽기 요청이 가장 최근의 값을 반환한다. r, w가 n/2 이상이 되도록 할 수 있지만 다른 방법도 가능하다. w, r의 합이 n 이하이면 일관성 보장이 깨진다. 하지만 w + r > n이어도 두 개의 쓰기가 동시에 일어난다거나, 쓰기가 읽기와 동시에 일어난다거나, 하는 경우에는 일관성이 깨질 수 있다. 또한 쓰기 실패 이후에 써진 쓰기들이 롤백되지 않을 수도 있다. 그러므로 실제로는 그리 간단하지 않다. 또한, 복제 지연 관련 보장도 깨진다.

Monitoring staleness

데이터베이스가 업데이트된 결과를 반환하는지를 감시하는 것은 중요하다. 리더 기반 복제에서는 데이터베이스는 복제 지연 관련 측정치를 노출하므로 이를 검사하면 된다. 리더가 없으면 감시는 더 복잡해진다. 이 경우의 감시는 아직은 일반적인 접근이 아니다.

Sloppy Quorums and Hinted Handoff

w, r값이 적절히 설정된 데이터베이스는 전체 실패 우려 없이 각 노드의 실패를 감당할 수 있다. 하지만 네트워크 간섭 등으로 인해 w, r값에 맞추지 못하는 경우에는 시스템이 실패한다. 이 경우에 어쨌거나 w값을 맞추지 못해도 쓰기를 허용할지 말지를 결정할 수 있는데, 허용할 경우에는 가용성은 높아지지만 일관성 보장은 깨진다. 네트워크가 복구되면 복구된 노드에 쓰기 내역을 업데이트해야 한다. 이런 접근은 쓰기 가용성을 높이지만, 엄밀한 의미에서 정족수라고 할 수는 없다. 대개 이 선택을 채택할지는 선택으로 남겨둔다.

Multi-datacenter operation

리더 없는 복제는 데이터 센터가 여럿 있는 경우에도 가능하다. 이 경우에는 각 데이터 센터에 몇 개의 복제가 들어갈지를 지정할 수 있다. 이 수는 데이터 센터마다 같을 수도 있고 다를 수도 있다.

Detecting Concurrent Writes

같은 키에 대한 동시성 쓰기를 허용한다면 충돌이 발생할 수 있다. 이벤트가 다른 노드에 다른 순서로 전달될 수 있기 때문이다. 어떻게 이를 방지할까? 간략히 알아보자.

Last write wins (discarding concurrent writes)

한 가지 방법은 가장 최근의 쓰기만 허용하는 것이다. 하지만 순서가 정의되어 있지 않으므로 어느 것이 가장 최근인지 결정하는 것이 어렵다. 이 경우 정해진 타임스탬프를 각 쓰기에 붙여 해결 가능하다. 이 경우 쓰기 가용성을 일부 희생해야 한다. 캐싱처럼 쓰기 유실이 허용 가능한 경우가 있다. 이 접근이 안전한 경우에는 키가 쓰여진 후 변경되지 않을 때뿐이다.

The “happens-before” relationship and concurrency

두 연산이 동시적인지 아닌지를 어떻게 결정하나? “이전에 발생한” 경우엔 인과성이 존재할 경우가 있고 그렇지 않으면 동시적인 것이다. 그러므로 두 이벤트간에는 세 가지 경우가 있다.

Capturing the happens-before relationship

두 연산이 동시성인지, 인과성이 있는지를 판단하는 알고리즘을 알아보자. 서버는 각 키에 대해 버전 번호를 유지한다. 키를 읽을 때는 덮어씌워지지 않은 모든 값을 반환한다. 키를 쓸 때에는 이전 읽기에서 받은 버전 번호를 함께 포함한 뒤 이를 병합해야 한다. 서버가 특정한 버전 번호와 함께 쓰기를 받으면 이 버전 번호 이전의 모든 값을 덮어쓸 수 있다. 버전 번호가 부여되지 않으면 값을 덮어쓰지 않는다.

Merging concurrently written values

이 알고리즘은 동시에 쓰여진 값을 병합하는 작업이 추가로 필요하다. 이는 복수 리더 세팅에서 충돌 해결 문제와 본질적으로 같다. 이는 단순한 합집합으로 할 수도 있지만 제거 요청이 있을 때에는 이렇게 할 수 없다. 이 경우엔 CRDT 등의 자료구조를 사용한다.

Version vectors

리더 없는 복수 복제 세팅에서는 어떻게 할까? 이 경우에는 복제마다 버전 번호를 유지한다. 이 모임을 버전 벡터라 한다. 동작하는 방식은 버전 번호와 같다. 또한, 병합 과정도 똑같은 식으로 이루어져야 한다.

Summary

이 장에서는 복제에서의 여러 문제를 알아보았다. 복제는 여러 목적이 있다:

  • 높은 가용성. 한 기기가 다운되어도 시스템이 계속 동작할 수 있다.
  • 연결이 끊길 시에도 동작. 네트워크 간섭이 있을 때 애플리케이션이 계속 동작할 수 있다.
  • 지연 시간. 데이터를 지정학적으로 사용자와 가까이 놓아 사용자와 빠르게 상호작용할 수 있다.
  • 확장 가능성. 읽기를 여러 복제에 수행해 단일 기기가 대응할 수 있는 크기의 읽기보다 더 큰 읽기를 다룰 수 있다.

같은 데이터에 대한 복제를 여러 기기에 둔다는 목표는 간단해 보이지만 실제 문제는 매우 까다롭다. 이는 동시성에 대해서 매우 세심하게 고려해야 하며 잘못될 수 있는 모든 일에 대해 이 결과를 책임져야 한다. 최소한, 사용 불가능한 노드들과 네트워크 간섭에 대해서는 대처할 수 있어야 한다. 소프트웨어 버그로 인한 데이터 오염 같은 문제는 더 심하다. 복제에 대해서는 3가지 방법이 주로 존재한다.

  • 단일 리더 복제. 클라이언트는 단일 노드에 쓰기를 전송하고 이 리더는 데이터 변화 이벤트의 흐름을 다른 복제, 팔로워들에 전달한다. 읽기는 어느 복제에서나 일어날 수 있지만 팔로워에 대한 읽기는 업데이트가 안 된 상태일 수 있다.
  • 복수 리더 복제. 클라이언트는 쓰기를 여러 리더에 전송하고 이 리더들 중 어느 것도 쓰기를 받을 수 있다. 리더들은 데이터 변경 이벤트의 흐름을 각자에 전달한 뒤에 그것의 팔로워 노드들에 보낸다.
  • 리더 없는 복제. 클라이언트는 여러 노드에 쓰기를 보내고 여러 노드로부터 병렬로 읽기를 수행함으로써 오래된 데이터가 있는지를 감지하고 이를 수정한다.

각각의 접근법은 장점과 단점이 존재한다. 단일 리더 복제는 이해하기 쉽고 충돌 해소를 고려할 필요가 없으므로 인기가 높다. 복수 리더 복제와 리더 없는 복제는 노드가 실패했을 때, 네트워크가 간섭되었을 때, 지연 시간이 치솟을 때 더 강건하다. 단 이에 대한 대가로 일관성 보장이 매우 약한 수준으로만 제공되고 이를 인식하기도 더 어렵다. 복제는 동기적일 수도 있고 비동기적일 수도 있는데 이는 실패 시에 시스템 동작에서 엄청난 영향을 끼친다. 비동기 복제는 시스템이 매끄럽게 동작할 때에는 빠를 수 있지만, 복제 지연 시간이 증가하고 서버가 실패할 경우 어떤 일이 발생하는지를 알아내는 것이 중요하다. 리더가 실패하고 비동기적으로 업데이트된 팔로워가 새 리더로 승격된다면, 최근의 쓰기는 유실된다. 복제 지연으로 인해 발생할 수 있는 미묘한 효과들에 대해서도 알아보았다. 또한, 복제 지연 이후 애플리케이션이 어떻게 동작해야 하는지를 결정하는 데 있어 도움이 되는 몇 개의 일관성 모델도 알아보았다.

  • 쓰기 이후 읽기 일관성. 사용자들은 그들 자신이 쓴 데이터를 읽는다.
  • 단조적 읽기. 사용자가 한 시점에서 데이터를 보았다면 그 시점 이전의 데이터를 보아서는 안 된다.
  • 일관적인 전치 읽기. 사용자들은 인과성이 존재하는 데이터를 읽어야 한다. 예를 들면, 질문을 읽고 그에 대한 대답을 읽는 것은 올바른 순서로 이루어져야 한다.

마지막으로, 복수 리더와 리더 없는 복제 접근법에서 내재하는 동시성 문제들을 알아보았다. 이런 시스템들은 복수의 쓰기가 동시적으로 발생할 수 있음을 허용하므로, 충돌이 발생할 수 있다. 데이터베이스가 한 동작이 다른 동작 이전에 발생했는지 아니면 동시에 발생했는지를 판별하는 알고리즘을 알아보았다. 동시에 써진 값들을 병합함으로써 충돌을 해결하는 방법들도 알아보았다. 다음 장에서는 복제에 대해 대응 관계가 있는, 큰 데이터셋을 파티션으로 분할했을 때 복수의 기기에 분산된 데이터를 읽는 법을 알아본다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중