본문 바로가기

Database

[DOIK 스터디 2기] Percona Operator for MongoDB 개념 및 실습

반응형

DOIK 스터디 2기 - 4번째 글로 Percona Operator for MongoDB 오퍼레이터에 대해서 정리해본다.

 

공부하는 내용을 정리하는 글이라 부정확할 수 있음 주의

 

 


 

MongoDB 개념에 대해서 간단히 정리해보기

Document : MongoDB에서 데이터를 저장하는 단위이며 JSON 형태로 표현되며 저장될 때는 Binary JSON(BSON) 경량 형태로 저장되기 떄문에 더 빠르고 효율적으로 데이터를 저장할 수 있다.
>> 모든 Document에는 “_id” 필드가 있고 없게 생성할 경우 ObjectId 타입의 고유 값을 저장한다.

Collection : MongoDB Document(문서)의 모음이며 RDB의 Table과 유사함

> 컬렉션은 서브컬렉션을 가질 수 있지만 종속성은 없다. (ex. blog.posts, blog.authors)
> MongoDB는 동적 스키마를 가지고 있어서 같은 Collection 내에 서로 다른 스키마를 가진 Document들이 올 수 있다. 따라서 스키마의 제약이 없지만 그래도 스키마를 정해줘야 좋은데 그 이유는 같은 스키마를 가지면 Collection 단위로 Index를 생성하는데 용이하기 때문이다.

MongoDB에서는 3개의 Reserved Database가 존재한다.
admin : 인증 및 권한 부여를 담당한다.
local : 단일 서버에 대한 데이터를 저장한다. (모든 인스턴스는 local DB를 소유하며 local DB 자체는 복제되지 않음)
config : 샤딩된 MongoDB 클러스터는 config DB를 사용하여 각 샤드의 정보를 저장한다.

 

복제에 대하여
MongoDB에서 복제를 하기 위해서는 ReplicaSet이란 개념을 사용한다.


Primary : 읽기 및 쓰기 작업 담당
Secondary : 프라이머리 구성원의 정보를 동기화하고 읽기 작업 담당
아비터 : 정보를 저장하지 않고 복제 세트의 복구를 돕는 역할이며 선거 시 세컨더리의 상태를 보고 투표 가능

구성원 공통 기능으로 Heartbeat를 10초마다 Ping을 보낸다.

 


동기화 작동 방식
OPLog : MongoDB 인스턴스 내에서 변경된 정보 내역을 저장하고 있는 특별한 컬렉션이다

→ Document를 쓰거나 지우면 OPLog에 기록이 남는다.

Primary의 OPLog를 이용하여 ReplicaSet 정보를 동기화할 수 있다.

 


ReadConcern : 프라이머리에 기록된 정보를 가져올지 대다수의 복제 세트에 기록된 정보를 가져올지 정함
WriteConcern : 복제 세트에 장애가 발생했을 때 중요한 정보가 사라질지 유지될지 정할 수 있음

 

샤딩에 대하여
샤딩은 여러 장비(서버)에 데이터를 분할하여 저장하는 것을 뜻한다.

for 반복문으로 100개의 Document를 저장한다고 가정해보자. 이 때 샤딩이 활성화된 몽고 디비 인스턴스 3대에 데이터를 분할하여 저장한다고 하면 다음처럼 저장할 수 있을 것이다. (물론 간단한 예시로 아래와 같이 저장할 수 있을 것이라 말한 것이며 이는 샤드를 어떻게 분할할 것인지에 따라서 다르게 저장됨)

SERVER01 : 33개
SERVER02 : 33개
SERVER03 : 34개

이렇게 분할하여 저장해두면 분산하여 데이터를 가져오기 때문에 쿼리 속도가 향상된다고 볼 수 있겠다.

쿼리할 때 어떤 데이터가 어떤 서버에 있는지는 별도 Config Server와 mongos라고 하는 Router 서버를 두어 애플리케이션은 Mongos로 쿼리하고 어떤 데이터가 어떤 서버에 있는지는 Config 서버에서 조회하여 특정 서버에 쿼리할 수 있도록 한다.

