리셋 되지 말자

[Mongodb] mongodb scaling on kubernetes 본문

Kubernetes

[Mongodb] mongodb scaling on kubernetes

kyeongjun-dev 2023. 6. 9. 01:13

개요

보통 데이터베이스는 쿠버네티스에 올려서 사용하지 않는다지만, operator 패턴을 이용해 데이터베이스를 쿠버네티스에서 운영하는 사례가 많아지고 있다고 한다.(링크)

그래서 mongodb operator를 찾아보던 중, community 버전으로 사용이 가능한 operator를 발견했다. (깃허브링크)
하지만 단점이 있었는데, 우선 MongoCommunity라는 CRD를 사용해서 mongodb 클러스터를 생성하기 때문에 CPU/Memory 기반으로 Pod 스케일링이 가능한 HPA를 사용할 수 없었다.

실제로 테스트 해보니, statefulset의 replica 수를 늘리면 operator에서 강제로 replica 수를 원래대로 수정해버렸다... 방법은 MongoCommunity CRD가 기재된 yaml 파일의 members 값을 늘려서 kubectl apply 명령어를 실행하는 건데, 이러면 리소스 기반으로 스케일링 하기가 번거로워 진다. 물론 불가능한건 아니지만 매우 매우 번거로워질 것이다.

그래서 찾아보던 중, statefulset과 sidecar를 이용해 scale이 가능한 소스를 발견했고 실제로 테스트 해보니 잘 동작하여 기록하기 위해 글을 작성한다.

자료

먼저 구글링을 통해 발견한 블로그(링크)는 mongo:4.2.6 이미지를 사용하면 sidecar (nodejs로 작성되어 있다. 깃허브링크)가 잘 동작하지만, 최근버전인 6.0.6 버전을 사용하면 nodejs의 mongodb 드라이버 패키지가 호환이 안되어 sidecar가 동작하지 않는다.

아래 yaml 파일은 docker-desktop에서 동작하도록 storage class를 hostpath로 지정하여 작성하고 실제 동작하는걸 확인한 파일 내용이다.

apiVersion: v1
kind: Namespace
metadata:
  name: mongo
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: mongo
  namespace: mongo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: mongo
subjects:
  - kind: ServiceAccount
    name: mongo
    namespace: mongo
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: Service
metadata:
 name: mongo
 namespace: mongo
 labels:
   name: mongo
spec:
 ports:
 - port: 27017
   targetPort: 27017
 clusterIP: None
 selector:
   role: mongo
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongo
  namespace: mongo
spec:
  serviceName: mongo
  replicas: 3
  selector:
    matchLabels:
      app: mongo
  template:
    metadata:
      labels:
        app: mongo
        role: mongo
        environment: staging
        replicaset: MainRepSet
    spec:
      terminationGracePeriodSeconds: 10
      serviceAccountName: mongo
      containers:
        - name: mongo
          image: mongo:4.2.6
          command:
            - mongod
            - "--wiredTigerCacheSizeGB"
            - "0.25"
            - "--bind_ip"
            - "0.0.0.0"
            - "--replSet"
            - MainRepSet
          ports:
            - containerPort: 27017
          volumeMounts:
            - name: mongo-persistent-storage
              mountPath: /data/db
          resources:
            requests:
              cpu: 100m
              memory: 1Gi
        - name: mongo-sidecar
          image: cvallance/mongo-k8s-sidecar
          env:
            - name: MONGO_SIDECAR_POD_LABELS
              value: "role=mongo,environment=staging"
            - name: KUBE_NAMESPACE
              value: "mongo"
            - name: KUBERNETES_MONGO_SERVICE_NAME
              value: "mongo"
  volumeClaimTemplates:
  - metadata:
      name: mongo-persistent-storage
      annotations:
        volume.beta.kubernetes.io/storage-class: "hostpath"
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: hostpath
      resources:
        requests:
          storage: 10Gi

 

sidecar에 접속해보면 nodejs 버전이 11 버전이고, 소스코드도 mongodb 패키지를 업그레이드 했을 경우 호환되지 않는다.
다음으로 찾은 소스코드 깃허브(링크)의 경우는 mongo:6.0.6 버전을 사용했을 때, 정상적으로 동작했다.

