티스토리 뷰

DB/MongoDB

[MongoDB] 복제 셋 설정

snail voyager 2023. 7. 11. 16:50
728x90
반응형

10.1 복제 소개

  • 복제는 데이터의 동일한 복사본을 여러 서버상에서 보관하는 방법
  • 실제 서비스를 배포할 때 권장
  • 한 대 또는 그 이상의 서버에 이상이 발생하더라도 복제 애플리케이션이 정상적으로 동작하게 하고 데이터를 안전하게 보존
  • 복제 셋은 클라이언트 요청을 처리하는 프라이머리 서버 한대,
  • 프라이머리 데이터의 복사본을 갖는 세컨더리 서버 여러 대로 구성
  • 프라이머리 서버에 장애가 발생하면 세컨더리 서버 중 새로운 프라이머리 서버를 선출
  • 복제를 사용하는 상태에서 서버가 다운되면, 복제 셋에 잇는 다른 서버를 통해 데이터에 접근 가능

10.2 복제 셋 설정

각 별도의 터미널에서 mongod 실행

mongod --replSet mdbDefGuide --dbpath ~/data/rs1 --port 27017 --oplogSize 200
mongod --replSet mdbDefGuide --dbpath ~/data/rs2 --port 27018 --oplogSize 200
mongod --replSet mdbDefGuide --dbpath ~/data/rs3 --port 27019 --oplogSize 200

10.3 네트워크 고려 사항

인스턴스를 각기 다른 서버의 멤버와 함께 복제 셋의 멤버로 실행하려면

명령행 매개변수 --bint_ip 를 지정하거나 인스턴스 구성 파일에 있는 bind_ip를 사용

mongod --bind_ip localhost,192.51.100.1 --replSet mdbDefGuide --dppath ~/data/rs1 --port 27017 --oplogSize 200

10.4 보안 고려 사항

복제 셋을 구성할 때, 권한 제어를 활성화하고 인증 메커니즘을 지정해야함

디스크의 데이터를 암호화하고, 복제 셋 멤버 간 통신 및 셋과 클라이언트 간 통신을 암호화

10.5 복제 셋 설정

  • 환경설정 문서를 작성한 후 rs.initiate() 에 전달하여 복제 set을 시작
  • 3개의 구성원이 포함된 replica set이 시작되고, 복제본 set이 형성되도록 구성이 나머지 mongod로 전파
test> rsconf = {
... _id : "mdbDefGuide",
... members: [
... {_id : 0, host:"localhost:27017"},
... {_id : 1, host:"localhost:27018"},
... {_id : 2, host:"localhost:27019"}
... ]
... }
{
  _id: 'mdbDefGuide',
  members: [
    { _id: 0, host: 'localhost:27017' },
    { _id: 1, host: 'localhost:27018' },
    { _id: 2, host: 'localhost:27019' }
  ]
}
test> rs.initiate(rsconf)
{ ok: 1 }

복제 셋의 상태는 rs.status()

