MongoDB Replica Set 구성

누군가·2024년 2월 7일
0

MongoDB Replica Set

목록 보기
3/4

Replica Set 구성하기

1. Mongo DB 설치

추후 추가 예정

2. MongoDB 관리자 계정 생성

MongoDB의 Authentication 설정을 위해서 Primary 노드에 root 계정을 생성합니다.

# MongoDB Shell
> use admin
> db.createUser({
		user: "admin", // root 계정명
		pwd: "xxxxx", // root 패스워드
		roles: [ {role: "root", db: "admin"} ]
		}
	)

3. MongoDB 설정 파일 수정

MongoDB 접속 및 Replica Set 설정을 위해 /etc/mongod.conf 파일을 수정합니다.

# 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

net:
  port: 27017
  bindIp: localhost,{해당 인스턴스의 host name}
# bindIp: 0.0.0.0 (모든 외부 IP 허용, 누구든 접근할 수 있게되므로 인증 추가 필요)

# how the process runs
processManagement:
  timeZoneInfo: /usr/share/zoneinfo

#security

#operationProfiling:

replication:
# oplogSizeMB: 2000 # oplog 사이즈 2000MB
  replSetName: "{원하는 이름}"
#sharding:
## Enterprise-Only Options:
#auditLog:
#snmp:
  • net.port: MongoDB 접속 포트 정보 (기본: 27017)
  • net.bindIp: MongoDB 접속 허용 IP (기본: 127.0.0.1)
    • 127.0.0.1 (Localhost)로 설정되어있으면 본인 (Localhost)만 접속할 수 있기 때문에 외부 접속을 허용하기 위해서는 0.0.0.0 (모든 IP 허용)을 입력할 수 있습니다.
    • 다만, 모든 IP의 접속을 허용할 경우 보안상 문제가 있을 수 있으므로 계정을 설정하거나 security 옵션을 활성화할 필요성이 있습니다.
  • replication.oplogSizeMB: Replication에서 사용되는 oplog의 용량을 설정합니다. (선택 사항)
    • 공식 문서에 따르면 파일시스템에서 활용 가능한 용량의 5%를 권장합니다.
  • replication.replSetName: Replica Set의 명칭을 입력합니다.

MongoDB에 접근 할 수 있도록 해당 포트에 대한 방화벽을 오픈합니다.

// 데비안 계열 (Ubuntu 등)
# TCP 27017 포트 오픈
> sudo ufw allow 27017/tcp

// 레드햇 계열 (CentOS 등)
# TCP 27017 포트 오픈
> firewall-cmd --permanent --add-port=27017/tcp

# 방화벽 정책 재반영
> firewall-cmd --reload

# 방화벽 정책 확인
> firewall-cmd --list-all

설정이 완료되었다면 MongoDB 각 노드를 재시작 합니다.

sudo systemctl start (or restart) mongod

Replica Set 구성에 앞서 각 MongoDB 노드 간 통신이 되는지 확인합니다.

편의상 각 노드를 mongodb-1, mongodb-2, mongodb-3로 표현,
MongoDB Host는 IP 주소 보다 도메인 주소로 설정하여 접속하는 것을 권장하고 있습니다.

**# MongoDB 접속 확인**
> mongo --host mongodb-1 --port 27017
MongoDB shell version v4.4.13
connecting to: mongodb://mongodb-1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session
MongoDB server version: 4.4.13

> mongo --host mongodb-2 --port 27017
MongoDB shell version v4.4.13
connecting to: mongodb://mongodb-2:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session
MongoDB server version: 4.4.13

> mongo --host mongodb-3 --port 27017
MongoDB shell version v4.4.13
connecting to: mongodb://mongodb-3:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session
MongoDB server version: 4.4.13

---
※ MongoDB 버전 6.0 버전 이상인 경우 "mongo" 명령어 대신 "mongosh" 명령어로 접속 가능

4. Replica Set 초기화 및 설정

Replica Set을 구성하기 위해서는 초기화해야 하며, 초기화 방법으로는 크게 2가지가 있습니다.

a. 초기화 후 멤버 추가

**# 초기화 수행**
> rs.initiate()
{
    "info2" : "no configuration specified. Using a default configuration for the set",
    "me" : "mongodb-1:27017",
    "ok" : 1
}

**# 멤버 추가**
> rs.add("mongodb-2:27017")

또는

> rs.add({_id: 1, host: "mongodb-2:27017"})

b. 멤버를 지정하여 초기화

**# 멤버 지정하여 초기화 수행**
> rs.initiate({
		_id: "replSetName에서 지정한 이름", 
		members: [
			{_id: 0, host: "mongodb-1:27017"}, 
			{_id: 1, host: "mongodb-2:27017"}, 
			...
		]
	})