해당 소스코드는 위에서 사용한 cvallance/mongo-k8s-sidecar의 개선된 코드라고 명시되어 있다.

It's a fork of cvallance/mongo-k8s-sidecar with (many) changes and improvements.

 

실제로 사용한 yaml 파일은 아래와 같으며 docker-desktop에서 동작하도록 storage class를 hostpath로 지정했다.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: mongo
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: read-pod-service-endpoint
rules:
  - apiGroups:
      - ""
    resources:
      - pods
      - services
      - endpoints
    verbs:
      - get
      - list
      - watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: system:serviceaccount:default:mongo
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: read-pod-service-endpoint
subjects:
  - kind: ServiceAccount
    name: mongo
    namespace: default
---
apiVersion: v1
kind: Service
metadata:
  name: mongo
  labels:
    name: mongo
spec:
  ports:
    - port: 27017
      targetPort: 27017
  clusterIP: None
  selector:
    role: mongo
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongo
spec:
  serviceName: "mongo"
  replicas: 3
  selector:
    matchLabels:
      role: mongo
  template:
    metadata:
      labels:
        role: mongo
        environment: test
    spec:
      serviceAccountName: mongo
      automountServiceAccountToken: true
      terminationGracePeriodSeconds: 30
      containers:
        - name: mongo
          image: mongo
          command:
            - mongod
          args:
            - "--replSet=rs0"
            - "--bind_ip=0.0.0.0"
          ports:
            - containerPort: 27017
          volumeMounts:
            - name: mongo-persistent-storage
              mountPath: /data/db
        - name: mongo-sidecar
          image: morphy/k8s-mongo-sidecar
          env:
            - name: KUBERNETES_POD_LABELS
              value: "role=mongo,environment=test"
            - name: KUBERNETES_SERVICE_NAME
              value: "mongo"
  volumeClaimTemplates:
    - metadata:
        name: mongo-persistent-storage
        annotations:
          volume.beta.kubernetes.io/storage-class: "hostpath"
      spec:
        storageClassName: hostpath
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 10Gi

테스트

두번째 yaml 파일을 default 네임스페이스에서 apply 하면 아래와 같이 3개의 pod가 동작한다.

$ kubectl get pod
NAME      READY   STATUS    RESTARTS   AGE
mongo-0   2/2     Running   0          59s
mongo-1   2/2     Running   0          52s
mongo-2   2/2     Running   0          43s

 

mongo-0 파드의 mongo 컨테이너의 mongh shell로 접속한다.

$ kubectl exec -it mongo-0 -c mongo -- mongosh
Current Mongosh Log ID: 6481fb98178a136f012e1ea3
Connecting to:          mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+1.9.1
Using MongoDB:          6.0.6
Using Mongosh:          1.9.1

For mongosh info see: https://docs.mongodb.com/mongodb-shell/


