04-28 00:20
Notice
Recent Posts
Recent Comments
관리 메뉴

Scientific Computing & Data Science

[MongoDB] Replication / Replica Sets Part 1. 본문

Data Science/MongoDB

[MongoDB] Replication / Replica Sets Part 1.

cinema4dr12 2014. 3. 27. 15:22

by Geol Choi | 

이번 글에서는 MongoDB의 가장 중요한 개념 중 하나인 Replica Set에 대해 알아보도록 하겠다. Replica Set은 기본적으로 자동 패일오버 기능을 갖는 마스터-슬레이브 클러스터이지만, 마스터-슬레이브 클러스터의 가장 큰 차이점은 마스터 노드가 가변적이라는 것이다. 즉, 현재 마스터 노드로 지정된 인스턴스가 다운될 경우 다른 노드가 마스터 노드로 자동 지정된다는 점이다. 마스터-슬레이브 클러스터와 Replica Set과의 공통점은, 단 하나의 마스터 노드(PRIMARY 노드라고도 함)와 여러 개의 슬레이브 노드(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. 각 슬레이브 노드에 대한 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.]과 같이 DOS 명령 콘솔을 실행하여 ipconfig -all을 입력하여 hostname을 확인할 수 있다.

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


hostname을 "gchoi"로 가정하고 rsconf 환경설정을 입력한다:

> rsconf = {_id: "rs0", members: [{_id: 0, host: "gchoi:27017"}]}
{
        "_id" : "rs0",
        "members" : [
                {
                        "_id" : 0,
                        "host" : "gchoi:27017"
                }
        ]
}

* Tip: 로컬 작업 시 hostname인 "gchoi" 대신 "localhost"로 입력하여도 된다. 즉, 다음과 같이 입력할 수 있다. 만약 이 방식으로 작업한다면 이후 나오는 모든 "gchoi"는 "localhost"로 바꾸어 입력하도록 한다.

> 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

위와 같이 db를 local로 전환하고, 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

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

연결상태

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


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

Comments