스토리지 선택 기준 - (2) Kubernetes에서 티어를 어떻게 다룰 것인가
이 글은 시리즈의 2편입니다. 1편 (티어링) 에서 다룬 Hot/Warm/Cold/Archive 개념을 전제로 합니다. 또한, k8s를 충분히 사용해 본 경험이 있음을 전제로 합니다.
1편에서는 스토리지 티어링에 대해 다뤘다. 간단하게 정리하면 메모리 계층을 클라우드로 그대로 옮겼다고 보면 될 정도로 유사한 점을 많이 갖는다고 했었다. 이번에는 이 내용을 조금 현실적인 문제로 끌고가보자.
k8s 를 학습해 본분들의 대부분은 일반적인 Stateless 애플리케이션 Pod 를 띄웠거나, Stateful 이라고 해도 스토리지가 크게 요구되지 않는 use-case 정도를 경험했을 것이다. 그렇다면 OpenSearch 같은걸 띄워버린다면? 각자 쓰고 있는 클라우드 플랫폼이 있다면 곰곰히 생각해보자. Managed k8s를 쓴다고 하면, Hot/Warm/Cold 스토리지까지 전부 달고 운영해야 한다고 치면, Persistent Volume을 어떻게 관리해야 할 까?
StorageClass 몇 개 슥슥 만든다고 끝날 문제는 전혀 아니고, 워크로드별 I/O 특성을 PV 설계에 녹여낼 수 있을까? 를 고민해야 하는 것이다.
결국,
"Hot/Warm/Cold를 전부 PVC로 균일하게 추상화하자."
같은 생각을 하면 안된다는 것이다.
뭘 PV로 관리해야 할까?
Kubernetes의 PV/PVC는 "Pod가 mount해서 쓰는 볼륨" 을 추상화한다. 블록·파일 볼륨 추상화에는 1대1 매칭이 잘 되겠지만, S3-Compatiable Storage/AWS S3Glacier 같은 object/archive 계층의 비용·복구 지연·API semantics라면?
(https://github.com/s3fs-fuse/s3fs-fuse 와 같이, s3 Object Storage 를 일반 파일시스템처럼 마운트 할 수 있는 호환성 유틸을 제공하지만, 사실 성능 측면이나 완벽한 호환성 측면에선 다소 아쉬움이 있을 수 밖에...)
AWS/EKS에서는 EBS, EFS, FSx, S3 계열까지 전부 CSI driver로 PV에 연결할 수는 있다. (https://github.com/kubernetes-sigs/aws-ebs-csi-driver) 하지만 연결할 수 있다 는 것과 그게 좋은 설계다 는 다른 이야기이다.
| 계층 | Kubernetes에서의 관리 방식 | AWS 예시 | 적합한 용도 |
|---|---|---|---|
| Hot | PVC + StatefulSet | EBS gp3/io2, local NVMe | OpenSearch data node, DB, write-heavy |
| Warm | PVC + 별도 StorageClass | EBS gp3 (low IOPS), 일부 st1/sc1 | 덜 비싼 block storage, sequential 로그 처리 |
| Shared | PVC | EFS, FSx | RWX 공유, artifact, shared dataset |
| Cold | PVC를 할 순 있지만 잘 안 씀 | S3 | snapshot, log archive, Parquet/Iceberg |
| Archive | PVC 아님 | Glacier / Deep Archive | 장기 보관, 감사, 재처리용 원본 |
즉, Hot/Warm은 PV로, Cold/Archive는 object storage lifecycle로 관리하는 게 제일 자연스럽지 않을끼? 어떻게든 죄다 PVC로 감싸겠다고 Cold/Archive 까지 건드리는 순간 1편에서 본 retrieval fee와 복구 지연이라는 S3/Glacier의 본질이 PV 추상화 뒤로 숨어버려서 오히려 통제 불능이 된다.
StorageClass는 어떻게 관리하면 좋을까...
Kubernetes의 StorageClass는 class별로 provisioner, parameters, reclaimPolicy 등을 정의하는 리소스다. 그런데 Kubernetes 자체는 class의 구체적인 의미에 대해 언급하고 있진 않다. 즉, hot, warm, shared, scratch 같은 의미는 플랫폼 운영자가 직접 고민하고, 매칭해야 한다.
그런 관점에서 스스로 관리하는 방식을 적어보면... StorageClass를 단순한 프로비저너 설정이 아니라, "이 이름의 볼륨을 쓰면 이 정도 성능을 보장한다" 는 SLA 카탈로그 느낌으로 사용하고 있다.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-gp3-hot
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
parameters:
type: gp3
fsType: xfs
encrypted: "true"
iops: "12000"
throughput: "500"
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-gp3-warm
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
parameters:
type: gp3
fsType: xfs
encrypted: "true"
iops: "3000"
throughput: "125"
이 관점에선, 특히 중요한 값이 세 가지 있다.
volumeBindingMode: WaitForFirstConsumer는 거의 필수에 가깝다. 이 모드는 Pod가 실제로 스케줄링될 때까지 volume binding/provisioning을 지연시켜서, 스케줄러가 Pod의 node/zone 제약을 함께 고려하도록 유도한다. EBS는 같은 Availability Zone의 instance에만 attach할 수 있기 때문에, 이게 없으면 볼륨은 a존에 만들어졌는데 Pod는 c존으로 스케줄링되는 참사가 벌어질 수 있다.reclaimPolicy: Retain은 OpenSearch/DB류에 사용하기 위한 안전장치다. PVC 삭제가 곧 EBS 삭제로 이어지면, 장애 대응 중에 데이터까지 날려먹을 수 있다. (반대로 CI scratch volume이나 임시 batch volume은Delete가 맞을 수 있을테니, 전반적으로 생각을 해볼 필요는 있다.)allowVolumeExpansion: true는 운영 중 디스크 증설을 위해 기본으로 켜두는 게 좋다. 단, 늘리는 것과 별개로 줄이는 작업은 절대로 쉽지 않다는 걸 생각해봐야 한다.
Hot tier - OpenSearch data node: EBS gp3/io2 또는 local NVMe
이제 본격적으로 워크로드별로 내려가 보자. OpenSearch hot node는 일반적으로 다음과 같은 특성을 갖는다.
- ingest write
- segment flush
- merge
- mmap 기반 random read
- page cache 의존
- shard recovery
- replica sync
이 목록을 보면 알겠지만, write도 많고 random read도 많다. 그래서 Hot PV는 IOPS 중심 block storage 여야 한다. (여기서 등장하는 mmap 과 page cache 에 대해서는 길이상 3부로 미룰 예정이다.)
OpenSearch 기준으로는 대략 이렇게 나눌 수 있다.
| 워크로드 | 권장 |
|---|---|
| 일반 hot data node | gp3 + 명시적 IOPS/throughput |
| 대형 shard, 높은 검색/ingest 부하 | gp3 상향 또는 io2 |
| 극저지연 / 초고성능, 데이터 재생성 가능 | local NVMe instance store |
| 비용 민감한 dev/test | 낮은 spec의 gp3 |
| NFS/EFS | data path로는 비추천 |
OpenSearch StatefulSet은 이런 식의 volumeClaimTemplates 를 가진다.
volumeClaimTemplates:
- metadata:
name: opensearch-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: ebs-gp3-hot
resources:
requests:
storage: 2Ti
여기서 ReadWriteOnce 를 쓰는 이유라면? OpenSearch shard는 여러 Pod가 같은 volume에 동시에 write하는 구조가 아니니, 각 data node Pod가 자기 전용 disk를 가지는 구조로 잡기 위해서다.
Warm tier - 단순히 느린 스토리지가 아니라니깐...
1편에서도 언급했지만, Warm node니까 싼 거 쓰자는 생각에 EBS st1 / sc1 으로 바로 보내고 싶을 수 있는데, 생각이 좀 많이 필요하다. HDD 계열인 st1/sc1은 throughput 중심의 large streaming workload에 최적화되어 있다. st1은 big data/data warehouse/log processing, sc1은 infrequently accessed throughput-oriented storage와 lowest storage cost 시나리오가 주 용도다. 이 말을 뒤집으면,
작은 random I/O가 많은 OpenSearch warm searchable index에는 st1/sc1이 항상 좋은 선택은 아니다.
라고 말할 수 있지 않을까?
Warm OpenSearch가 "가끔 검색되지만 그래도 검색 latency는 중요하다" 라면, 차라리 ebs-gp3-warm 같은 스토리지가 나을 수도 있을 것이다.
이 맞을 가능성이 크다. 반대로 warm이 거의 순차적으로 읽는 로그 적재용 목적이거나 Spark 등의 데이터 프로세싱 엔진의 스토리지라면 st1도 검토할 수 있다. 결국 Warm은 "느린 tier" 가 아니라 "덜 비싼 block tier" 로 봐야 한다는 게 핵심이다.
Cold tier - S3를 PVC 로 생각하면 안 된다!
앞에서 말한 fuse나, CSI Driver 이야기를 조금만 더 해보자. 보통 아래와 같은 제약 조건이 같이 붙어있다.
- static provisioning만 지원 한다. dynamic provisioning이나 새 bucket 생성은 지원하지 않는다.
- 모든 POSIX filesystem feature를 지원하지 않는다.
그래서 S3 CSI는 이런 용도에는 괜찮다.
- batch job이 S3 dataset을 file처럼 읽음
- ML training input dataset mount
- Spark/Trino 주변 보조 workload
- migration/reprocessing job
하지만 이런 용도에는 부적합하다.
- OpenSearch data path
- MySQL datadir
- MongoDB dbPath
- Kafka log.dirs
- POSIX rename/fsync/lock semantics에 민감한 workload
즉, Cold tier는 PVC로 대충 뭉개는게 아니라, 보통 이렇게 데이터 lifecycle로 관리해야 한다.
또는 로그라면 이런 식이다.
Glacier까지 가면 더더욱 PV가 아니다. Glacier는 "mount해서 쓰는 filesystem" 이 아니라, 1편에서 봤듯이 복구 요청을 통해 다시 접근 가능 상태로 만드는 archive tier에 가깝기 때문이다.
EFS/FSx는 "공유 볼륨" 이지 data path 대체재가 아니다
EFS CSI driver를 쓰면 EFS file system을 PV로 mount할 수 있다. EFS는 serverless하고 여러 곳에서 공유 가능한 file storage다. 그래서 이런 용도엔 좋다.
- 여러 Pod가 공유하는 config/artifact
- user upload shared area
- batch job 간 중간 산출물
- 작은 규모의 shared filesystem
하지만 OpenSearch data path로는 보통 좋지 않다. OpenSearch/Lucene이 원하는 건...
- 낮은 latency
- page cache 친화적인 local/block device
- random I/O
- fsync/merge 안정성
정도인데, EFS/NFS 계열은 network filesystem이라 metadata/latency/locking semantics가 병목이 되기 쉽다.
FSx for Lustre는 또 성격이 다르다. S3와 연동해서 S3 object를 파일처럼 보이게 하고, cloud dataset을 고성능 파일시스템으로 처리하는 용도에 가깝다. 정리하면 이렇게 나누는 게 좋다.
EFS → 범용 shared filesystem
FSx for Lustre → 고성능 병렬 파일시스템, ML/HPC/batch
EBS → Stateful DB/Search engine의 기본 data path
S3 → object lake, snapshot, archive
당연히 Node Pool 도...
OpenSearch 를 써봤으면 알겠지만, 당연히 Node Pool 도 나눠야 할 것이다.
NodePool: os-hot
- instance: storage/network strong
- disk: gp3/io2 또는 local NVMe
- taint: storage-tier=hot:NoSchedule
NodePool: os-warm
- instance: memory/cost balanced
- disk: gp3 lower spec
- taint: storage-tier=warm:NoSchedule
그러니 OpenSearch hot node StatefulSet에는 nodeSelector/tolerations로 명확히 묶어주면 좋을 것이다.
nodeSelector:
storage-tier: hot
tolerations:
- key: storage-tier
operator: Equal
value: hot
effect: NoSchedule
EBS PV가 AZ에 묶이기 때문에, Pod가 다 른 AZ로 가려고 하면 volume attach 자체가 안 된다. 그래서 WaitForFirstConsumer, node affinity, topology spread, PDB, StatefulSet rolling policy까지 한 묶음으로 같이 봐야 한다. (이 중 하나라도 빠뜨리면 롤링 배포하다 Pod가 영영 Pending에 빠지는 걸 보게 된다.)
플랫폼 관점에서는 정책을 강제해야 한다
마지막으로, 실제 플랫폼이라면 사용자가 아무 PVC나 만들게 두면 비용과 장애가 터진다. 그래서 정책을 강제하는 게 좋다.
- default StorageClass는 하나만 두거나, 민감한 클러스터에서는 아예 제거해서
storageClassName명시를 강제한다. - Admission policy(Kyverno/Gatekeeper) 로 위험한 PVC를 막는다.
storageClassName없는 PVC 금지- prod namespace에서 dev용 StorageClass 사용 금지
- OpenSearch namespace에서 efs 사용 금지
- 5Ti 이상 PVC 생성 시 approval label 요구
Retain없는 prod PVC 금지,encrypted=false금지
- Quota 로 namespace별 PVC 총량(
requests.storage,persistentvolumeclaims)을 제한한다. - Tagging 으로 EBS volume/snapshot/S3에
service,owner,environment,cost-center,data-classification,retention같은 태그를 강제한다. (이건 나중에 비용 분석할 때 없으면 정말 곤란하다.)
전체 그림
지금까지의 이야기를 OpenSearch 기준으로 한 장에 모으면 대략 이런 구조가 된다.
결론
K8s platform을 AWS에 붙여서 Hot/Warm/Cold까지 운영한다면, PV 관리는 이렇게 가져가는 게 좋다.
- StorageClass를 tier별 SLA로 정의 한다. (
ebs-gp3-hot,ebs-io2-critical,ebs-gp3-warm,efs-shared...) - OpenSearch data path는 EBS/local NVMe 중심 으로 둔다.
- Cold/Archive를 PVC로 억지 추상화하지 않는다. S3는 snapshot repository/data lake로, Glacier는 lifecycle 계층으로 본다.
- EFS/FSx/S3 CSI는 용도를 제한 한다.
- PV lifecycle과 data lifecycle을 분리 한다.
그런데 이 글 내내 "OpenSearch data path는 S3/EFS에 올리면 안 된다" 고 반복했으면서, 정작 왜 안 되는지는 "network filesystem이라 병목" 정도로만 넘어갔다. 3편에서는 이쪽을 다시 살펴보고, 다음글을 쓰러 도망을 시리즈를 끝내려고 한다.