Shard : 애플리케이션이 사용할 데이터를 나누어 저장
Config Server : 샤드 클러스터가 동작되기 위해 필요한 정보를 저장 (메타데이터)
Config Server의 메타데이터를 캐싱하여 알맞은 샤드에 명령을 보내 원하는 정보를 가져오게 된다.


샤드키 : 샤드 클러스터에서 어떻게 각 Mongo 인스턴스에 데이터들을 나눠 저장할지 정하는 기준을 샤드키로 한다.
샤드키는 반드시 인덱스를 가져야 하는데, 이 때문에 샤드키는 인덱싱이 되어 있는 필드에 설정하도록 MongoDB가 제한한다.

 

 

 

 

Percona Operator for MongoDB 개념

Percona Operator for MongoDB를 선택하는 가장 큰 이유는 무엇일까?

바로 Sharding 기능을 기본으로 제공한다는 것이다.

MongoDB Community 버전은 샤딩을 제공하지 않는다.

이러한 이유로 Percona Operator for MongoDB가 k8s에서 MongoDB를 운영할 때 가장 많이 쓰인다고 한다.

레플리카세트 클러스터 구성
ReplicaSet 클러스터는 하나의 Primary 서버와 여러 개의 Replica 서버로 구성되며 클라이언트 애플리케이션은 Driver를 통해 접근한다.

 

 

샤드 클러스터 구성

샤드 클러스터의 경우 각 샤드는 DB에 저장된 데이터의 하위 집합을 포함하는 ReplicaSet이며 Mongos라우터는 클라이언트 애플리케이션의 단일 진입점 역할을 한다.

최소 3개의 노드가 있는 단일 복제본 세트로 배포된다. 노드가 실패하면 MongoD 프로세스가 있는 Pod가 다른 노드에서 자동으로 생성된다.

클라이언트 애플리케이션은 mongo+srv URI를 사용해야 한다. 이를 통해 드라이버는 동적으로 할당된 노드에 대한 호스트 이름을 나열하지 않고도 DNS SRV 항목에서 복제 세트 구성원 목록을 검색할 수 있다.

StatefulSet 애플리케이션에 데이터 스토리지를 제공하기 위해 Kubernetes PV를 사용한다.

 

 

 

 

Operator 구성

 

Operator 기능은 PerconaServerMongoDB Object를 사용하여 Kubernetes API를 통해 제어하게 된다.

각 PerconaServerMongoDB Object는 MongoDB 설정을 위한 하나의 별도 Percona Server에 매핑된다.

 

 

 

 

Percona Operator for MongoDB 설치

 

1. CRD 설치

 

 

 

2. RBAC 설치

 

 

 

3. Percona Operator 설치

curl -s -O <https://raw.githubusercontent.com/gasida/DOIK/main/psmdb/operator.yaml>
cat operator.yaml | yh
kubectl apply -f operator.yaml

 

 

 

4. Secret 세팅 (계정 정보 설정을 위함)

 

 

5. 클러스터 생성

-> PerconaServerMongoDB CR을 통해 MongoDB Cluster를 손쉽게 생성할 수 있다.

 

**curl -s -O <https://raw.githubusercontent.com/gasida/DOIK/main/psmdb/cluster1.yaml**>
cat cluster1.yaml | yh
cat cluster1.yaml | sed -e "s/my-cluster-name/$MYNICK/" | **kubectl apply -f -** && kubectl get psmdb -w

 

 

 

apiVersion: psmdb.percona.com/v1
kind: PerconaServerMongoDB
metadata:
  name: nyyang
  finalizers:
    - delete-psmdb-pods-in-order
