Last Updated on 1월 16, 2024 by Jade(정현호)
안녕하세요
이번 포스팅은 MongoDB의 복제 아키텍처, 컨센서스 알고리즘 등의 복제에 관련된 내용을 추가로 정리해보려고 합니다.
해당 내용은 Real MongoDB 책 과 MongoDB Document 에 대해서 정리한 내용이며 아래 포스팅에서 이어지는 글입니다.
Contents
컨센서스 알고리즘
여러 서버가 복제에 참여하여 서로 같은 데이터를 동기화 하고 데이터를 공유하는 그룹을 레플리카 셋(Replica Set) 이라고 합니다. 그리고 하나의 레플리카 셋에는 프라이머리와 세컨더리로 각자 역할을 나누어져 있습니다.
Replica set 에 참여하는 각 멤버들은 각자의 역할에 맞춰서 작동을 하고 서로의 데이터를 동기화하고, 특정 노드가 응답 불능 상태가 되었을 때 어떻게 대처를 할 것 인지 등을 결정하게 됩니다.
이렇게 어떻게 동작 할지를 결정하는 것을 컨센서스 알고리즘(Consensus Algorithm) 이라고 하며, MongoDB 에서는 확장된 형태의 Raft 컨센서스 모델을 사용하고 있습니다.
Raft 컨센서스 알고리즘의 가장 큰 특징은 리더 기반의 복제(Leader-based Replication) 와 각 멤버 노드가 상태를 가진다는 것입니다. 하나의 레플리카 셋에는 반드시 하나의 리더(Leader)만 존재할 수 있게 되고, 리더는 사용자의 모든 데이터 변경 요청을 처리하게 됩니다.
그리고 리더는 사용자의 변경 요청 내용을 로그에 기록하고, 모든 Follow 멤버는 리더의 로그를 가져와서 데이터 동기화를 수행하게 됩니다.
Raft 컨센서스 알고리즘의 리더(Leader) 를 MongoDB 에서는 프라이머리(Primary) 노드 라고 하고, 팔로워(Follow)는 세컨더리(Secondary) 노드 라고 합니다. 그리고 세컨더리에서 로그를 가져와서 데이터 동기화를 하는 로그를 OpLog(Operation Log) 라고 합니다
더 자세한 내용은 아래 문서에서 내용을 참조해 보시기 바랍니다.
Paxos와 Raft 컨센서스 알고리즘
컨센서스 에는 대표적으로 Paxos 와 Raft 알고리즘이 있습니다.
MySQL 의 Galera Cluster 와 같이 동일한 데이터를 레플리카 셋의 아무 멤버나 변경할 수 있는 형태가 일반적으로 Paxos 알고리즘을 사용하는 것입니다.
그리고 MongoDB 서버에 기본으로 내장된 복제 방식과 같이 하나의 데이터를 레플리카 셋에서 특정 멤버만 변경할 수 있는 형태가 Raft 알고리즘에 속하게 됩니다.
다만 Raft 알고리즘을 채택한 것이지 Raft 컨센서스 알고리즘의 표준 코드를 채택 또는 그대로 사용한 것이 아니기 때문에 구현 과정에서 MongoDB 에 의해서 변형될 수 있는 것입니다.
복제의 목적
복제의 가장 큰 목적은 동일한 데이터를 다중으로 유지함으로써 레플리카 셋의 특정 멤버에서 데이터 손실이 발생하더라도 다른 멤버의 데이터로 대체할 수 있도록 하기 위함입니다. 즉 고가용성(High Availability) 을 위해서 중복(Data Redundancy) 된 데이터 셋을 준비하는 것입니다.
추가적인 복제의 목적 자세한 내용은 이전 포스팅에서 확인하시면 됩니다.
복제 아키텍처
MongoDB 의 복제는 하나의 Primary 와 여러 개의 Secondary 멤버로 구성되며, 실시간으로 변경되는 데이터는 Secondary 멤버들이 Primary의 OpLog 를 가져온 다음 다시 재생하면서 동기화를 합니다.
아래 그림과 같이 Secondary 멤버는 Primary 로 접속하여 OpLog를 복제하여 데이터 동기화 하며, Secondary 멤버는 다른 멤버의 OpLog 를 통해서 데이터 복제(재생)을 할 수도 있습니다.
OpLog 의 재생은 실시간으로 변경되는 데이터에 대한 동기화입니다.
실제 MongoDB의 복제 동기화는 초기 데이터의 동기화(Inittial Sync) 와 실시간 복제(Replication) 두 단계로 나누어서 생각 해 볼 수 있습니다.
복제 로그(OpLog) 구조
MongoDB 의 Replica set 에서 사용자가 데이터 변경 처리는 Primary 에서만 처리 할 수 있으며, Primary 멤버는 처리된 변경 내용을 별도의 컬렉션에 기록을 하게 됩니다. Replica set 의 모든 Secondary 멤버는 Primary 멤버로 부터 변경이 기록된 로그를 가져와서 다시 재생함으로써 Primary 와 Secondary 간의 데이터를 동기화를 하게 됩니다.
이 복제용 로그를 OpLog(Operation Log) 라고 하며, 다른 DBMS 와 달리 MongoDB는 이 로그를 데이터베이스 서버의 "oplog.rs" 라는 이름의 컬렉션(테이블) 으로 기록을 하게 됩니다.
oplog.rs 컬렉션은 다음과 같은 필드를 가지게 되며, 모든 필드가 항상 존재하는 것은 아니며 필요한 경우에만 저장되는 필드도 있습니다.
- ts(Timestamp)
이 필드는 OpLog의 저장 순서를 결정하는 기준이 되는 필드이며, 다른 Replica set 들이 OpLog 동기화를 잠깐 멈추거나 새로 동기화를 재시작 할 때 기준으로 삼는 필드입니다.
MongoDB의 Timestamp 필드는 2개의 값으로 구성되어 있고, 첫번째 값은 초 단위의 Unix Epoch 을 표현하며, 두 번째 값은 동일 시간(초)에 발생된 이벤트의 논리 시간을 표현한 것입니다.
두번 째 값은 그 자체로 현실 세계의 시간을 의미를 가지지 않고, 각 이벤트의 순서만 결정하는 기준이라는 의미가 되게 됩니다.
동일한 시점에 OpLog 컬렉션에 저장되는 경우가 있을 경우 첫번째 도큐먼트 논리 시간 1, 그 다음은 2, 마지막 도큐먼트는 3 이라는 값을 가지는, 즉 이벤트의 순서의 의미를 나타내게 됩니다.
- t(Primary Term)
이 값도 Timestamp 와 마찬가지로 계속 증가만 하는 값이며, 이 값은 현실 세계의 시간과 무관하게 Replica set의 Primary를 선출하는 투표가 실행될 때 마다 증가하는 값입니다.
투표가 짧은 시간에 여러 번 발생하더라도 Replica set 멤버들이 어떤 투표에 응답 인지를 식별할 수 있게 해주는 값입니다.
- h(hash)
OpLog의 각 도큐먼트는 Primary 멤버에서 실행된 데이터 변경 작업을 의미하며, 각 작업에는 OpLog의 해시 값을 이용해서 식별자가 할당되는데 이 식별자를 h 필드에 저장하게 됩니다.
-v(version)
OpLog 도큐먼트 버전을 의미하며, MongoDB 3.2 버전 부터는 버전 2를 사용하게 됩니다.
- op(Operation Type)
Primary 멤버에서 실행된 오퍼레이션의 종류를 저장하며, op필드에 저장될 수 있는 값으로는 i(Insert), d(Delete), u(Update), c(Command), n(No Operation) 등이 있습니다. 여기서 c(Command) 는 데이터베이스나 컬렉션 생성 또는 삭제 그리고 컬렉션의 속성 변경 등을 위한 것이고, n(No Operation) 은 단순 정보성 메세지들을 저장하는 경우에 사용됩니다.
- ns(Namespace)
데이터가 변경된 대상 컬렉션의 네임스페이스(데이터베이스 이름과 컬렉션의 이름의 조합)이 저장됩니다.
- o(Operation)
op 필드에 저장된 오퍼레이션 타입별로 실제 변경된 정보가 저장됩니다.
즉, 실제 컬렉션의 도큐먼트가 변경된 값을 저장하는 필드가 됩니다.
- o2(Operation 2)
"o" 필드에는 변경될 값들을 저장하는데, 오퍼레이션이 변경인 경우(op 필드가 "u" 인 경우) 에는 변경될 대상 도큐먼트에 대한 정보가 필요 합니다. 그래서 o2 필드는 u 인 경우, 즉 업데이트 될 대상 도큐먼트의 Primary Key 인 "_id" 필드의 정보가 저장되게 됩니다.
OpLog 내용 확인
구성된 MongoDB에서 OpLog 내용을 확인해보도록 하겠습니다.
먼저 도큐먼트 하나를 입력하겠습니다.
use test db.users.insertOne({username:"Tom"})
그 다음 local DB 이동 후 oplog 를 조회해보겠습니다.
use local DBQuery.shellBatchSize=1000 db.oplog.rs.find({ns: "test.users"}).pretty() <--!! 결과 { "op" : "i", "ns" : "test.users", "ui" : UUID("5e7255a2-da73-4da4-95d2-53cd6b1b468b"), "o" : { "_id" : ObjectId("624a731ac95dbb042b281a19"), "username" : "Tom" }, "ts" : Timestamp(1649046298, 1), "t" : NumberLong(2), "v" : NumberLong(2), "wall" : ISODate("2022-04-04T04:24:58.124Z") }
MongoDB의 모든 컬렉션은 기본적으로 Primary Key 역할을 하는 "_id" 필드가 같이 저장되게 됩니다.
하지만 OpLog 컬렉션은 특별한 형태의 컬렉션이기 때문에 "_id" 필드가 존재하지 않으며 또한 별도의 인덱스를 가질 수 없습니다.
Note
DBQuery.shellBatchSize 는 find() 로 컬렉션 조회 시 20건 조회 후 Type "it" for more 내용이 나오는 부분에 대해서 한번의 출력 단위를 20건에서 변경하는 내용입니다.
위의 예제에서 컬렉션에 도큐먼트 하나를 추가(insert) 하였고 OpLog 의 op 필드는 "i" 으로 확인됩니다.
ns 에는 네임스페이스 정보인 데이터베이스. 콜렉션 정보가 표기되며, o 필드에는 변경에 대한 정보가 기록되어 있습니다.
update 인 경우에는 o2 필드도 표기가 되게 됩니다.
ts 는 첫번째 값은 초 단위의 Unix Epoch 을 표현하며, 두 번째 값은 동일 시간(초)에 발생된 이벤트의 논리 시간(순서)을 표현한 것 으로 Unix Epoch 는 리눅스의 경우 아래와 같이 date 명령어로 convert 를 해서 우리가 익숙한 시간으로 내용을 확인할 수 있습니다.
$ date -d @1649046298 Mon Apr 4 13:24:58 KST 2022
v는 OpLog 도큐먼트 버전을 의미하며, MongoDB 3.2 버전 부터는 버전 2를 사용하게 되며 위의 조회 결과에서도 "v" : NumberLong(2) 으로 확인되고 있습니다.
OpLog Status
OpLog 의 Status 와 사이즈, 작업의 크기 및 시간 범위 등은 rs.printReplicationInfo() 명령어를 통해 확인하면 됩니다.
test-rs-0:PRIMARY> rs.printReplicationInfo() configured oplog size: 2000MB log length start to end: 571097secs (158.64hrs) oplog first event time: Tue Mar 29 2022 00:54:52 GMT+0900 (KST) oplog last event time: Mon Apr 04 2022 15:33:09 GMT+0900 (KST) now: Mon Apr 04 2022 15:33:11 GMT+0900 (KST)
OpLog 는 데이터베이스에서 발생되는 모든 수정 작업에 대한 내용을 기록하며, 특수한 capped collection 에 저장하게 됩니다.
capped collection 는 최대 크기에 도달하면 가장 오래된 항목을 자동으로 덮어쓰는 고정 크기 컬렉션이며, MongoDB OpLog 는 capped collection 입니다.
MongoDB 4.0 부터 일반적인 capped collection 과 달리 oplog는 majority commit point 이 삭제되는 것을 피하기 위해 구성된 크기 제한을 초과하여 커질 수 있습니다.
majority commit point 는 Read Concern/Write Concern 와 연관된 내용으로 향후에 별도의 포스팅에서 기술하도록 하겠습니다.
MongoDB 4.4 버전 부터는 minimum oplog retention period(시간/hour) 지정 기능을 지원하며 MongoDB는 다음과 같은 조건에만 oplog 항목을 제거하게 됩니다.
- oplog가 구성된 최대 크기에 도달한 경우, 그리고
- OPLOG entry가 설정된 시간보다 오래된 경우
기본적으로 MongoDB는 최소 oplog 보존 기간을 설정하지 않으며 구성된 최대 oplog 크기를 유지하기 위해 가장 오래된 항목부터 시작하여 oplog를 자동으로 자릅니다.
Oplog 크기와 보관주기를 변경하기 위해서는 conf 파일에 설정 후 MongoDB를 재시작 하거나 db.adminCommand를 통해 온라인 중에 변경이 가능합니다.
• mongod.conf
storage: dbPath: <string> journal: commitIntervalMs: <num> directoryPerDB: <boolean> wiredTiger: engineConfig: cacheSizeGB: <number> journalCompressor: <string> directoryForIndexes: <boolean> <..중략..> oplogMinRetentionHours: <double> replication: oplogSizeMB: 9000
Oplog 사이즈는 replication 항목의 oplogSizeMB에서 설정하고, 보관주기는 storage 항목의 oplogMinRetentionHours 에서 설정합니다.
• command
db.adminCommand( { replSetResizeOplog: <int>, size: <double>, minRetentionHours: <double> } )
보관시간 설정인 minRetentionHours(oplogMinRetentionHours)의 타입은 double이며 소수점으로 설정 가능합니다. 예를 들어 값이 1.5인 경우, 1시간 30분을 의미합니다.
local 데이터베이스
MongoDB 의 복제 로그는 oplog.rs 라는 컬렉션을 통해서 Secondary 멤버로 전달되게 됩니다.
그런데 oplog.rs 컬렉션도 결국 데이터베이스 안에 존재하는 하나의 콜렉션에 속하는데, oplog.rs 컬렉션에 저장되는 INSERT 처리까지 Secondary 멤버로 전달되게 된다면 이중으로 데이터가 전달되는 것이 되게 될 것입니다.
하지만 사용자 컬렉션에 데이터가 입력되게 되면, 복제를 위해서 oplog.rs 컬렉션에 해당 내용을 insert 하지만, oplog.rs 에 insert 한 행위는 oplog.rs 컬렉션에 입력하지는 않습니다.
그리고 Secondary 멤버는 Primary 에서 생성된 OpLOG 를 가져와서 Secondary 에서 재생하게 되면서 별도의 중복 또는 불필요한 입력을 하지는 않게 됩니다.
MongoDB의 처음 시작 시 기본적으로 local 을 비롯해서 몇개의 기본적인 데이터베이스를 생성하게 되며 그 중에서 local 데이터베이스는 oplog.rs 를 포함해서 몇 개의 MongoDB 를 위한 컬렉션이 생성하게 됩니다.
그리고 local 데이터베이스에 있는 컬렉션의 변경 내용은 oplog.rs 에 기록하지 않기 때문에 Secondary 멤버로도 전달되지 않게 됩니다.
local 이 이러한 기능을 하는 데이터베이스이지만, 사용자가 특별한 목적을 위해서 local 데이터베이스내에서 별도의 컬렉션을 만들어서 사용할 수 있습니다.
복제가 불필요한 데이터가 특별 한 용도로 사용할 수 있을 것 같으며 MongoDB 서버의 OS 모니터링 정보나 임시성 데이터가 그것이 될 것입니다.
초기 동기화(Initial Sync)
MongoDB 서버를 처음 설치하고 데이터 디렉토리가 완전히 비어 있는 상태로 레플리카 셋에 투입하게 되면 MongoDB 서버는 이미 투입되어 있던 다른 멤버로 부터 모든 데이터를 일괄로 가져와야 하며, 이 과정을 초기 동기화(Initial Sync) 라고 합니다.
이런 초기 동기화 작업은 Replica set 에 처음 추가되거나 기존에 투입되었던 멤버가 재시작 하면서 Replica set 에 투입하게 되면 실행됩니다
다만 Replica set 에 투입되는 멤버의 데이터 디렉터리가 완전히 비어 있는 경우에는 초기 동기화를 수행하고, 데이터 디렉토리에 이미 데이터가 있다면 초기 동기화 과정을 건너 뛰게 됩니다.
이렇게 이미 데이터를 가지고 있는 MongoDB 서버를 복제의 새로운 멤버로 투입하는 것을 부트스트랩(Bootstrap) 이라고 합니다.
이렇게 초기 동기화는 아래 와 같은 내용으로 주의가 필요 합니다.
- 초기 동기화 작업은 단일 쓰레드로 진행되기 때문에 상당히 많은 시간이 필요 할 수 있습니다. 데이터의 복제(clone)도 단일 쓰레드이지만 인덱스 생성도 하나씩 단일 쓰레드로 생성되므로 다른 RDBMS 등 보다 많은 시간이 소요될 수 있습니다.
- 초기 동기화 작업은 중간에 멈추었다가 다시 시작하는 경우 처음부터 다시 시작해야 합니다. 또한 실제 초기 동기화 작업을 멈추고 다시하는 명령어는 없습니다. 따라서 초기 동기화를 멈추려면 MongoDB 서버를 종료해야 합니다.
수동 초기 동기화
이전 단계에서 설명한 초기 동기화에는 "수동 초기 동기화" 와 "자동 초기 동기화" 2가지 방법으로 실행할 수 있습니다
먼저 수동 초기 동기화 방법은 정상적으로 사용중인 Replica set 멤버의 데이터 파일을 그대로 복사해서 새로운 멤버의 데이터 디렉토리로 복사해서 사용하는 방법입니다.
이렇게 데이터 파일을 수동으로 복사하고 초기 동기화를 수행하는 방식을 부트스트랩이라고 하며, 데이터 파일을 물리적으로 복사하려면 기존의 Replica set 멤버의 MongoDB 서버는 종료 후에 데이터 파일을 복사해야 합니다. 따라서 이 작업 과정에서는 해당 멤버를 사용할 수 없게 됩니다.
물론 LVM Snapshot 과 같은 백업이 있다면 해당 백업을 사용해서 새로운 멤버의 데이터 디렉토리를 초기화 할 수도 있습니다.
이렇게 할 때는 반드시 기존 멤버의 데이터 파일 디렉토리에 있는 모든 파일(특히 local 데이터베이스를 포함)을 그대로 가져와야 합니다.
이렇게 수동으로 데이터 파일을 복사해서 사용하는 방법의 경우 Replica set 멤버 중에서 복사 시점의 OpLog 를 가지고 있어야 합니다.
그래야지 복사 본 이후의 데이터 변경에 대해서 OpLog를 통해서 최신 시점까지 재생이 가능 합니다.
자동 초기 동기화
자동 초기 동기화 방법은 MongoDB 서버가 자동으로 다른 멤버로 부터 데이터베이스를 복사하는 방법입니다.
이 방법은 관리자가 직접 데이터를 복사하는 것이 아닌 MongoDB 서버가 자동으로 데이터를 복제하는 것이기 때문에 별도로 오퍼레이션은 필요 하지 않습니다.
아래 과정을 거쳐서 멤버로 부터 데이터를 복사하게 됩니다.
1) 데이터베이스 복제(Clone)
새로 추가된 멤버는 Replica set 의 특정 멤버를 복제 소스(Source) 로 선택하고, 그 멤버로 접속하여 모든 데이터베이스의 모든 컬렉션을 쿼리 해서 읽어 온 다음 자기 자신의 데이터베이스 컬렉션에 저장하게 됩니다.
초기 데이터를 가져오는 시점에는 컬렉션의 Primary Index("_id") 만 생성한 다음 복사를 실행하게 됩니다.
(인덱스가 많다면 초기 데이터 복제에 시간 소요가 더 오래 걸리기 때문)
2) OpLog 를 통한 데이터 동기화
데이터베이스를 복제 과정은 데이터에 따라서 상당히 오래 걸릴 수 있습니다.
하지만 OpLog의 용량은 제한적이기 때문에 너무 오랜 시간이 걸리면 초기 동기화에 실패하게 됩니다.
3) 인덱스 생성
초기 데이터베이스 복제를 수행하는 시간 동안 적용하지 못하고 밀려 있던 OpLog를 모두 재생하고 나면 다시 필요한 모든 컬렉션의 인덱스를 생성하는 작업을 수행하면서 동기화 작업은 마무리가 되게 됩니다.
해당 포스팅은 Real MongoDB 책의 많은 내용 중에서 일부분의 내용만 함축적으로 정리한 것으로 모든 내용 확인 및 이해를 위해서 직접 책을 통해 모든 내용을 확인하시는 것을 권해 드립니다.
이어지는 글
Reference
Reference Book
• Real MongoDB
Reference URL
• mongodb.com/replica-set-oplog
• mongodb.com/consensus-algorithm-mongodb-3-2
• mongodb.com/#std-term-capped-collection
• mongodb.com/read-concern-majority
관련된 다른 글
Principal DBA(MySQL, AWS Aurora, Oracle)
핀테크 서비스인 핀다에서 데이터베이스를 운영하고 있어요(at finda.co.kr)
Previous - 당근마켓, 위메프, Oracle Korea ACS / Fedora Kor UserGroup 운영중
Database 외에도 NoSQL , Linux , Python, Cloud, Http/PHP CGI 등에도 관심이 있습니다
purityboy83@gmail.com / admin@hoing.io