mdbDefGuide [direct: other] test> rs.status()
{
  set: 'mdbDefGuide',
  date: ISODate("2023-07-11T07:32:51.737Z"),
  myState: 1,
  term: Long("1"),
  syncSourceHost: '',
  syncSourceId: -1,
  heartbeatIntervalMillis: Long("2000"),
  majorityVoteCount: 2,
  writeMajorityCount: 2,
  votingMembersCount: 3,
  writableVotingMembersCount: 3,
  optimes: {
    lastCommittedOpTime: { ts: Timestamp({ t: 1689060762, i: 1 }), t: Long("1") },
    lastCommittedWallTime: ISODate("2023-07-11T07:32:42.146Z"),
    readConcernMajorityOpTime: { ts: Timestamp({ t: 1689060762, i: 1 }), t: Long("1") },
    appliedOpTime: { ts: Timestamp({ t: 1689060762, i: 1 }), t: Long("1") },
    durableOpTime: { ts: Timestamp({ t: 1689060762, i: 1 }), t: Long("1") },
    lastAppliedWallTime: ISODate("2023-07-11T07:32:42.146Z"),
    lastDurableWallTime: ISODate("2023-07-11T07:32:42.146Z")
  },
  lastStableRecoveryTimestamp: Timestamp({ t: 1689060712, i: 1 }),
  electionCandidateMetrics: {
    lastElectionReason: 'electionTimeout',
    lastElectionDate: ISODate("2023-07-11T07:31:11.651Z"),
    electionTerm: Long("1"),
    lastCommittedOpTimeAtElection: { ts: Timestamp({ t: 1689060659, i: 1 }), t: Long("-1") },
    lastSeenOpTimeAtElection: { ts: Timestamp({ t: 1689060659, i: 1 }), t: Long("-1") },
    numVotesNeeded: 2,
    priorityAtElection: 1,
    electionTimeoutMillis: Long("10000"),
    numCatchUpOps: Long("0"),
    newTermStartDate: ISODate("2023-07-11T07:31:11.983Z"),
    wMajorityWriteAvailabilityDate: ISODate("2023-07-11T07:31:12.753Z")
  },
  members: [
    {
      _id: 0,
      name: 'localhost:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 1043,
      optime: { ts: Timestamp({ t: 1689060762, i: 1 }), t: Long("1") },
      optimeDate: ISODate("2023-07-11T07:32:42.000Z"),
      lastAppliedWallTime: ISODate("2023-07-11T07:32:42.146Z"),
      lastDurableWallTime: ISODate("2023-07-11T07:32:42.146Z"),
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: '',
      electionTime: Timestamp({ t: 1689060671, i: 1 }),
      electionDate: ISODate("2023-07-11T07:31:11.000Z"),
      configVersion: 1,
      configTerm: 1,
      self: true,
      lastHeartbeatMessage: ''
    },
    {
      _id: 1,
      name: 'localhost:27018',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 111,
      optime: { ts: Timestamp({ t: 1689060762, i: 1 }), t: Long("1") },
      optimeDurable: { ts: Timestamp({ t: 1689060762, i: 1 }), t: Long("1") },
      optimeDate: ISODate("2023-07-11T07:32:42.000Z"),
      optimeDurableDate: ISODate("2023-07-11T07:32:42.000Z"),
      lastAppliedWallTime: ISODate("2023-07-11T07:32:42.146Z"),
      lastDurableWallTime: ISODate("2023-07-11T07:32:42.146Z"),
      lastHeartbeat: ISODate("2023-07-11T07:32:49.833Z"),
      lastHeartbeatRecv: ISODate("2023-07-11T07:32:50.867Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: 'localhost:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 1,
      configTerm: 1
    },
    {
      _id: 2,
      name: 'localhost:27019',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 111,
      optime: { ts: Timestamp({ t: 1689060762, i: 1 }), t: Long("1") },
      optimeDurable: { ts: Timestamp({ t: 1689060762, i: 1 }), t: Long("1") },
      optimeDate: ISODate("2023-07-11T07:32:42.000Z"),
      optimeDurableDate: ISODate("2023-07-11T07:32:42.000Z"),
      lastAppliedWallTime: ISODate("2023-07-11T07:32:42.146Z"),
      lastDurableWallTime: ISODate("2023-07-11T07:32:42.146Z"),
      lastHeartbeat: ISODate("2023-07-11T07:32:49.833Z"),
      lastHeartbeatRecv: ISODate("2023-07-11T07:32:51.336Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: 'localhost:27018',
      syncSourceId: 1,
      infoMessage: '',
      configVersion: 1,
      configTerm: 1
    }
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1689060762, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1689060762, i: 1 })
}

10.6 복제 관찰

isMaster() 으로 rs.status() 보다 간결한 형태로 복제 셋의 상태 출력

mdbDefGuide [direct: primary] test> db.isMaster()
{
  topologyVersion: {
    processId: ObjectId("64ad01909b0affd66e870a7e"),
    counter: Long("6")
  },
  hosts: [ 'localhost:27017', 'localhost:27018', 'localhost:27019' ],
  setName: 'mdbDefGuide',
  setVersion: 1,
  ismaster: true,
  secondary: false,
  primary: 'localhost:27017',
  me: 'localhost:27017',
  electionId: ObjectId("7fffffff0000000000000001"),
  lastWrite: {
    opTime: { ts: Timestamp({ t: 1689064253, i: 1 }), t: Long("1") },
    lastWriteDate: ISODate("2023-07-11T08:30:53.000Z"),
    majorityOpTime: { ts: Timestamp({ t: 1689064253, i: 1 }), t: Long("1") },
    majorityWriteDate: ISODate("2023-07-11T08:30:53.000Z")
  },
  maxBsonObjectSize: 16777216,
  maxMessageSizeBytes: 48000000,
  maxWriteBatchSize: 100000,
  localTime: ISODate("2023-07-11T08:30:57.018Z"),
  logicalSessionTimeoutMinutes: 30,
  connectionId: 35,
  minWireVersion: 0,
  maxWireVersion: 17,
  readOnly: false,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1689064253, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1689064253, i: 1 }),
  isWritablePrimary: true
}

