MongoDB 클러스터를 구축하려고 하는데, MongoDB 에서는 Replication 이 어떤식으로 동작하는지, 주의할 점이 어떤 게 있는지, 그리고 주요한 설정 값은 뭔지 정리해보았다. MongoDB 클러스터 구축시 적절하게 활용하고자 한다.
주로 서적으로 공부를 하는 편인데, MongoDB 는 최근 시장이 매우 커짐과 동시에 버전이 계속 업데이트 되어서 레퍼런스를 찾기가 쉽지 않았다. 국문 서적중에는 가장 최근에 나온 MongoDB 완벽가이드? 이게 4.x. 버전으로 글 작성 기준의 최신 버전인 8.0 버전과 비교해서 상당히 뒤떨어져 있기 때문에, 좀 아쉬울 것 같았다. (그래도 주문은 해놓았음...)
근데 MongoDB 의 공식 문서을 봤는데, 별도로 서적이 필요 없을 정도로 매우 정리가 잘되어 있어서 이걸 레퍼런스로 공부했다. MongoDB 는 국문 번역도 되어있긴 힌데, 읽어본 결과 번역이 그냥 번역기 돌린 수준이라서 국문은 볼 이유가 없는 것간다. 개인적으로 그냥 영문본을 꾸역꾸역 읽는 것이 더 낫다고 생각한다. (이 글을 한번 훑고 보면 더 읽기 수월할 지 모른다...)
레퍼런스 : https://www.mongodb.com/docs/manual/replication/
1. Replica Set
Mysql, Redis 에서의 Replication 은 Master 와 Slave 각각의 node 가 독립적으로 존재하고, 요청을 처리하는 역할과 복제되는 역할의 구분이 뚜렷하게 나뉘어 있었다. 그러나 MongoDB 에서는 Replication 에 참여하는 node 각각은 독립적인 주체라는 느낌보다는 Replica Set 이라고 하는 하나의 큰 집합으로 동작한다. Mysql 에서 Replication 을 하기 위해서는 Slave node 에서 Master node 로 복제를 하라는 명령만 하면 되지만, MongoDB 에서는 인스턴스를 실행하는 시점부터 Replica Set 구성 정보를 포함하여 실행해야 한다. 그리고 나서 Primary 에서 모든 노드의 존재를 Replica Set 에 포함시키는 작업을 해주면 하나의 큰 Set 으로 동작하게 된다. 물론 클라이언트 쪽에서도 모든 Replica Set 을 이루는 node 의 호스트를 알아야 한다.
Replica Set 은 다음과 같이 세 종류의 node 로 구성된다.
종류 | 역할 |
Primary | 데이터 읽기와 쓰기 작업을 처리하는 노드로 Replication 의 대상이 된다. |
Secondary | Primary 로부터 비동기적으로 Replication 을 수행하며 이를 기반으로 읽기 요청도 수행할 수 있다. |
Arbiter | 데이터를 저장하지 않고 장애 상황시 선거에만 참여하여 Primary 를 선출하는 역할을 한다. (여건상 2개의 node 만 운용할 수 있는 경우, 과반을 채우기 위한 목적) |
2. Oplog
MongoDB 에서 쓰기 작업이 일어나면 두가지 로그가 생성된다. 우선, 예기치 못한 인스턴스가 종료 상황에서 데이터를 다시 복구하기 위한 데이터인 Journal 이 생성 되고, 이어서 Replication 을 위한 로그인 Oplog 가 생성된다. (둘다 동기적으로 생성되는 데이터로, 트랜잭션 성공 시점에 두 로그 모두 존재하게 된다.) 둘 다 로그성 데이터로 비슷한 개념이지만 각각의 역할도 다르고 완전히 독립적으로 운영된다. 또한 Journal 은 데이터베이스의 적용 내용을 아주 간결하게 적성하고, 주기적으로 데이터베이스에 적용되는 Write Ahead Log (WAL) 개념의 로그이고, Oplog 는 쓰기 작업 내용을 좀 더 고수준에서 표현된다는 차이도 있다. 여기서는 Journal 에 대해서는 이야기하지 않고 Replication 을 위한 데이터인 Oplog 만 알아본다.
Oplog 는 쓰기 작업과 동기적으로 수행되며, 곧바로 디스크에 쓰이는 작업이다. 쓰기 작업이 성공했는데 oplog 저장에 실패하면 클라이언트에는 실패 응답이 반환되며, 전체 트랜잭션이 롤백된다. 다시 말해, 쓰기 작업이 성공적으로 완료된 순간 디스크에는 Oplog 가 존재한다는 것이다. Replication 은 생성된 Oplog 를 Secondary 로 복제하고, 이를 기반으로 Journal 이 쓰이며 데이터베이스를 갱신한다. 때문에 복제 속도는 I/O 성능에 매우 직접적인 영향을 받는다고 할 수 있다. oplog 는 local 이라는 데이터베이스에 기록된다.
Oplog 는 쓰기 명령을 기록하는 방식이 아니라 변경 내역을 기록한다. 말하자면 Mysql 의 Row Based Replication 과 비슷한 방식이다. 때문에 비결정적인 함수 문제, 복잡한 작업에서 발생할 수 있는 충돌 문제 등에서 자유롭기 때문에 데이터 일관성 측면에서 좋다. 그러나 대량의 데이터가 변경되는 쓰기 작업의 경우, 변경된 Document 각각에 대해서 oplog 가 생성이 되므로 oplog 의 크기가 매우 커질 수 있다. 이는 읽기 성능의 저하를 유발할 수 있고 이에 따른 Replication 지연이 일어날 수 있다. 또한 oplog 컬렉션 사이즈 문제, TTL 문제 등 여러 부작용이 있을 수 있기 때문에 대규모 update 가 예상되는 경우 oplog 컬렉션 사이즈를 늘린다거나 하는 적절한 조치가 필요하다.
3. Replication 프로세스
Replication 이 일어나는 과정은 아래와 같다.
- Primary 에서 쓰기 작업이 수행되면, 해당 변경 내역에 대한 oplog 가 생성된다.
- Secondary 는 Primary 디스크에 쓰인 oplog 를 자신의 디스크에 동일하게 복제한다.
- Secondary 가 다수인 경우 Secondary 간의 oplog 복제가 일어나기도 한다.
- Secondary 는 저장된 oplog 를 토대로 동기화 작업을 수행한다.
4. Replication Lag 와 Slow Operation
상기한 대로 비동기적인 Replication 으로 인해 Primary 와 Secondary 사이에는 필연적으로 불일치가 있게 되며, 이를 두고 Replication Lag, 복제 지연이라고 한다. 때문에 일관성이 중요한 읽기 작업, 예를 들어 분산 트랜잭션이 포함된 읽기 작업은 Secondary 가 읽기 요청을 처리할 수 있는지와는 별개로 반드시 Primary 에서 수행해야한다.
Replication Lag 는 다음과 같은 요인으로 인해 커질 수도, 작아질 수도 있다.
- 낮은 디스크 I/O 성능 (oplog 복제를 수반하기 때문에)
- 높은 쓰기 부하 (쓰기 부하가 너무 크면 Replication 이 이를 따라잡지 못할 수 있다.)
- 잘못된 설계로 인해 오랜 시간이 소요되는 복제 (인덱스가 없다거나)
- 이외 네트워크 지연 및 리소스 부족 등
위와 같이 복제 지연은 여러가지 이유로 발생하게 된다. 이 때 Replication Lag 가 일어나는 원인은 Primary 에서 요청 처리에 오랜 시간이 소요 된다거나 병목이 발생하는 원인과는 결이 다르다. (왜냐하면 Primary 는 쓰기 명령을 직접 수행하는 반면 Secondary 는 변경 내역을 적용하기 때문이다.) 그래서 Primary 에서 수행하는 프로파일링과는 별도로 Secondary 에서는 Replication 관련 프로파일링을 수행한다. 이때 Replication 에 오랜 시간이 소요된는 Operation 을 Slow Operation 이라고 지칭하며, 특정 시간 기준값 설정을 해두면 그보다 더 오랜 시간이 소요된 oplog 항목을 diagnostic log 에 별도 기록한다. 개발자는 이를 토대로 인덱스를 다시 설계하거나 리소스를 추가적으로 투입하는 등의 조치를 할 수 있을 것이다. Slow Operation 값은 기본적으로 100ms 로 설정이 된다.
Replication Lag 가 불가피한 부작용이긴 하지만, 적어도 데이터의 일관성의 수준을 조정할 수는 있다. Primary node 의 쓰기 속도를 늦춰 Replication Lag 최소화하는 것이다. 이를 Flow Control 기능이라고 한다. flowControlTargetLagSeconds 설정값으로 복제 지연 시간의 최대값을 설정해두면 해당 지연시간 미만으로 유지하기 위해 Primary 는 Flow Control 을 수행한다. 기본적으로 10 초로 설정된다.
참고로 Secondary 가 2개 이상인 경우 노드마다 리소스 상황이 다 다르기 때문에 지연 시간의 차가 날 수 있는데, Primary node 는 과반이상의 Secondary node 에서 동기화 ACK 을 받아야 동기화 된 것으로 인식한다. (이를 majority committed 라 한다.)
5. 자동 Failover
Primary 가 electionTimeoutMillis 로 설정된 값(기본 10초)을 초과하여 Secondary 나 Arbiter 와 소통하지 않으면 (Heartbeat) 장애로 인식하고 Failover 과정이 시작된다. 이 과정이 끝날 때 까지 쓰기 작업은 중단되지만, 만약 Secondary 에서 읽기를 허용해 놓은 경우 읽기 요청은 계속 처리된다.
장애 발생 시, Secondary 에서 Primary 를 선출하고 정상화 되기까지 보통 12초를 넘지 않도록 설정하도록 권장된다. 12초에는 장애 발생을 감지하는 시간까지 포함되는데 서비스 중단 시간을 최소화하기 위해 electionTimeoutMillis 을 짧게 설정 해놓으면 더 자주 장애 탐지를 하게 된다. 이는 w : 1 쓰기 작업 롤백 등 부작용을 가지고 있으므로 신중하게 선택해야한다.
w : 1 쓰기란
MongoDB 의 쓰기 요청에서는 특이하게도 클라이언트 측에서 Write Concern 이라는 옵션을 줄 수 있다. 이는 내가 요청한 데이터가 MongoDB 클러스터에 얼마나 동기화 될 때까지 기다릴 것인지를 조정 할 수 있는 옵션으로 옵션 별로 다음과 같은 효과를 가진다. (서버에서는 write concern 기본값을 지정하여, 별도의 write concern 설정이 없는 경우 적용할 write concern 을 설정할 수 있다.)
Write Concern | 의미 |
w: 0 | Primary node 에 요청을 보낸 즉시 성공 응답 |
w: 1 | Primary node 에 쓰기 완료 후 성공 응답 (1 개 node 에 쓰기 보장) |
w: n(숫자) | Primary node 를 포함해 n 개 node 에 쓰기 완료 후 성공 응답 (n 개 node 에 쓰기 보장) |
w: majority | Primary node 포함 과반수 노드에 반영되면 성공 응답 |
이 때 w: 1 쓰기를 하면 아래와 같은 예시 상황이 생겨 Rollback 이 일어나게 된다.
- Primary node A 에 데이터 쓰기 성공
- 그러나 Replication Lag 으로 인해 Secondary node 인 B, C 에 해당 데이터 동기화 실패
- 이 때 A 가 다운되어 B 가 새로운 Primary node 로 승격
- 이후 A 가 복구 되었을 때, A 에만 쓰여진 데이터가 Primary 와 일치하지 않아 Rollback 발생
6. 이외 사항
클러스터를 이루는 노드마다 각 역할 관련 설정을 줄 수 있다.
- 특정 Secondary node 는 Primary node 로 승격되지 못하게 구성할 수 있다.(cold stanby)
- 특정 Secondary node 는 읽지 못하게 할 수 있다. (일반 트래픽과 분리하기 위함)
- 특정 Secondary node 는 실수로 데이터를 삭제하는 등 예기치 못한 사고에 대비해 스냅샷을 저장하도록 할 수 있다.