Skip to content

32. MongoDB Replication Part 2. - Replica Sets Part 1

이번 포스팅에서는 MongoDB의 가장 중요한 개념 중 하나인 Replica Set에 대해 알아보도록 하겠습니다.

Replica Set은 기본적으로 자동 패일오버 기능을 갖는 Master-Slave 클러스터이지만, Master-Slave 클러스터의 가장 큰 차이점은 master 노드가 가변적이라는 것입니다. 즉, 현재 master 노드로 지정된 인스턴스가 다운될 경우 다른 노드가 master 노드로 자동 지정된다는 점입니다.

Master-Slave 클러스터와 Replica Set과의 공통점은, 단 하나의 master 노드(PRIMARY 노드라고도 함)와 여러 개의 slave 노드(SECONDARY 노드라고도 함)를 갖는다는 점입니다.

개념적으로는 별 것 아니지만 실행에 있어서 어려움이 있을 것이란 예상이 들기도 합니다. 하지만, 한번 연습해 보면 그리 어려운 것은 아닐 것입니다. 단지, mongod 인스턴스 실행 시 공통의 Replica Set 이름을 지정해 주면 됩니다. 물론 이것이 다는 아니지만 비교적 간단한 절차에 의해 Replica Set을 구성할 수 있습니다. 그리고 작동 환경의 성능에 따라 SECONDARY 노드를 추가하는데에는 특별한 제한은 없습니다.

[그림 1.]은 Replica Set에 따른 자동 패일오버에 대한 개념을 나타낸 것이다.

[그림 1.] PRIMARY 노드의 패일오버 시 새로운 PRIMARY 자동 지정.
[그림 2.] 하나의 PRIMARY 노드에 여러 개의 SECONDARY 노드가 연결된 모습.
[그림 3.] 만약 기존의 PRIMARY 노드의 연결이 끊어지면 SECONDARY 노드 중 하나가 PRIMARY 노드로 전환.
[그림 4.] 연결이 끊겼던 노드가 복귀되면 SECONDARY 노드로 전환되어 연결.

Replica Set을 구성하는 전체적인 절차는 다음과 같습니다:

  1. mongod 인스턴스를 각각의 노드에 대해 실행.
  2. Replica Set에 대한 환경설정.
  3. 각 slave 노드에 대한 mongod 인스턴스 추가.

그러면 위에 설명된 절차를 구체적으로 튜토리얼 형식으로 4개의 노드로 구성된 Replica Set 구성하는 방법에 대하여 알아보도록 하겠습니다.

1. 데이터 디렉터리에 4개의 노드에 대한 각각의 디렉터리를 만들기

예를 들어 [그림 2.]와 같이이 이름을 node_1, node_2, node_3, node_4로 하였습니다.

[그림 5.] 각 노드에 대해 데이터 디렉터리를 생성한 예.

2. 각각의 노드에 대한 mongod 인스턴스를 실행

  • 첫번째 노드에 대한 mongod 인스턴스 실행:

    $ mongod --port 27017 --dbpath [DATA_PATH]/node1/ --replSet rs0 --smallfiles --oplogSize 128

  • 두번째 노드에 대한 mongod 인스턴스 실행:

    $ mongod --port 27018 --dbpath [DATA_PATH]/node2/ --replSet rs0 --smallfiles --oplogSize 128

  • 세번째 노드에 대한 mongod 인스턴스 실행:

    $ mongod --port 27019 --dbpath [DATA_PATH]/node3/ --replSet rs0 --smallfiles --oplogSize 128

  • 네번째 노드에 대한 mongod 인스턴스 실행:

    $ mongod --port 27020 --dbpath [DATA_PATH]/node4/ --replSet rs0 --smallfiles --oplogSize 128

--port는 node 별로 다르게 지정하였으며, --dbpath는 방금 생성한 노드 별 데이터 디렉터리로 지정합니다.

Replica set의 이름은 rs0으로 --replSet 옵션으로 지정됩니다. --smallfiles--oplogSize 옵션은 디스크 공간을 적게 차지하기 위한 것으로서 개발이나 테스트 단계에서 머쉰의 부담을 줄일 수 있는 유용한 옵션입니다.

3. mongo 쉘을 통해 앞서 실행한 mongod 인스턴스 중 하나에 연결