To help improve our products, anonymous usage data is collected and sent to MongoDB periodically (https://www.mongodb.com/legal/privacy-policy).
You can opt-out by running the disableTelemetry() command.

------
   The server generated these startup warnings when booting
   2023-06-08T16:00:38.304+00:00: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem
   2023-06-08T16:00:39.137+00:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted
   2023-06-08T16:00:39.137+00:00: You are running this process as the root user, which is not recommended
   2023-06-08T16:00:39.137+00:00: /sys/kernel/mm/transparent_hugepage/enabled is 'always'. We suggest setting it to 'never'
   2023-06-08T16:00:39.138+00:00: vm.max_map_count is too low
------

rs0 [direct: primary] test>

 

레플리카셋의 상태를 확인하면(rs.status()) 아래와 같이 3개의 mongo 컨테이너가 레플리카셋(rs0)으로 등록된걸 확인할 수 있다.

rs0 [direct: primary] test> rs.status()
{
  set: 'rs0',
  date: ISODate("2023-06-08T16:02:40.408Z"),
  myState: 1,
  term: Long("1"),
  syncSourceHost: '',
  syncSourceId: -1,
  heartbeatIntervalMillis: Long("2000"),
  majorityVoteCount: 2,
  writeMajorityCount: 2,
  votingMembersCount: 3,
  writableVotingMembersCount: 3,
  optimes: {
    lastCommittedOpTime: { ts: Timestamp({ t: 1686240152, i: 1 }), t: Long("1") },
    lastCommittedWallTime: ISODate("2023-06-08T16:02:32.114Z"),
    readConcernMajorityOpTime: { ts: Timestamp({ t: 1686240152, i: 1 }), t: Long("1") },
    appliedOpTime: { ts: Timestamp({ t: 1686240152, i: 1 }), t: Long("1") },
    durableOpTime: { ts: Timestamp({ t: 1686240152, i: 1 }), t: Long("1") },
    lastAppliedWallTime: ISODate("2023-06-08T16:02:32.114Z"),
    lastDurableWallTime: ISODate("2023-06-08T16:02:32.114Z")
  },
  lastStableRecoveryTimestamp: Timestamp({ t: 1686240092, i: 1 }),
  electionCandidateMetrics: {
    lastElectionReason: 'electionTimeout',
    lastElectionDate: ISODate("2023-06-08T16:00:41.998Z"),
    electionTerm: Long("1"),
    lastCommittedOpTimeAtElection: { ts: Timestamp({ t: 1686240041, i: 1 }), t: Long("-1") },
    lastSeenOpTimeAtElection: { ts: Timestamp({ t: 1686240041, i: 1 }), t: Long("-1") },
    numVotesNeeded: 1,
    priorityAtElection: 1,
    electionTimeoutMillis: Long("10000"),
    newTermStartDate: ISODate("2023-06-08T16:00:42.074Z"),
    wMajorityWriteAvailabilityDate: ISODate("2023-06-08T16:00:42.112Z")
  },
  members: [
    {
      _id: 0,
      name: 'mongo-0.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 122,
      optime: { ts: Timestamp({ t: 1686240152, i: 1 }), t: Long("1") },
      optimeDate: ISODate("2023-06-08T16:02:32.000Z"),
      lastAppliedWallTime: ISODate("2023-06-08T16:02:32.114Z"),
      lastDurableWallTime: ISODate("2023-06-08T16:02:32.114Z"),
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: 'Could not find member to sync from',
      electionTime: Timestamp({ t: 1686240042, i: 1 }),
      electionDate: ISODate("2023-06-08T16:00:42.000Z"),
      configVersion: 7,
      configTerm: 1,
      self: true,
      lastHeartbeatMessage: ''
    },
    {
      _id: 1,
      name: 'mongo-1.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 107,
      optime: { ts: Timestamp({ t: 1686240152, i: 1 }), t: Long("1") },
      optimeDurable: { ts: Timestamp({ t: 1686240152, i: 1 }), t: Long("1") },
      optimeDate: ISODate("2023-06-08T16:02:32.000Z"),
      optimeDurableDate: ISODate("2023-06-08T16:02:32.000Z"),
      lastAppliedWallTime: ISODate("2023-06-08T16:02:32.114Z"),
      lastDurableWallTime: ISODate("2023-06-08T16:02:32.114Z"),
      lastHeartbeat: ISODate("2023-06-08T16:02:38.628Z"),
      lastHeartbeatRecv: ISODate("2023-06-08T16:02:38.628Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: 'mongo-0.mongo.default.svc.cluster.local:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 7,
      configTerm: 1
    },
    {
      _id: 2,
      name: 'mongo-2.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 101,
      optime: { ts: Timestamp({ t: 1686240152, i: 1 }), t: Long("1") },
      optimeDurable: { ts: Timestamp({ t: 1686240152, i: 1 }), t: Long("1") },
      optimeDate: ISODate("2023-06-08T16:02:32.000Z"),
      optimeDurableDate: ISODate("2023-06-08T16:02:32.000Z"),
      lastAppliedWallTime: ISODate("2023-06-08T16:02:32.114Z"),
      lastDurableWallTime: ISODate("2023-06-08T16:02:32.114Z"),
      lastHeartbeat: ISODate("2023-06-08T16:02:38.628Z"),
      lastHeartbeatRecv: ISODate("2023-06-08T16:02:39.630Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: 'mongo-1.mongo.default.svc.cluster.local:27017',
      syncSourceId: 1,
      infoMessage: '',
      configVersion: 7,
      configTerm: 1
    }
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1686240152, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1686240152, i: 1 })
}

 

위 상태에서 mongo 스테이트풀셋을 5개로 스케일한다.

$ kubectl scale statefulset mongo --replicas=5
statefulset.apps/mongo scaled

 

파드가 5개로 늘어난것을 확인할 수 있다.

$ kubectl get pod
NAME      READY   STATUS    RESTARTS   AGE
mongo-0   2/2     Running   0          5m27s
mongo-1   2/2     Running   0          5m20s
mongo-2   2/2     Running   0          5m11s
mongo-3   2/2     Running   0          20s
mongo-4   2/2     Running   0          12s

 

그리고 현재 primary인 mongo-0 파드의 mongo 컨테이너에서 다시 레플리카셋 상태를 확인해보면 5개로 늘어난것을 확인할 수 있다.

rs0 [direct: primary] test> rs.status()
{
  set: 'rs0',
  date: ISODate("2023-06-08T16:06:33.254Z"),
  members: [
    {
      _id: 0,
      name: 'mongo-0.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
    },
    {
      _id: 1,
      name: 'mongo-1.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
    },
    {
      _id: 2,
      name: 'mongo-2.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
    },
    {
      _id: 3,
      name: 'mongo-3.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
    },
    {
      _id: 4,
      name: 'mongo-4.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
    }
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1686240392, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1686240392, i: 1 })
}

 

위 상태에서 현재 primary인 mongo-0 파드를 삭제한다. 현재 스테이트풀셋의 레플리카가 5이기 때문에 mongo-0 파드가 다시 생성된다.

$ kubectl delete pod mongo-0
pod "mongo-0" deleted

$ kubectl get pod
NAME      READY   STATUS    RESTARTS   AGE
mongo-0   2/2     Running   0          12s
mongo-1   2/2     Running   0          8m23s
mongo-2   2/2     Running   0          8m14s
mongo-3   2/2     Running   0          3m23s
mongo-4   2/2     Running   0          3m15s

 

mongo-0 파드의 mongo 쉘로 접속해서 레플리카셋의 상태를 확인해보면, mongo-1 파드의 mongo 컨테이너가 새로운 primary로 승격한걸 확인할 수 있다.

rs0 [direct: secondary] test> rs.status()
{
  set: 'rs0',
  date: ISODate("2023-06-08T16:10:14.266Z"),
  myState: 2,
  term: Long("2"),
  syncSourceHost: 'mongo-4.mongo.default.svc.cluster.local:27017',
  members: [
    {
      _id: 0,
      name: 'mongo-0.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
    },
    {
      _id: 1,
      name: 'mongo-1.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
    },
    {
      _id: 2,
      name: 'mongo-2.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
    },
    {
      _id: 3,
      name: 'mongo-3.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
    },
    {
      _id: 4,
      name: 'mongo-4.mongo.default.svc.cluster.local:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
    }
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1686240604, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1686240604, i: 1 })
}

고려사항

  • 클러스터 내부에서 스케일링되는 mongodb 파드의 엔드포인트를 잘 인식하는지?
  • 클러스터 외부에서 접속할때는 어떻게 할지? - 파드마다 nodeport나 loadbalancer를 붙여서 서비스?
  • 파라미터값 조절? - read preference를 조정
  • pvc와 pod가 다른 zone에 뜰 경우는 어떻게 되나?
  • ssl 접속 활성화?

'Kubernetes' 카테고리의 다른 글

Weaviate  (0) 2024.04.06
Grafana, Tempo 모니터링 간단 예시 - springboot, mongodb  (1) 2024.01.02
awscli irsa 테스트  (0) 2023.12.27
[Trino] 설치 및 맛보기  (0) 2023.08.10
Python Pod 예시...  (0) 2023.07.07
Comments