[doik2] - Percona Operator for MongoDB
Published: 2023-11-05 11:02:52

# Overview
CloudNet@ 에서 운영하고 있는 DOIK (Database Operator In Kubernetes) 4주차 스터디에서 Percona 오퍼레이터를 사용하여 MongoDB를 쿠버네티스에서 운영하는 실습을 진행하고 학습한 내용을 정리하였습니다.

## MongoDB 소개

MongoDB 는 NoSQL 데이터베이스.
NoSQL이란, 관계형 데이터베이스의 한계를 극복하기 위해 만들어진 데이터베이스로 스키마가 존재하지 않고 탄력적인 데이터 저장 모델을 가진 데이터베이스이다. 
- MongoDB 문서 또는 문서 컬렉션은 데이터의 기본 단위. 
- 문서들은 이진 JSON(JavaScript 객체 표기법) 형식으로 지정되어 다양한 유형의 데이터를 저장할뿐 아니라, 여러 시스템 전반에 분산 처리될 수 있다. 
- MongoDB는 동적 스키마 설계를 활용하므로 사용자는 독보적인 유연성을 확보해 데이터 레코드를 생성하고, MongoDB 집계를 통해 문서 컬렉션을 쿼리하며, 대량의 정보를 분석한다.

**주요 용어**
- Document : 기본 데이터 단위로 관계형 데이터베이스에서 행과 유사. Document 는 BSON 형식에 저장된 필드와 값 쌍으로 구성된다.

- Collection : Document 들의 그룹으로 관계형 데이터베이스의 테이블과 유사하다. 스키마를 강제하지 않으므로 Collection 내의 Document 들은 각기 다른 필드와 구조를 가질 수 있다.

- Database : MongoDB 인스턴스는 여러 개의 데이터베이스를 호스팅할 수 있고 각각의 데이터베이스는 컬렉션의 컨테이너로 작용한다. DB의 디스크 별로 파일에 데이터를 저장하고, 각 DB는 고유한 이름을 가진다.



## Percona Operator 소개

Percona Operator 는 Percona 서버의 생성, 수정, 삭제를 자동화하는 쿠버네티스 Operator.
Percona Server for MongoDB는 MongoDB 인스턴스 관리를 자동화 해주는 Percona 쿠버네티스 Operator이다.

**주요 특징**
- CloudNative 
  - 클라우드 프로바이더의 쿠버네티스 엔진 서비스 내 설치 지원
- MonboDB 샤딩
- 통신구간 암호화 (TLS)
- 저장 데이터 암호화
- 백업 및 복원
- MongoDB와 Operator 업그레이드 지원
- 수평 및 수직 스케일
- 멀티 클러스터 및 멀티 리전 배포 지원
- 모니터링
- 다양한 백업 전략 지원

등등.. 손쉬운 MongoDB 운영을 위한 다양한 기능을 지원한다.

## Percona Operator 설치

**오퍼레이터 설치**

Percona server for MongoDB on Kubernetes 설치
아래 커맨드를 입력하여 Percona Operator 및 실습을 위한 환경을 준비합니다.
```
kubectl apply --server-side -f https://raw.githubusercontent.com/gasida/DOIK/main/psmdb/crd.yaml

kubectl create ns psmdb

kubectl apply -f https://raw.githubusercontent.com/gasida/DOIK/main/psmdb/rbac.yaml

curl -s -O https://raw.githubusercontent.com/gasida/DOIK/main/psmdb/operator.yaml

curl -s -O https://raw.githubusercontent.com/gasida/DOIK/main/psmdb/secrets.yaml
```

**신규 클러스터 생성**

`cat cluster1.yaml | sed -e "s/my-cluster-name/$MYNICK/" | kubectl apply -f - && kubectl get psmdb -w`

cluster1.yaml 파일에 아래 내용을 생성한뒤 위의 명령을 통해 실행합니다.

```
apiVersion: psmdb.percona.com/v1
kind: PerconaServerMongoDB
metadata:
  name: my-cluster-name
  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: my-cluster-name-secrets
    encryptionKey: my-cluster-name-mongodb-encryption-key
  pmm:
    enabled: false
    image: percona/pmm-client:2.39.0
    serverHost: monitoring-service
  replsets:
  - name: rs0
    size: 3
    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
```

