| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- Java
- JPA
- webflux
- mysql
- SpringBoot
- 백엔드개발
- docker
- 동시성제어
- netty
- NIO
- redis
- spring boot
- selector
- helm
- Kubernetes
- CloudNative
- grafana
- 데이터베이스
- monitoring
- 성능 최적화
- DevOps
- 백엔드
- prometheus
- jvm
- 성능최적화
- RDBMS
- 트랜잭션
- Kotlin
- GitOps
- kafka
- Today
- Total
유성
Kubernetes에서 MySQL 마스터-슬레이브(Replication) 구축하기 본문
쿠버네티스에서 데이터베이스를 운영할 때 가장 큰 고민은 "데이터의 영속성과 성능"일 것이다.
이번 글에서는 로컬 디스크를 활용해 성능을 최적화하고, 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 부분은 다음과 같이 처리된다.
- Hostname(mysql-0, mysql-1)에서 숫자를 추출한다.
- 0번이면 'Master'로 판단하고 Binary Log(log-bin)를 활성화한다.
- 1번 이상이면 'Slave'로 판단하고 Read-Only 모드를 활성화한다.
- 각 포드에 겹치지 않는 고유한 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
'DevOps' 카테고리의 다른 글
| GitHub Actions와 ArgoCD로 완성하는 선언적 쿠버네티스 배포 시스템 (0) | 2026.01.29 |
|---|---|
| 정답 없는 CI/CD, 우리 환경에 맞는 최적의 파이프라인 설계 (0) | 2026.01.23 |
| Linux Page Cache와 Linux OS의 메모리 사용 전략 (0) | 2025.12.09 |
| 랜섬웨어 공격 경험 기록: Docker 설정이 만든 보안 구멍 (1) | 2025.08.21 |
| ELK 스택으로 실시간 로그 수집 및 분석하기: Logback, Filebeat, Elasticsearch, Kibana Quick Starter (1) | 2024.08.18 |