카테고리 보관물: MySQL&MariaDB

Migrating Standalone MariaDB to a Galera Cluster on Kubernetes

개요

MariaDB Galera 전환 작업을 진행했습니다. 그동안 단일 인스턴스(단일 replica + ReadWriteOnce 볼륨) 구조로 MariaDB를 운영해왔는데, Kubernetes 클러스터 업그레이드 도중 MariaDB가 떠 있는 워커 노드를 drain할 때마다 2~3분 정도 서비스가 끊기는 문제가 반복됐습니다. 단일 replica + RWO 볼륨 조합에서는 drain 시 기존 노드에서 볼륨이 detach되고 새 노드에 attach되는 동안의 지연을 구조적으로 피할 수 없었습니다.

이 글에서는 이 문제를 해결하기 위해 mariadb-operator 기반 3노드 Galera 클러스터로 전환한 전체 과정 — 사전 조사, 클러스터 구성, 데이터 마이그레이션, 최종 트래픽 전환(cutover), 그리고 그 과정에서 만난 트러블슈팅을 정리합니다.

환경

  • Kubernetes 클러스터 (Control-plane 3대, Worker 3대)
  • Storage: Rook-Ceph RBD (StorageClass, ReadWriteOnce)
  • 기존: MariaDB 10.11 단일 인스턴스(Deployment, replica 1)
  • 전환 후: MariaDB 10.11 Galera 3노드 (mariadb-operator 관리, StatefulSet)
  • 데이터 규모: 2개 DB, 총 143개 테이블, 약 2.3GB

단계별 절차

1. 전환 배경 조사

워커 drain 시 실제 단절 시간을 측정해보니, MariaDB가 떠 있는 노드를 drain할 때마다 매번 2~3분씩 서비스가 끊기는 것으로 확인됐습니다. 원인은 단일 replica라 drain되는 순간 파드가 다른 노드로 옮겨가야 하는데, RWO(ReadWriteOnce) 블록 볼륨이라 기존 노드에서 detach가 완료되어야 새 노드에서 attach가 시작되기 때문이었습니다. 이 대기 시간에 InnoDB 복구 시간까지 더해지면서 단절이 발생했습니다.

해결 방법을 두 갈래로 검토했습니다.

  • A안 — cordon-only 절차: drain 대신 cordon만 하고 kubelet을 재기동하는 방식. 컨테이너 런타임이 kubelet 재기동으로 기존 컨테이너를 죽이지 않는다는 점을 이용해, 계획된 업그레이드에 한해서는 단절을 0초로 만들 수 있음. 다만 노드 장애 등 예기치 못한 상황에는 도움이 안 됨
  • B안 — Galera 3노드 전환: 계획된 업그레이드는 물론 노드 장애 시에도 무중단이 되는 진짜 HA 구조

적합성을 미리 조사해보니, 전체 테이블이 InnoDB로 전환 가능했고(예외 1개, 아래에서 다룸), 쓰기 부하도 초당 1~2건 수준으로 매우 낮아 Galera의 동기 복제 오버헤드가 문제 될 수준이 아니었습니다. 워커 노드들의 메모리 여유도 충분해 3노드 확장에 무리가 없다고 판단해 B안으로 진행했습니다.

2. mariadb-operator 설치

OSS mariadb-operator는 Galera 클러스터의 부트스트랩·장애 감지·자동 복구를 대신 처리해주고, 무엇보다 현재 Primary 파드로만 트래픽을 라우팅하는 Service(primaryService)를 자체 제공합니다. Galera는 원래 멀티 마스터 구조라 여러 파드로 쓰기가 분산되면 오히려 인증(certification) 충돌 위험이 있는데, 이 기능 덕분에 별도의 쓰기 라우팅 설계나 MaxScale 도입 없이 “쓰기는 항상 Primary로” 요구사항을 해결할 수 있었습니다.

helm install mariadb-operator-crds oci://ghcr.io/mariadb-operator/charts/mariadb-operator-crds --version 26.6.0 -n mariadb-operator --create-namespace
helm install mariadb-operator oci://ghcr.io/mariadb-operator/charts/mariadb-operator --version 26.6.0 -n mariadb-operator