spec:
  crVersion: 1.15.0
  image: percona/percona-server-mongodb:6.0.9-7
  imagePullPolicy: Always
  allowUnsafeConfigurations: false
  updateStrategy: SmartUpdate
  upgradeOptions:
    versionServiceEndpoint: <https://check.percona.com>
    apply: disabled
    schedule: "0 2 * * *"
    setFCV: false
  secrets:
    users: nyyang-secrets
    encryptionKey: nyyang-mongodb-encryption-key
  pmm:
    enabled: false
    image: percona/pmm-client:2.39.0
    serverHost: monitoring-service
  replsets:
  - name: rs0
    size: 3
    # for more configuration fields refer to <https://docs.mongodb.com/manual/reference/configuration-options/>
    configuration: |
      operationProfiling:
        mode: slowOp
      systemLog:
        verbosity: 1
    affinity:
      antiAffinityTopologyKey: "kubernetes.io/hostname"
    podDisruptionBudget:
      maxUnavailable: 1
    expose:
      enabled: false
      exposeType: ClusterIP
    resources:
      limits:
        cpu: "300m"
        memory: "0.5G"
      requests:
        cpu: "300m"
        memory: "0.5G"
    volumeSpec:
      persistentVolumeClaim:
        resources:
          requests:
            storage: 3Gi
    nonvoting:
      enabled: false
      size: 3
      affinity:
        antiAffinityTopologyKey: "kubernetes.io/hostname"
      podDisruptionBudget:
        maxUnavailable: 1
      resources:
        limits:
          cpu: "300m"
          memory: "0.5G"
        requests:
          cpu: "300m"
          memory: "0.5G"
      volumeSpec:
        persistentVolumeClaim:
          resources:
            requests:
              storage: 3Gi
    arbiter:
      enabled: false
      size: 1
      affinity:
        antiAffinityTopologyKey: "kubernetes.io/hostname"
      resources:
        limits:
          cpu: "300m"
          memory: "0.5G"
        requests:
          cpu: "300m"
          memory: "0.5G"
  sharding:
    enabled: false
    configsvrReplSet:
      size: 3
      affinity:
        antiAffinityTopologyKey: "kubernetes.io/hostname"
      podDisruptionBudget:
        maxUnavailable: 1
      expose:
        enabled: false
        exposeType: ClusterIP
      resources:
        limits:
          cpu: "300m"
          memory: "0.5G"
        requests:
          cpu: "300m"
          memory: "0.5G"
      volumeSpec:
        persistentVolumeClaim:
          resources:
            requests:
              storage: 3Gi
    mongos:
      size: 3
      affinity:
        antiAffinityTopologyKey: "kubernetes.io/hostname"
      podDisruptionBudget:
        maxUnavailable: 1
      resources:
        limits:
          cpu: "300m"
          memory: "0.5G"
        requests:
          cpu: "300m"
          memory: "0.5G"
      expose:
        exposeType: ClusterIP
  backup:
    enabled: false
    image: percona/percona-backup-mongodb:2.3.0
    serviceAccountName: percona-server-mongodb-operator
    pitr:
      enabled: false
      oplogOnly: false
      compressionType: gzip
      compressionLevel: 6

 

 

  • secret은 기 생성해둔 secret을 사용
  • pmm(Percona MongoDB Monitoring)은 우선 비활성화해둔다 → Production이고 Prometheus를 모니터링으로 활용하고 있다면 필수로 활성화하여 모니터링해야 한다.
  • replsets는 레플리카셋을 정의할 수 있다.
    • size는 3개
    • PVC나 PDB, Affinity, Configuration(slowOp, systemLog) 등을 설정할 수 있다.
    • nonvoting, arbiter, sharding은 비활성화해둔다.
  • backup 또한 비활성화해둔다.

 

 

6. 클러스터 설치 완료 확인

StatefulSet을 통해 3대의 Pod가 생성된 것을 확인할 수 있다.

 

 

 

7. Headless 주소 확인

 

 

8. Client Pod 생성하기 후 Client Pod를 통해 Mongo Shell 이용하기

: Mongo Shell을 통해 MongoDB에 접속하기 위한 MongoDB Client Pod를 생성하자.

 

 

 

9. 복제 관련 내용 확인

