이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글입니다.

 

 

 

복제가 필요한 이유?

우리가 제공하는 API 서비스는 일반적으로 Stateless 한 형태를 준수하여 개발하여야 합니다.

 

이는 State를 다른 서비스(RDBMS, Global Session Storage - Redis 등)에서 관리하게 하고, API 서비스는 필요에 의해 조회하도록 하여 Stateful로 개발하였을 때 발생하는 API 서비스 간의 상태 동기화 이슈나 확장성 저하 문제를 최소화할 수 있기 때문입니다.

 

이러한 구조는 트래픽 급증에 대응하는 Scale-out을 가능케 합니다.

 

 

이런 형태로 개발하게 되면 결국 다수의 API 서비스가 조회하는 State를 관리하는 서비스에는 대량의 부하가 전달되어 자연스레 해당 서비스에 장애가 발생할 확률이 증가하게 됩니다. 이때 저장되어야 할 데이터가 유실되거나 (Transaction 실패, Connection Timeout 등) 또는 서비스가 중단된다면 이는 전체 서비스에 영향을 미치는 큰 문제가 됩니다. (SPoF)

 

이를 대비하기 위해 우리는 High Availability 이라는 속성을 만족하도록 서비스 설계를 진행하여야만 합니다. 그리고 이 속성을 지원하는 설계 방법 중 하나로는 복제 구조가 있습니다.

 

 

 

 

복제?

복제란 기본적으로 네트워크로 연결된 여러 물리 장비에 동일한 데이터를 분산 및 복사하여 저장하는 것을 의미합니다.

 

이를 통해 우리는 서비스 데이터를 좀 더 안정적으로 보관할 수 있으며, 다른 네트워크 간의 복제 시 특정 네트워크의 전체 장애를 대비할 수 있고 또한 동일한 데이터를 가지고 있기 때문에 이를 활용하여 서비스 API의 조회 요청을 여러 장비로 분산시킬 수도 있습니다.

  • 이렇게 서비스 API의 조회 요청을 여러 장비(RDBMS, MongoDB, Redis 등)로 분산 시키는 행위는 Read Query Load Balancing 또는 Query Off Loading이라고 합니다.
이후 설명에서는 Master - Slave 용어가 특정 시기의 차별을 나타냄으로 이제 사용되지 않기에 Origin - Replica로 대체하여 작성하도록 하겠습니다.

MongoDB나 특정 Opensource 에서는 Primary - Secondary로 명명하기도 합니다.

 

 

 

 

복제 구현 방식?

복제 로그 구현 방식으로는 크게 Statement based, Write-ahead logging based, Row based log 3가지가 존재합니다.

 

 

Statement based는 Origin 모든 쓰기 요청의 구문을 특정 영역(버퍼, 큐 등)에 기록하고 write를 수행한 다음 Replica에게 해당 기록을 전송하는 방식입니다.

 

Origin이 INSERT, UPDATE, DELETE 구문 수행 기록을 전송하면 각 Replica는 이를 파싱 하고 실행하여 보유한 데이터를 동일한 상태로 만들어 갑니다.

  • 식별 값 자동 생성, 노드가 보유한 데이터를 의존하는 처리 (UPDATE ~ WHERE) 등은 각 노드마다 정확하게 같은 순서로 실행되어야만 합니다. 인과성의 보장 필요
  • 이 방식의 장점은 복제하는 데이터가 경량이기에 Network I/O 비용이 감소하고, 감사 등의 운영 상 이슈가 발생하였을 때 대처하기 좋다는 장점이 있습니다.
  • 이 방식의 단점은 현재 날짜나 시간, 임의의 숫자를 구하는 비 결정적 함수 혹은 요청에 대해서 각 Replica마다 다른 결과를 얻고 반영하여 데이터 일관성 문제가 발생할 수 있다는 점입니다.
또한 부수 효과를 일으키는 요청 (Trigger, Procedure) 또한 결과가 비 결정적이라면 다른 결과를 얻을 수 있습니다.

 

 