operator는 DB 워크로드와 별도 네임스페이스에 설치했습니다. 이미 이 클러스터에서 Ingress 컨트롤러, LoadBalancer 컨트롤러 등을 “컨트롤러는 자기 네임스페이스, 워크로드는 자기 네임스페이스”로 분리해온 컨벤션과 동일하며, operator를 재설치/업그레이드할 때 워크로드에 영향을 줄 위험을 줄여줍니다.

3. Galera 클러스터 구성 설계

CRD로 3노드 Galera 클러스터를 선언했습니다. 실제 사용량(메모리 약 1.4Gi)과 여유분을 고려해 request/limit을 산정했고, 워커 3대 = replica 3개인 구조라 topologySpreadConstraints로 반드시 노드당 1개씩 배치되도록 강제했습니다(그래야 노드 장애 내성이 실제로 성립합니다).

apiVersion: k8s.mariadb.com/v1alpha1
kind: MariaDB
metadata:
  name: mariadb-galera
spec:
  image: mariadb:10.11
  replicas: 3
  galera:
    enabled: true
    sst: mariabackup
  storage:
    size: 10Gi
    storageClassName: rook-ceph-block
  resources:
    requests: { cpu: 250m, memory: 1Gi }
    limits: { cpu: "1", memory: 2Gi }
  primaryService:
    type: LoadBalancer
  topologySpreadConstraints:
    - maxSkew: 1
      topologyKey: kubernetes.io/hostname
      whenUnsatisfiable: DoNotSchedule
      labelSelector:
        matchLabels:
          app.kubernetes.io/instance: mariadb-galera

4. 데이터 이관 준비

Galera는 InnoDB 테이블만 복제합니다. 기존 DB를 점검해보니 전체 143개 테이블 중 보안 플러그인이 쓰는 캐시 테이블 1개만 MEMORY 엔진이었고, 그마저도 항상 빈 테이블이라 InnoDB로 즉시 전환했습니다.

계정/권한 이관은 평문 비밀번호를 몰라도 되는 방법을 썼습니다. SHOW GRANTS FOR 'user'@'%'로 확인되는 IDENTIFIED BY PASSWORD '*HASH' 값을 그대로 새 클러스터에 CREATE USER ... IDENTIFIED BY PASSWORD '*HASH'로 재생성한 것입니다. 외부 애플리케이션이 쓰는 계정처럼 평문 비밀번호를 알 수 없는 계정도 문제없이 동일하게 이관할 수 있었습니다.

5. 데이터 마이그레이션

논리 덤프(mysqldump)로 실제 데이터를 옮겼습니다. 이 과정에서 트러블슈팅이 하나 있었는데, 아래 트러블슈팅 섹션에서 자세히 다룹니다. 최종적으로는 클러스터 내부에서 실행되는 일회성 Job으로 소스와 타겟 DB를 직접 연결하는 방식으로 안정적으로 완료했습니다.

6. 최종 컷오버

실제 트래픽 전환 직전에는, 쓰기 중인 애플리케이션을 짧게 멈추고 최종 재동기화를 한 번 더 돌려 모든 테이블에서 정확히 0건 차이임을 확인한 뒤 진행했습니다. 외부에서 접속하는 클라이언트가 있었기 때문에, 기존에 쓰던 LoadBalancer IP를 그대로 새 클러스터의 primaryService로 이전해서 그 클라이언트 쪽 설정은 전혀 바꿀 필요가 없도록 했습니다. 클러스터 내부에서 DNS 이름으로 접속하던 애플리케이션(WordPress 등)만 새 Service 이름으로 연결 문자열을 바꿔주면 됐습니다.

트러블슈팅

Galera 설정 볼륨이 StorageClass 미지정으로 Pending

현상: 클러스터를 처음 생성했을 때 3개 파드 모두 Pending 상태로 멈췄습니다. 이벤트를 보니 pod has unbound immediate PersistentVolumeClaims, PVC 쪽에는 no persistent volumes available for this claim and no storage class is set 에러가 나 있었습니다.