# (Arbiter 필요 시) Arbiter 노드 멤버 추가
> rs.addArb("mongodb-3:27017")

또는

> rs.add({host: "mongodb-3:27017", arbiterOnly: true})

또는

> rs.initiate({
		_id: "replSetName에서 지정한 이름", 
		members: [
			{_id: 0, host: "mongodb-1:27017"}, 
			{_id: 1, host: "mongodb-2:27017"}, 
			{_id: 2, host: "mongodb-3:27017", arbiterOnly: true}, 
			...
		]
	})

Replica Set 멤버 삭제

# 멤버 삭제
> rs.remove("mongodb-3:27017")

5. Replica Set 확인

Replica Set 정보 확인

**# Replica Set 구성 확인**
> rs.conf()
{
  _id: 'replSetName에서 지정한 이름',
  version: 10,
  term: 2,
  members: [
    {
      _id: 0,
      host: 'mongodb-1:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 1,
      host: 'mongodb-2:27017',
      arbiterOnly: false,
      buildIndexes: true,
      hidden: false,
      priority: 1,
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    },
    {
      _id: 2,
      host: 'mongodb-3:27017',
      arbiterOnly: false, // Arbiter 노드일 경우 true
      buildIndexes: true,
      hidden: false,
      priority: 1, // Arbiter 노드일 경우 0
      tags: {},
      secondaryDelaySecs: Long("0"),
      votes: 1
    }
  ],
  protocolVersion: Long("1"),
  writeConcernMajorityJournalDefault: true,
  settings: {
    chainingAllowed: true,
    heartbeatIntervalMillis: 2000,
    heartbeatTimeoutSecs: 10,
    electionTimeoutMillis: 10000,
    catchUpTimeoutMillis: -1,
    catchUpTakeoverDelayMillis: 30000,
    getLastErrorModes: {},
    getLastErrorDefaults: { w: 1, wtimeout: 0 },
    replicaSetId: ObjectId("64dc403e529739256a0f9c25")
  }
}

Replica Set 상태 확인

**# Replica Set 상태 확인**
> rs.status()
{
  set: 'replSetName에서 지정한 이름',
  date: ISODate("2023-08-17T00:49:54.342Z"),
  myState: 1,
  term: Long("6"),
  syncSourceHost: '',
  syncSourceId: -1,
  heartbeatIntervalMillis: Long("2000"),
  majorityVoteCount: 2,
  writeMajorityCount: 2,
  votingMembersCount: 2,
  writableVotingMembersCount: 2,
  optimes: {
    lastCommittedOpTime: { ts: Timestamp({ t: 1692233393, i: 1 }), t: Long("6") },
    lastCommittedWallTime: ISODate("2023-08-17T00:49:53.179Z"),
    readConcernMajorityOpTime: { ts: Timestamp({ t: 1692233393, i: 1 }), t: Long("6") },
    appliedOpTime: { ts: Timestamp({ t: 1692233393, i: 1 }), t: Long("6") },
    durableOpTime: { ts: Timestamp({ t: 1692233393, i: 1 }), t: Long("6") },
    lastAppliedWallTime: ISODate("2023-08-17T00:49:53.179Z"),
    lastDurableWallTime: ISODate("2023-08-17T00:49:53.179Z")
  },
  lastStableRecoveryTimestamp: Timestamp({ t: 1692233352, i: 1 }),
  electionCandidateMetrics: {
    lastElectionReason: 'electionTimeout',
    lastElectionDate: ISODate("2023-08-17T00:14:12.992Z"),
    electionTerm: Long("6"),
    lastCommittedOpTimeAtElection: { ts: Timestamp({ t: 1692231251, i: 1 }), t: Long("5") },
    lastSeenOpTimeAtElection: { ts: Timestamp({ t: 1692231251, i: 1 }), t: Long("5") },
    numVotesNeeded: 2,
    priorityAtElection: 1,
    electionTimeoutMillis: Long("10000"),
    priorPrimaryMemberId: 1,
    numCatchUpOps: Long("0"),
    newTermStartDate: ISODate("2023-08-17T00:14:13.013Z"),
    wMajorityWriteAvailabilityDate: ISODate("2023-08-17T00:14:14.024Z")
  },
  electionParticipantMetrics: {
    votedForCandidate: true,
    electionTerm: Long("5"),
    lastVoteDate: ISODate("2023-08-16T09:02:17.902Z"),
    electionCandidateMemberId: 1,
    voteReason: '',
    lastAppliedOpTimeAtElection: { ts: Timestamp({ t: 1692176530, i: 1 }), t: Long("4") },
    maxAppliedOpTimeInSet: { ts: Timestamp({ t: 1692176530, i: 1 }), t: Long("4") },
    priorityAtElection: 1
  },
  members: [
    {
      _id: 1,
      name: 'mongodb-2:27017',
      health: 1,
      state: 2,
      **stateStr: 'SECONDARY',**
      uptime: 61637,
      optime: { ts: Timestamp({ t: 1692233383, i: 1 }), t: Long("6") },
      optimeDurable: { ts: Timestamp({ t: 1692233383, i: 1 }), t: Long("6") },
      optimeDate: ISODate("2023-08-17T00:49:43.000Z"),
      optimeDurableDate: ISODate("2023-08-17T00:49:43.000Z"),
      lastAppliedWallTime: ISODate("2023-08-17T00:49:53.179Z"),
      lastDurableWallTime: ISODate("2023-08-17T00:49:53.179Z"),
      lastHeartbeat: ISODate("2023-08-17T00:49:52.740Z"),
      lastHeartbeatRecv: ISODate("2023-08-17T00:49:53.716Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: '192.168.56.102:27017',
      syncSourceId: 2,
      infoMessage: '',
      configVersion: 11,
      configTerm: 6
    },
    {
      _id: 2,
      name: 'mongodb-1:27017',
      health: 1,
      state: 1,
      **stateStr: 'PRIMARY',**
      uptime: 76741,
      optime: { ts: Timestamp({ t: 1692233393, i: 1 }), t: Long("6") },
      optimeDate: ISODate("2023-08-17T00:49:53.000Z"),
      lastAppliedWallTime: ISODate("2023-08-17T00:49:53.179Z"),
      lastDurableWallTime: ISODate("2023-08-17T00:49:53.179Z"),
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: '',
      electionTime: Timestamp({ t: 1692231252, i: 1 }),
      electionDate: ISODate("2023-08-17T00:14:12.000Z"),
      configVersion: 11,
      configTerm: 6,
      self: true,
      lastHeartbeatMessage: ''
    },
    ...
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1692233393, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1692233393, i: 1 })
}
※ Replica Set 상태를 확인했을 때 노드의 상태 (“stateStr”)가 “STARTUP” 또는 "STARTUP2"로 출력되는 경우 참조
- 구성 시간이 약간 소요되므로 대기하면 해결되거나
- 그래도 연결되지 않는다면 Replica Set 재구성이 필요합니다.