# [터미널1] 클러스터 접속(ADMIN_USER)
kubectl exec ds/myclient -it -- mongo --quiet "mongodb+srv://userAdmin:userAdmin123456@$MYNICK-rs0.psmdb.svc.cluster.local/admin?replicaSet=rs0&ssl=false"

# [터미널2] 클러스터 접속(CLUSTER_USER)
kubectl exec ds/myclient -it -- mongo --quiet "mongodb+srv://clusterAdmin:clusterAdmin123456@$MYNICK-rs0.psmdb.svc.cluster.local/admin?replicaSet=rs0&ssl=false"

# [터미널3] 클러스터 접속(doik)
kubectl exec ds/myclient -it -- mongo --quiet "mongodb+srv://doik:qwe123@$MYNICK-rs0.psmdb.svc.cluster.local/admin?replicaSet=rs0&ssl=false"

 

 

 

복제 셋 정보 확인

rs0:PRIMARY> rs.status()['members']
[
	{
		"_id" : 0,
		"name" : "nyyang-rs0-0.nyyang-rs0.psmdb.svc.cluster.local:27017",
		"health" : 1,
		"state" : 1,
		"stateStr" : "PRIMARY",
		"uptime" : 7941,
		"optime" : {
			"ts" : Timestamp(1699715326, 1),
			"t" : NumberLong(1)
		},
		"optimeDate" : ISODate("2023-11-11T15:08:46Z"),
		"lastAppliedWallTime" : ISODate("2023-11-11T15:08:46.521Z"),
		"lastDurableWallTime" : ISODate("2023-11-11T15:08:46.521Z"),
		"syncSourceHost" : "",
		"syncSourceId" : -1,
		"infoMessage" : "",
		"electionTime" : Timestamp(1699707460, 2),
		"electionDate" : ISODate("2023-11-11T12:57:40Z"),
		"configVersion" : 8,
		"configTerm" : 1,
		"self" : true,
		"lastHeartbeatMessage" : ""
	},
	{
		"_id" : 1,
		"name" : "nyyang-rs0-1.nyyang-rs0.psmdb.svc.cluster.local:27017",
		"health" : 1,
		"state" : 2,
		"stateStr" : "SECONDARY",
		"uptime" : 7857,
		"optime" : {
			"ts" : Timestamp(1699715326, 1),
			"t" : NumberLong(1)
		},
		"optimeDurable" : {
			"ts" : Timestamp(1699715326, 1),
			"t" : NumberLong(1)
		},
		"optimeDate" : ISODate("2023-11-11T15:08:46Z"),
		"optimeDurableDate" : ISODate("2023-11-11T15:08:46Z"),
		"lastAppliedWallTime" : ISODate("2023-11-11T15:08:46.521Z"),
		"lastDurableWallTime" : ISODate("2023-11-11T15:08:46.521Z"),
		"lastHeartbeat" : ISODate("2023-11-11T15:08:47.848Z"),
		"lastHeartbeatRecv" : ISODate("2023-11-11T15:08:47.216Z"),
		"pingMs" : NumberLong(1),
		"lastHeartbeatMessage" : "",
		"syncSourceHost" : "nyyang-rs0-0.nyyang-rs0.psmdb.svc.cluster.local:27017",
		"syncSourceId" : 0,
		"infoMessage" : "",
		"configVersion" : 8,
		"configTerm" : 1
	},
	{
		"_id" : 2,
		"name" : "nyyang-rs0-2.nyyang-rs0.psmdb.svc.cluster.local:27017",
		"health" : 1,
		"state" : 2,
		"stateStr" : "SECONDARY",
		"uptime" : 7823,
		"optime" : {
			"ts" : Timestamp(1699715326, 1),
			"t" : NumberLong(1)
		},
		"optimeDurable" : {
			"ts" : Timestamp(1699715326, 1),
			"t" : NumberLong(1)
		},
		"optimeDate" : ISODate("2023-11-11T15:08:46Z"),
		"optimeDurableDate" : ISODate("2023-11-11T15:08:46Z"),
		"lastAppliedWallTime" : ISODate("2023-11-11T15:08:46.521Z"),
		"lastDurableWallTime" : ISODate("2023-11-11T15:08:46.521Z"),
		"lastHeartbeat" : ISODate("2023-11-11T15:08:48.276Z"),
		"lastHeartbeatRecv" : ISODate("2023-11-11T15:08:47.925Z"),
		"pingMs" : NumberLong(1),
		"lastHeartbeatMessage" : "",
		"syncSourceHost" : "nyyang-rs0-1.nyyang-rs0.psmdb.svc.cluster.local:27017",
		"syncSourceId" : 1,
		"infoMessage" : "",
		"configVersion" : 8,
		"configTerm" : 1
	}
]

 

 

 