이 때 포트 번호를 명시해야 합니다. 예를 들어 포트번호 27017의 mongod 인스턴스에 연결합니다:

$ mongo --port 27017

4. rsconf라는 변수에 Replica Set의 환경설정 정보를 저장

이 때 hostname을 입력하는 부분이 있습니다. Mac OS의 경우에는 터미널을 실행할 때 프롬프트를 확인하면 됩니다:

[그림 6.]에서 보듯 Mac OS의 경우 시스템 환경설정 > 공유 > 컴퓨터 이름 > 편집에서 로컬 hostname을 확인할 수 있습니다.

[그림 6.] Mac OS에서 터미널을 실행하여 hostname 확인. 그림의 예에서 로컬 hostname은 "gchoiui-MacBook-Pro"이다.

Windows의 경우에는 [그림 7.]과 같이 커맨드라인 명령 콘솔을 실행하여 ipconfig -all을 입력하여 hostname을 확인할 수 있습니다.

[그림 7.] Windows에서 DOS 명령 콘솔을 실행하여 hostname 확인.

hostnamegchoi로 가정하고 rsconf 환경설정을 입력합니다:

> rsconf = {_id: "rs0", members: [{_id: 0, host: "gchoi:27017"}]}
{
        "_id" : "rs0",
        "members" : [
                {
                        "_id" : 0,
                        "host" : "gchoi:27017"
                }
        ]
}
  • Tip: 로컬 작업 시 hostnamegchoi 대신 localhost로 입력하여도 됩니다. 즉, 다음과 같이 입력할 수 있습니다. 만약 이 방식으로 작업한다면 이후 나오는 모든 gchoilocalhost로 바꾸어 입력하도록 합니다.
> rsconf = {_id: "rs0", members: [{_id: 0, host: "localhost:27017"}]}
{
        "_id" : "rs0",
        "members" : [
                {
                        "_id" : 0,
                        "host" : "localhost:27017"
                }
        ]
}

5. rs.initiate() 메써드를 통해 Replica Set을 초기화

> rs.initiate( rsconf )
{
        "info" : "Config now saved locally.  Should come online in about a minute.",
        "ok" : 1
}

초기화가 성공적으로 수행되면 위와 같이 ok가 1을 갖습니다.

6. 현재 포트번호 27017인 PRIMARY에 mongo 쉘이 연결되어 있으며, res.add() 메써드를 이용하여 나머지 SECONDARY(포트번호 27018, 27019, 27020)들을 Replica Set에 추가

추가 시, 자신의 시스템의 hostname 이름으로 변경하는 것을 잊지 말시기 바랍니다.

> rs.add("gchoi:27018")
{ "ok" : 1 }
rs0:PRIMARY> rs.add("gchoi:27019")
{ "ok" : 1 }
rs0:PRIMARY> rs.add("gchoi:27020")
{ "ok" : 1 }

눈여겨 보아야 할 것은, 포트번호 27018의 SECONDARY를 추가한 후에는 프롬프트가 >에서 rs0:PRIMARY>로 변경되어 있는 것입니다.

7. Secondary 중 하나를 mongo 쉘로 연결

$ mongo --port 27018
MongoDB shell version: 2.4.9
connecting to: 127.0.0.1:27018/test
rs0:SECONDARY>

예를 들어 위와 같이 포트번호 27018로 연결하면 프롬프트가 rs0:SECONDARY>로 되어 있는 것을 확인할 수 있을 것입니다.

8. 현재 primary로 지정된 멤버와 SECONDARY로 지정된 멤버를 확인

Replica Set에 대한 정보는 local.system.replset에 저장되어 있으며, 다음과 같이 입력하면 확인할 수 있습니다 (mongo 쉘이 PRIMARY든 SECONDARY든 상관없습니다):

rs0:PRIMARY> use local
switched to db local

위와 같이 dblocal로 전환하고, local의 컬렉션 리스트를 확인해 보도록 합니다:

rs0:PRIMARY> db.getCollectionNames()
[
        "me",
        "oplog.rs",
        "replset.minvalid",
        "startup_log",
        "system.indexes",
        "system.replset"
]

컬렉션 중 system.replset이 있는 것을 확인할 수 있을 것입니다. 이제 find() 메써드를 통해 PRIMARY와 SECONDARY 멤버를 확인해 보겠습니다:

rs0:PRIMARY> db.system.replset.find().pretty()
{
        "_id" : "rs0",
        "version" : 4,
        "members" : [
                {
                        "_id" : 0,
                        "host" : "gchoi:27017"
                },
                {
                        "_id" : 1,
                        "host" : "gchoi:27018"
                },
                {
                        "_id" : 2,
                        "host" : "gchoi:27019"
                },
                {
                        "_id" : 3,
                        "host" : "gchoi:27020"
                }
        ]
}

9. 마지막으로 rs.status() 메써드를 통해 현재 상태를 확인

rs0:PRIMARY> rs.status()
{
        "set" : "rs0",
        "date" : ISODate("2014-03-28T11:15:57Z"),
        "myState" : 1,
        "members" : [
                {
                        "_id" : 0,
                        "name" : "gchoi:27017",
                        "health" : 1,
                        "state" : 1,
                        "stateStr" : "PRIMARY",
                        "uptime" : 819,
                        "optime" : Timestamp(1396004648, 1),
                        "optimeDate" : ISODate("2014-03-28T11:04:08Z"),
                        "self" : true
                },
                {
                        "_id" : 1,
                        "name" : "gchoi:27018",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 722,
                        "optime" : Timestamp(1396004648, 1),
                        "optimeDate" : ISODate("2014-03-28T11:04:08Z"),
                        "lastHeartbeat" : ISODate("2014-03-28T11:15:56Z"),
                        "lastHeartbeatRecv" : ISODate("2014-03-28T11:15:55Z"),
                        "pingMs" : 0,
                        "syncingTo" : "gchoi:27017"
                },
                {
                        "_id" : 2,
                        "name" : "gchoi:27019",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 714,
                        "optime" : Timestamp(1396004648, 1),
                        "optimeDate" : ISODate("2014-03-28T11:04:08Z"),
                        "lastHeartbeat" : ISODate("2014-03-28T11:15:57Z"),
                        "lastHeartbeatRecv" : ISODate("2014-03-28T11:15:57Z"),
                        "pingMs" : 0,
                        "syncingTo" : "gchoi:27017"
                },
                {
                        "_id" : 3,
                        "name" : "gchoi:27020",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 709,
                        "optime" : Timestamp(1396004648, 1),
                        "optimeDate" : ISODate("2014-03-28T11:04:08Z"),
                        "lastHeartbeat" : ISODate("2014-03-28T11:15:56Z"),
                        "lastHeartbeatRecv" : ISODate("2014-03-28T11:15:55Z"),
                        "pingMs" : 0,
                        "syncingTo" : "gchoi:27017"
                }
        ],
        "ok" : 1
}

현재 gchoi:27017이 PRIMARY로 지정된 것을 확인할 수 있으며, 나머지는 SECONDARY로 지정되어 있는 것을 확인할 수 있습니다. 그리고 SECONDARY는 모두 gchoi:27017으로 동기화되는 것과 마지막으로 동기화 된 시간을 확인할 수 있습니다.

그 외 Replica Set에 관련된 메써드들은 MongoDB 도큐먼트를 참고한다.

이렇게 해서 Replica Set을 구성하는 방법을 튜토리얼 형식으로 설명하였습니다.