# Replica Set 재구성 (Mongo Shell)
> var cfg = {
			_id: "replSetName에서 지정한 이름",
			members: [
					{_id: 0, host: "mongodb-1:27017"}, 
					{_id: 1, host: "mongodb-2:27017"},
					...
			]
	};
> rs.reconfig(cfg);

6. Key 파일 생성 및 보안 설정

1) 인증 모드 설정

keyfile 생성

  • keyfile 없이 security.authorization: enabled 설정을 하면 오류가 발생합니다.
  • 올바른 인증을 위해서 security.keyFile 설정을 추가해야 하며, 생성한 key는 최소한의 권한만 가져야 합니다.
  • keyfile은 어떠한 방법으로 생성해도 무관하지만, 6 ~ 1024자 길이의 base64 세트의 문자만 포함할 수 있습니다.
# keyfile 저장 경로 생성 (해당 경로는 원하는 경로로 생성)
> mkdir "keyfile 경로"

# keyfile 경로로 이동
> cd "keyfile 경로"

# Keyfile 생성 (1024자 base64) 및 권한 부여
> openssl rand -base64 756 > "keyfile 경로/keyfile.key"
> chmod 400 "keyfie 경로/keyfile.key" // 400: 읽기 권한만
> chown mongod.mongod "keyfile 경로/keyfile.key"

※ 테스트 환경에서는 Keyfile 경로를 "/var/lib/mongodb/key/mongo.key"로 지정했습니다.

2) MongoDB Security 설정 변경

Replica Set에 연결된 각 노드는 동일한 keyfile을 공유해야 하므로 생성한 keyfile을 복제하여 동일 경로에 저장합니다.

/etc/mongod.conf

systemLog:
...
security:
	authorization: enabled
  clusterAuthMode: keyFile
  keyFile: "keyfile 경로/keyfile.key"

설정을 완료했다면 각 노드에 해당하는 mongod 서비스를 재실행합니다.

> sudo systemctl restart mongod

3) Security 적용 확인