원인: 데이터 볼륨 외에 Galera 설정 파일 전용으로 별도 PVC가 하나 더 자동 생성되는데, 이 PVC 템플릿에는 StorageClass가 지정되지 않았고, 클러스터에 기본(default) StorageClass도 지정되어 있지 않아 바인딩될 곳이 없었습니다.

해결: 설정 파일용 볼륨을 따로 만들지 않고 데이터 볼륨을 재사용하도록 옵션을 켰습니다. 다만 이 옵션은 리소스 생성 후에는 변경 불가능한(immutable) 필드라, 처음부터 이 옵션을 켠 상태로 리소스를 삭제 후 재생성해야 했습니다.

  galera:
    enabled: true
    config:
      reuseStorageVolume: true   # 생성 후 변경 불가 - 처음부터 켜야 함

대용량 마이그레이션 중 kubectl exec 스트림 단절

현상: 소스 파드에서 mysqldump를 실행해 그 표준출력을 타겟 파드의 mysql 클라이언트로 바로 파이프 연결하는 방식으로 마이그레이션을 시도했는데, 수백만 줄이 넘어간 시점에 unexpected EOF로 스트림이 끊겼습니다. 타겟 쪽에서는 끊긴 지점의 잘린 SQL 구문 때문에 문법 오류가 발생했습니다.

원인: 두 파드를 각각 별도의 exec 세션으로 열어 로컬 셸의 파이프로 연결하는 방식이라, 데이터가 API 서버를 두 번 거치는 이중 스트리밍 구조가 됩니다. 데이터량이 크고 전송 시간이 길어질수록 이 경로가 끊길 가능성이 커집니다.

해결: 클러스터 내부에서 직접 실행되는 일회성 Job으로 전환했습니다. Job 안에서 소스/타겟 Service의 DNS 이름으로 직접 연결해 mysqldump | mysql을 실행하니, API 서버를 거치지 않고 클러스터 네트워크 안에서 바로 전송되어 안정적으로 완료됐습니다.

apiVersion: batch/v1
kind: Job
metadata:
  name: mariadb-galera-migrate
spec:
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: mariadb:10.11
          command: ["sh", "-c"]
          args:
            - |
              mysqldump -h  -uroot -p"$SRC_PW" \
                --databases db1 db2 --single-transaction --routines --triggers --events \
              | mysql -h  -uroot -p"$DST_PW"

결과 확인

전환 후 다음 항목들을 확인했습니다.

  • 마이그레이션 후 모든 테이블에서 정확히 0건 차이 검증 완료
  • 4일 이상 무재시작·무단절 운영 확인 (Galera 3노드 전부 Synced)
  • 기존 인스턴스는 행 수가 더 이상 늘지 않음(트래픽 완전히 이전) vs 신규 클러스터는 계속 증가 — 컷오버가 실제로 적용됐음을 재확인
  • 관찰 기간 이후 기존 단일 인스턴스는 설정을 문서로 백업한 뒤 정리(삭제)

단일 인스턴스 대비 이번 MariaDB Galera 전환으로, 계획된 업그레이드는 물론 예기치 못한 노드 장애 상황에서도 서비스 단절 없이 운영할 수 있는 구조를 갖추게 됐습니다.

참고

관련 포스트:

참고 문서: mariadb-operator (GitHub) · Galera Cluster Documentation

Kubernetes MariaDB Resource Limits and Health Probes

개요

MariaDB 리소스 제한 설정과 헬스체크(liveness/readiness probe) 구성을 자체 관리형 Kubernetes 클러스터에 반영했습니다. 기존 MariaDB Deployment는 리소스 request/limit이 전혀 설정되지 않은 상태(resources: {})로 2년 넘게 운영되고 있었고, liveness/readiness probe도 없어 mysqld 프로세스가 응답 없이 멈추더라도 Kubernetes가 이를 감지하고 자동으로 재시작할 방법이 없는 구조였습니다.