27019에 대한 연결을 인스턴스화해서 세컨더리 사용

세컨더리는 기본적으로 읽기 요청을 거부

setReadPref('secondary') 로 읽기 허용

세컨더리는 쓰기 불가

mdbDefGuide [direct: primary] test> secondaryConn = new Mongo("localhost:27019")
mongodb://localhost:27019/?directConnection=true&serverSelectionTimeoutMS=2000
mdbDefGuide [direct: secondary] test> secondaryDB = secondaryConn.getDB("test")
test
//읽기 불가
mdbDefGuide [direct: secondary] test> secondaryDB.coll.find()
MongoServerError: not primary and secondaryOk=false - consider using db.getMongo().setReadPref() or readPreference in the connection string

mdbDefGuide [direct: secondary] test> secondaryConn.setSlaveOk()
//에러
MongoshDeprecatedError: [COMMON-10003] Setting slaveOk is deprecated, use setReadPref instead.

mdbDefGuide [direct: secondary] test> secondaryConn.setReadPref('secondary')
//읽기 가능
mdbDefGuide [direct: secondary] test> secondaryDB.coll.find()
[
  { _id: ObjectId("64ad11e2ff820f08127df906"), count: 0 },
  { _id: ObjectId("64ad11e2ff820f08127df907"), count: 1 },
  { _id: ObjectId("64ad11e2ff820f08127df909"), count: 3 },
  { _id: ObjectId("64ad11e2ff820f08127df90a"), count: 4 },
  { _id: ObjectId("64ad11e2ff820f08127df908"), count: 2 },
  
//쓰기 불가
mdbDefGuide [direct: secondary] test> secondaryDB.coll.insert({"count" : 1001})
Uncaught:
MongoBulkWriteError: not primarymdbDefGuide [direct: secondary] test> secondaryDB.coll.insert({"count" : 1001})
Uncaught:
MongoBulkWriteError: not primary

장애 조치

프라이머리가 중단되면 세컨더리 중 하나가 자동으로 프라이머리로 선출

프라이머리의 중단을 가장 먼저 발견한 세컨더리가 선출

mdbDefGuide [direct: secondary] test> db.adminCommand({"shutdown" : 1})
MongoNetworkError: connection 1 to 127.0.0.1:27017 closed

test> secondaryDB.isMaster()
{
  topologyVersion: {
    processId: ObjectId("64acfe82a4c8ceec5bcc9d18"),
    counter: Long("1064")
  },
  hosts: [ 'localhost:27017', 'localhost:27018', 'localhost:27019' ],
  setName: 'mdbDefGuide',
  setVersion: 1,
  ismaster: false,
  secondary: true,
  primary: 'localhost:27018',
  me: 'localhost:27019',
  lastWrite: {
    opTime: { ts: Timestamp({ t: 1689126873, i: 1 }), t: Long("544") },
    lastWriteDate: ISODate("2023-07-12T01:54:33.000Z"),
    majorityOpTime: { ts: Timestamp({ t: 1689126873, i: 1 }), t: Long("544") },
    majorityWriteDate: ISODate("2023-07-12T01:54:33.000Z")
  },
  maxBsonObjectSize: 16777216,
  maxMessageSizeBytes: 48000000,
  maxWriteBatchSize: 100000,
  localTime: ISODate("2023-07-12T01:54:40.450Z"),
  logicalSessionTimeoutMinutes: 30,
  connectionId: 3537,
  minWireVersion: 0,
  maxWireVersion: 17,
  readOnly: false,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1689126873, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1689126873, i: 1 }),
  isWritablePrimary: false
}

핵심 개념 3가지

  1. 클라이언트는 독립 실행형 서버에 보낼 수 있는 모든 작업을 프라이머리 서버에 보낼 수 있다. (읽기, 쓰기, 명령, 인덱스 구축 등)
  2. 클라이언트는 세컨더리에 쓰기를 할 수 없다
  3. 기본적으로 클라이언트는 세컨더리로부터 읽을 수 없다. 세컨더리에서 읽고 있음을 알수 있다를 뜻하는 설정을 연결에 명시적으로 설정하면 읽기를 활성화

