EKS에서 TimescaleDB PVC 연결 오류로 인한 데이터 소실 원인 분석
배경
- 워크로드:
tsdb-weather네임스페이스의timescaledb-weather(TimescaleDB/PostgreSQL) - 배포 방식: Kustomize + Argo CD,
StatefulSet+PersistentVolumeClaim(gp3, 100Gi) - 외부 접근: 내부 NLB (
timescaledb-weatherService, typeLoadBalancer)
증상은 크게 두 가지였다.
- Pod OOM 및 재시작 이후 데이터가 사라진 것처럼 보임
- Argo CD에서
StatefulSet삭제 → 재생성했을 때, 새로 만든 테이블이 사라지는 현상
PVC(timescaledb-weather-data)는 삭제되지 않았고 계속 Bound 상태였기 때문에,
처음에는 "DB가 정말 PVC 위에 데이터를 쓰고 있는가?"가 핵심 의심 포인트였다.
1차 분석 – 매니페스트와 DB 설정
관련 매니페스트:
app-manifest/timescaledb-weather/base/statefulset.yamlapp-manifest/timescaledb-weather/base/pvc.yamlapp-manifest/timescaledb-weather/base/configmap.yaml(postgresql.conf)app-manifest/timescaledb-weather/base/init-script-configmap.yaml
1.1 StatefulSet의 볼륨 마운트
초기 설정:
volumeMounts:
# PVC는 /var/lib/postgresql 에 마운트되고,
# PostgreSQL 기본 PGDATA(/var/lib/postgresql/data)는 이 안의 서브디렉토리를 사용
- name: timescaledb-weather-data
mountPath: /var/lib/postgresql
- PVC는
/var/lib/postgresql에 마운트. - 공식 Timescale/Postgres 이미지의 기본
PGDATA는/var/lib/postgresql/data. - 컨테이너는 공식 entrypoint (
docker-entrypoint.sh postgres)를 그대로 사용.
이론상으로는 /var/lib/postgresql(PVC) 아래의 data 디렉토리를 PGDATA로 쓰게 되므로 문제가 없어 보였다.
1.2 ConfigMap / Init 스크립트
postgresql.conf:
- 성능/복제 파라미터만 설정 (
shared_buffers,wal_level=replica,max_wal_senders, …). - 데이터 삭제/초기화 관련 설정 없음.
init-extensions.sql:
CREATE EXTENSION timescaledb,CREATE EXTENSION pg_cron,CREATE EXTENSION postgres_fdw.replicator유저 생성.DROP나TRUNCATE는 전혀 없음.
즉, 매니페스트 상으로는 커밋된 데이터/테이블을 지울 만한 동작은 보이지 않았다.
2차 분석 – 실제 런타임 파일시스템 상태 확인
Pod 안에서 df -h 와 SHOW data_directory; 를 확인했다.
kubectl exec -it timescaledb-weather-0 -n tsdb-weather -- df -h
요약된 결과:
/dev/nvme3n1 97.9G ... /var/lib/postgresql # → PVC(EBS)
/dev/nvme0n1p1 99.9G ... /var/lib/postgresql/data # → 노드 로컬 디스크
SHOW data_directory;
-- 결과: /var/lib/postgresql/data
정리하면:
- PVC(EBS)는
/var/lib/postgresql에 마운트되어 있었고, /var/lib/postgresql/data는 다시 노드 루트 디스크에 마운트되어 있었다.- PostgreSQL의
data_directory도/var/lib/postgresql/data를 가리키고 있었기 때문에, 실제 DB 파일은 PVC가 아니라 노드 로컬 디스크 위에 저장되고 있었다.
이 패턴은, 공식 이미지에서 VOLUME /var/lib/postgresql/data 가 선언되어 있고,
Kubernetes가 이 VOLUME을 위해 익명 볼륨을 생성해 /var/lib/postgresql/data에 한 번 더 마운트하면서 발생하는 전형적인 문제다.
결과적으로 일어난 일
- Pod가 특정 노드에서 동작할 때는, 그 노드의 로컬 디스크 위에 데이터가 쌓인다.
StatefulSet삭제 후 다른 노드에 새 Pod가 잡히면,- PVC는 여전히 유지되지만,
- 새 노드에는
/var/lib/postgresql/data에 해당하는 로컬 데이터가 없으므로 빈 클러스터처럼 보이고, 기존 테이블이 모두 사라진 것처럼 보이는 현상이 발생한다.
- PVC가 살아 있어서 더더욱 디버깅이 혼란스러웠다.
3차 수정 – PGDATA를 PVC에 직접 연결
3.1 첫 번째 시도 – 마운트를 /var/lib/postgresql/data로 변경
처음에는 PVC를 PGDATA 경로에 직접 마운트했다.
volumeMounts:
- name: timescaledb-weather-data
mountPath: /var/lib/postgresql/data
그 결과, ArgoCD/Pod 로그에서 다음 에러가 발생했다.
The files belonging to this database system will be owned by user "postgres".
...
initdb: error: directory "/var/lib/postgresql/data" exists but is not empty
initdb: hint: If you want to create a new database system, either remove or empty the directory "/var/lib/postgresql/data" or run initdb with an argument other than "/var/lib/postgresql/data".
원인:
- PVC에 마운트된 ext4 루트에는 기본적으로
lost+found디렉터리가 존재한다. - Postgres
initdb는 PGDATA 디렉터리가 완전히 비어 있지 않으면 실패한다.
즉, 경로는 맞췄지만, ext4 기본 디렉터리 때문에 initdb가 실패한 것이다.
3.2 최종 해결 – PGDATA를 하위 디렉터리로 이동
해결책은 **“PVC는 /var/lib/postgresql/data에 마운트하고, 실제 PGDATA는 그 안의 하위 디렉터리를 쓰도록 바꾸는 것”**이다.
statefulset.yaml 수정:
volumeMounts:
# PVC는 PostgreSQL 기본 PGDATA 경로에 직접 마운트하여
# 데이터가 항상 PVC(EBS)에 기록되도록 한다.
- name: timescaledb-weather-data
mountPath: /var/lib/postgresql/data
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: timescaledb-weather-credentials
key: postgres-user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: timescaledb-weather-credentials
key: postgres-password
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: timescaledb-weather-credentials
key: postgres-db
# PGDATA를 PVC 내부의 하위 디렉터리로 지정하여
# 상위 디렉터리에 존재하는 lost+found 등으로 인한 initdb 오류를 방지한다.
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
# pg_cron을 사용하기 위해 shared_preload_libraries 설정
- name: POSTGRES_SHARED_PRELOAD_LIBRARIES
value: "timescaledb,pg_cron"
이제 동작 방식은 다음과 같다.
- PVC(EBS)는
/var/lib/postgresql/data에 마운트된다. - Postgres는
PGDATA=/var/lib/postgresql/data/pgdata를 사용한다.pgdata디렉터리는 PVC 내부의 하위 디렉터리이므로,- 상위 디렉터리의
lost+found와 무관하게 initdb가 정상 수행된다.
SHOW data_directory;결과는/var/lib/postgresql/data/pgdata가 된다.df -h /var/lib/postgresql/data /var/lib/postgresql/data/pgdata를 확인해 보면 두 경로 모두 PVC(EBS 디바이스)를 가리킨다.
4. 최종 상태 및 검증
수정 후 검증한 내용:
파일시스템
kubectl exec -it timescaledb-weather-0 -n tsdb-weather -- \ df -h /var/lib/postgresql/data /var/lib/postgresql/data/pgdata- 두 경로 모두
/dev/nvme…(EBS gp3) 디바이스로 표시됨.
- 두 경로 모두
PostgreSQL 설정
SHOW data_directory; -- /var/lib/postgresql/data/pgdataStatefulSet 삭제 / Pod 재스케줄 후에도 데이터 유지
CREATE TABLE및 간단한 테스트 데이터를 넣은 뒤,kubectl delete pod또는 Argo CD로 StatefulSet 롤링 재시작을 수행.- 재기동 후에도 해당 테이블과 데이터가 그대로 존재함을 확인.
5. 교훈 / 베스트 프랙티스
이미지의 VOLUME 선언을 항상 확인하기
- Postgres/Timescale 공식 이미지는
VOLUME /var/lib/postgresql/data를 선언하고 있다. - Kubernetes에서 PVC를 마운트할 때, 이 VOLUME이 가리키는 경로와 겹치지 않게 설계해야 한다.
- Postgres/Timescale 공식 이미지는
PVC는 “PGDATA의 부모”가 아니라, “PGDATA 혹은 그 상위에 딱 한 번만” 마운트하기
- 부모에 PVC, 자식에 또 다른 볼륨(이미지 VOLUME)이 올라가면, PVC가 가려져 버린다.
PGDATA를 하위 디렉터리로 두는 패턴을 활용하기
mountPath: /var/lib/postgresql/dataPGDATA=/var/lib/postgresql/data/pgdata- ext4의
lost+found문제를 피하면서도 PVC 위에 안전하게 데이터를 저장할 수 있다.
의심스러울 때는 항상 런타임에서 확인
# DB가 실제로 어디에 쓰고 있는지 확인 kubectl exec -it <pod> -- df -h /var/lib/postgresql /var/lib/postgresql/data kubectl exec -it <pod> -- psql -U postgres -c "SHOW data_directory;"data_directory와df출력이 일치하는지 보는 것이 문제를 추적하는 핵심이었다.
이번 이슈의 핵심 원인은 **“PVC는 제대로 붙어 있었지만, Docker 이미지의 VOLUME 마운트가 그 위를 덮어쓰고 있었다”**는 점이었다.
최종적으로는 PGDATA를 PVC 내부의 하위 디렉터리로 옮기는 것으로 문제를 해결했다.