이번 글에서는 ① 현재 구성 점검 ② 운영 중인 리소스의 YAML 추출 및 Git 반영 ③ 리소스 제한/프로브 설계와 적용, 이렇게 세 가지 작업을 순서대로 정리합니다.

환경

  • Kubernetes v1.33.7 (Control-plane 3대, Worker 3대 HA 구성)
  • Container Runtime: Docker + cri-dockerd
  • Storage: Rook-Ceph RBD (StorageClass rook-ceph-block, ReadWriteOnce)
  • MariaDB: 10.11, mariadb-system 네임스페이스, Deployment 단일 replica
  • PVC: 10Gi, PV ReclaimPolicy: Retain
  • 서비스 노출: MetalLB LoadBalancer (내부망 전용)

단계별 절차

1. 현재 구성 형상 점검

먼저 운영 중인 Deployment, PVC, Service, ConfigMap 현황과 실제 리소스 사용량을 확인했습니다.

$ kubectl get all -n mariadb-system -o wide
$ kubectl get pvc -n mariadb-system -o wide
$ kubectl top pod -n mariadb-system

점검 결과 Deployment의 strategy는 이미 Recreate로 설정되어 있었습니다. MariaDB처럼 ReadWriteOnce 볼륨을 사용하는 단일 파드 워크로드는 RollingUpdate 방식으로 배포하면 이전 파드가 볼륨을 반납하기 전에 새 파드가 같은 볼륨을 마운트하려다 충돌하는 경우가 있어, Recreate 전략이 올바른 선택이었습니다. PV의 reclaimPolicyRetain으로 설정되어 있어 PVC가 실수로 삭제되더라도 실제 데이터는 보존되는 안전한 구조였습니다.

반면 컨테이너 리소스는 request/limit이 전혀 없는 상태였고, 실제 메모리 사용량은 약 1.4Gi 수준이었습니다. ConfigMap으로 주입한 커스텀 설정은 다음과 같았습니다.

[mysqld]
innodb_buffer_pool_size=512M
innodb_log_file_size=256M
max_connections=300

2. YAML 추출 및 Git 반영

운영 중인 리소스(PVC, ConfigMap, Deployment, Service)를 그대로 추출해 저장소에 매니페스트 파일로 문서화했습니다. Secret은 정책상 Git에 포함하지 않고, 클러스터 재구성 시 수동으로 생성할 수 있도록 커맨드만 주석으로 남겼습니다.

# root 비밀번호(mariadb-secret)는 Git에 없음 - 클러스터 재구성 시 수동 생성 필요
#   kubectl create secret generic mariadb-secret -n mariadb-system \
#     --from-literal=password='<ROOT_PASSWORD>'

추출한 매니페스트를 kubectl diff로 실제 클러스터 상태와 비교해, 새로 작성한 YAML이 운영 중인 리소스와 완전히 동일한지 먼저 확인했습니다. 차이가 없다는 것을 확인한 뒤에야 Git에 커밋했습니다.

3. 리소스 Request/Limit 및 Probe 설계

실제 메모리 사용량(~1.4Gi)과 innodb_buffer_pool_size(512M) 설정, 그리고 노드의 여유 용량을 함께 고려해 다음과 같이 값을 산정했습니다.

resources:
  requests:
    cpu: 250m
    memory: 1Gi
  limits:
    cpu: "1"
    memory: 2Gi
livenessProbe:
  exec:
    command:
      - sh
      - -c
      - mysqladmin ping -uroot -p"$MYSQL_ROOT_PASSWORD" --silent
  initialDelaySeconds: 30
  periodSeconds: 20
  timeoutSeconds: 5
  failureThreshold: 3
readinessProbe:
  exec:
    command:
      - sh
      - -c
      - mysqladmin ping -uroot -p"$MYSQL_ROOT_PASSWORD" --silent
  initialDelaySeconds: 10
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

