MongoDB Replica Set 구축하기

📌 읽기 전에

- 해당 글은 저희가 운영중인 MongoDB 서버를 Atlas로 마이그레이션하기 전, 동일한 환경을 구성하여 프로덕션 환경에서의 마이그레이션을 시뮬레이션(?)하기 위한 과정에서 Replica Set을 구축한 과정을 기록한 글입니다. - Replica Set을 간단히 구축해보고 싶거나, 구축 과정에 대한 이해를 하기에 적합한 수준으로 작성하였습니다. - Replica Set의 개념에 대해 알고싶으시다면 앞부분을, 직접 구축해보고 싶으신 분들은 뒷부분을 집중해주세요.


📌 Replica Set?

  • Kubernetes에도 동일한 개념이 존재하지만 여기서는 MongoDB 관점에서의 개념을 설명하고자 합니다.
  • Replica(Replication)부터 설명하자면, DB의 데이터들을 여러 서버에 동기화(synchronization)하는 것을 의미합니다.. 여러 서버가 모두 동일한 데이터를 가짐에 따라 하나의 서버가 다운되더라도 제공하는 서비스에 문제가 생기지 않고 운영을 할 수 있다는 장점이 있습니다.. 각 서버에 데이터 복구/리포팅/백업 역할(용도)을 설정할 수도 있습니다.

Replica Set을 구축하는 이유?

서비스 운영에 MongoDB를 사용할 경우, Replica Set을 구축하지 않을 이유는 없어보입니다. 물론 구축과 실제 운영 알아야할 것들에 차이가 있지만, 후회하지 않는 선택이라고 생각합니다.

  • 데이터를 안전하게 보존하기 위해
  • 24시간 접근 가능한 데이터의 상태를 유지하기 위해
  • 서비스 운영시 다운타임(인덱스 적용, 백업 작업 등에 의해)을 없애기 위해
  • 기타 등등…

용어

  • Primary node, Secondary node

    모든 write 작업을 수행합니다. primary node에 해당 작업이 수행되면 oplog라는 것에 모든 작업 로그를 저장하는데, secondary node에서는 이 oplog를 보고 동일한 작업을 수행합니다. 쉽게 말해서 primary node에서 변화된 데이터를 복사하는 것이죠.

    출처 : MongoDB 공식 문서

  • Election

    우리 말로는 ‘선거’, primary node가 이용 불가능한 상태에 빠졌을 때, secondary node들 중에서 primary node를 하나 정하는 과정을 의미합니다.

  • Arbiter node

    primary나 secondary node처럼 데이터를 가지진 않고 secondary node들 중에서 primary node를 선정하는 election 과정에 참여합니다. arbiter node가 primary node가 될 수는 없습니다.

  • Heartbeat

    Replica set 내의 모든 노드들은 정해진 초(second)마다 서로에게 heartbeat(일종의 ping)를 보냅니다. 귀엽지 않나요? 이러한 Heartbeat가 특정 초 동안 수신되지 않으면 다른 node들이 Election을 주섬주섬 준비합니다.

구성

Replica Set은 동일한 데이터를 가진 여러 node(여기서는 서버)로 이루어져있으며, 선택적으로 하나의 Aribiter node를 포함시킬 수 있습니다. 데이터를 가진 node들 중에서는 반드시 하나의 primary node를 지정해줘야하며, 나머지 node들은 secondary node라고 칭합니다.

node들의 구성에 따라 대표적인 구성 방식을 소개하고자 하는데요, Replica Set을 구성하는 node의 개수는 최소 3개 이상입니다. 그 중에 대표적인 3개로 이루어진 경우는 다음과 같습니다.

  • P-S-S(Primary + Secondary + Secondary)

    하나의 Primary와 두개의 Secondary node로 이루어진 구성입니다. Primary node에 문제가 생기더라도 Secondary node 2개나 그 자리를 대신할 수 있으니 든든(?)합니다. 높은 안정성( high availability )을 보장할 수 있습니다.

  • P-S-A(Primary + Secondary + Arbiter)

    Primary, Secondary, Arbiter node 각 1개씩으로 이루어진 구성입니다. P-S-S 구성과는 다르게 Arbiter node가 추가되었는데요, 이 node는 Primary나 Secondary와는 다르게 서버의 리소스는 많이 필요하지 않지만 데이터를 실제로 담고있지는 않기 때문에 상대적으로는 구성의 안정성이 낮습니다(그렇다고 안정성이 좋지 않은 것은 아닙니다).


📌 구축하기

Step #1: 각 인스턴스에 MongoDB 설치 Step #2: Replica Set 설정

이번에는 실제로 MongoDB 서버를 3개 구축하고 이들을 P-S-A Replica Set으로 설정하는 방법을 알아보겠습니다. 전체 과정은 MongoDB의 공식 문서 를 참고하였습니다.

필자의 경우 Replica Set으로 운영중인 서비스를 마이그레이션하기 전, 개발 환경에서 동일한 환경을 구축하여 테스트하는 용도로 진행하였습니다.

