본문 바로가기

Database/MongoDB

MongoDB Replication

Intro

MongoDB의 복제에 대해서 알고 있지만 정확하게 어떻게 동작하는지는 잘 모르고 있다. MongoDB에서는 Replica Set의 형태로 데이터베이스 단에서 복제 기능을 제공하기 때문에 별도의 다른 설정 없이 쉽게 복제 구성이 가능하다. 그리고 안정적으로 동작한다. 복제 방식 쪽에서 문제가 발생할 확률이 적기 때문에 트러블 슈팅이나 깊게 공부할 기회가 적다. 따라서 이렇게 체계적인 형태로 정리하여 남기고 싶다. 공부하며 정리한 내용이기 때문에 추상적이고 정확하지 않은 정보가 있을 수 있다.

Create Oplog of Primary

Replica Set에서는 Primary가 쓰기 요청을 받는다. Primary는 DML을 받으면 다음과 같은 일들을 수행한다.

  1. DML의 데이터를 메모리상에 업데이트한다.
  2. 데이터의 영속성을 보장하기 위해 journaling을 통해 journal path에 데이터를 기록한다.
  3. 요청받은 DML을 멱등성이 보장되는 Oplog의 형태로 변경한다. 예를 들어, 1000개의 document를 업데이트 하는 DML을 받으면, 하나의 operation을 1000개의 oplog entries로 변경한다.
  4. rs.oplog 컬렉션에 데이터를 쓴다.
  5. oplog의 영속성을 보장하기 위해 journaling을 통해 journal path에 oplog 데이터를 기록한다.
  6. 추후 checkpoint 시점에 데이터 및 oplog를 Disk에 Flush한다.

DML을 oplog로 변환하는 작업은 OpObserver 에 의해 수행된다.

Contents of Oplog

Oplog는 멱등성이 지켜지는 형태인데 oplog의 실제 내용을 보면 어떻게 멱등성이 보장되는지 알 수 있다.

lsid : session id 
txnNumber : transaction number 
op : i : insert / u : update / d : delete / c : db cmd / n : no op / xi : insert global index key / xd : delete global index key 
ns : document가 위치한 namespace. database.collection 의 형태 
ui : UUID 
o : document의 최종 형태 
o2 : 수정된 document에 대한 \_id 필드.
ts : an OpTime timestamp 
t : term for election 
v : version 
wall : wall time 
prevOpTime : 이전 OpTime 

o 필드는 document가 수정된 최종 형태를 나타내고 있다. timestamp 및 wall은 oplog가 적용된 시점에 대한 정보를 갖고 있다. 따라서, 특정 시점에 document의 형태를 정의함으로써 멱등성을 보장하는 형태가 된다고 볼 수 있다.

Secondary의 Replication

SyncSourceResolver

MongoDB가 initial sync를 시작할 때 또는 BackgroundSync를 생성하거나 OplogFetcher에서 에러가 발생했을 때 새로운 sync source를 설정해야한다. Sync source는 SyncSourceResolver에 의해 결정된다. SyncSourceResolver는 신규 sync source 선택을ReplicationCoordinator에게 위임하고, ReplicationCoordinator는 TopologyCoordinator 에게 다시 요청한다.

OplogFetcher

OplogFetcher는 sync source로부터 oplog를 가져오는 역할을 한다. sync source에 dedicated connection을 생성하며, exhaust cursor를 통해서 모든 sync source로 부터 추가적인 getMore 함수를 호출하지 않고도 oplog들을 가져올 수 있다. OplogFetcher가 oplog를 가져오기 위해 sync source에 쿼리를 수행하는 경우, 자신의 마지막 oplog보다 timestamp가 크거나 같은 oplog들만을 가져오게 되는데 이렇게 되면 최소 1개 이상의 oplog를 가져오게 된다. 이를 통해서 sync source가 oplog를 가져오기 적절한 상태인지를 확인할 수 있다. 만약 oplog를 1개 이상을 가져오지 못한다면, 대상 sync source가 자신보다 너무 앞서있거나, sync source와 자신의 oplog가 동일한 상태가 아니므로 롤백이 필요한 상태임을 의미한다.

OplogFetcher는 oplog를 가져오기만 할 뿐, 적용하는데 까지는 책임이 없다. 적용에 대한 부분은 OplogWriter, OplogApplier가 담당한다.

OplogWriter

OplogWriter는 OplogFetcher가 쌓아준 OplogBuffer를 보고 이를 rs.oplog 컬렉션에 쓴 뒤, 영속성을 보장하기 위해 oplog를 journal에 flush한다. OplogWriter는 다음을 반복한다.

  1. OplogWriterBatcher으로 캡슐화되어있는 writer batcher에서 batch를 가져온다.
  2. oplog 컬렉션에 oplog entries를 쓴다.
  3. storage engine에 new oplog entries를 알림으로써 oplog visibililty를 업데이트한다.
  4. node의 lastWritten optime을 배치의 lastoptime으로 업데이트한다.
  5. storage engine에 journal flush를 요청한다.
  6. 쓰여진 oplog batch들을 OplogApplier의 buffer에 넣는다.