Probe는 이미 컨테이너 환경변수로 주입되어 있던 MYSQL_ROOT_PASSWORD를 그대로 활용하는 mysqladmin ping exec 방식을 선택했습니다. 별도 Secret을 추가로 마운트할 필요 없이, 실제로 mysqld가 인증까지 정상 처리하는지 확인할 수 있는 방식입니다. Limit 산정 시에는 노드 전체 할당량 대비 여유가 충분한지(CPU 할당 비율, 메모리 할당 비율) 함께 확인한 뒤 값을 확정했습니다.

4. 적용 및 롤아웃 검증

kubectl apply 전에 반드시 kubectl diff로 변경 범위를 먼저 확인했습니다. Deployment의 strategyRecreate이기 때문에 적용 시 기존 파드가 먼저 종료되고 새 파드가 그 자리에 생성되는, 짧은 다운타임이 있는 변경이라는 점을 미리 인지하고 진행했습니다.

$ kubectl diff -f manifests/mariadb/mariadb.yaml
$ kubectl apply -f manifests/mariadb/mariadb.yaml
deployment.apps/mariadb configured

$ kubectl rollout status deployment/mariadb -n mariadb-system --timeout=120s
Waiting for deployment "mariadb" rollout to finish: 0 of 1 updated replicas are available...
deployment "mariadb" successfully rolled out

$ kubectl get pods -n mariadb-system -l app=mariadb
NAME                       READY   STATUS    RESTARTS   AGE
mariadb-xxxxxxxxxx-xxxxx   1/1     Running   0          12s

새 파드가 정상적으로 Ready 상태가 되었고, readiness probe가 통과하는 것을 확인했습니다. 로그에도 mariadbd: ready for connections가 정상 출력되어 별다른 문제 없이 전환이 완료되었습니다.

트러블슈팅

mysql.event 테이블 정의 불일치

현상: 파드 재시작 로그에 Incorrect definition of table mysql.event 에러와 함께 Event Scheduler가 비활성화된다는 메시지가 출력되었습니다.

원인: 과거 MariaDB 마이너 버전 업그레이드 이후 mysql_upgrade를 실행하지 않아, 시스템 테이블(mysql.event)의 컬럼 정의가 현재 바이너리 버전이 기대하는 스키마와 어긋난 상태로 남아있었습니다.

해결: 현재 Event Scheduler(예약 이벤트) 기능을 사용하고 있지 않아 서비스에는 영향이 없는 것으로 확인해, 이번 작업 범위에서는 별도 조치 없이 별도 후속 작업으로 분리했습니다. Event Scheduler를 사용할 계획이라면 mysql_upgrade 실행이 선행되어야 합니다.

결과 확인

최종적으로 다음 항목들을 확인해 이번 MariaDB 리소스 제한 및 헬스체크 반영 작업을 마무리했습니다.

$ kubectl get deployment mariadb -n mariadb-system \
  -o jsonpath='{.spec.template.spec.containers[0].resources}'
{"limits":{"cpu":"1","memory":"2Gi"},"requests":{"cpu":"250m","memory":"1Gi"}}

$ kubectl get pod -n mariadb-system -l app=mariadb
NAME                       READY   STATUS    RESTARTS   AGE
mariadb-xxxxxxxxxx-xxxxx   1/1     Running   0          2m
  • 리소스 request/limit 적용 완료 (request 250m/1Gi, limit 1core/2Gi)
  • liveness/readiness probe 정상 동작 확인
  • 클러스터 재기동 없이 무중단으로 서비스(LoadBalancer)가 재공지되어 애플리케이션 연결 영향 없음

참고

관련 포스트:

참고 문서: Kubernetes – Configure Liveness, Readiness and Startup Probes · Kubernetes – Resource Management for Pods and Containers

Kubernetes MariaDB Failover with Rook Ceph

개요

Kubernetes MariaDB Failover는 단일 MariaDB Pod가 실행 중인 worker node에서 다른 node로 이동할 때, Rook Ceph RBD 볼륨을 다시 연결하고 데이터베이스 서비스를 복구할 수 있는지 확인한 과정입니다. 별도의 Galera cluster를 구성하지 않고 기존 Deployment와 ReadWriteOnce PVC가 제공하는 장애 복구 범위를 검증하였습니다.