생성할 PerconaServerMongoDB 리소스를 분석해봅시다.

**replsets**
https://docs.percona.com/percona-operator-for-mongodb/operator.html#replsets-section
- replsets 섹션은 MongoDB Replica Sets 을 컨트롤한다.
  - site: Replica Set 3개가 생성된다.
  - 각각의 볼륨은 PVC를 사용하여 3Gi 할당
  - arbiter 옵션 false
    - 특정 상황에서, (Primary, Secondary 가 있으나 비용 제약으로 인해 Secondary 세트를 추가할 수 없는 경우) arbiter는 Secondary 의 역할을 수행하여 Primary 선출 과정의 선거에 참여한다. 하지만 데이터를 가지고 있지 않으며, Primary 로 선출 될 수 없다.

**sharding**
샤딩은 여러 머신에 데이터를 분산하는 방법이다. MongoDB는 샤딩을 사용하여 매우 큰 데이터 세트와 높은 처리량의 작업으로 배포를 지원한다.
- 현재 Sharding 옵션은 비활성화 되어 있지만
- affinity.antiAffinityTopologyKey: "kubernetes.io/hostname" 를 통해서 같은 호스트에 여러 노드가 배치되지 않도록 합니다.

**추가정리**

MongoDB의 Replica Set
Replica Set 은 MongoDB 데이터 복사본을 유지하는 서버들의 그룹. 이 구조는 데이터의 고가용성과 내구성을 보장한다. Primary에서 데이터를 쓰면 이 변경사항이 Secondary 로 복제.
Replica Set은 데이터 손실을 방지하고, 서버 장애시 자동 장애조치 (Failover)를 통해 데이터베이스의 지속적인 가용성을 보장한다.

Sharding
Sharing은 데이터베이스의 데이터를 여러 서버(샤드)에 분산시키는 과정. 이는 대용량 데이터셋을 관리하고 높은 처리량과 Read,Write 성능 달성을 위해 사용한다.
각 샤드는 데이터의 일부를 보유하고 있으며 , 이들은 별도의 Replica Set으로 구성될 수 있다.

Mongos
Mongos란 MongoDB 샤딩 환경에서 쿼리 라우터의 역할을 한다. 클라이언트 애플리케이션과 상호작용하며, 클라이언트의 쿼리를 적절한 샤드로 라우팅
Mongos는 샤딩 환경에서의 데이터 접근을 단순화함. 클라이언트는 데이터가 어느 샤드에 있는지 알 필요가 없고, Mongos를 통해 쿼리할 수 있다.

**생성된 리소스를 확인**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r9kypfb6ahxr0k4hhiix.png)

**클라이언트 설치**

```
curl -s -O https://raw.githubusercontent.com/gasida/DOIK/main/psmdb/myclient.yaml

VERSION=4.4.24-23 envsubst < myclient.yaml | kubectl apply -f -
```


## MongoDB 실습

**1. 실습! 장애 테스트 - Primary Pod 삭제**

모니터링을 시작한다.
```
echo $'rs.secondaryOk()\nuse doik\ndb.test.count()' | mongo --quiet "mongodb://doik:qwe123@$MYNICK-rs0-2.$MYNICK-rs0.psmdb.svc/admin?ssl=false"
while true; do echo $'rs.secondaryOk()\nuse doik\ndb.test.count()' | mongo --quiet "mongodb://doik:qwe123@$MYNICK-rs0-2.$MYNICK-rs0.psmdb.svc/admin?ssl=false" | grep -v Error; date; sleep 1; done

watch -d "kubectl get psmdb;echo; kubectl get pod,pvc -l app.kubernetes.io/component=mongod -owide"
```

Primary Pod를 확인한다.

MongoDB Client 로 member를 확인한다.
`rs.status()['members']`

```
{
	"_id" : 0,
	"name" : "chan-rs0-0.chan-rs0.psmdb.svc.cluster.local:27017",
	"health" : 1,
	"state" : 1,
	"stateStr" : "PRIMARY",
	"uptime" : 5297,
	"optime" : {
		"ts" : Timestamp(1699757759, 1),
		"t" : NumberLong(1)
	},
	"optimeDate" : ISODate("2023-11-12T02:55:59Z"),
	"lastAppliedWallTime" : ISODate("2023-11-12T02:55:59.393Z"),
	"lastDurableWallTime" : ISODate("2023-11-12T02:55:59.393Z"),
	"syncSourceHost" : "",
	"syncSourceId" : -1,
	"infoMessage" : "",
	"electionTime" : Timestamp(1699752539, 2),
	"electionDate" : ISODate("2023-11-12T01:28:59Z"),
	"configVersion" : 8,
	"configTerm" : 1,
	"self" : true,
	"lastHeartbeatMessage" : ""
},
```

