유성

Kubernetes에서 MySQL 마스터-슬레이브(Replication) 구축하기 본문

DevOps

Kubernetes에서 MySQL 마스터-슬레이브(Replication) 구축하기

백엔드 유성 2026. 1. 21. 20:14

쿠버네티스에서 데이터베이스를 운영할 때 가장 큰 고민은 "데이터의 영속성과 성능"일 것이다.

이번 글에서는 로컬 디스크를 활용해 성능을 최적화하고, StatefulSet을 이용해 마스터-슬레이브 복제 구조를 자동화하는 방법을 알아보자.

1. 데이터베이스 disk 전략: 왜 Local PV인가?

데이터베이스는 I/O 성능이 매우 중요하다. NFS나 클라우드 기반 네트워크 스토리지(EBS 등)는 관리가 편하지만, 네트워크 지연이 발생할 수밖에 없다.

 

DBMS가 실행되는 노드의 로컬 디스크를 직접 사용하면 네트워크 오버헤드 없이 최고의 속도를 낼 수 있고, 별도의 스토리지 비용을 아낄 수 있다.

이를 위해 우리는 Local Persistent Volume을 사용한다.

 

2. Storage Class 설정: 지능적인 바인딩

먼저 로컬 스토리지를 관리할 클래스를 정의한다.

# mysql-sc.yml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: manual-local
provisioner: kubernetes.io/no-provisioner # 수동으로 PV를 생성하므로 자동 프로비저너 제외
volumeBindingMode: WaitForFirstConsumer   # 핵심 설정
  • WaitForFirstConsumer 가 중요한 이유: 로컬 PV는 특정 노드에 물리적으로 묶여 있다.
    이 설정이 없으면 pod가 어느 노드에 뜰지 결정되기도 전에 PV가 아무 노드나 선점해버려, 정작 포드는 자원이 부족해 해당 노드에 뜨지 못하는 '스케줄링 데드락'이 발생할 수 있다. 이 옵션은 "pod가 뜰 위치가 결정되면 그때 PV를 연결하라" 라는 지시이다.

 

3. PersistentVolume 설정: 노드와 데이터의 고정

이제 각 워커 노드의 물리적 경로를 쿠버네티스 자원으로 등록한다.

# mysql-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mysql-pv-0
spec:
  storageClassName: manual-local
  capacity:
    storage: 10Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain # 포드가 삭제되어도 데이터 유지
  local:
    path: /mnt/mysql-master # 노드의 실제 경로
  nodeAffinity: # PV가 물리적으로 존재하는 노드 명시
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - k8s-worker1 # 노드 이름
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mysql-pv-1
spec:
  storageClassName: manual-local
  capacity:
    storage: 10Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  local:
    path: /mnt/mysql-slave # 두번째 노드의 실제 경로
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - k8s-worker2 # 노드 이름

 

  • 주의사항: PV를 생성하기 전, 해당 워커 노드(k8s-worker1, k8s-worker2)에 접속하여 mkdir, chmod 777 로 디렉토리를 생성하고 권한을 부여해야 함
  • 데이터의 안정성: persistentVolumeReclaimPolicy: Retain 설정을 통해 포드가 삭제되어도 실제 노드의 데이터는 유지되도록 설계했다.

 

4. 네트워크와 동적 설명: Headless Service & ConfigMap

MySQL 복제 구조에서 각 노드가 고유한 주소를 가져야 하며, 자신의 역할(Master/Slave)을 알아야 한다.

# mysql-config.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
spec:
  ports:
  - port: 3306
    name: mysql
  clusterIP: None # Headless Service
  selector:
    app: mysql
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
data:
  mysql-setup.sh: |
    set -ex
    # 명령어 `hostname` 대신 환경 변수 $HOSTNAME 사용
    [[ $HOSTNAME =~ -([0-9]+)$ ]] || exit 1
    ordinal=${BASH_REMATCH[1]}

    mkdir -p /etc/mysql/conf.d

    echo "[mysqld]" > /etc/mysql/conf.d/server-id.cnf
    echo "server-id=$((100 + $ordinal))" >> /etc/mysql/conf.d/server-id.cnf

    if [[ $ordinal -eq 0 ]]; then
      # 마스터 설정
      echo -e "[mysqld]\nlog-bin" > /etc/mysql/conf.d/master.cnf
    else
      # 슬레이브 설정
      echo -e "[mysqld]\nread-only" > /etc/mysql/conf.d/slave.cnf
    fi