이번 작업에서는 MariaDB 매니페스트와 runtime 설정을 비교하고, 시스템 테이블 업그레이드와 물리 백업을 수행하였습니다. 또한 LoadBalancer 접근 대역을 제한한 뒤 Pod 이동, PVC 재부착, SQL 실행과 서비스 복구 시간까지 단계별로 확인하였습니다.

검증 결과 clean failover에서는 MariaDB Pod가 다른 worker node에 배치되고 기존 데이터를 사용하여 정상적으로 기동하였습니다. 다만 갑작스러운 node 전원 장애는 Pod toleration과 volume fencing 시간이 추가되므로, 이번 결과는 계획된 유지보수 상황의 복구 기준으로 해석해야 합니다.

환경

구성 요소 검증 환경 역할
Kubernetes v1.33 계열, multi control-plane Pod 재스케줄 및 Service 제공
MariaDB 10.11 LTS, single replica Deployment 애플리케이션 데이터베이스
Rook Ceph RBD StorageClass, 10Gi RWO PVC node 간 영구 볼륨 재부착
MetalLB LoadBalancer Service, TCP 3306 클러스터 외부 내부망 연결

MariaDB에는 1Gi memory request와 2Gi limit를 지정하고 InnoDB buffer pool은 512MiB로 설정하였습니다. 점검 전 PVC 여유 공간과 Ceph cluster의 HEALTH_OK 상태를 확인하였습니다. 이 구성은 추가 database replica 없이 Ceph의 storage 내구성과 Kubernetes의 Pod 재스케줄 기능을 활용합니다.

단계별 절차

1. 매니페스트와 운영 상태 확인

먼저 Git에 저장된 PVC, ConfigMap, Deployment, Service를 실제 cluster object와 비교하였습니다. Deployment는 RWO volume의 동시 attach를 피하기 위해 Recreate 전략을 사용하고 있었으며 PVC는 Bound 상태였습니다. Pod, node, Ceph 상태와 최근 event를 함께 확인하여 점검 전에 진행 중인 장애가 없는지도 검증하였습니다.

kubectl get nodes
kubectl get pods -n mariadb-system -o wide
kubectl get pvc mariadb-pv-claim -n mariadb-system -o wide
kubectl get cephcluster -n rook-ceph -o wide
kubectl diff -f manifests/mariadb/mariadb.yaml

Pod의 Running 상태만 확인하지 않고 MariaDB version, InnoDB 설정, 연결 수와 filesystem 사용량을 함께 점검하였습니다. SQL 검사로 시스템 테이블과 실제 적용 변수를 확인해야 매니페스트와 runtime 사이의 차이를 발견할 수 있습니다.

2. 업그레이드 전 물리 백업

시스템 테이블을 변경하기 전에 mariadb-backup으로 전체 data directory의 streaming physical backup을 생성하였습니다. backup stream은 클러스터 외부 경로에서 압축하고 완료 메시지, gzip 무결성, 파일 크기와 SHA-256 checksum을 확인하였습니다.

kubectl exec -n mariadb-system deploy/mariadb -- sh -c \
  'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" \
  mariadb-backup --backup --stream=xbstream --user=root' \
  | gzip -1 > /secure-backup/mariadb-pre-upgrade.xb.gz

gzip -t /secure-backup/mariadb-pre-upgrade.xb.gz
sha256sum /secure-backup/mariadb-pre-upgrade.xb.gz

Ceph replica는 disk와 OSD 장애에 대한 내구성을 제공하지만 잘못된 SQL이나 시스템 테이블 변경을 되돌리는 backup은 아닙니다. 따라서 storage 상태가 정상이더라도 database upgrade 전에는 독립적인 backup이 필요합니다.

3. MariaDB 시스템 테이블 업그레이드