Pod 가 배포된 Node 이름을 확인한다.
`kubectl get pod -l app.kubernetes.io/instance=$MYNICK -owide`

```
NAME         READY   STATUS    RESTARTS   AGE   IP              NODE                                              NOMINATED NODE   READINESS GATES
chan-rs0-0   1/1     Running   0          93m   192.168.1.6     ip-192-168-1-31.ap-northeast-2.compute.internal   <none>           <none>
chan-rs0-1   1/1     Running   0          92m   192.168.2.101   ip-192-168-2-48.ap-northeast-2.compute.internal   <none>           <none>
chan-rs0-2   1/1     Running   0          91m   192.168.3.39    ip-192-168-3-76.ap-northeast-2.compute.internal   <none>           <none>
```



모니터링
```
kubectl logs -l name=percona-server-mongodb-operator -f
watch -d "kubectl get psmdb;echo; kubectl get pod,pvc -l app.kubernetes.io/component=mongod -owide"
```

Primary Pod 삭제
`kubectl delete pod $MYNICK-rs0-0`


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wfkw3ygtlwbmmsohvqll.png)

rs0-0 Pod가 삭제되자마자 rs0-1 Pod 가 Primary 로 승격되었음을 확인할 수 있다.

```
{
	"_id" : 1,
	"name" : "chan-rs0-1.chan-rs0.psmdb.svc.cluster.local:27017",
	"health" : 1,
	"state" : 1,
	"stateStr" : "PRIMARY",
	"uptime" : 6794,
	"optime" : {
		"ts" : Timestamp(1699759309, 3),
		"t" : NumberLong(2)
	},
	"optimeDate" : ISODate("2023-11-12T03:21:49Z"),
	"lastAppliedWallTime" : ISODate("2023-11-12T03:21:49.882Z"),
	"lastDurableWallTime" : ISODate("2023-11-12T03:21:49.882Z"),
	"syncSourceHost" : "",
	"syncSourceId" : -1,
	"infoMessage" : "",
	"electionTime" : Timestamp(1699759293, 1),
	"electionDate" : ISODate("2023-11-12T03:21:33Z"),
	"configVersion" : 8,
	"configTerm" : 2,
	"self" : true,
	"lastHeartbeatMessage" : ""
},
```

**2. 실습! 장애 테스트 - Primary Node Drain**

Primary Pod가 배포되어있는 Node를 확인한다.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l7qnknxznk6d08x4dp0g.png)
현재 Primary Pod 는 두번째 Node에 배포되어 있다.


```
NODE=ip-192-168-2-48.ap-northeast-2.compute.internal
kubectl drain $NODE --delete-emptydir-data --force --ignore-daemonsets && kubectl get node -w
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rdkwf5pjj7wi0fzsdizl.png)

```
{
	"_id" : 0,
	"name" : "chan-rs0-0.chan-rs0.psmdb.svc.cluster.local:27017",
	"health" : 1,
	"state" : 1,
	"stateStr" : "PRIMARY",
	"uptime" : 285,
	"optime" : {
		"ts" : Timestamp(1699759622, 1),
		"t" : NumberLong(3)
	},
	"optimeDate" : ISODate("2023-11-12T03:27:02Z"),
	"lastAppliedWallTime" : ISODate("2023-11-12T03:27:02.077Z"),
	"lastDurableWallTime" : ISODate("2023-11-12T03:27:02.077Z"),
	"syncSourceHost" : "",
	"syncSourceId" : -1,
	"infoMessage" : "",
	"electionTime" : Timestamp(1699759602, 1),
	"electionDate" : ISODate("2023-11-12T03:26:42Z"),
	"configVersion" : 8,
	"configTerm" : 3,
	"self" : true,
	"lastHeartbeatMessage" : ""
},
```
Primary 였던 두번째 Pod, Node가 비활성화되자마자 첫뻔째 Pod 가 Primary 로 승격한다.

아래 명령을 통해 drain 상태였던 노드를 uncordon 후 원복
`kubectl uncordon $NODE`


## MongoDB 샤딩 실습

실습준비
```
# 삭제 정책이 변경된 gp3 스토리지 클래스를 배포한다.
kubectl delete sc gp3
kubectl apply -f https://raw.githubusercontent.com/gasida/DOIK/main/1/gp3-sc-retain.yaml
kubectl get sc gp3