lastWritten이 업데이트되게 되는데, 이를 통해서 readConcern 혹은 writeConcer를 majority로 사용하는 경우 과반수에 반영된 데이터의 timestamp를 알 수 있는 것 같다.

OplogApplier

ReplBatcher thread에서는 OplogApplierBatcher가 동작하고 있다. OplogApplierBatcher는 OplogBuffer에 쌓인 oplog들을 가져오고, 이를 적용하기 위한 batch를 만든다. 이 batch는 Oplog applier batches 라고 불린다. Oplog applier batches는 batch의 사이즈에 특히 제약이 있다. Oplog applier batches는 보통 병렬로 수행되는데, command와 같이 특정 operation type이 있어 operation의 반영 순서가 보장되어야하는 경우 직렬로 수행되어야한다. 예를 들어, dropDatabase 명령의 경우는 다른 operation들과 병렬로 수행될 수 없다. 이러한 경우 OplogApplierBatcher는 Oplog applier batches의 사이즈를 1로 만든다.

OplogApplier는 만들어진 batch들에 대해 oplog를 실제로 적용해야할 책임이 있다. OplogApplier는 다음 일들을 반복한다.

  1. batcher에서 다음 oplog applier batch를 가져온다.
  2. multiple thread를 사용하여 병렬로 batch를 적용한다. 각각의 batch 내 operation들은 writer thread로 배치되고 writer thread는 이 operation들을 병렬로 수행한다. 이로 인해 같은 배치 안의 oplog들의 적용 시점은 순서가 보장되지 않는다.
  3. global timestamp ( node's lastApplied optime) 을 배치의 last optime으로 변경하여 배치를 종료한다.

writer thread들이 수행할 batch를 만드는 일은 적용되는 operation들과 연관이 있다. MongoDB에서는 document에 대한 operation의 원자성 ( atomic )과 순서를 보장하기 때문에, 같은 document에 대한 operation은 같은 writer thread에 배치되고 순서대로 수행되어야한다. 하지만 이외의 작업들에 대해서는 병렬화될 수 있기 때문에, 성능 향상을 위해 oplog의 operation들이 group화되고 개별적으로 적용된다. 즉, oplog의 반영 자체는 가능하다면 multi thread로 수행된다.

전반적인 흐름

Primary 에 기록된 oplog는 Secondary가 주기적으로 가져가게 된다. Secondary 상태에 있는 MongoDB node들은 BackgroundSync thread가 활성화되는데, Background Sync thread는 oplog를 가져가서 자신의 데이터에 반영하게 된다.

전반적인 흐름 자체는

  1. BackgroundSync thread의 시작에서, SyncSourceResolver가 sync source를 선택한다. ( 어느 멤버에서 oplog를 가져올지 결정한다. )
  2. sync source가 설정되면, OplogFetcher가 sync source로 부터 oplog를 주기적으로 가져온다.
  3. OplogFetcher가 sync source로 가져온 oplog들은 OplogWriter 또는 OplogApplier의 OplogBuffer에 쌓이게 된다.
  4. OplogWriter는 oplog를 메모리상에 반영하고 영속성을 보장하기 위해 journaling을 수행한다.
  5. OplogWriter가 영속성이 보장된 oplog를 OplogApplier의 Buffer에 기록한다.
  6. OplogerApplierBatcher(In ReplBatcher)가 OplogApplier배치를 생성한다.
  7. OplogApplier가 oplog entries를 실제 데이터에 적용 및 disk에 쓴다.

결론

일을 하면서 MySQL의 Replication Lag이 발생하는 시점에, MongoDB는 Replication Lag이 발생하지 않는 상황이 있었다. 이때는 막연하게 MySQL의 Replication은 Single Thread로 수행되고 MongoDB의 Replication은 Multi Thread로 수행되기 때문에 MongoDB의 Replication 성능이 더 좋다고 생각을 했었다. 이번 조사를 통해서 MongoDB에서는 OplogApplier가 Multi Thread의 형태로 Disk에 데이터를 쓴다는 사실을 알게 되었다. 조사를 하고 보니, 내가 겪었던 현상은 사실 Multi Thread 이기 때문이 아니라 Oplog를 메모리와 journal에만 쓴 뒤 Ack를 반환하기 때문인 것 같다는 생각이 들었다. oplog를 가져오는 OplogFetcher 자체는 Single Thread로 동작하며 Oplog Writer가 journal 및 rs.oplog collection에 데이터를 쓴 시점에 lastoptime이 업데이트 되기 때문이다.

Reference

https://github.com/mongodb/mongo/blob/master/src/mongo/db/repl/README.md

'Database > MongoDB' 카테고리의 다른 글

MongoDB Journaling  (1) 2024.10.09
mongodb 인스턴스 만들기  (1) 2023.12.07
MongoDB의 Failover  (0) 2023.11.19
MongoDB 로그 관리 : logRotate와 로그파일 권한  (1) 2023.11.16
MongoDB는 1 Petabyte를 저장할 수 있을까?  (0) 2023.08.26