점검 과정에서 MariaDB binary와 data directory의 시스템 테이블 형식이 일치하지 않는 문제를 확인하였습니다. mysql.user view와 mysql.event, mysql.proc, mysql.column_stats에서 column definition 오류가 발생하였습니다. 기존 upgrade marker로 인해 일반 검사가 완료 상태로 판단하였으므로 backup을 확인한 뒤 --force 옵션을 적용하였습니다.

kubectl exec -n mariadb-system deploy/mariadb -- sh -c \
  'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb-upgrade -uroot --force'

업그레이드 후 문제 테이블을 다시 검사하여 모두 OK 상태인 것을 확인하였습니다. application schema의 table 개수는 이전과 동일하였으며 startup log에서도 기존 definition 오류가 재발하지 않았습니다.

4. LoadBalancer 접근 소스 제한

MariaDB LoadBalancer는 클러스터 외부의 내부망 애플리케이션 서버가 접속하기 위해 유지하였습니다. 모든 source를 허용하지 않도록 loadBalancerSourceRanges에 접근 가능한 CIDR만 지정하였습니다. 아래 문서용 CIDR은 적용 환경에서 허용할 내부 대역으로 변경해야 합니다.

spec:
  type: LoadBalancer
  loadBalancerSourceRanges:
    - 192.0.2.0/24
  ports:
    - name: mariadb
      port: 3306
      targetPort: 3306

적용 전 client dry-run과 server-side diff로 Service 이외의 resource가 변경되지 않는지 확인하였습니다. 적용 후에는 source range, LoadBalancer VIP, EndpointSlice와 cluster 내부 database 연결을 검증하였습니다. NodePort가 별도 경로로 노출될 수 있으므로 routed network가 있다면 node firewall과 상위 network ACL도 함께 확인해야 합니다.

5. Clean failover 시험

node 전체를 종료하면 같은 node의 다른 workload에도 영향을 줄 수 있습니다. 이번 시험에서는 영향 범위를 MariaDB로 제한하기 위해 현재 node를 cordon하고 MariaDB Pod만 재생성하였습니다. 다음 명령은 backup과 영향 범위를 확인한 유지보수 환경에서 실행해야 합니다.

kubectl cordon <worker-node-a>
kubectl delete pod <mariadb-pod> -n mariadb-system --wait=false
kubectl get pods -n mariadb-system -l app=mariadb -o wide --watch
kubectl get volumeattachment -o wide
kubectl uncordon <worker-node-a>

LoadBalancer TCP 3306을 짧은 간격으로 확인하여 서비스 중단과 복구 시점을 기록하였습니다. 새 Pod는 <worker-node-b>에 배치되었으며 기존 RBD attachment가 해제된 후 같은 PVC를 연결하였습니다. container 시작과 readiness probe가 완료된 뒤 LoadBalancer 접속도 다시 성공하였습니다.

6. 복구 후 데이터베이스 검증

새 node에서 MariaDB가 Ready 상태가 된 뒤 version, system table, application schema, active connection과 startup log를 다시 검사하였습니다. TCP port만 확인하지 않고 실제 SQL query와 Ceph health, VolumeAttachment 대상 node까지 함께 비교하였습니다.

CHECK TABLE mysql.user;
CHECK TABLE mysql.event;
CHECK TABLE mysql.proc;
CHECK TABLE mysql.column_stats;

SELECT VERSION();
SELECT 1 AS read_test;

검증 완료 후 원래 node를 uncordon하여 scheduler가 다시 사용할 수 있도록 복원하였습니다. MariaDB Pod는 새 node에서 계속 실행되었으며 불필요한 추가 재시작은 발생하지 않았습니다.

트러블슈팅

시스템 테이블 형식 불일치

현상: MariaDB 접속은 가능했지만 system view와 statistics table 오류가 반복되었고 Event Scheduler 초기화도 실패하였습니다.

원인: container image의 MariaDB patch version은 변경되었지만 data directory의 일부 system table과 privilege가 현재 binary 형식으로 정리되지 않았습니다. 기존 upgrade marker로 인해 자동 검사는 추가 작업이 필요하지 않다고 판단하였습니다.

