- Published on
Kubernetes DB 심화 가이드 2025: Operator 패턴, HA 구성, 백업/복구, 성능 튜닝 실전
- Authors

- Name
- Youngju Kim
- @fjvbn20031
목차
1. K8s에서 DB를 운영해야 하는가?
1.1 2025년 기준 장단점 분석
K8s DB 운영의 장점
- 일관된 배포: 애플리케이션과 동일한 GitOps 워크플로우로 DB도 관리
- 리소스 효율: 노드 리소스를 다른 워크로드와 공유 (전용 VM 대비 비용 절감)
- 자동 복구: Operator가 장애 감지 및 자동 Failover 수행 (RTO 30초 이하)
- 이식성: 클라우드 벤더 종속 없이 어디서든 동일한 구성
- 개발 환경 통합: 개발/스테이징에서 프로덕션과 동일한 DB 스택
K8s DB 운영의 단점
- 운영 복잡성: StorageClass, PV, Operator 업그레이드 등 추가 관리 포인트
- 스토리지 성능: 네트워크 스토리지(EBS/PD)는 로컬 디스크 대비 레이턴시 높음
- 전문성 필요: DBA + K8s 운영 양쪽 모두 깊은 이해 필요
- 백업 복잡성: K8s 특유의 볼륨 스냅샷, 오브젝트 스토리지 연동 필요
1.2 의사결정 프레임워크
| 기준 | K8s DB 적합 | 관리형 DB(RDS 등) 적합 |
|---|---|---|
| 팀 역량 | K8s + DBA 전문가 보유 | DBA 없음 |
| 비용 민감도 | 높음 (인프라 최적화 필요) | 중간 (관리 비용 포함 OK) |
| 규정 준수 | 데이터 위치 제어 필요 | 클라우드 리전 충분 |
| 멀티클라우드 | 필수 | 단일 클라우드 |
| 워크로드 규모 | 중소규모 (수백 GB) | 대규모 (수 TB 이상) |
| SLA 요구 | 99.9% 자체 달성 가능 | 99.99% 벤더 SLA 필요 |
2. StatefulSet 심화
2.1 Pod Identity (순서 인덱스와 안정적 호스트명)
StatefulSet은 각 Pod에 **순서 인덱스(ordinal index)**를 부여합니다.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: database
spec:
serviceName: postgres-headless
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gp3-encrypted
resources:
requests:
storage: 100Gi
이 StatefulSet이 생성하는 Pod:
postgres-0 → postgres-0.postgres-headless.database.svc.cluster.local
postgres-1 → postgres-1.postgres-headless.database.svc.cluster.local
postgres-2 → postgres-2.postgres-headless.database.svc.cluster.local
각 Pod는 재시작 후에도 동일한 이름, 동일한 PVC에 바인딩됩니다.
2.2 PodManagementPolicy
spec:
podManagementPolicy: OrderedReady # 기본값
# OrderedReady: 0→1→2 순서대로 생성, 2→1→0 순서대로 삭제
# Parallel: 모든 Pod 동시 생성/삭제 (DB에서는 주의 필요)
- OrderedReady: Primary(0번)가 먼저 시작되고 Ready 상태가 된 후 Replica가 시작
- Parallel: 초기 클러스터 부트스트랩 시에만 사용 (기존 데이터가 없는 경우)
2.3 volumeClaimTemplates
volumeClaimTemplates:
- metadata:
name: data
labels:
type: database-storage
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gp3-encrypted
resources:
requests:
storage: 100Gi
이 템플릿은 각 Pod에 대해 개별 PVC를 생성합니다:
data-postgres-0 → 100Gi PV (gp3-encrypted)
data-postgres-1 → 100Gi PV (gp3-encrypted)
data-postgres-2 → 100Gi PV (gp3-encrypted)
StatefulSet 삭제 시 PVC는 자동 삭제되지 않습니다 (데이터 보호).
2.4 Update 전략
spec:
updateStrategy:
type: RollingUpdate # 또는 OnDelete
rollingUpdate:
partition: 0 # partition 이상의 인덱스만 업데이트
maxUnavailable: 1 # K8s 1.24+
- RollingUpdate: 높은 인덱스부터 역순으로 업데이트 (2→1→0). Replica 먼저, Primary 마지막
- OnDelete: Pod를 수동 삭제해야 업데이트됨. 카나리 업데이트에 유용
- partition:
partition: 2로 설정하면 인덱스 2 이상만 업데이트 (카나리)
2.5 Headless Service
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
namespace: database
spec:
type: ClusterIP
clusterIP: None # Headless Service
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
Headless Service는 DNS A 레코드로 각 Pod의 IP를 직접 반환합니다:
# 모든 Pod IP 조회
nslookup postgres-headless.database.svc.cluster.local
# 특정 Pod 직접 접근
psql -h postgres-0.postgres-headless.database.svc.cluster.local -U postgres
3. CloudNativePG (PostgreSQL) Deep Dive
3.1 Cluster CRD
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: my-postgres
namespace: database
spec:
instances: 3
postgresql:
parameters:
shared_buffers: "2GB"
effective_cache_size: "6GB"
work_mem: "64MB"
maintenance_work_mem: "512MB"
max_connections: "200"
max_wal_size: "2GB"
min_wal_size: "1GB"
wal_buffers: "64MB"
random_page_cost: "1.1"
effective_io_concurrency: "200"
max_worker_processes: "8"
max_parallel_workers_per_gather: "4"
max_parallel_workers: "8"
storage:
size: 100Gi
storageClass: gp3-encrypted
pvcTemplate:
accessModes:
- ReadWriteOnce
walStorage:
size: 20Gi
storageClass: gp3-encrypted
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "4"
memory: "8Gi" # limits = requests (QoS Guaranteed)
monitoring:
enablePodMonitor: true
customQueriesConfigMap:
- name: custom-queries
key: queries
bootstrap:
initdb:
database: myapp
owner: myapp
secret:
name: myapp-db-credentials
3.2 자동 Failover
CloudNativePG는 Primary 장애 시 자동으로 Replica를 승격합니다.
정상 상태:
my-postgres-1 (Primary, RW)
my-postgres-2 (Replica, RO) - streaming replication
my-postgres-3 (Replica, RO) - streaming replication
Primary 장애 발생:
1. CloudNativePG가 Primary 장애 감지 (health check 실패)
2. 가장 최신 WAL 위치를 가진 Replica 선택
3. pg_promote() 실행 → 새 Primary 승격
4. 나머지 Replica가 새 Primary를 팔로우
5. pg_rewind로 이전 Primary를 Replica로 재합류
Failover 후:
my-postgres-2 (Primary, RW) ← 자동 승격
my-postgres-3 (Replica, RO) - 새 Primary 팔로우
my-postgres-1 (Replica, RO) - pg_rewind 후 재합류
Failover 소요 시간: 일반적으로 10-30초.
3.3 백업 설정 (Barman + Object Storage)
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: my-postgres
spec:
instances: 3
backup:
barmanObjectStore:
destinationPath: s3://my-backup-bucket/postgres/
endpointURL: https://s3.ap-northeast-2.amazonaws.com
s3Credentials:
accessKeyId:
name: aws-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: aws-creds
key: ACCESS_SECRET_KEY
wal:
compression: gzip
maxParallel: 4
data:
compression: gzip
immediateCheckpoint: true
retentionPolicy: "30d"
스케줄 백업 설정:
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: daily-backup
namespace: database
spec:
schedule: "0 3 * * *" # 매일 오전 3시
backupOwnerReference: self
cluster:
name: my-postgres
immediate: false
suspend: false
수동 백업:
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
name: manual-backup-20240901
namespace: database
spec:
method: barmanObjectStore
cluster:
name: my-postgres
3.4 WAL 아카이빙과 PITR
WAL(Write-Ahead Log) 아카이빙은 연속적인 데이터 보호를 제공합니다.
# WAL 아카이빙은 backup.barmanObjectStore.wal 설정으로 자동 활성화
spec:
backup:
barmanObjectStore:
wal:
compression: gzip
maxParallel: 4
# WAL이 지속적으로 S3에 아카이브됨
PITR(Point-in-Time Recovery) 수행:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: my-postgres-restored
spec:
instances: 3
bootstrap:
recovery:
source: my-postgres
recoveryTarget:
targetTime: "2024-09-01 14:30:00.00000+09"
# 또는 특정 트랜잭션 ID
# targetXID: "12345"
# 또는 특정 LSN
# targetLSN: "0/1234567"
externalClusters:
- name: my-postgres
barmanObjectStore:
destinationPath: s3://my-backup-bucket/postgres/
endpointURL: https://s3.ap-northeast-2.amazonaws.com
s3Credentials:
accessKeyId:
name: aws-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: aws-creds
key: ACCESS_SECRET_KEY
3.5 Connection Pooling (내장 PgBouncer)
apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
name: my-postgres-pooler-rw
namespace: database
spec:
cluster:
name: my-postgres
instances: 2
type: rw # rw 또는 ro
pgbouncer:
poolMode: transaction
parameters:
max_client_conn: "1000"
default_pool_size: "25"
min_pool_size: "5"
reserve_pool_size: "5"
reserve_pool_timeout: "5"
server_idle_timeout: "300"
log_connections: "1"
log_disconnections: "1"
# 읽기 전용 풀러 (Replica 대상)
apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
name: my-postgres-pooler-ro
spec:
cluster:
name: my-postgres
instances: 2
type: ro
pgbouncer:
poolMode: transaction
parameters:
max_client_conn: "2000"
default_pool_size: "50"
애플리케이션 연결:
# 읽기/쓰기 (Primary)
my-postgres-pooler-rw.database.svc.cluster.local:5432
# 읽기 전용 (Replica)
my-postgres-pooler-ro.database.svc.cluster.local:5432
3.6 모니터링 (Prometheus + Grafana)
# CloudNativePG에서 PodMonitor 자동 생성
spec:
monitoring:
enablePodMonitor: true
customQueriesConfigMap:
- name: pg-custom-queries
key: queries
커스텀 쿼리 ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: pg-custom-queries
namespace: database
data:
queries: |
pg_database_size:
query: "SELECT datname, pg_database_size(datname) as size_bytes FROM pg_database WHERE datname NOT IN ('template0', 'template1')"
master: true
metrics:
- datname:
usage: "LABEL"
description: "Database name"
- size_bytes:
usage: "GAUGE"
description: "Database size in bytes"
pg_stat_activity_count:
query: "SELECT state, count(*) as count FROM pg_stat_activity GROUP BY state"
master: true
metrics:
- state:
usage: "LABEL"
description: "Connection state"
- count:
usage: "GAUGE"
description: "Number of connections"
pg_replication_lag:
query: "SELECT CASE WHEN pg_is_in_recovery() THEN EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))::int ELSE 0 END as lag_seconds"
master: false
metrics:
- lag_seconds:
usage: "GAUGE"
description: "Replication lag in seconds"
3.7 Rolling Update (무중단 업데이트)
# 이미지 업데이트
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:16.3
# CloudNativePG가 자동으로:
# 1. Replica부터 하나씩 재시작
# 2. 각 Replica가 Ready 상태 확인 후 다음 진행
# 3. 마지막에 Primary를 switchover (새 Primary 승격 후 이전 Primary 재시작)
# → 다운타임 없이 업데이트 완료
4. Percona Operator for MySQL
4.1 XtraDB Cluster (동기 복제)
apiVersion: pxc.percona.com/v1
kind: PerconaXtraDBCluster
metadata:
name: my-mysql
namespace: database
spec:
crVersion: "1.14.0"
secretsName: my-mysql-secrets
pxc:
size: 3
image: percona/percona-xtradb-cluster:8.0.35
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "4"
memory: "8Gi"
volumeSpec:
persistentVolumeClaim:
storageClassName: gp3-encrypted
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
configuration: |
[mysqld]
innodb_buffer_pool_size=4G
innodb_log_file_size=1G
innodb_flush_method=O_DIRECT
max_connections=500
wsrep_sync_wait=3
wsrep_slave_threads=4
haproxy:
enabled: true
size: 2
image: percona/haproxy:2.8.5
resources:
requests:
cpu: "500m"
memory: "1Gi"
proxysql:
enabled: false
backup:
image: percona/percona-xtradb-cluster-operator:1.14.0-pxc8.0-backup-pxb8.0.35
storages:
s3-backup:
type: s3
s3:
bucket: my-mysql-backups
region: ap-northeast-2
credentialsSecret: aws-s3-secret
schedule:
- name: daily-full
schedule: "0 3 * * *"
keep: 7
storageName: s3-backup
4.2 ProxySQL 연결 라우팅
spec:
proxysql:
enabled: true
size: 2
image: percona/proxysql2:2.5.5
resources:
requests:
cpu: "500m"
memory: "1Gi"
configuration: |
datadir="/var/lib/proxysql"
admin_variables=
{
admin_credentials="admin:admin"
mysql_ifaces="0.0.0.0:6032"
}
mysql_variables=
{
threads=4
max_connections=2048
default_query_delay=0
default_query_timeout=36000000
poll_timeout=2000
interfaces="0.0.0.0:3306;/tmp/proxysql.sock"
default_schema="information_schema"
stacksize=1048576
connect_timeout_server=3000
monitor_history=600000
monitor_connect_interval=60000
monitor_ping_interval=10000
}
4.3 Percona XtraBackup
# 수동 백업 실행
apiVersion: pxc.percona.com/v1
kind: PerconaXtraDBClusterBackup
metadata:
name: manual-backup-20240901
namespace: database
spec:
pxcCluster: my-mysql
storageName: s3-backup
# 백업 상태 확인
kubectl get pxc-backup -n database
# NAME CLUSTER STORAGE STATUS
# manual-backup-20240901 my-mysql s3-backup Succeeded
# 백업에서 복원
kubectl apply -f - <<EOF
apiVersion: pxc.percona.com/v1
kind: PerconaXtraDBClusterRestore
metadata:
name: restore-20240901
namespace: database
spec:
pxcCluster: my-mysql
backupName: manual-backup-20240901
EOF
5. MongoDB Community Operator
5.1 ReplicaSet 구성
apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
name: my-mongodb
namespace: database
spec:
members: 3
type: ReplicaSet
version: "7.0.12"
security:
authentication:
modes: ["SCRAM"]
users:
- name: admin
db: admin
passwordSecretRef:
name: mongodb-admin-password
roles:
- name: clusterAdmin
db: admin
- name: userAdminAnyDatabase
db: admin
scramCredentialsSecretName: admin-scram
- name: myapp
db: myapp
passwordSecretRef:
name: mongodb-myapp-password
roles:
- name: readWrite
db: myapp
scramCredentialsSecretName: myapp-scram
statefulSet:
spec:
template:
spec:
containers:
- name: mongod
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "4"
memory: "4Gi"
volumeClaimTemplates:
- metadata:
name: data-volume
spec:
storageClassName: gp3-encrypted
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi
- metadata:
name: logs-volume
spec:
storageClassName: gp3-encrypted
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
additionalMongodConfig:
storage.wiredTiger.engineConfig.cacheSizeGB: 2
net.maxIncomingConnections: 500
5.2 백업 전략
# mongodump 기반 논리 백업 (CronJob)
apiVersion: batch/v1
kind: CronJob
metadata:
name: mongodb-backup
namespace: database
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: mongo:7.0
command:
- /bin/sh
- -c
- |
mongodump \
--uri="mongodb://myapp:password@my-mongodb-0.my-mongodb-svc.database.svc.cluster.local:27017/myapp?replicaSet=my-mongodb&authSource=myapp" \
--gzip \
--archive=/backup/myapp-$(date +%Y%m%d).gz
# S3에 업로드
aws s3 cp /backup/myapp-$(date +%Y%m%d).gz \
s3://my-backup-bucket/mongodb/
volumeMounts:
- name: backup-volume
mountPath: /backup
volumes:
- name: backup-volume
emptyDir: {}
restartPolicy: OnFailure
6. 스토리지 심화
6.1 StorageClass 설정
# AWS EBS gp3 StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3-encrypted
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: "true"
iops: "6000" # gp3 기본 3000, 최대 16000
throughput: "250" # gp3 기본 125MB/s, 최대 1000MB/s
fsType: ext4
reclaimPolicy: Retain # DB 데이터는 반드시 Retain
volumeBindingMode: WaitForFirstConsumer # 토폴로지 인식 바인딩
allowVolumeExpansion: true
# GCP PD SSD StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: pd-ssd-encrypted
provisioner: pd.csi.storage.gke.io
parameters:
type: pd-ssd
disk-encryption-kms-key: projects/my-project/locations/global/keyRings/my-ring/cryptoKeys/my-key
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
6.2 Local PV vs Cloud EBS/PD 성능 비교
| 지표 | Local NVMe | EBS gp3 (6000 IOPS) | EBS io2 (16000 IOPS) | GCP PD-SSD |
|---|---|---|---|---|
| 랜덤 읽기 IOPS | 500K+ | 6,000 | 16,000 | 30,000 |
| 랜덤 쓰기 IOPS | 200K+ | 6,000 | 16,000 | 30,000 |
| 순차 읽기 MB/s | 3,000+ | 250 | 1,000 | 1,200 |
| 순차 쓰기 MB/s | 2,000+ | 250 | 1,000 | 1,200 |
| P99 레이턴시 | 0.1ms 미만 | 1-3ms | 0.5-1ms | 1-2ms |
| 데이터 지속성 | 노드 장애 시 손실 | 99.999% | 99.999% | 99.999% |
| 크기 조정 | 불가 | 동적 확장 | 동적 확장 | 동적 확장 |
6.3 CSI 드라이버
# AWS EBS CSI 드라이버 설치
helm repo add aws-ebs-csi-driver https://kubernetes-sigs.github.io/aws-ebs-csi-driver
helm install aws-ebs-csi-driver aws-ebs-csi-driver/aws-ebs-csi-driver \
--namespace kube-system \
--set controller.serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="arn:aws:iam::ACCOUNT:role/ebs-csi-role"
# local-path-provisioner (개발/테스트 환경)
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
6.4 스토리지 벤치마킹
# fio 벤치마크 Pod
kubectl run fio-bench --image=nixery.dev/fio \
--overrides='{
"spec": {
"containers": [{
"name": "fio-bench",
"image": "nixery.dev/fio",
"command": ["sleep", "3600"],
"volumeMounts": [{
"name": "test-vol",
"mountPath": "/data"
}]
}],
"volumes": [{
"name": "test-vol",
"persistentVolumeClaim": {
"claimName": "bench-pvc"
}
}]
}
}'
# 랜덤 읽기 벤치마크
kubectl exec fio-bench -- fio \
--name=randread \
--ioengine=libaio \
--iodepth=32 \
--rw=randread \
--bs=4k \
--direct=1 \
--size=1G \
--numjobs=4 \
--runtime=60 \
--directory=/data \
--group_reporting
# pgbench (PostgreSQL 성능 테스트)
kubectl exec -it my-postgres-1 -- pgbench \
-i -s 100 myapp # 초기화 (100 scale factor)
kubectl exec -it my-postgres-1 -- pgbench \
-c 50 -j 4 -T 300 -P 10 myapp # 50 클라이언트, 300초
7. High Availability 패턴
7.1 동기 vs 비동기 복제
# CloudNativePG: 동기 복제 설정
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: my-postgres-sync
spec:
instances: 3
postgresql:
parameters:
synchronous_standby_names: "ANY 1 (*)"
# ANY 1: 최소 1개 Replica에 동기 복제 보장
# FIRST 1: 첫 번째 Replica에 동기 복제
# minSyncReplicas / maxSyncReplicas로 세밀 제어
minSyncReplicas: 1
maxSyncReplicas: 1
| 방식 | 데이터 일관성 | 쓰기 레이턴시 | RPO | 사용 시나리오 |
|---|---|---|---|---|
| 비동기 | 최종적 일관성 | 낮음 | 수초 데이터 손실 가능 | 일반 워크로드 |
| 동기 (ANY 1) | 강한 일관성 | 높음 (2x) | 0 (데이터 손실 없음) | 금융, 결제 |
| 반동기 | 중간 | 중간 | 매우 작음 | 대부분의 프로덕션 |
7.2 Pod Disruption Budget
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: postgres-pdb
namespace: database
spec:
maxUnavailable: 1 # 동시에 최대 1개 Pod만 중단 허용
selector:
matchLabels:
app: postgres
CloudNativePG는 PDB를 자동 생성합니다. 기본적으로 maxUnavailable: 1.
7.3 Topology Spread Constraints
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: my-postgres
spec:
instances: 3
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
cnpg.io/cluster: my-postgres
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
cnpg.io/cluster: my-postgres
이 설정은 DB Pod를 다른 가용 영역(AZ)과 다른 노드에 분산합니다.
7.4 Anti-Affinity 규칙
spec:
affinity:
enablePodAntiAffinity: true
topologyKey: kubernetes.io/hostname
# 또는 topology.kubernetes.io/zone (AZ 분산)
podAntiAffinityType: required
# required: 반드시 분산 (Pod 스케줄 불가 시 Pending)
# preferred: 가능하면 분산 (스케줄 불가 시 동일 노드 허용)
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role
operator: In
values:
- database
tolerations:
- key: database-only
operator: Equal
value: "true"
effect: NoSchedule
8. 백업 및 복구 전략
8.1 논리 백업 vs 물리 백업
| 구분 | 논리 백업 (pg_dump) | 물리 백업 (pgBackRest) |
|---|---|---|
| 속도 | 느림 (대용량 시 수시간) | 빠름 (증분 백업 지원) |
| 크기 | SQL 텍스트 (압축 가능) | 바이너리 (작음) |
| 복원 유연성 | 테이블 단위 복원 가능 | 전체 클러스터 복원 |
| 버전 호환 | 다른 PG 버전 간 가능 | 동일 PG 버전만 |
| PITR | 불가 | 가능 |
| 사용 시나리오 | 마이그레이션, 부분 복원 | 프로덕션 재해 복구 |
8.2 연속 WAL 아카이빙
WAL 아카이빙 흐름:
PG Primary → WAL 세그먼트 생성 (16MB 단위) → S3/GCS 아카이브
↓
연속적 데이터 보호
↓
PITR: 임의 시점 복구 가능
# CloudNativePG WAL 아카이빙 설정 (자동)
spec:
backup:
barmanObjectStore:
destinationPath: s3://my-backup-bucket/wal-archive/
wal:
compression: gzip
maxParallel: 4
# WAL 세그먼트가 완성될 때마다 자동 업로드
8.3 PITR (Point-in-Time Recovery) 워크스루
# 1. 현재 백업 목록 확인
kubectl get backup -n database
# 2. 복원 대상 시간 결정 (예: 실수로 DELETE 실행 직전)
# 시간: 2024-09-01 14:29:00 (삭제가 14:30에 실행됨)
# 3. 새 클러스터로 PITR 복원
kubectl apply -f pitr-restore.yaml
# (앞서 3.4절의 PITR YAML 참조)
# 4. 복원 진행 상황 모니터링
kubectl get cluster my-postgres-restored -n database -w
# 5. 복원 완료 후 데이터 검증
kubectl exec -it my-postgres-restored-1 -- psql -U myapp -d myapp \
-c "SELECT count(*) FROM important_table;"
# 6. 애플리케이션 연결 전환
kubectl patch service myapp-db -n database \
-p '{"spec":{"selector":{"cnpg.io/cluster":"my-postgres-restored"}}}'
8.4 Velero 클러스터 레벨 백업
# Velero 설치
velero install \
--provider aws \
--plugins velero/velero-plugin-for-aws:v1.9.0 \
--bucket my-velero-bucket \
--backup-location-config region=ap-northeast-2 \
--snapshot-location-config region=ap-northeast-2
# DB 네임스페이스 전체 백업 (PV 포함)
velero backup create db-full-backup \
--include-namespaces database \
--include-resources '*' \
--snapshot-volumes=true \
--volume-snapshot-locations default
# 스케줄 백업
velero schedule create daily-db-backup \
--schedule="0 4 * * *" \
--include-namespaces database \
--snapshot-volumes=true \
--ttl 720h # 30일 보관
8.5 교차 리전 재해 복구
# Source 리전 (ap-northeast-2)의 CloudNativePG 클러스터
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: my-postgres-primary
spec:
instances: 3
backup:
barmanObjectStore:
destinationPath: s3://my-backup-bucket-primary/
s3Credentials:
accessKeyId:
name: aws-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: aws-creds
key: ACCESS_SECRET_KEY
---
# DR 리전 (us-west-2)의 Replica 클러스터
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: my-postgres-dr
spec:
instances: 2
replica:
enabled: true
source: my-postgres-primary
externalClusters:
- name: my-postgres-primary
barmanObjectStore:
destinationPath: s3://my-backup-bucket-primary/
s3Credentials:
accessKeyId:
name: aws-creds-dr
key: ACCESS_KEY_ID
secretAccessKey:
name: aws-creds-dr
key: ACCESS_SECRET_KEY
9. 성능 튜닝
9.1 리소스 Requests/Limits
# DB Pod는 반드시 QoS Guaranteed 클래스로 설정
resources:
requests:
cpu: "4"
memory: "16Gi"
limits:
cpu: "4" # requests == limits → Guaranteed
memory: "16Gi" # OOM Killer 방지
메모리 배분 가이드:
총 메모리 16Gi 기준:
- shared_buffers: 4GB (25%)
- effective_cache_size: 12GB (75%)
- work_mem: 64MB (세션당)
- maintenance_work_mem: 1GB
- OS/기타: 약 2GB
9.2 Huge Pages 설정
# 노드에 Huge Pages 설정 필요
# /etc/sysctl.conf: vm.nr_hugepages=2048
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: my-postgres
spec:
postgresql:
parameters:
shared_buffers: "4GB"
huge_pages: "on"
resources:
requests:
cpu: "4"
memory: "16Gi"
hugepages-2Mi: "4Gi"
limits:
cpu: "4"
memory: "16Gi"
hugepages-2Mi: "4Gi"
9.3 PostgreSQL 성능 파라미터 튜닝
postgresql:
parameters:
# 메모리
shared_buffers: "4GB"
effective_cache_size: "12GB"
work_mem: "64MB"
maintenance_work_mem: "1GB"
wal_buffers: "64MB"
# WAL
max_wal_size: "4GB"
min_wal_size: "1GB"
checkpoint_completion_target: "0.9"
wal_compression: "zstd"
# 쿼리 플래너
random_page_cost: "1.1" # SSD인 경우 (HDD는 4.0)
effective_io_concurrency: "200" # SSD인 경우 (HDD는 2)
seq_page_cost: "1.0"
# 병렬 처리
max_worker_processes: "8"
max_parallel_workers_per_gather: "4"
max_parallel_workers: "8"
max_parallel_maintenance_workers: "4"
# 연결
max_connections: "200" # PgBouncer 사용 시 낮게
idle_in_transaction_session_timeout: "30000" # 30초
# 로깅
log_min_duration_statement: "1000" # 1초 이상 쿼리 로깅
log_checkpoints: "on"
log_lock_waits: "on"
log_temp_files: "0"
# 자동 VACUUM
autovacuum_max_workers: "4"
autovacuum_naptime: "30"
autovacuum_vacuum_cost_limit: "1000"
9.4 Connection Pooling 구성
PgBouncer 권장 설정:
pool_mode = transaction
- 트랜잭션 단위로 커넥션 할당/반환
- K8s 환경에서 수백 Pod 동시 연결 시 필수
- PREPARE 문 사용 제한 (session mode에서만 가능)
default_pool_size = 25
- 백엔드 DB 커넥션 수 (max_connections의 12.5%)
- 너무 높으면 DB 과부하, 너무 낮으면 대기 증가
max_client_conn = 1000
- 프론트엔드 최대 연결 수
- Pod 수 x Pod당 커넥션 수 기준으로 산정
reserve_pool_size = 5
- 피크 트래픽 대비 예비 커넥션
server_idle_timeout = 300
- 유휴 커넥션 5분 후 정리
10. 보안
10.1 Network Policies
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: postgres-network-policy
namespace: database
spec:
podSelector:
matchLabels:
cnpg.io/cluster: my-postgres
policyTypes:
- Ingress
- Egress
ingress:
# 애플리케이션 네임스페이스에서만 접근 허용
- from:
- namespaceSelector:
matchLabels:
app.kubernetes.io/part-of: myapp
- podSelector:
matchLabels:
app: myapp
ports:
- protocol: TCP
port: 5432
# 같은 클러스터 내 복제 트래픽 허용
- from:
- podSelector:
matchLabels:
cnpg.io/cluster: my-postgres
ports:
- protocol: TCP
port: 5432
egress:
# S3 백업을 위한 아웃바운드 (HTTPS)
- to: []
ports:
- protocol: TCP
port: 443
# 클러스터 내 통신
- to:
- podSelector:
matchLabels:
cnpg.io/cluster: my-postgres
ports:
- protocol: TCP
port: 5432
# DNS
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
10.2 Secrets 관리 (External Secrets Operator)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: postgres-credentials
namespace: database
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: myapp-db-credentials
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: production/database/postgres
property: username
- secretKey: password
remoteRef:
key: production/database/postgres
property: password
10.3 TLS 설정
# CloudNativePG는 기본적으로 TLS 활성화
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: my-postgres
spec:
certificates:
serverTLSSecret: my-postgres-server-tls
serverCASecret: my-postgres-ca
clientCASecret: my-postgres-client-ca
replicationTLSSecret: my-postgres-replication-tls
postgresql:
parameters:
ssl: "on"
ssl_min_protocol_version: "TLSv1.3"
11. 모니터링 및 알림
11.1 핵심 메트릭
# Prometheus 알림 규칙
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: postgres-alerts
namespace: monitoring
spec:
groups:
- name: postgres.rules
rules:
- alert: PostgresReplicationLagHigh
expr: cnpg_pg_replication_lag > 10
for: 5m
labels:
severity: warning
annotations:
summary: "PostgreSQL 복제 지연이 10초를 초과합니다"
- alert: PostgresConnectionsNearLimit
expr: >
cnpg_pg_stat_activity_count /
cnpg_pg_settings_setting{name="max_connections"} > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "연결 수가 최대치의 80%를 초과했습니다"
- alert: PostgresCacheHitRatioLow
expr: >
cnpg_pg_stat_database_blks_hit /
(cnpg_pg_stat_database_blks_hit + cnpg_pg_stat_database_blks_read) < 0.95
for: 15m
labels:
severity: warning
annotations:
summary: "캐시 히트율이 95% 미만입니다. shared_buffers 증가를 고려하세요"
- alert: PostgresStorageNearFull
expr: >
kubelet_volume_stats_used_bytes{namespace="database"} /
kubelet_volume_stats_capacity_bytes{namespace="database"} > 0.85
for: 10m
labels:
severity: critical
annotations:
summary: "DB 스토리지 사용량이 85%를 초과했습니다"
- alert: PostgresFailoverDetected
expr: changes(cnpg_pg_replication_is_primary[5m]) > 0
labels:
severity: critical
annotations:
summary: "PostgreSQL Failover가 감지되었습니다"
11.2 Grafana 대시보드 핵심 패널
핵심 모니터링 패널:
1. 복제 지연 (초) - cnpg_pg_replication_lag
2. 활성 연결 수 - cnpg_pg_stat_activity_count
3. 캐시 히트율 - blks_hit / (blks_hit + blks_read)
4. TPS (트랜잭션/초) - rate(cnpg_pg_stat_database_xact_commit[5m])
5. 쿼리 레이턴시 P99
6. 디스크 IOPS / 처리량
7. WAL 생성 속도 - rate(cnpg_pg_stat_archiver_archived_count[5m])
8. 스토리지 사용량 - kubelet_volume_stats_used_bytes
9. CPU / 메모리 사용률
10. Vacuum / Analyze 실행 상태
12. 마이그레이션 전략
12.1 VM에서 K8s로 마이그레이션
# 방법 1: pg_dump/pg_restore (소규모 DB)
# 소스 VM에서:
pg_dump -Fc -d myapp -h vm-db.internal -U postgres > myapp.dump
# K8s Pod에서 복원:
kubectl cp myapp.dump database/my-postgres-1:/tmp/myapp.dump
kubectl exec -it my-postgres-1 -n database -- \
pg_restore -d myapp -U postgres /tmp/myapp.dump
# 방법 2: 논리 복제 (대규모 DB, 무중단)
# 1. K8s에서 빈 클러스터 생성
# 2. VM DB에서 publication 생성
# CREATE PUBLICATION my_pub FOR ALL TABLES;
# 3. K8s DB에서 subscription 생성
# CREATE SUBSCRIPTION my_sub
# CONNECTION 'host=vm-db.internal dbname=myapp user=replicator'
# PUBLICATION my_pub;
# 4. 초기 데이터 동기화 완료 대기
# 5. 애플리케이션 연결을 K8s DB로 전환
# 6. subscription 삭제
12.2 RDS에서 K8s DB로 마이그레이션
# AWS DMS (Database Migration Service) 활용
# 1. DMS 복제 인스턴스 생성
# 2. 소스 엔드포인트: RDS PostgreSQL
# 3. 대상 엔드포인트: K8s PostgreSQL (NLB/Port-forward 통해 접근)
# 4. Full Load + CDC (Change Data Capture) 모드로 실행
# 5. 동기화 완료 후 애플리케이션 전환
12.3 무중단 마이그레이션 체크리스트
마이그레이션 전:
[ ] 원본 DB 크기 및 테이블 수 파악
[ ] K8s 클러스터 리소스 여유 확인
[ ] StorageClass / PV 크기 산정 (원본의 2배 여유)
[ ] 네트워크 연결 확인 (원본 DB → K8s 클러스터)
[ ] 논리 복제 사용자 생성 및 권한 부여
마이그레이션 중:
[ ] 초기 데이터 복사 완료 확인
[ ] 복제 지연이 0에 수렴하는지 확인
[ ] 시퀀스 값 동기화 확인
[ ] 애플리케이션 읽기 트래픽 K8s로 전환 (테스트)
[ ] 쓰기 트래픽 전환 (짧은 다운타임 또는 이중 쓰기)
마이그레이션 후:
[ ] 데이터 정합성 검증 (행 수, 체크섬)
[ ] 애플리케이션 성능 확인
[ ] 원본 DB 복제 중단
[ ] 백업 정책 적용 확인
[ ] 모니터링/알림 동작 확인
13. 프로덕션 체크리스트
인프라 및 스토리지
- StorageClass의
reclaimPolicy가Retain으로 설정됨 -
volumeBindingMode가WaitForFirstConsumer로 설정됨 - EBS/PD IOPS가 워크로드에 적합한 수준 (최소 6000 IOPS)
- 볼륨 자동 확장(
allowVolumeExpansion: true) 활성화 - 노드에 DB 전용 레이블/테인트 적용
HA 및 복제
- 최소 3개 인스턴스로 구성 (Primary 1 + Replica 2)
- Pod Anti-Affinity로 다른 노드에 분산
- Topology Spread로 다른 AZ에 분산
- PodDisruptionBudget 설정 (
maxUnavailable: 1) - 동기/비동기 복제 모드 결정 및 설정
백업 및 복구
- 자동 백업 스케줄 설정 (최소 일 1회)
- WAL 아카이빙 활성화 (PITR 가능)
- 백업 보관 정책 설정 (최소 30일)
- 백업 복원 테스트 완료 (최소 분기 1회)
- 교차 리전 백업 복제 (DR 요구 시)
성능
- 리소스 requests == limits (QoS Guaranteed)
- shared_buffers = 총 메모리의 25%
- effective_cache_size = 총 메모리의 75%
- Connection Pooling 활성화 (PgBouncer transaction mode)
- 쿼리 로깅 활성화 (
log_min_duration_statement)
보안
- NetworkPolicy로 DB 접근 제한
- TLS 활성화 (최소 TLSv1.2)
- Secret은 External Secrets Operator 또는 Vault로 관리
- RBAC: Operator 서비스 계정 최소 권한
- 데이터 암호화 (스토리지 레벨 + 전송 레벨)
모니터링
- Prometheus 메트릭 수집 설정
- Grafana 대시보드 구성
- 복제 지연 알림 (10초 초과)
- 스토리지 용량 알림 (85% 초과)
- 연결 수 알림 (80% 초과)
- Failover 발생 알림
운영
- Operator 업그레이드 절차 문서화
- DB 마이너/메이저 버전 업그레이드 절차 문서화
- 장애 대응 런북 작성
- 정기 복원 테스트 일정 수립
14. 퀴즈
Q1: StatefulSet에서 Pod Identity가 중요한 이유는?
A: StatefulSet의 Pod Identity(순서 인덱스 + 안정적 호스트명 + 전용 PVC)는 데이터베이스 운영에 필수입니다. Primary/Replica 역할이 특정 Pod에 바인딩되고, 재시작 후에도 동일한 스토리지에 연결되며, DNS 기반으로 다른 Pod가 특정 인스턴스에 직접 접근할 수 있습니다. 일반 Deployment로는 이런 보장이 불가능합니다.
Q2: CloudNativePG의 자동 Failover는 어떻게 동작하는가?
A: CloudNativePG는 Primary Pod의 health check 실패를 감지하면, 가장 최신 WAL 위치를 가진 Replica를 선택하여 pg_promote()로 새 Primary로 승격합니다. 나머지 Replica는 새 Primary를 팔로우하도록 재설정되고, 이전 Primary는 pg_rewind를 통해 Replica로 재합류합니다. 전체 과정은 보통 10-30초 내에 완료됩니다.
Q3: DB Pod의 QoS 클래스를 Guaranteed로 설정해야 하는 이유는?
A: QoS Guaranteed(requests == limits)로 설정하면 K8s가 해당 Pod의 리소스를 보장하며, 노드 메모리 부족 시에도 가장 마지막에 OOM Kill됩니다. DB는 메모리 내 캐시(shared_buffers)와 안정적인 CPU가 필수이므로, Burstable이나 BestEffort로 설정하면 예기치 않은 성능 저하나 장애가 발생할 수 있습니다.
Q4: volumeBindingMode를 WaitForFirstConsumer로 설정하는 이유는?
A: WaitForFirstConsumer는 Pod가 실제로 스케줄될 노드가 결정된 후에 PV를 프로비저닝합니다. 이를 통해 Pod와 PV가 동일한 가용 영역(AZ)에 위치하게 되어, 교차 AZ 네트워크 레이턴시를 방지합니다. Immediate 모드에서는 PV가 먼저 생성되어 다른 AZ에 위치할 수 있으며, 이 경우 Pod 스케줄이 실패하거나 성능이 저하됩니다.
Q5: PgBouncer의 transaction 모드와 session 모드의 차이는?
A: Transaction 모드는 트랜잭션이 끝나면 백엔드 커넥션을 풀로 반환하여 수백 개의 클라이언트가 소수의 DB 커넥션을 공유합니다. K8s에서 다수의 Pod가 동시 접근하는 환경에 최적입니다. Session 모드는 클라이언트 세션이 끝날 때까지 커넥션을 점유하여, PREPARE 문이나 세션 변수가 필요한 경우 사용합니다. 일반적으로 K8s 환경에서는 transaction 모드를 권장합니다.
15. 참고 자료
- CloudNativePG 공식 문서 - https://cloudnative-pg.io/documentation/
- Percona Operator for MySQL - https://docs.percona.com/percona-operator-for-mysql/pxc/
- MongoDB Community Operator - https://github.com/mongodb/mongodb-kubernetes-operator
- Kubernetes StatefulSet 문서 - https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/
- PgBouncer 문서 - https://www.pgbouncer.org/config.html
- Velero 공식 문서 - https://velero.io/docs/
- External Secrets Operator - https://external-secrets.io/
- AWS EBS CSI Driver - https://github.com/kubernetes-sigs/aws-ebs-csi-driver
- PostgreSQL 성능 튜닝 가이드 - https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server
- Kubernetes Network Policies - https://kubernetes.io/docs/concepts/services-networking/network-policies/
- pgBackRest 문서 - https://pgbackrest.org/
- Prometheus PostgreSQL Exporter - https://github.com/prometheus-community/postgres_exporter
- Barman (Backup and Recovery Manager) - https://pgbarman.org/