복제 셋 정보 확인

rs0:PRIMARY> db.isMaster()
{
	"topologyVersion" : {
		"processId" : ObjectId("654f79fb386465a53eb08a51"),
		"counter" : NumberLong(13)
	},
	"hosts" : [
		"nyyang-rs0-0.nyyang-rs0.psmdb.svc.cluster.local:27017",
		"nyyang-rs0-1.nyyang-rs0.psmdb.svc.cluster.local:27017",
		"nyyang-rs0-2.nyyang-rs0.psmdb.svc.cluster.local:27017"
	],
	"setName" : "rs0",
	"setVersion" : 8,
	"ismaster" : true,
	"secondary" : false,
	"primary" : "nyyang-rs0-0.nyyang-rs0.psmdb.svc.cluster.local:27017",
	"tags" : {
		"podName" : "nyyang-rs0-0",
		"serviceName" : "nyyang"
	},
	"me" : "nyyang-rs0-0.nyyang-rs0.psmdb.svc.cluster.local:27017",
	"electionId" : ObjectId("7fffffff0000000000000001"),
	"lastWrite" : {
		"opTime" : {
			"ts" : Timestamp(1699715284, 2),
			"t" : NumberLong(1)
		},
		"lastWriteDate" : ISODate("2023-11-11T15:08:04Z"),
		"majorityOpTime" : {
			"ts" : Timestamp(1699715284, 2),
			"t" : NumberLong(1)
		},
		"majorityWriteDate" : ISODate("2023-11-11T15:08:04Z")
	},
	"maxBsonObjectSize" : 16777216,
	"maxMessageSizeBytes" : 48000000,
	"maxWriteBatchSize" : 100000,
	"localTime" : ISODate("2023-11-11T15:08:05.603Z"),
	"logicalSessionTimeoutMinutes" : 30,
	"connectionId" : 14175,
	"minWireVersion" : 0,
	"maxWireVersion" : 17,
	"readOnly" : false,
	"ok" : 1,
	"$clusterTime" : {
		"clusterTime" : Timestamp(1699715284, 2),
		"signature" : {
			"hash" : BinData(0,"OIC9i//r6Ys07KbQJCf5vPPc5ro="),
			"keyId" : NumberLong("7300187953467228166")
		}
	},
	"operationTime" : Timestamp(1699715284, 2)
}

 

 

 

오피로그 정보 확인 : 크기, 연산 시간

 

 

 

동기화 상태 확인 : 세컨더리 구성원이 프라이머리의 어디까지 정보를 동기화했는지 확인 (복제 지연 확인)

 

 

복제 옵션이 어떻게 설정되었는지 확인