10.7 복제 셋 구성 변경

복제 셋에 새로운 멤버를 추가할 때는 rs.add

제거할 때는 rs.remove

재구성 성공 여부는 rs.config()으로 현재 구성을 출력

> rs.add("localhost:27020")

> rs.remove("localhost:27017")

> rs.config()

수정을 하려면 구성 도큐먼트를 만들고 rs.reconfig 호출

> var config = rs.config()
> config.members[0].host = "localhost:27017"

> rs.reconfig(config)

10.8 복제 셋 설계 방법

  • 프라이머리는 과반수 이상이어야만 프라이머리 자격 유지
  • 쓰기는 과반수 이상에 복제되면 안전
  • 과반수는 복제 셋 내 모든 멤버의 절반보다 많은 것
  • 과반수는 복제 셋의 구성에 따라 산정되므로, 얼마나 많은 멤버가 다운되거나 사용할 수 없는 상태인지는 관련 X
  • 네트워크 파티션의 경우 파티션 양쪽에서 프라이머리를 선출하기를 원하지 않는데,
  • 복제 셋이 두개의 프라이머리를 갖게 될 수 있기 때문에 방지하는 해결책
  • 몽고DB는 오직 단일 프라이머리만 지원
  • 개발이 쉬워질 수 있지만 복제 셋이 읽기 전용 상태일 때는 어느 정도 시간이 걸림

10.8.1 어떻게 선출하는가

  • 요청받은 멤버가 프라이머리에 도달할 수 있는가?
  • 선출되고자 하는 멤버의 복제 데이터가 최신인가?
  • 대신 선출돼야 하는 우선순위가 더 높은 멤버는 없는가?

10.9 멤버 구성 옵션

10.9.1 우선순위

  • 우선순위는 특정 멤버가 얼마나 프라이머리가 되기를 원하는지 나타내는 지표
  • "priority"의 절대값은 다른 복제 셋 멤버의 우선순위와 비교해 큰지 혹은 작은지만 중요
> rs.add({"host": "server-4:27017", "priority":1.5})

10.9.2 숨겨진 멤버

  • 클라이언트는 숨겨진 멤버에 요청을 라우팅하지 않음
  • 숨겨진 멤버는 복제 소스로서 바람직하지 않음
  • 덜 강력한 서버 또는 백업 서버를 숨김
  • hidden : true 필드를 구성에 추가해야함
  • 우선순위는 0
  • 클라이언트는 복제 셋에 연결할 때 isMaster()를 호출해 복제 셋 멤버를 확인하는데 숨겨진 멤버는 hidden
> var config = rs.config()
> config.members[2].hidden = true
> config.members[2].priority = 0

10.9.3 아비터 선출

  • 프라이머리 선출에 참여하는 용도로만 사용되는 특수한 멤버
  • 데이터를 가지지 않으며 클라이언트에 의해 사용되지 않음
  • 오로지 멤버 복제 셋에서 과반수를 구성하는데 사용
  • 아비터는 복제 셋에 추가되고 나면 영원히 아비터
> rs.addArb("server-5:27017")  //아비터를 복제 셋에 추가
> rs.add("_id" : 4, "host" : "server-5:27017", "arbiterOnly" : true})	//멤버 구성에서 아비터 옵션
  • 큰 클러스터상에서 동정 상황을 없앨 수 있다는 장점
  • 아비터는 최대 하나까지만 사용
  • 노드의 갯수가 홀수이면 아바타는 필요 없음
  • 데이터 노드와 아비터 중 하나를 골라야 한다면 데이터 노드를 선택

10.9.4 인덱스 구축

  • 세컨더리를 데이터 백업이나 오프라인 배치 작업에만 사용한다면 "buildIndexes" : false 를 멤버구성에 명시
  • 세컨더리가 인덱스를 구축하지 않도록하는 옵션
  • 영구적인 설정
  • 멤버의 우선순위는 0
728x90
반응형

'DB > MongoDB' 카테고리의 다른 글

[MongoDB] 애플리케이션에서 복제 셋 연결  (0) 2023.08.01
[MongoDB] 복제 셋 구성 요소  (0) 2023.07.27
[MongoDB] 트랜잭션  (0) 2023.07.02
[MongoDB] 애플리케이션 설계  (0) 2023.06.20
[MongoDB] 집계 프레임워크  (0) 2023.05.10
반응형
300x250