이러한 문제를 해결하기 위해 Origin이 구문을 기록할 때 모든 비결정적 함수 호출과 부수 효과 요청에 대해서 고정 값을 반환하게 끔 할 수 있으나, 여러 예외 케이스가 있기 때문에 일반적으로 다른 복제 방법을 사용하는 것을 선호합니다.

  • MySQL에서는 지속적으로 구문 기반 복제를 사용해왔으며 사용된 구문에 비 결정적 함수가 존재한다면 논리적 로그 기반 복제로 변경하여 사용도록 되어 있습니다.
  • VoltDB도 구문 기반 복제를 사용하고 있지만, MySQL과 다르게 트랜잭션 단위를 결정적이 되게 끔 제약하여 문제를 해결하고 있습니다.
  • Redis 또한 구문 기반의 복제를 사용하고 있습니다.

 

 

 

 

Write ahead logging based는 말 그대로 저 수준의 로그인 WAL을 활용하여 Replica에도 동일한 복제 데이터를 구축할 수 있다는 점을 이용하는 방법입니다. 이 방식은 Postgresql과 Oracle 등에서 사용하고 있습니다.

Origin는 모든 write 요청에 대해서 WAL을 기록하고 Replica에게 Network I/O를 통해 전송합니다. 그리고 Replica는 전송받은 WAL을 그대로 반영하여서 최종적으로 Origin와 동일한 데이터를 구성하게 됩니다.

  • 이 방식의 장점은 Replica에 데이터를 복제할 때의 성능이 다른 방법보다 매우 빠르다는 점입니다. 또한 로그는 인과적으로 저장되는 것을 보장하기 때문에 WAL 동기화 비용 또한 최소화됩니다.
  • 이 방식의 단점은 WAL가 코어 수준에서 사용되는 데이터를 기술하기 때문에 저장소에서 사용하는 엔진에 대한 버전 의존성이 생겨 다른 버전을 사용하는 Origin, Replica 간의 복제가 거의 불가능하다는 점입니다.
이러한 제약 때문에 버전 업데이트 시에는 Replica를 우선 업데이트한 뒤 Origin를 업데이트해야 합니다

 

 

 

 

Row based log는 WAL에서 발생하던 문제점인 저장소 엔진 의존성을 제거하기 위해 구상된 방법으로 추상화된 다른 형태의 로그를 활용하여 데이터를 복제하도록 구현되어 있습니다. 일반적으로 로우 단위로 구성된 데이터베이스 테이블을 기준으로 write 로그를 작성하여 모든 칼럼에 속한 값을 같이 로그로 남깁니다.

 

이렇게 논리적 로그를 데이터와 WAL 중간에 추가함으로써 엔진 간의 하위 호환성을 유지하는 것이 수월해졌으며, 다른 버전이나 다른 저장소 엔진 기반으로 동작하는 노드와의 복제 또한 가능하게 되었습니다.

또한 기록된 로그를 파싱을 하는 것도 수월하여 분석, 로그 시스템, 외부 시스템에 내용 전송 등 여러 측면에서 유용한 방식입니다.

 

 

MySQL에서 사용하는 이진 로그 복제는 해당 방식을 활용합니다.

  • 이 방식의 장점은 WAL의 장점을 대부분 수용하면서도 하위 호환성을 지키고, 복제할 수 있다는 점입니다.
  • 이 방식의 단점은 Origin과 Replica의 테이블 Schema가 동일하여야 한다는 점, 실제 값을 복제하기 때문에 반영하는데 상대적으로 지연 시간이 발생하는 점 그리고 특정 기록이나 대용량의 로그를 반영하기 위해 Bin log로 쓰는데 동시성 문제가 발생할 수 있다는 점입니다.

 