Step #1 : 각 인스턴스에 MongoDB 설치

  • 본 글에서 ‘인스턴스’는 AWS의 EC2 인스턴스를 의미합니다.
  • EC2 인스턴스 생성과 초기 설정에 익숙한 독자를 기준으로 설명하였습니다.
  • 아래와 같은 과정을 총 3번 진행해주세요.

(1) EC2 인스턴스에 MongoDB를 설치하기(이 문서 참고)

  • Platform(x86_64, arm64 등)에 따른 MongoDB 버전을 잘 체크해야합니다.
  • 인스턴스 OS(ex. Ubuntu, Debian 등)도 지원되는 버전을 잘 확인해야합니다.
  • 설치 커맨드 예시(MongoDB 4 버전)

      $wget -qO - https://www.mongodb.org/static/pgp/server-4.0.asc | sudo apt-key add -
        
      $echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.0.list
        
      $sudo apt-get update
        
      $sudo apt-get install -y mongodb-org
    

(2) 인스턴스의 27017번 포트를 열기

  • Security Group의 inbound rule을 수정해야합니다.

(3) Primary node로 설정할 인스턴스를 정하고, admin 유저의 비밀번호 설정하기

$mongo

> use admin
> db.createUser({"user": "admin", "pwd":"{비밀번호}", "roles": [{"role":"root", "db": "admin"}]}) # 예시 권한
  • Primary node에 비밀번호를 설정하지 않으면 불시에(?) 해킹을 당할 수 있으므로 반드시 진행해주세요.



Step #2: Replica Set 설정

고려해야할 것

  • 공식 문서에도 나와있지만, 위의 Step#1 에서 생성한 인스턴스들은 ip보다는 host name(도메인 주소)로 접근하는 것을 권장하고있습니다. AWS의 경우 Route53을 통해 도메인을 설정할 수 있으니 먼저 진행해주세요.(ex. {primary/secondary/arbitery}.mongo.db})

(1) 각 인스턴스의 MongoDB 설정 파일(/etc/mongo.conf) 수정하기

  • Replica Set으로 설정할 모든 인스턴스에서 동일하게 수정해줘야합니다.

  • 설정값을 아래와 같이 수정해줍니다.

    (수정1) net: localhost와 해당 인스턴스의 host name으로 설정

    (수정2) security: Replica Set 내의 node들 간의 인증에 사용되는 키를 생성해줘야합니다.
    만약 이 부분을 설정해주지 않으면 DBClientConnection failed to receive message from 127.0.0.1:27017 - HostUnreachable: Connection closed by peer 에러가 발생할 수 있습니다. 이 문서를 참고하여 키파일을 생성하고, 키파일의 경로를 추가해주세요.

    (수정3) replication: 원하는 replica set 이름을 설정해주세요.

      "/etc/mongod.conf" 43L, 637C        24,30         All
      # mongod.conf
        
      # for documentation of all options, see:
      #   http://docs.mongodb.org/manual/reference/configuration-options/
        
      # Where and how to store data.
      storage:
        dbPath: /var/lib/mongodb
        journal:
          enabled: true
      #  engine:
      #  mmapv1:
      #  wiredTiger:
        
      # where to write logging data.
      systemLog:
        destination: file
        logAppend: true
        path: /var/log/mongodb/mongod.log
        
      # 수정(1)
      net:
        port: 27017
        bindIp: localhost,{해당 인스턴스의 host name}
        
      # how the process runs
      processManagement:
        timeZoneInfo: /usr/share/zoneinfo
        
      # 수정(2)
      security:
        authorization: "enabled"
        clusterAuthMode: "keyFile"
        keyFile: "{키 파일 경로}"
        
      #operationProfiling:
        
      # 수정(3)
      replication:
        replSetName: "{원하는 이름}"
        
      #sharding:
        
      ## Enterprise-Only Options:
        
      #auditLog:
        
      #snmp:      37,36         All
    