Replica Set에는 서로 다른 유형의 노드들이 존재하는데 각 노드들의 특징에 대해 간단하게 알아보도록 하겠습니다.

  • PRIMARY

    PRIMARY 노드는 Replica Set에서 유일하게 데이터를 기록할 수 있는 권한을 가지고 있는 멤버입니다. 즉, 쓰기에 대한 오퍼레이션을 가지고 있습니다. MongoDB는 PRIMARY 상에서 쓰기 오퍼레이션을 수행하고 이러한 오퍼레이션을 PRIMARY의 oplog에 기록합니다. SECONDARY는 이 로그를 복제하고 각자의 데이터 세트에 오퍼레이션을 동일하게 적용합니다.

  • SECONDARY

    SECONDARY는 PRIMARY의 데이터 세트의 복제를 유지합니다. 데이터 복제를 위해 SECONDARY는 PRIMARY의 oplog로부터 오퍼레이션들을 비동기 방식으로 자신의 데이터 세트에 적용합니다.

  • ARBITER

    ARBITER는 "결정권자"라는 뜻입니다. 이름이 의미하는 바와 같이 ARBITER는 PRIMARY의 패일(fail) 시, 새로운 PRIMARY 선출을 위해 "투표"에 참여할 수 있는 멤버입니다. 그러나, ARBITER는 데이터 PRIMARY의 데이터를 복제하지 않으며 PRIMARY가 될 수도 없습니다.

    ARBITER 멤버의 수는 홀수여야 합니다. 만약 짝수로 지정되면 PRIMARY 선출 시 동일 득표를 얻는 일이 발생할 수 있습니다.

    또한 주의할 사항은, Replica Set의 PRIMARY 또는 SECONDARY 멤버를 호스팅하는 동일한 시스템에서 ARBITER를 구동하면 절대 안 됩니다!

    ARBITER를 추가하는 방법은 비교적 간단합니다.

    다음과 같이 mongod 인스턴스를 실행합니다:

    $ mongod --port 30000 --dbpath [DATA_PATH] --replSet rs

    mongo 쉘을 통해 PRIMARY에 연결하고 Replica Set에 ARBITER를 추가합니다:

    > rs.addArb("gchoi-PC:30000")

    이제 PRIMARY가 어떻게 선출되는지 그 과정에 대해 알아보도록 하겠습니다. PRIMARY 선출은 Replica Set이 초기화 될 때와 PRIMARY가 연결이 끊기는 시기에 일어납니다. 그러면 PRIMARY 선출에 영향을 미치는 요소들은 무엇일까요?

  • Hearbeats

    Replica Set에 등록된 멤버들 간에는 매 2초마다 서로가 서로에게 heartneat (ping)을 주고 받습니다. 만약 어느 특정 멤버가 10초 이내에 응답이 없으면, 다른 멤버들은 이 멤버가 접근할 수 없는 죽은 멤버로 판단합니다. 만약 현재의 PRIMARY의 heartbeats가 대다수의 멤버들보다 떨어지면 자동으로 SECONDARY 상태로 전환됩니다.

  • 우선순위 비교

    우선순위 설정은 PRIMARY 선출에 영향을 미칩니다. 멤버들은 우선순위 값이 높은 멤버들에게 투표를 하는 경향이 있습니다. 우선순위 값이 0인 멤버들은 PRIMARY가 될 수 없습니다. Replica Set은 현재의 PRIMARY가 가장 높은 우선순위이며 이 멤버가 마지막 oplog에 대해 10초 이내에 응답하는 이상 절대 투표를 시행하지 않습니다. 만약 이 현재의 PRIMARY의 마지막 oplog의 10초 이내에 이 보다 높은 우선순위를 갖는 멤버가 나타나게 되면 이 멤버에게 PRIMARY가 될 수 있는 기회를 제공하기 위해 투표를 시행합니다.

  • Optime

    optimeoplog에 적용된 마지막 오퍼레이션의 타임스탬프(Timestamp)입니다. Replica Set 내 멤버 중 가장 높은(가장 최근의) optime을 갖지 않는다면 그 멤버는 PRIMARY가 될 수 없습니다.

  • 연결상태

    너무도 당연한 얘기이겠지만, Replica Set 내 대다수의 멤버들에게 연결되지 않는 멤버는 PRIMARY가 될 수 없습니다. 여기서 "대다수"의 의미는 전체 멤버수가 아닌 전체 투표수입니다.

    새로운 PRIMARY 멤버가 선출되면 이 멤버의 데이터는 가장 최신의 데이터로 간주되며, 이전의 PRIMARY가 돌아온다하더라도 다른 멤버들의 데이터는 새로운 PRIMARY 멤버의 데이터에 맞추어 롤백(Rollback)됩니다. 데이터 롤백을 위해 모든 멤버들은 재동기화(Resync) 과정을 수행하게 되는데, oplog를 통해 PRIMARY 상에 아직 적용되지 않은 오퍼레이션들을 검토하고 새로운 PRIMARY에게 이러한 오퍼레이션들을 SECONDARY에 전달하게 됩니다. 이러한 재동기화 과정을 회복(Recovery) 과정이라고 하며 이 과정이 종료되기까지는 새로운 PRIMARY 선출을 하지 않습니다.