이 방식에서 제공하는 로그 정보는 이렇게 기록됩니다.

  • 삭제된 Row based log는 고유하게 식별하기 위한 메타 데이터를 포함하는데 일반적으로 PK를 사용하고 PK가 없다면 모든 칼럼의 예전 값들을 Logging 하는 방식으로 구현됩니다.
  • 갱신된 Row based log는 모든 칼럼의 새로운 값과 식별하는데 필요한 메타 데이터를 포함하여 기록됩니다.
  • 여러 Row를 동시에 수정하는 Transaction의 경우 우선 그 Row 수만큼 로그 레코드를 생성한 다음 해당 Transaction이 커밋되었음을 나타내는 정보를 같이 표시합니다.

 

 

 

복제 동기화 방식?

복제 동기화 방식으로는 크게 Synchronous, Semi-synchronous, Asynchronous 3가지가 존재합니다.

 

 

Synchronous Replication은 Origin에 데이터를 반영한 직후나 반영하기 전에 Replica node에 먼저 복제 요청을 전송하여 정상 수신이 되었을 때 이후 처리를 진행하는 방식입니다. 이를 통해 분산 저장된 데이터의 일관성과 무결성을 지키는데 집중합니다.

  • 이 방식의 장점은 분산된 장비 간의 데이터 일관성과 무결성을 가지는 것을 보장한다는 점이며, Origin가 작동하지 않는 상황에서도 Replica를 통해 올바른 데이터를 사용할 수 있음을 알 수 있다는 것입니다.
  • 이 방식의 단점은 네트워크 지연 문제, Replica의 처리 지연, 장애 발생 등으로 인해 확인 요청에 대한 응답을 하지 않는다면, 새로운 데이터를 쓰는 것이 불가능한 상태에 빠진다는 점입니다. 이때 기본적으로 Origin은 Replica가 응답하기 전까지 Blocking 상태에 빠집니다.
해당 구조에서는 Origin에 장애가 발생하지 않더라도 Replica 문제로 인해 이를 바라보고 Write 하는 모든 서비스로 장애가 전파될 수 있습니다.

 

 

또한 Scale-out이 될수록 발생하는 응답을 수신하는 지연 시간이 길어지고 위와 같은 상황이 발생할 확률이 증가하여 서비스의 확장성이 매우 떨어지게 됩니다.

 

 

 

Semi-synchronous Replication은 Synchronous Replication을 현실적으로 풀어내는 방법 중 하나입니다. 해당 방식은 한 시점에 최소한 하나의 Replica와 Synchronous Replication를 진행하는데, 해당 Replica가 추가된 Transaction을 반영하면 Origin에 해당 내용을 반영합니다. 이를 통해 최소한의 데이터 무결성을 보장합니다.

  • 그 외 Replica들은 Asynchronous Replication을 수행하는 방식이며, Synchronous Replication를 진행하는 대상은 현재 (Synchronous Replication) 대상에 장애 혹은 처리 지연이 발생하여 변경될 수 있습니다.
  • 이 방식의 장점은 적어도 하나 2개의 서비스에서 데이터 일관성과 무결성을 보장한다는 점이며 그러면서도 Synchronous Replication의 단점을 최소화한다는 것입니다.
  • 이 방식의 단점은 최신의 데이터를 가진, 즉 데이터 일관성과 무결성을 보장하는 Replica가 Synchronous Replication을 수행 중인 Replica가 아닐 수 있다는 점이며 또한 여전히 다른 Replica와 Origin 간의 데이터 불일치가 발생한다는 점입니다.

조회하는 Node에 따라 데이터 일관성 깨질 수 있는 문제는 여전하지만, 현재 Node 개수와 대비하여 읽어올 읽기-쓰기 Node 개수를 조정하면 최종적으로 지연 시간과 일관성을 보완할 수 있습니다.

또한 Synchronous Replication을 통해 데이터를 넘겨받는 Node가 꼭 최신 상태가 아닐 수 있습니다. MySQL의 Bin Log를 활용한 Semi-Sync Replication은 모든 Replica가 동일한 Bin log를 바라보고 있고, 결국에는 동일한 Transaction을 같은 시점에 수신받을 수 있기 때문에 Asynchronous Replication을 수행하는 Replica가 오히려 먼저 최신 상태에 도달할 수 있습니다.