rs0:PRIMARY> rs.conf()
{
	"_id" : "rs0",
	"version" : 8,
	"term" : 1,
	"members" : [
		{
			"_id" : 0,
			"host" : "nyyang-rs0-0.nyyang-rs0.psmdb.svc.cluster.local:27017",
			"arbiterOnly" : false,
			"buildIndexes" : true,
			"hidden" : false,
			"priority" : 2,
			"tags" : {
				"podName" : "nyyang-rs0-0",
				"serviceName" : "nyyang"
			},
			"secondaryDelaySecs" : NumberLong(0),
			"votes" : 1
		},
		{
			"_id" : 1,
			"host" : "nyyang-rs0-1.nyyang-rs0.psmdb.svc.cluster.local:27017",
			"arbiterOnly" : false,
			"buildIndexes" : true,
			"hidden" : false,
			"priority" : 2,
			"tags" : {
				"serviceName" : "nyyang",
				"podName" : "nyyang-rs0-1"
			},
			"secondaryDelaySecs" : NumberLong(0),
			"votes" : 1
		},
		{
			"_id" : 2,
			"host" : "nyyang-rs0-2.nyyang-rs0.psmdb.svc.cluster.local:27017",
			"arbiterOnly" : false,
			"buildIndexes" : true,
			"hidden" : false,
			"priority" : 2,
			"tags" : {
				"podName" : "nyyang-rs0-2",
				"serviceName" : "nyyang"
			},
			"secondaryDelaySecs" : NumberLong(0),
			"votes" : 1
		}
	],
	"protocolVersion" : NumberLong(1),
	"writeConcernMajorityJournalDefault" : true,
	"settings" : {
		"chainingAllowed" : true,
		"heartbeatIntervalMillis" : 2000,
		"heartbeatTimeoutSecs" : 10,
		"electionTimeoutMillis" : 10000,
		"catchUpTimeoutMillis" : -1,
		"catchUpTakeoverDelayMillis" : 30000,
		"getLastErrorModes" : {

		},
		"getLastErrorDefaults" : {
			"w" : 1,
			"wtimeout" : 0
		},
		"replicaSetId" : ObjectId("654f7a44386465a53eb08a8a")
	}
}

 

 

 

(참고) 구성원 설정값

  • arbiterOnly : (Optional) 구성원을 아비터로 사용할 경우 true로 설정
  • buildIndexes : (Optional) priority가 0이 아닐 때 기본값은 true이며 프라이머리를 따라서 인덱스를 백업할것인지 정할 수 있음
  • hidden : (Optional) true일 경우 드라이버가 해당 구성원에 명령을 내리지 못하도록 숨길 수 있음
  • priority : (Required) 선거가 열렸을 때 해당 구성원이 프라이머리로 선출될 상대적인 가능성을 의미함
  • votes : (Optional) 기본적으로 1표씩 가지며 이 값에 따라 선거 시 해당 구성원의 투표수가 결정됨

 

 

 

 

 

10. 복제 확인

1번은 Primary 인스턴스, 2번과 3번은 Replica 인스턴스이다.

1번 터미널에서 test Collection에 1개의 Collection 데이터를 insert 하자마자 2번 터미널, 3번 터미널에서 곧바로 확인된 것을 확인할 수 있다.

 

 

 

for문으로 1000개의 데이터를 계속 넣었을 때도 준실시간급으로 데이터가 복제가 되고 있는 것을 확인할 수 있다.

 

 

 

11. 장애 테스트

Primary 파드를 죽였을 때 얼마나 다운타임이 발생하는지 확인을 해보자.

현재 rs0-0 파드가 Primary 노드인 것을 확인할 수 있다.

 

 

rs0-0번 파드를 삭제해봅시다.

 

 

Operator 파드가 이를 감지하고 그에 따라 조치를 취해주는 것을 확인할 수 있다.

StatefulSet is not up to date	{"controller": "psmdb-controller", "object": {"name":"nyyang","namespace":"psmdb"}, "namespace": "psmdb", "name": "nyyang", "reconcileID": "d62f0048-fa77-4967-beea-7f0d8f69c1e0", "sts": "nyyang-rs0"

 

 

읽는 데 다운타임이 발생하지 않은 것을 확인

 

 

db.isMaster()

rs0-1번 파드가 Primary로 승격된 것을 확인할 수 있다.

 

 

 

 

지금까지 MongoDB 개념, Percona Operator for MongoDB에 대한 개념과 간단한 실습까지 진행해보았다.

 

샤딩 설정, PMM(모니터링) 설정, 백업, 아비터 등 해보지 않은 작업들이 많이 남았는데 기회가 된다면 다음에 꼭 해볼 생각이다.

반응형