# 신규 클러스터 생성
curl -s -O https://raw.githubusercontent.com/gasida/DOIK/main/psmdb/cluster2.yaml
cat cluster2.yaml | yh
cat cluster2.yaml | sed -e "s/my-cluster-name/$MYNICK/" | kubectl apply -f -
kubectl annotate service $MYNICK-mongos "external-dns.alpha.kubernetes.io/hostname=mongos.$MyDomain"
```

mongos 라우터 접속 서비스 정보를 확인한다.
`kubectl get svc,ep $MYNICK-mongos`

클러스터 유저로 샤딩 정보 확인한다.
```
> use config
> db.shards.find().pretty()
```

```
{
	"_id" : "rs0",
	"host" : "rs0/chan-rs0-0.chan-rs0.psmdb.svc.cluster.local:27017,chan-rs0-1.chan-rs0.psmdb.svc.cluster.local:27017,chan-rs0-2.chan-rs0.psmdb.svc.cluster.local:27017",
	"state" : 1,
	"topologyTime" : Timestamp(1699760356, 4)
}
{
	"_id" : "rs1",
	"host" : "rs1/chan-rs1-0.chan-rs1.psmdb.svc.cluster.local:27017,chan-rs1-1.chan-rs1.psmdb.svc.cluster.local:27017,chan-rs1-2.chan-rs1.psmdb.svc.cluster.local:27017",
	"state" : 1,
	"topologyTime" : Timestamp(1699760356, 12)
}
```

샤딩 활성화 하기 (클러스터 사용자로 접속)
```
use config
sh.enableSharding("doik")
db.settings.save({_id: "chunksize", value: 1})
db.settings.find()
```

```
{ "_id" : "ReadWriteConcernDefaults", "defaultReadConcern" : { "level" : "majority" }, "defaultWriteConcern" : { "w" : "majority", "wtimeout" : 0 }, "updateOpTime" : Timestamp(1699760770, 2), "updateWallClockTime" : ISODate("2023-11-12T03:46:10.965Z") }
{ "_id" : "chunksize", "value" : 1 }
```

샤딩 활성화를 위해 샤딩하려는 키에 해시 인덱스를 설정 (doik 유저)
```
db.test.createIndex({"username" : "hashed"})
```

해시 인덱스 활성화 확인 (클러스터유저)
```
sh.shardCollection("doik.test", {"username" : "hashed"})
```

```
{
	"collectionsharded" : "doik.test",
	"ok" : 1,
	"$clusterTime" : {
		"clusterTime" : Timestamp(1699760894, 139),
		"signature" : {
			"hash" : BinData(0,"OBIPwrS9/ZgPY7MzRpLLr6htoTk="),
			"keyId" : NumberLong("7300414710560587801")
		}
	},
	"operationTime" : Timestamp(1699760894, 135)
}
```

데이터 입력
`for (i=0; i<200; i++) {db.test.insert({"username" : "user"+i, "created_at" : new Date()})}`

입력된 데이터를 질의해본다.
`db.test.find({username: "user124"})`

`db.test.find({username: "user124"}).explain()`

```
{
  "queryPlanner" : {
    "mongosPlannerVersion" : 1,
    "winningPlan" : {
      "stage" : "SINGLE_SHARD",
      "shards" : [
      {
        "shardName" : "rs1",
        "connectionString" : "rs1/chan-rs1-0.chan-rs1.psmdb.svc.cluster.local:27017,chan-rs1-1.chan-rs1.psmdb.svc.cluster.local:27017,chan-rs1-2.chan-rs1.psmdb.svc.cluster.local:27017",

...
}
```
쿼리 내부 정보를 살펴보면 SHARD 로 질의한 쿼리임을 확인할 수 있다.