기타 설명된 동작은 구현 방식에 따라 달라질 수 있습니다.

 

 

 

Asynchronous Replication은 Origin에 반영된 데이터 정보를 Replica들이 지속적으로 수집하여 반영하는 High Availability에 좀 더 초점을 맞춘 방식입니다.

  • 이 방식의 장점은 서비스에서 요청한 데이터의 Write를 재시도를 통해 계속 보장할 수 있다는 점이며, Synchronous Replication에 비해 쓰기에 대한 서비스 지연 시간이 짧아지고 확장성이 좋다는 것입니다.
  • 이 방식의 단점은 Origin에 장애가 발생하였을 경우 네트워크 지연 등으로 인해 최신 데이터가 유실될 수 있다는 점과 Origin와 Replica 간의 데이터 일관성이 일정 시간이 지나야 만족된다는 점이 있습니다.(Eventual Consistency)
부가적으로 Asynchronous Replication을 수행하면서 새로운 Origin을 선출하였더라도 이전 Origin이 반영했던 혹은 반영 중이던 데이터를 온전히 가지지 못하게 됩니다.

 

 

이러한 문제를 해결하는 방법은 단순히 무시하고 넘어가는 것이지만 이 경우 서비스의 내구성이 매우 떨어지게 됩니다. 시스템의 확장성이나 HA가 높은 것은 좋지만 저장되어야 할 데이터의 유실은 도메인, 서비스에 따라서 심각한 문제가 되기 때문입니다.

 

그렇기에 지속적으로 좋은 성능과 HA를 제공하면서도 데이터 유실이 없는 복제 방법이 개발되고 있는 상황입니다. 이를 통해 성공적인 결과물로 구현된 방식 중 하나로는 체인 복제 방식이 있습니다.

 

 

 

 

기타

Trigger-Based Replication은 특정 데이터나 이기종 데이터베이스 간의 Replication을 위해 서비스에서 Oracle의 Golden gate, Batch Framework, 데이터베이스의 Trigger나 Procedure를 활용하는 방식입니다.

 

서비스에 복제를 위한 코드를 등록하고 데이터베이스 시스템에서 데이터가 변경이 있을 때 이벤트 등을 받거나 직접 실행하도록 하여 데이터를 가공하고 다른 서비스로 전달시킬 수 있습니다.

  • 이 방식의 장점은 해당 방식은 다른 복제 방식에 비해 유연하다는 점입니다.
  • 이 방식의 단점은 다른 복제 방식에 비해 많은 Overhead를 동반한다는 점이며, 이를 수행하는 서비스 또한 장애 지점이 될 수 있고, 작성된 코드를 통한 버그가 발생할 확률이 매우 높습니다.
Oracle의 Databus나 Postgresql의 Bucardo가 이러한 방식으로 동작합니다.

 

 

 

 

마치면서. 현재 제가 있는 팀에서는 Galera Cluster라는 Opensource를 도입하여 사용하고 있습니다. 이것은 설정에 따라 특정 동작에서 Semi-synchronous Replication과 Asynchronous Replication 방식을 지원하는 MySQL, MariaDB Cluster입니다. 요약하고 넘어가자면,

  • Multi Master 구조의 경우에는 데이터 일관성 유지를 위해 Certification-Based Replication를 사용하게 됩니다. 이는 Dead lock의 주범입니다..
  • Replica들은 기본적으로 Asynchronous Replication을 통해 데이터를 처리하다가 recv queue의 정해진 임계 값을 만족했을 때 해당 Replica와 Flow Control이라는 것을 발생시켜 이때에는 Asynchronous Replication를 중지하고 recv queue에 있는 것을 처리하는 방식으로 구현되어 있습니다.

 

Galera Cluster에서 설명하는 것을 보면 virtually synchronous replication을 수행한다고 합니다. 이는 논리적으로 완전 동기이나 write나 commit이 별개로 수행되고 replication은 비동기로 동작하기 때문에 그렇게 명명되었다고 하네요..

 

 

 

 

참고 자료

+ Recent posts