(2) Primary node에서 replica set 생성하기

  • (1)에서 설정 파일을 잘 생성했다면, 이제는 각 인스턴스에서 mongod(mongo daemon)을 실행해줘야합니다.

      // mongo daemon 실행
      $sudo mongod --config /etc/mongod.conf --fork(백그라운드로 실행)
    
  • Primary node로 설정하고자하는 인스턴스의 mongo daemon에 접속합니다.

      $mongo --port 27017 -u "admin" -p
      Enter password: {Step#1의 (3)에서 설정한 비밀번호 입력}
    
  • Replica set 생성

      > rs.initiate()
    



(3) Replica Set에 Secondary, Arbiter node 추가하기

  • Secondary node 인스턴스에 접속해서 추가하기

      $mongo
      > rs.add({host:"{Secondary node 도메인주소:27017}", priority:1})
    
  • Arbiter node 인스턴스에 접속해서 추가하기

      $mongo
      > rs.add({host:"{Arbitrer node 도메인주소:27017}", priority:1, arbiterOnly:true})
    



(4) Replica set 생성 결과 확인(Primary node 인스턴스에서)

$mongo --port 27017 -u "admin" -p
Enter password: {Step#1의 (3)에서 설정한 비밀번호 입력}
> rs.status()

{
	"set" : "{Replica set 이름}",
	"date" : ISODate("2021-11-23T15:29:07.810Z"),
	"myState" : 1,
	"term" : NumberLong(1),
	"syncingTo" : "",
	"syncSourceHost" : "",
	"syncSourceId" : -1,
	"heartbeatIntervalMillis" : NumberLong(2000),
	"optimes" : {
		"lastCommittedOpTime" : {
			"ts" : Timestamp(1637681346, 1),
			"t" : NumberLong(1)
		},
		"readConcernMajorityOpTime" : {
			"ts" : Timestamp(1637681346, 1),
			"t" : NumberLong(1)
		},
		"appliedOpTime" : {
			"ts" : Timestamp(1637681346, 1),
			"t" : NumberLong(1)
		},
		"durableOpTime" : {
			"ts" : Timestamp(1637681346, 1),
			"t" : NumberLong(1)
		}
	},
	"lastStableCheckpointTimestamp" : Timestamp(1637681296, 1),
	"electionCandidateMetrics" : {
		"lastElectionReason" : "electionTimeout",
		"lastElectionDate" : ISODate("2021-11-23T08:51:26.230Z"),
		"electionTerm" : NumberLong(1),
		"lastCommittedOpTimeAtElection" : {
			"ts" : Timestamp(0, 0),
			"t" : NumberLong(-1)
		},
		"lastSeenOpTimeAtElection" : {
			"ts" : Timestamp(1637657486, 1),
			"t" : NumberLong(-1)
		},
		"numVotesNeeded" : 1,
		"priorityAtElection" : 1,
		"electionTimeoutMillis" : NumberLong(10000),
		"newTermStartDate" : ISODate("2021-11-23T08:51:26.231Z"),
		"wMajorityWriteAvailabilityDate" : ISODate("2021-11-23T08:51:26.314Z")
	},
	"members" : [
		{
			"_id" : 0,
			"name" : "{Primary node 도메인 주소:27017}",
			"health" : 1,
			"state" : 1,
			"stateStr" : "PRIMARY",
			"uptime" : 23873,
			"optime" : {
				"ts" : Timestamp(1637681346, 1),
				"t" : NumberLong(1)
			},
			"optimeDate" : ISODate("2021-11-23T15:29:06Z"),
			"syncingTo" : "",
			"syncSourceHost" : "",
			"syncSourceId" : -1,
			"infoMessage" : "",
			"electionTime" : Timestamp(1637657486, 2),
			"electionDate" : ISODate("2021-11-23T08:51:26Z"),
			"configVersion" : 3,
			"self" : true,
			"lastHeartbeatMessage" : ""
		},
		{
			"_id" : 1,
			"name" : "{Arbiter node 도메인 주소:27017}",
			"health" : 1,
			"state" : 7,
			"stateStr" : "ARBITER",
			"uptime" : 193,
			"lastHeartbeat" : ISODate("2021-11-23T15:29:07.005Z"),
			"lastHeartbeatRecv" : ISODate("2021-11-23T15:29:07.013Z"),
			"pingMs" : NumberLong(1),
			"lastHeartbeatMessage" : "",
			"syncingTo" : "",
			"syncSourceHost" : "",
			"syncSourceId" : -1,
			"infoMessage" : "",
			"configVersion" : 3
		},
		{
			"_id" : 2,
			"name" : "{Secondary node 도메인 주소:27017}",
			"health" : 1,
			"state" : 2,
			"stateStr" : "SECONDARY",
			"uptime" : 160,
			"optime" : {
				"ts" : Timestamp(1637681346, 1),
				"t" : NumberLong(1)
			},
			"optimeDurable" : {
				"ts" : Timestamp(1637681346, 1),
				"t" : NumberLong(1)
			},
			"optimeDate" : ISODate("2021-11-23T15:29:06Z"),
			"optimeDurableDate" : ISODate("2021-11-23T15:29:06Z"),
			"lastHeartbeat" : ISODate("2021-11-23T15:29:06.943Z"),
			"lastHeartbeatRecv" : ISODate("2021-11-23T15:29:06.251Z"),
			"pingMs" : NumberLong(0),
			"lastHeartbeatMessage" : "",
			"syncingTo" : "test.primary.mongo.db:27017",
			"syncSourceHost" : "test.primary.mongo.db:27017",
			"syncSourceId" : 0,
			"infoMessage" : "",
			"configVersion" : 3
		}
	],
	"ok" : 1,
	"operationTime" : Timestamp(1637681346, 1),
	"$clusterTime" : {
		"clusterTime" : Timestamp(1637681346, 1),
		"signature" : {
			"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
			"keyId" : NumberLong(0)
		}
	}
}

위와같이 보인다면 Replica Set 설정이 완료된 것입니다. 이제 Primary node의 주소를 가지고 어플리케이션 코드에서 적용시켜보면서 테스트해보자구요!