해결: physical backup을 확보한 뒤 mariadb-upgrade --force를 실행하고 system table을 재검사하였습니다. 모든 table이 OK 상태가 되었으며 이후 startup에서도 동일한 오류가 발생하지 않았습니다.

RBD Multi-Attach 경고

현상: 새 Pod가 다른 worker node에 생성된 직후 volume이 이전 Pod에서 사용 중이라는 Multi-Attach warning이 한 차례 발생하였습니다.

원인: ReadWriteOnce RBD volume의 이전 attachment 정리와 새 attachment 요청 사이에 짧은 시간 차이가 있었습니다. 이는 두 node가 같은 block volume을 동시에 사용하지 못하도록 보호하는 정상적인 동작입니다.

해결: 이전 Pod가 종료되고 CSI controller가 attachment를 정리할 때까지 기다렸으며 잠시 후 attach가 자동으로 성공하였습니다. 이전 node의 상태를 확인하지 않고 VolumeAttachment를 강제로 삭제하면 data corruption 위험이 있으므로 피해야 합니다.

TCP 점검으로 증가한 Aborted Connections

현상: failover 측정 후 Aborted_connects 값과 unauthenticated connection warning이 증가하였습니다.

원인: TCP port monitor가 MariaDB protocol authentication을 수행하지 않고 연결 직후 종료했기 때문입니다.

해결: 측정 종료 후 증가가 멈추는지 확인하였습니다. 상시 monitoring에는 단순 TCP connect 대신 권한이 제한된 health check 계정으로 ping 또는 query를 실행하는 방식이 적합합니다.

Clean failover와 실제 node 장애의 차이

현상: clean failover는 약 30초대에 완료되었지만 실제 node 장애도 같은 시간에 복구된다고 단정할 수 없습니다.

원인: 비정상 장애에서는 Kubernetes가 NotReady 또는 Unreachable 상태를 판단하고 Pod toleration이 만료될 때까지 기다립니다. 기존 node의 volume attachment가 남아 있다면 CSI fencing과 detach에도 시간이 필요합니다.

해결: 이번 결과는 계획된 유지보수 상황의 RTO로 기록하였습니다. 갑작스러운 전원 장애는 수 분 이상의 RTO를 예상하고 별도 점검에서 node shutdown과 MetalLB 경로 전환을 포함하여 검증해야 합니다.

결과 확인

Kubernetes MariaDB Failover 시험 결과, 단일 MariaDB Deployment와 Rook Ceph RWO PVC 구성에서도 계획된 Pod 이동 후 다른 worker node에서 데이터베이스를 정상적으로 기동할 수 있었습니다. 기존 데이터, system table, application schema, SQL query와 Ceph health가 모두 정상임을 확인하였습니다.

검증 항목 결과
다른 worker node에 Pod 배치 성공
기존 RBD PVC 재부착 성공
MariaDB 기동 및 SQL 실행 정상
LoadBalancer 서비스 복구 약 30초대
갑작스러운 node 전원 장애 이번 시험 범위에서 제외

측정 시간에는 기존 Pod 종료, RBD detach와 attach, container 시작, MariaDB 기동과 readiness probe 통과가 포함됩니다. 실제 복구 시간은 image cache, node 부하, Ceph 상태와 transaction recovery 양에 따라 달라질 수 있습니다.

현재 workload 규모에서는 Galera cluster의 추가 memory와 storage 소비보다 단일 instance, Ceph RBD, 정기 physical backup과 source-restricted LoadBalancer 조합이 적합하다고 판단하였습니다. 향후 RTO와 RPO 요구가 강화되면 asynchronous replica 또는 Galera와 database proxy 도입을 다시 검토할 수 있습니다.

storage replica는 backup을 대체하지 않습니다. 정기 backup, checksum 검증, 별도 위치 보관과 restore test를 함께 운영해야 node 장애뿐 아니라 운영 실수와 데이터 손상에도 대응할 수 있습니다.

참고

관련 포스트:

참고 문서: Kubernetes Service · MariaDB Upgrade · MariaDB Backup · Rook Ceph Block Storage