Headless Service (clusterIp: None)

일반적인 서비스는 로드밸런싱을 위해 단일 IP를 제공하지만, DB 클러스터에서는 각 포드의 개별 IP에 접근해야 한다.

헤드리스 서비스를 통해 mysql-0.mysql, mysql-1.mysql 같은 고유한 DNS 이름을 부여받는다.

 

동적 설정을 위한 ConfigMap

하나의 이미지를 사용하면서 pod 번호에 따라 설정을 다르게 주입하기 위해 쉘 스크립트를 활용한다.

mysql-setup 부분은 다음과 같이 처리된다.

  1. Hostname(mysql-0, mysql-1)에서 숫자를 추출한다.
  2. 0번이면 'Master'로 판단하고 Binary Log(log-bin)를 활성화한다.
  3. 1번 이상이면 'Slave'로 판단하고 Read-Only 모드를 활성화한다.
  4. 각 포드에 겹치지 않는 고유한 server-id(100, 101...)를 부여한다.

 

5. MySQL 서비스 기동

마지막으로 위에서 준비한 모든 조각을 하나로 합친다.

# mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  serviceName: "mysql"
  replicas: 2
  template:
    metadata:
      labels:
        app: mysql
    spec:
      initContainers:
      - name: init-mysql
        image: mysql:8.0
        command: ["bash", "/mnt/config-map/mysql-setup.sh"]
        volumeMounts:
        - name: config-map
          mountPath: /mnt/config-map
        - name: conf
          mountPath: /etc/mysql/conf.d
      containers:
      - name: mysql
        image: mysql:8.0
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "password"
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
      volumes:
      - name: config-map
        configMap:
          name: mysql-config
      - name: conf
        emptyDir: {}
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      storageClassName: manual-local
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi

 

  • initContainers: 메인 컨테이너가 실행되기 전, 앞서 만든 스크립트를 실행해 설정 파일(.cnf)을 준비한다.
  • volumeClaimTemplates: 이 설정 덕분에 mysql-0은 mysql-pv-0을, mysql-1은 mysql-pv-1을 자동으로 찾아가 바인딩된다. 결과적으로 데이터가 있는 물리 노드에 포드가 딱 붙어서 실행된다.

기동을 수행하고 결과를 상태를 확인해보면 다음과 같다.

 

이번에는 쿼리를 수행하여 데이터가 복제 되는지 확인해보자.

## ubuntu@k8s-master:~/app$ sudo kubectl exec -it mysql-0 -- mysql -u root -p
mysql> use my_db;
Database changed
mysql> create table user (name varchar(20));
Query OK, 0 rows affected (0.04 sec)
mysql> insert into user(name) values ('youseong');
Query OK, 1 row affected (0.02 sec)

## ubuntu@k8s-master:~/app$ sudo kubectl exec -it mysql-1 -- mysql -u root -p
mysql> use my_db;
Database changed
mysql> select * from user;
+----------+
| name     |
+----------+
| youseong |
+----------+
1 row in set (0.00 sec)

 

1번 노드에 위치한 master에 들어가 테이블을 만들고, 값을 삽입했다.

이후 2번 노드에 위치한 slave에 들어가 값을 확인할 수 있다.

 

실무에서 활용하는 방식은 아닐것이고 해당 방식으로 Master가 다운된경우 Slave가 Master로 격상 되지 않는다.

수동방식으로 PV를 생성해서 pod를 연결해보았다.

 

다음 글에서 spirng 프로젝트를 통해 master slave 접근을 활용해보자.

 

---

+추가+

 

자동 방식에 대해서 글을 추가하였으니, 필요한 경우 아래 글을 참고하자

https://youseong.tistory.com/149

 

Kubernetes 기반 PostgreSQL 고가용성(HA) 아키텍처 구축 및 카오스 검증

과거 LifeKeeper 같은 고가의 솔루션으로 수일씩 걸려 구축했던 DB 고가용성(HA) 환경, 이제는 쿠버네티스와 Helm 명령어 단 몇 줄로 끝낼 수 있다.이번 글에서는 오픈소스를 활용해 비용 없이 무적의

youseong.tistory.com