복제
여러 서버가 서로의 데이터를 동기화하는 것을 의미하는데 서로 주고받는 데이터에 따라 논리 복제와 물리 복제로 나뉜다.
DRBD 와 같이 리눅스 서버가 데이터 내부를 전혀 모르는 상태에서 디스크의 블록만 복제하는 형태를 물리복제라고 한다.
데이터베이스 서버가 직접 서 버간 데이터를 동기화하는 방식을 논리적 복제라고 한다.
MongoDB 의 복제는 크게 2가지 '마스터-슬레이브 복제', '레플리카 셋 복제' 로 나뉘는데
3.2 버전 이후로 마스터-슬레이브 복제는 사용되고 있지 않다.
컨센서스 알고리즘
여러 서버가 복제에 참여해서 서로 같은 데이터를 동기화하는데, 이렇게 데이터를 공유하는 그룹을 레플리카 셋이라고 한다.
그리고 하나의 레플리카 셋에는 프라이머리와 세컨드리로 각자의 역할이 나뉜다.
레플리카 셋에 참여하는 각 멤버들은 각자의 역할에 맞게 작동하면서 서로의 데이터를 동기화한다.
또 특정 노드가 응답 불능 상태가 됐을 때 어떻게 대처할 것인지를 결정한다.
이 때 어떻게 작동할지 결정하는 것을 컨센서스 알고리즘이라 하는데 MongoDB 는 확장된 형태의 Raft 컨센서스 모델을 사용한다.
참고로 MySQL 은 Paxos 알고리즘을 사용하고 있다.
Raft 컨센서스 알고리즘
가장 큰 특징은 리더 기반의 복제와 각 멤버 노드가 상태를 가진다는 것이다.
하나의 레클리카 셋에는 반드시 하나의 리더만 존재할 수 있고, 리더는 사용자의 모든 데이터 변경 요청을 처리한다.
리더는 사용자와 요청 내용을 로그에 기록하고 모든 팔로워는 리도의 로그를 가져와서 동기화를 수행한다.
Raft 컨센서스 알고리즘의 리더를 MongoDB 에서는 Primary, 팔로워를 Secondary 노드라고 한다.
그리고 로그를 MongoDB 에서는 OpLog 라고 한다.
OpLog 와 Journal Log 차이
참고로 OpLog 는 일반적인 RDBMS 의 리두로그(Redo Log)와는 다르다.
MongoDB 에서의 리두로그는 저널로그라고 부른다.
OpLog 와 Journal Log 2개가 사용되는 이유는, OpLog 는 컬렉션의 레코드 형태로 저장되기 때문이다.
또한 OpLog 와 Journal Log 가 도입된 시점과 관리주체가 다르기 때문으로
OpLog 는 MongoDB 엔진이 처리하는 반면 Journal Log 는 각 스토리지 엔진이 처리해야 하는 부분이기 때문이다.
복제의 목적
복제의 가장 큰 목적은 동일한 데이터를 이중, 삼중으로 유지함으로 써 레플리카 셋의 특정 멤버에서 데이터 손실이 발생하더라도 다른 멤버의 데이터로 대체할 수 있도록 하기 위해서이다.
즉 고가용성을 위해서 중복된 데이터 셋을 준비하는 것이다.
MongoDB 의 고가용성을 위해 레플리카 셋의 각 멤버는 서로 다른 멤버가 살아있는지 계속 확인 메시지를 주고받는데, 이를 하트비트(HeartBeat) 메시지라고 한다.
만약 특정 멤버가 통신이 되지 않으면 다른 멤버들이 새로운 프라이머리 멤버를 선출해서 서비스가 지속적으로 처리될 수 있게 해준다.
이렇게 새로운 프라이머리 멤버를 선출하는 과정은 MongoDB 샤딩이나 컨피그 서버와 무관하게 진행된다.
복제의 또 다른 목적으로는 데이터 조회 쿼리의 로드 분산을 생각해볼 수 있다.
일반적으로 고가용성만을 위한 레플리카 셋은 3대 정도의 서버로 구성하는데, 만약 데이터 조회 쿼리가 많은 서비스에서는 복제 멤버를 더 추가할 수도 있다.
레플리카 셋에서 쓰기 쿼리를 처리할 수 있는 프라이머리 멤버는 하나만 존재할 수 있다. 그래서 레플리카 셋에 멤버를 추가한다고 해서 쓰기 쿼리의 처리를 확장할 수는 없다.
다만 세컨드리 멤버가 늘어나면 늘어난 멤버 수 만큼 읽기 쿼리를 분산할 수 있다.
읽기 쿼리를 프라이머리에서 수행할지 세컨드리에서 수행할지 결정할 수 있도록 MongoDB 클라이언트 드라이버는 Read Preference 옵션을 제공한다.
MongoDB 서버는 아직 서비스 도중이 가능한 물리적인 백업기능(온라인 물리백업)을 제공하지는 않는다.
mongoexport 와 같은 도구를 이용해서 서비스 도중 논리적인 백업은 가능하다.
하지만 논리적인 백업은 데이터를 복구하기 위한 시간이 상대적으로 오래걸리기 때문에 긴급하게 복구해야 할 때는 도움이 되지 못할 수 있어, 세컨드리 멤버를 멈추고 데이터 파일을 복사해야 할 수도 있다.
레플리카 셋 멤버
하나의 레플리카 셋에는 최대 50개까지의 멤버가 복제에 참여할 수 있다.
레플리카 셋의 모든 멤버는 서로의 상태를 주기적으로 체크해야 하기 때문에, 멤버가 많을수록 주기체크에 더 큰 비용이 발생한다.
최대 50개까지의 멤버가 레플리카 셋에 참여한다 하더라도 프라이머리 멤버 선출에 참여할 수 있는 멤버는 7개까지만 가능하다.
프라이머리 선출작업 또한 복잡한 과정이라 너무 많은 멤버간의 조율이 필요하지 않도록 제한했다.
그래서 멤버가 7개 이상이라면 반드시 추가되는 멤버들은 Non-Voting 멤버로 설정되어야 한다.
이런이유로 불필요하게 많은 멤버를 추가하지 않는게 좋다.
프라이머리
데이터변경을 처리할 수 있는 멤버이다.
세컨드리
프라이머리 멤버가 처리한 변경 데이터를 실시간으로 가져와서 프라이머리와 동일한 데이터 셋을 유지한다.
프라이머리 멤버가 응답 불능상태가 되면 투표를 통해서 세컨드리 멤버 중 하나가 프라이머리 멤버가 된다.
이렇게 역할이 변경되는 것을 프로모션 또는 스텝업이라고 한다.
아비터
레플리카 셋의 멤버로 참여해서 프라이머리 선출에는 관여하지만, 실제 사용자데이터를 전혀 가지지 않고 프라이머리 멤버로부터 OpLog 를 가져오지도 않는다.
정족수를 채우기 위해 추가로 멤버가 필요한 경우에는 아비터를 사용한다.
프라이머리 선출
레플리카 셋이 프라이머리를 선출해야 하는 이유는 한 가지 인데, 해당 레플리카 셋에 현재 프라이머리 멤버가 없기 때문이다.
다양한 이유에 의해 프라이머리 멤버가 없어지거나 연결되지 안하고 나올 수 있는데 어찌되었든 프라이머리 멤버가 없으면 데이터 변경 요청을 처리할 수 없게되며 옵션에 따라 때로는 읽기 쿼리조차도 불가능 할 수 있다.
그래서 레플리카 멤버들은 프라이머리가 없어진 것을 알아채면 즉시 새로운 프라이머리를 선출하는 로직을 진행한다.
MongoDB 3.0 까지는 각 서버가 인지하고 있는 시각(Wall Clock)에 의존했는데,
운영체제의 시각은 서버마다 차이가 있을 수 있고 정확하지 않을 수 있어
프라이머리 선출을 일정시간 주기로 한 번만 실행할 수 있도록 설계했다.
하지만 이런 방식은 많은 문제점을 안고있어 3.2 버전부터는 새롭게 논리적인 시간을 도입했다.
3.0 버전까지의 프라이머리 선출 방식을 Protocol Version 0 이고 3.2 부터 Protocol Version 1 이라고 한다.
프라이머리 텀(Primary Term)
3.0 까지는 프라이머리 텀이라는 개념이 없었다. 그래서 여러 멤버가 동시에 투표를 하면 중복 투표의 위험이 있었다.
이를 막기위해 3.2 이전 버전에서는 프라이머리 선출 투표가 30초에 한 번만 실행될 수 있게 설계되었다.
즉 프라이머리 선출을 위한 투표가 한번 실패하면 그 레플리카 셋은 30초동안 프라이머리가 없는 상태로 대기해야 한다.
이 기간동안 사용자의 데이터 변경 요청을 처리할 수 없다.
3.2 이전 버전까지는 이런 중복투표나 30초 대기시간이 최대한 발생하지 않게 실제 내부적으로는 2단계 투표를 실행하도록 설계되었다.
1단계 : 사전투표
프라이머리가 되고자 하는 세컨드리는 먼저 다른 세컨드리 멤버에게 자기 자신이 프라이머리가 되려고 한다면 반대하지 않을지 확인한다.
2단계 : 본투표
다른 세컨드리 멤버들이 반대하지 않으면 그때 본 선거를 시작한다.
즉 사전선거를 통해 본 선거의 실패 상황(프라이머리를 선출하지 못하는 상황)을 최소화하고자 한 것이다.
MongoDB 3.2 부터는 프라이머리 텀이라는 개념이 도입되었다.
프라이머리 텀은 투표 식별자이며, 레플리카 셋의 각 멤버들이 프라이머리 선출을 시도할 때마다 1씩 증가하는 논리적인 시간값이다.
그래서 각 멤버들은 투표요청이 오면 30초동안 기다리는 것이 아니라 그 투표의 식별자를 기준으로 자기가 이미 투표를 했는지 아니면 다시 투표에 참여해야 하는지 결정할 수 있게 된 것이다.
또한 프라이머리 텀은 투표할 때만 사용되는 것이 아니라 프라이머리 멤버가 사용자의 데이터 변경 요청을 실행한 다음 변경내용을 OpLog 에 기록할 때마다 현재 텀(Term) 식별자를 같이 기록한다.
이 정보를 이용해 특정 OpLog 가 어느 멤버가 프라이머리였을 때의 로그인지 식별할 수 있게 해준다.
새로운 투표가 시작되는 시점에 프라이머리 텀이 시작되고 그 텀이 유지된다.
그러다 어떤 이유에서든 프라이머리에 연결을 할 수 없게 되면 새로운 프라이머리를 선출하게 된다.
이 때는 프라이머리 텀이 1 증가하게 된다.
프라이머리 선출을 위한 투표가 항상 성공하지는 않는다.
이런경우에는 프라이머리 텀 값만 증가하고 끝나게 된다.
프라이머리 스텝 다운(Primary Step Down)
레플리카 셋에서 프라이머리가 없으면 다른 세컨드리 멤버들은 모두 자신의 레플리카 셋에 프라이머리 멤버가 없다고 판단한다.
만약 프라이머리가 레플리카 셋 설정에 명시된 electionTimeoutMillis 내에 응답이 없으면 레플리카 셋의 각 멤버는 프라이머리가 없다고 판단하고 즉시 새로운 프라이머리를 선출하기 위한 투표를 시작하게 된다.
또 아래 명령을 이용해 관리자가 의도적으로 기존의 프라이머리를 세컨드리로 내리는(Step Down)것도 가능하다.
이 둘은 관리 작업을 위해 프라이머리를 세컨드리로 전환하기 위해 사용하는 명령이기도 하다.
-- 프라이머리를 스텝 다운
rs.stepDown(stepDownSecs, secondaryCatchUpPeriodSecs)
-- 레플리카 셋 멤버의 우선순위 변경
rs.reconfig()
rs.stepDown()
현재 프라이머리인 멤버에서만 실행할 수 있는데,
이 명령이 실행되면 즉시 프라이머리를 내려놓고 stepDownSecs 파라미터에 지정된 시간 동안 다시 프라이머리가 될 수 없다.
즉 다른 세컨드리 멤버가 프라이머리가 될 수 있는 여유 시간을 설정하는 것이다.
만약 stepDownSecs 시간동안 다른 세컨드리 중에서 새로운 프라이머리가 선출되지 못하면 원래 프라이머리였던 멤버가 다시 프라이머리로 선출될 가능성도 있다.
프라이머리가 없는 시간을 최소화하면 할수록 사용자의 요청을 빨리 처리할 수 있지만,
프라이머리가 다운되는 시점에 다른 세컨드리가 기존 프라이머리의 OpLog 에서 모든 변경 사항을 가져왔다고 보장하기 어렵다.
만약 데이터변경이 많아 복제가 지연된 상태라면 밀린 복제를 동기화하기 위해 더 많은 시간이 필요할 수 있다.
그래서 두번째 인자인 secondaryCatchUpPeriodSecs 파라미터 시간동안 새로운 프라이머리를 선출하지 않고 기다리면서 밀려있던 복제가 동기화되기를 기다린다.
그렇다고 무조건 이 시간동안 미루지 않고 그 전에 동기화가 완료되면 새로운 프라이머리를 선출한다.
rs.reconifg()
레플리케이션 역할을 변경하는 직접적인 명령은 아니지만, 레플리카 셋 멤버의 priority 를 변경하면 기존의 프라이머리가 즉시 세컨드리로 전환된다.
MongoDB 내부적으로 reconfig 나 stepDown 의 처리로직은 동일하다.
차이점으는 reconfig 명령으로 롤이 변경될 때 stepDown(60, 10, true) 를 실행하는 것이다.
마지막 인자 true 는 강제 모드를 의미한다.
reconfig 명령으로 Priority 를 변경하는 작업은 secondaryCatchUpPeriodSecs 를 무시한다.
즉 reconfig 는 secondary 가 복제를 동기화할 시간을 주지 않고 프라이머리 선출이 수행된다.
그래서 데이터 변경이 빈번한 경우 데이터의 롤백이 발생할 가능성이 높다.
이는 3.6 버전에서 수정될 가능성이 높다.
프라이머리 선출 시나리오
정상적인 상태의 레플리카 셋에서 각 세컨드리들은 프라이머리가 처리한 데이터 변경 내역을 복제하면서 데이터를 동기화한다.
그리고 레플리카 셋의 각 멤버들은 서로 하트비트 메시지를 주고받으면서 정상적인 상태인지 주기적으로 확인한다.
만약 3개의 멤버로 구성되어 있다면 한 주기에 6개의 하트비트 메시지가 오가고
7개의 멤버로 구성되어 잇으면 42개의 하트비트 메시지(7개 멤버가 6개의 하트비트 메시지를 전송)가 오고간다.
세컨드리 멤버는 다른 멤버로 전송한 하트비트 메시지에 대해서 지정된 시간(electionTimeoutMillis)동안 응답을 기다린다.
만약 이 시간동안 다른 세컨드리 멤버가 응답이 없으면 그 멤버가 응답 불능상태라고만 인지한다.
그러나 프라이머리 멤버가 응답이 없다면 즉시 새로운 투표를 시작하게 된다.
만약 프라이머리 멤버가 정상적으로 작동하고 있지만, 멤버간의 네트워크 이슈로 문제가 있을 수도 있다.
이를 스플릿 브레인(Split-brain) 현상이라고 한다.
그래서 MongoDB 에서는 스플릿 브레인 현상을 막기 위해서 전체의 과반수 멤버와 통신이 되지 않을 때 자동으로 프라이머리에서 세컨드리 멤버로 강등되게 설계되어 있다.
프라이머리 멤버가 서비스가 불가한 상태가 되면, 레플리카 셋에 남은 두 세컨드리는 둘 중 하나를 프라이머리로 선출한다.
이 때 Self-Election 이 발생하여 다른 세컨드리 멤버를 프라이머리 후보로 추천하지 않고 자기 자신이 바로 프라이머리 선출 투표를 게시하는데 이 때 후보는 반드시 자기 자신이다.
Self-Election 방식을 채택한 이유는 프라이머리 선출 과정이 복잡하지 않고 그만큼 쉽게 구연가능하며 직관적으로 작동하기 때문이다.
Replica01 (R01) 과 Replica02 (R02) 간에 어떻게 선출되는지를 알아본다.
1. R01 은 R02 에게 '내가 이번 텀(Term) 의 프라이머리가 되고자 한다. 찬성하는가?' 의 메시지를 전달한다.
2. R02 는 몇 가지 상태를 체크한다.
- R01 이 현재 나(R02) 와 같은 레플리카 셋에 소속된 멤버인지
- R01 의 우선수위가 현재 레플리카 셋에 있는 모든 멤버의 우선순위와 같거나 더 큰 값을 가지는지
- R01 이 요청한 텀이 내(R02)가 지금까지 참여했던 투표의 텀보다 큰 값인지
- R01 이 요청한 텀에 내(R02)가 투표한 적이 없는지
- R01 이 나(R02)보다 더 최신 데이터를 가지고 있거나 동등한 데이터를 가지고 있는지 ( OpLog 의 OpTime 시점 )
3. R02 에서 체크한 사항이 모두 TRUE 이면 R02 는 R01 에게 찬성 메시지를 보낸다.
4. R01 는 새로운 프라이머리가 된다.
만약 체크사항 중 하나라고 거짓이 있으면 R02 는 R01 에게 거부 의사를 표한다.
레플리카 셋의 멤버 중에서 과반수 이상의 멤버가 통신이 가능한 상태에서만 투표를 실시할 수 있고,
그 멤버들 중 하나라도 거부하게 되면 프라이머리 선출은 실패하게 된다.
프라이머리 선출 시 정족수 의미
MongoDB 레플리카 셋의 각 멤버는 투표 옵션의 값으로 0 또는 1 을 가진다.
0 인 멤버는 Non-Voting 멤버라고 표현하는데, Non-Voting 멤버는 프라이머리 선출 시 정족수를 판단하는 기준에 포함되지 않는다.
실제 레플리카 셋 멤버는 3개로 구성되어 있지만, 한 멤버의 votes 값이 0인 경우에
votes 가 1인 멤버중 하나가 응답불능 상태가 되면 정족수를 채우지 못하기 때문에 자동으로 세컨드리로 스텝다운된다.
다만 운좋게 0 인 멤버가 응답 불능 상태가 된다면 프라이머리 역할을 계속 수행할 수 있다.
하지만 투표권을 가지고 있다고 해서 모든 멤버가 프라이머리 선출에 참여하지는 못한다.
레플리카 셋의 멤버는 데이터 복제의 동기화 상태에 따라 여러 상태를 가질 수 있는데, 아래 상태인 경우에만 투표에 참여할 수 있다.
- PRIMARY
- SECONDARY
- RECOVERING
- ARBITER
- ROLLBACK
레플리카 멤버가 시작되면서 데이터를 가지지 않는 경우 초기 동기화를 수행하는데, 이 때 상태는 STARTUP 이며 투표권을 가진 멤버라 하더라도 실제 프라이머리 선출에는 참여할 수 없다.
한가지 주의해야 할 점은 투표권을 가지지 않은 레플리카 멤버라 하더라도 프라이머리 선출 투표에서 거부권을 행사할 수는 있다.
프라이머리 후보 이외의 다른 멤버가 더 높은 우선순위를 가지고 이싿거나 더 최신의 데이터를 가지고 있다는 것을 알게되면 바로 프라이머리 선출을 취소한다.
이는 잘못된 멤버가 프라이머리로 선출되는 것을 방지하기 위해서이다.
롤백
MongoDB 의 롤백은 관계형 데이터베이스에서 트랜잭션 커밋의 반대표현인 롤백과는 전혀 다른 의미이다.
더구나 MongoDB 에서는 트랜잭션이라는 개념이 없기 때문에 트랜잭션 롤백은 있을 수도 없는 기능이다. (작성시점의 버전은 3 으로 현재는 될듯)
MongoDB 의 롤백은 레플리카 셋의 각 멤버끼리 데이터를 동기화하는 과정에서 이미 저장된 데이터를 다시 삭제하는 과정을 말한다.
롤백은 언제 발생할 수 있고 어떻게 실행되는가?
적용된 OpLog Identity 가 아래와 같다고 한다.
P01 1 2 3 4 5
R01 1 2 3 4
R02 1 2 3
3개의 멤버로 구성된 레플리카 셋이 복제지연으로 위와 같은 상태의 데이터를 가지고 있다고 해본다.
R01 은 5 가 프라이머리로부터 동기화되지 않고 R02 는 1 2 3 까지만 되어있다.
이 때 갑자기 프라이머리 멤버가 네트워크 장애로 인해 연결불가상태가 되었다고 한다.
그러면 두 세컨드리 노드끼리 새로운 프라이머리를 선출하는 과정을 거치며 더 최신 데이터를 가진 R02 가 새로운 프라이머리로 선출된다.
R02 가 새로운 프라이머리로 선출되었기 때문에 사용자 데이터 요청이 R02 에서 이루어지고, 자신의 OpLog 에 새로운 데이터 변경 이력을 기록한다.
R01 1 2 3 4 6 7
이전 프라이머리였던 P01 멤버가 정상상태가 되고 다시 레플리카 셋으로 참여하게 된다.
참여 시 자신의 OpLog 와 새로운 프라이머리인 멤버와의 OpLog 를 동기화 해야 한다.
즉 이전 프라이머리였던 멤버는 스위칭이 발새하기 전의 OpLog 마지막 시점을 R02 노드에 맞춰야 한다.
그렇지 않고 새로운 프라이머리 멤버에서 새로 쌓이기 시작한 OpLog 를 가져오기만 하면 서로 데이터가 달라지기 때문이다.
그래서 이전 프라이머리였던 멤버는 자신의 OpLog 를 마지막부터 시작해서 검색 비교작업을 시작한다.
자신의 OpLog 를 시간 역순으로 정렬해서 일겅온다.
시간 역순의 OpLog 를 한 건씩 가져와서 새로운 프라이머리인 R02 멤버의 OpLog 에 있는지 없는지 확인한다.
없으면 무시하고 다음 OpLog 로 넘어간다.
어느 순간 공통되는 OpLog 를 찾으면 이전 프라이머리였던 멤버는 자기 자신의 OpLog 에서 공통으로 있는 OpLog 이후의 모든 OpLog 를 삭제한다.
이 과정에서 단순히 OpLog 만 삭제하는게 아니라 내용을 참조해 실제 컬렉션의 도큐먼트를 찾아서 같이 삭제하거나 변경 전 데이터로 되돌리는 작업을 진핸한다.
즉 OpLog 내용을 기반으로 이전 데이터 상태로 되돌리는 롤백작업이 진행된다.
이렇게 롤백 과정을 거치면서 삭제되거나 변경된 도큐먼트들은 MongoDB 의 데이터 디렉터리의 rollback 이라는 경로에 보관된다.
데이터베이스 관리자나 개발자는 이 롤백 디렉터리의 데이터를 이용해서 필요한 재처리 작업을 수동으로 실행할 수 있다.
MongoDB 의 롤백은 최대 300MB 까지만 가능하다. 만약 세컨듸로 동기화되지 못한 데이터가 300MB 를 넘을 정도로 많은 상태에서 스텝 다운이 발생하면 이 레플리카 셋 멤버는 새로운 프라이머리 멤버와의 동기화 지점을 찾지 못하고 에러메시지를 로그에 출력하고 중간에 복구를 종료한다.
이 경우 수동 동기화 작업을 해야한다.
replSEt syncThread: 13410 replSet too much data to roll back
또 이렇게 자동 롤백에 실패한 경우에는 복구해야 할 데이터가 rollback 경로에 기록되지 않기 때문에 복구를 위해서 전체 데이터 파일을 백업하도록 한다.
롤백 데이터 재처리
롤백 데이터는 MongoDB 서버의 데이터 디렉터리 하위에 rollback 이라는 이름으로 생성된다.
MongoDB 는 롤백으로 인해서 취소된 변경 데이터를 컬렉션 단위로 BSON 파일 형식으로 기록한다.
이 기록은 bsondump 유틸리티를 이용해 JSON 포맷으로 변환할 수 있다.
복제 아키텍처
실시간으로 변경되는 데이터는 세컨드리 멤버들이 프라이머리의 OpLog 를 가져온 다음 재생하면서 동기화한다.
세컨드리는 프라이머리로 접속해서 OpLog 를 복제할수도 있지만, 다른 세컨드리 멤버의 OpLog 를 재생할 수도 있다.
하지만 OpLog 의 재생은 실시간으로 변경되는 데이터에 동기화하는 것인데,
복제 동기화는 초기 동기화와 실시간 복제 두 단계로 나누어서 생각해 볼 수 있다.
복제로그(OpLog) 구조
복제용 로그를 MongoDB 에서는 OpLog 라고 하는데, 다른 DBMS 와는 달리 MongoDB 는 이 로그를 데이터베이스 서버의 oplog.rs 라는 이름의 컬렉션으로 기록한다.
oplog.rs 컬렉션은 다음과 같은 필드를 가지는데 모든 필드가 항상 존재하는 것은 아니며 필요한 경우에만 저장되는 필드도 있다.
필드명 | 설명 |
ts(Timestamp) | OpLog 의 저장 순서를 결정하는 기준이 되는 필드 이 필드는 2개의 값으로 구성되어 있는데 첫 번째 값은 초 단위의 Unix Epoch 을 표현하고, 두 번째 값은 동일시간에 발생된 이벤트의 논리 시간을 표현 |
t(Primary Term) | 레플리카 셋의 프라이머리를 선출하는 투표가 실행될 때마다 증가하는 값 |
h(Hash) | OpLog 의 각 도큐먼트는 프라이머리 멤버에서 실행된 변경작업을 의미, 각각의 작업에는 OpLog 의 해시 값을 이용해서 식별자가 할당되는데 이 식별자가 저장 |
v(Version) | OpLog 도큐먼트의 버전 |
op(Operation Type) | 프라이머리 멤버에서 실행된 오퍼레이션 종류를 저장 i : insert d: delete u : update c : command |
ns(Namespace) | 데이터가 변경된 대상 컬렉션의 네임스페이스 |
o(Operation) | op 필드에 저장된 오퍼레이션 타입별로 실제 변경된 정보가 저장 |
o2(Operation 2) | o 필드에는 변경될 값들을 저장하는데, u 인경우 변경될 대상 도큐먼트에 대한 정보가 필요하다. 그래서 op 필드가 u 인경우 업데이트 될 대상 도큐먼트의 프라이머리 키인 _id 필드의 정보를 저장 |
MongoDB 의 모든 컬렉션은 기본적으로프라이머리 키 역할을 하는 _id 필드가 같이 저장되어야 한다.
하지만 OpLog 컬렉션은 특별한 형태의 컬렉션이기 때문에 _id 필드를 가지지 않고 별도의 인덱스도 가질 수 없다.
OpLog 의 내용을 조회해보면 2개의 도큐먼트가 저장된 것을 확인할 수 있는데
첫 번째 도큐먼트는 레플리카 셋이 초기화되었다는 내용이며 두 번째 도큐먼트는 현재 레플리카 멤버가 새로운 프라이머리가 되었다는 내용이다.
find 명령어는 저장 및 변경된 데이터를 확인해 보기 위한 것으로 OpLog 에 저장되는 변경 이력과는 아무런 관계가 없다.
op 필드가 c 인 OpLog 도큐먼트는 test 데이터베이스에서 user 컬렉션이 생성했음을 의미한다.
o 필드에는 변경할 값을 가지고 있고 o2 필드는 변경할 도큐먼트를 찾기 위한 조건이 저장되어 있다.
MongoDB 의 OpLog 는 Cap 컬렉션으로 생성되는데, Cap 컬렉션의 경우 Tailable Cursor 를 이용하여 OpLog 에 기록되는 내용을 조회하면서 MongoDB 서버의 모든 데이터의 변경을 추적할 수 있다.
그래서 Tailable Cursor 를 이용하여 특정 컬렉션의 변경 내용을 검색엔진으로 전달해서 전문 검색이 가능하도록 할 수도 있다.
local 데이터베이스
MongoDB 의 복제 로그는 oplog.rs 라는 컬렉션을 통해서 세컨드리 멤버로 전달된다.
그런데 oplog.rs 컬렉션도 결국 데이터베이스에 존재하는 하나의 테이블에 속하는데, oplog.rs 컬렉션에 저장되는 INSERT 처리까지 세컨드리 멤버로 전달되어 버리면 이중으로 데이터가 전달되는 것이 된다.
즉 사용자가 도큐먼트 1건을 INSERT 하면 대상 컬렉션에 1건의 INSERT 를 수행하고, 세컨드리로 전달하기 위해 oplog.rs 컬렉션에도 1건의 INSERT 를 하게 된다.
그런데 사용자 프라이머리 멤버는 사용자 컬렉션에 저장되는 도큐먼트는 oplog.rs 컬렉션에도 복사해서 저장하지만,
oplog.rs 컬렉션에 INSERT 되는 데이터는 중복해서 저장하지 않는다.
그리고 세컨드리 멤버는 프라이머리 멤버의 oplog.rs 컬렉션에 저장되는 데이터만 가져가므로 2건의 도큐먼트 INSERT 가 세컨드리로 전달되지 않는다.
MongoDB 를 처음 시작하면 기본적으로 local 이라는 이름의 데이터베이스를 생성하는데, local 데이터베이스는 oplog.rs 를 포함해서 몇 개의 서버 자신을 위한 컬렉션을 생성한다.
그리고 local 이라는 이름의 데이터베이스에 저장된 컬렉션의 변경 내용은 oplog.rs 에 기록하지 않기 때문에 세컨드리로 전달되지 않는다.
local 데이터베이스가 시스템 데이터베이스이긴 하지만, 사용자가 특별한 목적을 위해 local 데이터베이스 내에 다른 컬렉션을 만들어서 사용할 수도 있다.
굳이 복제가 필요하지 않는 데이터들은 local 데이터베이스를 활용할 수 있다. 그뿐만 아니라 local 데이터베이스의 컬렉션은 세컨드리 멤버도 INSERT, UPDATE, DELETE 를 수행할 수 있다.
초기 동기화
MongoDB 서버를 처음 설치하고 레플리카 셋에 투입하면 이미 투입되어 있던 멤버로부터 모든 데이터를 일괄로 가져오는데 이를 초기동기화 라고 한다.
이런 초기 동기화 작업은 레플리카 셋에 처음 추가되거나 기존에 투입되어 있던 멤버를 다시 시작하면서 레플리카 셋에 투입되면 실행된다.
다만 투입되는 멤버의 데이터 디렉터리가 완전히 비어있는 경우에는 초기 동기화를 수행하고, 데이터 디렉터리에 이미 데이터가 있다면 초기 동기화 과정을 건너뛴다.
이렇게 이미 데이터를 가지고 있는 MongoDB 서버를 복제의 새로운 멤버로 투입하는 것을 부트스트랩이라고 한다.
버전 3.2 시점
- 초기동기화 작업은 단일 쓰레드로 실행되기에 상당한 시간이 필요하다.
데이터의 복제도 단일 쓰레드이지만 인덱스 생성도 단일 쓰레드로 생성된다.
- 초기 동기화 작업은 중간에 멈췄다가 다시 시작하는 경우 처음부터 다시 시작해야 한다.
초기 동기화 작업을 멈추고 다시 시작하는 명령은 없다.
수동 초기 동기화
다른 정상적인 레플리카 멤버의 데이터 파일을 그대로 복사해서 새로운 멤버의 데이터 디렉터리로 복사하는 수동 초기 동기화 방법이다.
이렇게 수동으로 복사하고 초기동기화를 수행하는 방식을 부투스트랩이라고 한다.
물론 데이터 파일을 물리적으로 복사하려면 기존 멤버의 MongoDB 서버를 종료하고 데이터가 변경되지 않는 상태에서 복사해야 한다. 따라서 데이터 파일을 복사하는 동안 일시적으로 레플리카 셋의 멤버가 통신할 수 없는 상태가 된다.
데이터 파일을 수동으로 복사하는 방법으로 동기화를 수행하는 경우 반드시 레플리카 셋의 멤버 중 최소 하나이상의 멤버는 백업된 시점의 OpLog 를 가지고 있어야 한다.
자동 초기 동기화
MongoDB 서버가 자동으로 다른 멤버로부터 데이터베이스를 복사하는 방법이다.
이 방법은 관리자가 복사하는 것이 아니기 때문에 관리자 운영을 필요로 하지 않는다.
자동동기화는 다음과 같은 과정을 거쳐 다른 멤버가 가진 데이터를 복사한다.
1. 데이터베이스 복제
새로 추가되는 멤버는 레플리카 셋의 특정 멤버를 복제소스로 선택하고 그 멤버에 접속하여 모든 데이터베이스의 모든 컬렉션을 읽어온 다음 자신의 데이터베이스 컬렉션에 저장한다.
이 때는 프라이머리 인덱스(_id) 만 생성한 다음 복사를 실행한다. 인덱스가 많을수록 INSERT 성능이 느려지므로 데이터베이스를 복제하는 시간이 오래 걸리게 되기 때문이다.
2. OpLog 를 이용한 일시적인 데이터 동기화
데이터베이스를 복제하는 과정은 오랜 시간이 필요하기에, 복제기간동안 다른 멤버에서 추가되어 밀린 OpLog 를 재 동기화한다.
3. 인덱스 생성
OpLog 동기화가 완료되면 필요한 모든 인덱스를 생성하는 작업을 수행한다.
실시간 복제
프라이머리 멤버는 사용자의 요청을 처리하고 그 내용을 다시 OpLog 에 기록한다.
세컨드리 멤버는 초기 동기화가 완료되면 프라이머리나 다른 더 최신의 변경 정보를 가지고 있는 세컨드리 멤버로 연결한 다음 OpLog 를 가져와서 재생하고, 자기 자신의 OpLog 에 기록한다.
이렇게 동기화되는 것을 최종 일관성이라고 한다.
복제 아키텍처
MongoDB 서버가 처리한 모든 데이터 변경 내용은 Cap 컬렉션 구조의 oplog.rs 컬렉션ㅇ 저장된다.
Cap 컬렉션은 일반적인 컬렉션(테이블)과는 달리 큐(Queue) 형태로 데이터가 저장되며, 데이터를 조회하는 작업 또한 큐와 동일한 형태로 처리된다.
Cap 컬렉션의 가장 큰 특징 중 하나는 '테일러블 커서' 기능인데, 로그 파일에 추가되는 내용을 실시간으로 보여주는 tail 명령어와 비슷한 형태로 작동하는 커서이다.
Cap 컬렉션에서만 사용할 수 있는 커서로 oplog.rs 컬렉션에 대해 커서를 생성하면 oplog.rs 컬렉션에 데이터가 추가될 때마다 커서를 통해서 최신 데이터를 보내주는 형태로 작동한다.
세컨드리 멤버의 OpLog 수집을 위한 백그라운드 쓰레드(Observer)는 복제소스로부터 OpLog 를 가져와서 로컬 MongoDB 의 메모리 큐에 저장한다.
옵저버는 동기화 대상 멤버로부터 OpLog 를 가져와서 큐에 다믄ㄴ 역할만 수행한다. 이 큐는 최대 256MB 를 넘길 수 없고 큐에 쌓인 OpLog 를 OpLog 적용쓰레드가 빠르게 가져가지 못하면 옵저버 쓰레드는 큐에 여유공간이 생길 때까지 기다리게 된다.
래플리케이션 배치 쓰레드는 큐에서 일정 개수의 OpLog 를 가져와서 OpLog 적용쓰레드 개수에 맞게 작업량을 나눈 다음 OpLog 적용 쓰레드들에게 작업을 요청한다. 기본적으로 적용 쓰레드는 16개를 사용한다. 그리고 OpLog 적용 쓰레드는 각각 5,000 개 정도의 OpLog 아이템을 담을 수 있는 자체 캐시 메모리가 있으며, OpLog 아이템의 도큐먼트 크기가 너무 클 경우 과다한 메모리 사용을 막기 위해 각 OpLog 적용 쓰레드는 최대 512MB 의 메모리만 사용할 수 있다.
그래서 16개의 OpLog 적용 쓰레드 전체적으로 최대 8GB 의 캐시 메모리를 활용할 수 있다.
만약 개수를 조절하고 싶은경우 설정파일에 setParameter 로 적용한다.
setParameter:
replWriterThreadCount: 8
프라이머리와 세컨드리의 동기화 정보 교환 과정을 살펴본다.
프라이머리 멤버는 모든 멤버의 복제 상태(각 멤버가 OpLog 어디까지 동기화했는지) 를 가지고 있다.
프라이머리는 OpLog 6 까지 생성되었고, 세컨드리는 OpLog 4 까지 복제된 상태이다.
그리고 세컨드리 멤버의 복제 쓰레드는 프라이머리 멤버의 OpLog 에 대해서 Tailable Cursor 를 생성하여 프라이머리의 OpLog 에 새로운 데이터가 기록되었는지 모니터링 한다.
Tailable Cursor 는 프라이머리의 oplog.rs 컬렉션에 5, 6 OpLog 이벤트가 기록된 것을 확인하고 세컨드리의 복제 쓰레드로 결과를 내려준다.
세컨드리의 복제 쓰레디는 큐에 프라이머리로부터 가져온 OpLog 를 저장하고 다시 프라이머리의 OpLog 이벤트를 대기한다.
큐에 OpLog 이벤트가 저장되면 레플리케이션 배치 쓰레드는 큐에서 OpLog 이벤트를 가져와서 OpLog 적용쓰레드들에게 적절하게 작업을 분산시킨다.
세컨드리 멤버가 OpLog 이벤트를 가져왔지만 프라이머리 복제상태 정보에는 세컨드리의 복제 동기화 상태가 여전히 4 값을 유지한다.
OpLog 적용 쓰레드는 각자 자기 자신에게 부여된 OpLog 이벤트를 서로 간섭없이 실행한다.
이 때 OpLog 적용 쓰레드가 실제 작업을 처리하기 직전에 MongoDB 사용자의 쿼리 요청을 처리하지 못하도록 글로벌 잠끔 쓰기를 설정한다.
그리고 OpLog 적용 쓰레드가 할당받은 OpLog 이벤트를 모두 처리하면 글로벌 쓰기 잠금을 해제하여 사용자의 쿼리 요청을 처리할 수 있게 한다.
세컨드리 멤버에서 이렇게 OpLog 가 재생될 때마다 글로벌 쓰기잠금을 걸면 이 순간동안은 읽기 쿼리가 처리되지 못하고 잠금 해제를 기다리게 된다.
일정단위의 OpLog 재생이 완료되면 세컨드리는 즉시 자기 자신이 OpLog 를 어디까지 처리했는지 프라이머리멤버에 공유한다.
프라이머리는 세컨드리 멤버의 OpLog 동기화위치 정보를 복제상태 정보에 업데이트해서 세컨드리 멤버들의 동기화정보를 최신상태로 갱신한다.
이렇게 한 싸이클이 지나면 다시 프라이머리의 복제 OpLog 를 tailable Cursor 로 모니터링하다가 새롭게 저장된 OpLog 를 세컨드리의 큐로 가져오고 처리하는 작업을 반복하게 된다.
세컨드리 멤버의 읽기 일관성
세컨드리 멤버에서 OpLog 를 재생하는 시점에 이렇게 사용자의 쿼리를 블록킹하는 이유는 세컨드리의 읽기쿼리 결과가 프라이머리에서 나타날 수 없는 상태를 세컨드리 멤버에서 보여주지 않기 위해서이다.
즉 프라이머리에서는 멀티 쓰레드로 사용자의 데이터 변경이 적용되고 적용된 순서대로 OpLog 에 숱아적으로 기록된다.
하지만 세컨드리 멤버의 복제 쓰레드는 프라이머리로부터 OpLog 를 가져와서 다시 멀티쓰레드로 실행하는데,
세컨드리에서 OpLog 가 재생되는 순서와 프라이머리에서 실행된 데이터의 변경요청 처리 순서를 동일하게 유지하는 것이 불가능하기 때문이다.
그래서 MongoDB 에서는 세컨드리 멤버가 OpLog 를 재생할 때 OpLog 이벤트를 특정 단위로 묶어서 한번에 재생하는데, 이 순간에는 글로벌 쓰기 잠금을 이용해서 사용자 쿼리 요청을 중단하는 것이다.
그리고 OpLog 의 재생이 완료되고 잠금이 해제되면 최소한 이 시점에 프라이머리와 동일한 상태의 데이터를 보여주게 된다.
글로벌 잠금이 해제되는 시점에는 이미 세컨드리의 OpLog 재생이 완료된 시점이므로 프라이머리에서와 동일한 상태의 데이터를 보여줄 수 있다.
만약 MongoDB 세컨드리가 OpLog 를 적용할 때 글로벌 잠금을 걸지 않았다면 OpLog 1번과 OpLog 3번만 적용된 상태에서도 사용자가 읽을 수 있게 된다. 하지만 이 상태를 프라이머리에서는 나타날 수 없는 상태이다.
그래서 MongoDB 서버는 이런 문제를 원천적으로 차단하기 위해서 OpLog 를 청크 단위로 나눠서 각 청크를 멀티 쓰레드로 실행하며 그동안은 글로벌 잠금을 걸어서 사용자가 읽어가지 못하게 하는것이다.
세컨드리 멤버에서 OpLog 재생을 위해 글로벌 잠금을 거는 횟수와 시간은 db.serverStatus() 명령의 결과로 확인할 수 있다.
serverStatus 결과에서 다음 2개 필드의 의미이다.
metrics.repl.apply.batches.num : 세컨드리 멤버에서 OpLog 재생은 몇 개의 OpLog 를 소그룹으로 나눠서 실행하는데, 소그룹의 OpLog 재생 프로세스가 실행된 횟수 (즉, 글로벌 잠금이 걸린 횟수 )
emtrics.repl.apply.batches.totalMillis : OpLog 재생 프로세스가 실행되는데 소요된 시간의 합 (글로벌 잠금이 걸렸다가 풀린 시간의 합계 )
'Database > MongoDB' 카테고리의 다른 글
[MongoDB] 샤딩 (0) | 2024.06.28 |
---|---|
[MongoDB] 복제(2) (0) | 2024.05.26 |
[MongoDB] WiredTiger 의 내부 작동방식 (0) | 2024.05.13 |
[MongoDB] WiredTiger 스토리지 엔진, 데이터파일 구조 (0) | 2024.05.13 |
[MongoDB] 데이터를 읽고 쓰는 방법과 프레그멘테이션 관리 (0) | 2024.05.13 |