K3s 실전 2편 — 디스크 큰 노드(Solomon)에 DB 8개 몰빵하기
1편 에서 솔로몬을 storage tier 로 정했습니다. 이번 글은 그 솔로몬에 PostgreSQL 8 개를 StatefulSet 으로 박는 과정 을 정리합니다. local-path-provisioner 의 nodePathMap, WaitForFirstConsumer, Retain 같은 키워드가 핵심입니다.
이 글에서 다루는 것
- 왜 NFS 가 아니라 local-path 를 골랐는가
- StorageClass 3 가지 옵션의 의미 (
volumeBindingMode,reclaimPolicy,nodePathMap)solomon-localStorageClass 정의- StatefulSet → PVC → PV 가 어떻게 솔로몬으로 빨려 들어가는지
- 8 개 DB 를 일괄 마이그한 결과
1. 왜 local-path 인가 — NFS 와 비교
| 항목 | local-path (선택) | NFS |
|---|---|---|
| 성능 (DB IOPS) | ★★★★★ (로컬 SSD) | ★★ (네트워크 RTT) |
| 노드 이동 가능 | ❌ (노드 종속) | ✅ (어느 노드에서나) |
| 운영 복잡도 | ★ (K3s 기본 내장) | ★★★ (NFS 서버 별도) |
| 동시 마운트 (RWX) | ❌ (RWO 만) | ✅ |
DB 는 IOPS 가 우선 이고, RWO 면 충분합니다. 노드 이동 못 하는 건 단점이지만 어차피 솔로몬에만 박을 거라 무관. → local-path 선택.
(참고로 MinIO 같은 RWX 가 필요한 워크로드는 솔로몬에 NFS 서버 하나 띄워서 두 가지를 병용합니다 — 이건 다른 글로.)
2. local-path-provisioner 가 PV 를 어디에 만드는지 강제하기
K3s 에 기본 내장된 local-path-provisioner 는 그대로 두면 각 노드의 /var/lib/rancher/k3s/storage/ 에 PV 를 만듭니다. 솔로몬에만 박으려면 nodePathMap 을 ConfigMap 으로 덮어씌워야 합니다.
# kube-system/local-path-config
apiVersion: v1
kind: ConfigMap
metadata:
name: local-path-config
namespace: kube-system
data:
config.json: |
{
"nodePathMap": [
{
"node": "solomon",
"paths": ["/opt/local-path-provisioner/solomon"]
},
{
"node": "DEFAULT_PATH_FOR_NON_LISTED_NODES",
"paths": [] ← ★ 빈 배열 = 다른 노드에는 PV 못 만듦
}
]
}
paths: [] 가 핵심입니다. 이걸 안 하면 다른 노드에서 Pod 가 뜰 때 거기에 PV 가 만들어져버려서 다음에 같은 PVC 가 다른 노드로 가면 데이터를 못 봅니다.
솔로몬 콘솔 사전 작업:
sudo mkdir -p /opt/local-path-provisioner/solomon
sudo chmod 755 /opt/local-path-provisioner/solomon
3. StorageClass solomon-local 정의
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: solomon-local
annotations:
storageclass.kubernetes.io/is-default-class: "false"
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer # ★
reclaimPolicy: Retain # ★
parameters:
nodePath: /opt/local-path-provisioner/solomon
옵션 3 개의 의미
volumeBindingMode: WaitForFirstConsumer
PVC 만들었다고 PV 바로 안 만듦. Pod 가 스케줄링될 때까지 대기 했다가 그 노드에 PV 를 만듭니다.
→ Pod 와 PV 의 노드 어피니티가 자동으로 맞춰짐. 즉 “Pod 가 솔로몬에 떠야 → 솔로몬에 PV 가 생김” 이라는 흐름이 생김.
reclaimPolicy: Retain
PVC 를 지워도 PV 와 디스크 데이터는 안 지워짐. 실수 방어 용. (default 인 Delete 였으면 kubectl delete pvc 한 번에 데이터 날라감.)
nodePath
local-path-provisioner 가 디렉토리를 만들 base 경로. ConfigMap 에 적은 값과 일치해야 함.
4. StatefulSet 에서 솔로몬으로 강제
PVC 만으로는 다른 워커에 Pod 가 떠버릴 수 있습니다. StatefulSet 의 template.spec 에 nodeSelector + tolerations 를 추가해야 합니다:
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: jen-postgres }
spec:
serviceName: jen-postgres
replicas: 1
template:
spec:
nodeSelector:
kubernetes.io/hostname: solomon # ★ Pod 솔로몬 강제
tolerations:
- key: storage-only # ★ 솔로몬 taint 통과
operator: Exists
effect: NoSchedule
priorityClassName: lemuel-critical # ★ OOM 시 살아남기
containers:
- name: postgres
image: postgres:16-alpine
# ...
volumeClaimTemplates:
- metadata: { name: data }
spec:
accessModes: [ReadWriteOnce]
storageClassName: solomon-local
resources:
requests:
storage: 50Gi
흐름 정리
[StatefulSet] → Pod 생성 요청
↓
[Scheduler] nodeSelector=solomon → 솔로몬 선택
↓
[StatefulSet controller] PVC 생성 (jen-postgres-data-jen-postgres-0)
↓
[local-path-provisioner] WaitForFirstConsumer
→ Pod 가 솔로몬에 스케줄됨 확인
→ /opt/local-path-provisioner/solomon/<uuid> 에 디렉토리 생성
→ PV 생성 + PVC 바인딩
↓
[Pod] 솔로몬 SSD 에 마운트, 시작
5. 8 개 DB 일괄 마이그 — 자동화 스크립트
기존 docker compose 로 떠 있던 PostgreSQL 8 개 (jen / dart / pilgrim / fashion / goods / cost / crypto / trading) 를 한 줄에 옮기는 스크립트:
#!/bin/bash
# migrate-pg-to-solomon.sh
NS=$1
DB=$2
OLD_HOST=$3
OLD_PORT=$4
PGUSER=$5
PGPASS=$6
# 1) docker compose 에서 dump
ssh $OLD_HOST "PGPASSWORD=$PGPASS pg_dump -h localhost -p $OLD_PORT \
-U $PGUSER -d $DB --clean --if-exists" > /tmp/${DB}.sql
# 2) R2 로 백업
aws s3 cp /tmp/${DB}.sql s3://lemuel-backup/migration/${DB}-$(date +%Y%m%d).sql
# 3) K3s namespace + StatefulSet 생성 (helm 차트)
helm upgrade --install ${NS} ./charts/postgres-on-solomon \
--namespace ${NS} --create-namespace \
--set postgres.database=${DB}
# 4) Pod ready 대기
kubectl wait --for=condition=ready pod/${NS}-postgres-0 \
-n ${NS} --timeout=300s
# 5) 복원
kubectl exec -n ${NS} ${NS}-postgres-0 -- \
psql -U ${PGUSER} -d postgres -c "CREATE DATABASE ${DB};"
cat /tmp/${DB}.sql | kubectl exec -i -n ${NS} ${NS}-postgres-0 -- \
psql -U ${PGUSER} -d ${DB}
# 6) 검증
kubectl exec -n ${NS} ${NS}-postgres-0 -- \
psql -U ${PGUSER} -d ${DB} -c "\\dt+" | head
이걸 8 번 돌려서 약 30GB / 91만 row 를 옮겼습니다. 한 번에 끝나는 건 아니고 schema 가 public 이 아닌 케이스 (예: opslab) 같은 함정이 있어서 그건 건건이 잡았습니다.
6. 결과 — 솔로몬에서 본 디스크
$ ssh solomon df -h /opt/local-path-provisioner/solomon
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 686G 28G 658G 4% /opt/local-path-provisioner/solomon
$ ls /opt/local-path-provisioner/solomon
pvc-a3f1... pvc-b2c5... pvc-c4d8... ... (8 개)
$ kubectl get pvc --all-namespaces \
-o custom-columns=NS:.metadata.namespace,PVC:.metadata.name,SIZE:.spec.resources.requests.storage \
| grep postgres
NS PVC SIZE
jen-prod data-jen-postgres-0 50Gi
dart-prod data-dart-postgres-0 30Gi
pilgrim-prod data-pilgrim-postgres-0 30Gi
fashion-prod data-fashion-postgres-0 30Gi
goods-prod data-goodsonline-postgres-0 50Gi
cost-prod data-cost-postgres-0 20Gi
crypto-prod data-crypto-postgres-0 20Gi
trading-prod data-trading-postgres-0 20Gi
총 250Gi 확보, 28Gi 사용. 솔로몬 686GB 의 4% 만 썼고, 600GB 가 남아있어서 ELN, Settlement 같은 큰 거 들어와도 여유 있음.
7. 함정 — 내가 빠진 것
nodePathMap.DEFAULT_PATH_FOR_NON_LISTED_NODES.paths: []빠뜨려서 솔로몬 아닌 다른 노드에서 PVC 가 떠버린 적 있음. → ConfigMap 다시 적용 + 잘못 만들어진 PV 수동 삭제- opslab schema 못 보고
\\dt결과가 0 으로 나옴.\\dn으로 schema 목록 먼저 확인해야 함 reclaimPolicy: Delete였던 시절 PVC 잘못 지워서 data 통째로 날림 → 백업이 살려줌. 이후 모든 솔로몬 SC 는Retain필수
다음 글
- K3s 실전 3편 — LimitRange / ResourceQuota / PriorityClass 로 OOM 방어
- DB 가 솔로몬에 들어왔으니, 이제 OOM 시 DB 가 가장 마지막에 죽도록 우선순위를 매겨줘야 합니다.