[2) MongoDB 관리자 계정 생성](#2-MongoDB-관리자-계정-생성)에서 생성했던 root 계정을 통해 접속하여 Security가 정상적으로 작동되는지 확인합니다.

# MongoDB 관리자 로그인
> mongo -u admin -p "패스워드"

# Replica Set 상태 확인
> rs.status()

MongoDB 접속 후 Replica Set 상태 확인이 정상적으로 조회된다면 authentication 설정이 완료된 것입니다.

7. 동작 테스트

MongoDB에 데이터 CRUD를 통해 정상 동작 여부를 확인합니다.

  • Primary 서버에 테스트 전용 DB를 생성한 후 Secondary 서버에서도 동일한 DB가 생성되는지 확인
**# 테스트 전용 DB 생성**
primary> use replTest

**# DB 생성 확인**
primary> show dbs
admin     156.00 KiB
config    296.00 KiB
local     656.00 KiB
replTest   72.00 KiB

secondary> show dbs
admin     156.00 KiB
config    296.00 KiB
local     688.00 KiB
replTest   72.00 KiB
  • Primary에 컬렉션을 추가하고 데이터 생성 시도
    • Primary에서는 정상적으로 생성되지만 Secondary에서는 not primary라는 에러 메시지 출력
**# 컬렉션 추가 및 데이터 Insert (Create)**
primary> db.book.insertOne({"name": "book1", "author": "john"})
{
  acknowledged: true,
  insertedId: ObjectId("64df279ed10006156ad507fe")
}

secondary> use replTest
secondary> db.book.insertOne({"name": "book2", "author": "smith"})
MongoServerError: not primary
  • Primary에서 데이터 조회 시도
    • Primary에서는 정상적으로 조회되지만 Secondary에서는 not primary and secondaryOk=false라는 에러 메시지 출력
    • Secondary는 read preference에 따라 조회할 수 없음, 조회를 위해선 Read Preference 수정 필요
**# 데이터 Find (Read)**
primary> db.book.find({"name": "book1"})
[
  {
    _id: ObjectId("64df279ed10006156ad507fe"),
    name: 'book1',
    author: 'john'
  }
]

secondary> db.book.find({"name": "book1"})
MongoServerError: not primary and secondaryOk=false - consider using db.getMongo()
.setReadPref() or readPreference in the connection string
  • Primary에서 데이터 업데이트 시도
    • Primary에서는 정상적으로 업데이트되지만 Secondary에서는 not primary라는 에러 메시지 출력
**# 데이터 Update (Update)**
primary> db.book.updateOne({"name": "book1"}, {$set: {"pages": 300}})
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 0,
  upsertedCount: 0
}

secondary> db.book.updateOne({"name": "book1"}, {$set: {"pages": 720}})
MongoServerError: not primary
  • Primary에서 데이터 삭제 시도
    • Primary에서는 정상적으로 삭제되지만 Secondary에서는 not primary라는 에러 메시지 출력
**# 데이터 Delete (Delete)**
primary> db.book.deleteOne({"name": "book1"})
{ acknowledged: true, deletedCount: 1 }

secondary> db.book.deleteOne({"name": "book1"})
MongoServerError: not primary
  • 기존 Primary 노드 서버 문제 발생 시 신규 Primary 노드 선출 여부 확인
    • 기존 Primary 노드 서버를 종료했을 때 Secondary 노드 서버가 Primary로 변경되는지 확인

  • 우측 Shell (Primary 노드) 종료 시 좌측 Shell (Secondary 노드)가 Primary 노드로 변경되는 것이 확인됩니다.

8. (에러 발생 시) Replica Set 재설정

간혹 Replica Set 설정이 잘못되었거나 에러가 발생할 경우 Replica Set을 재설정 해야 할 필요가 있습니다.

  • 모든 Node (Primary/Secondary/Arbiter)에서 작업하는 것을 권장합니다.
# /etc/mongod.conf에서 Replication 주석 처리 (security 설정이 되어있다면 security도 주석 처리)
> vi /etc/mongod.conf
#replication:
#  replSetName: "Replica Set 이름"
...
#security:

# mongod 재시작
> systemctl restart mongod

# local DB 삭제 (MongoDB Shell 접속)
> mongo -u admin (관리자 계정) -p
> use local
> db.dropDatabase()

# /etc/mongod.conf에서 Replication 주석 해제 (security 설정이 되어있다면 security도 주석 해제)
> vi /etc/mongod.conf
replication:
  replSetName: "Replica Set 이름"
...
security:

# mongod 재시작
> systemctl restart mongod

# local DB에 "startup_log" Collection이 정상적으로 생성되었는지 확인
> use local
> show collections

Reference

https://hoing.io/archives/4282

https://www.mongodb.com/docs/manual/reference/replication/

https://velog.io/@tngusqkr1/MongoDB-replica-set-보안-설정

https://hyunki1019.tistory.com/173

profile
개발 중에 알게된 내용을 공유합니다 (나도 기억할겸)

0개의 댓글