K8s 와 VM, 그리고 Proxmox

  • 2025년 12월 14일
Table Of Contents

서론

재택근무를 자주하곤 한다. 그러다보면 아이러니한 경우가 자주 일어나는데, 방금 전까지 오퍼레이터 코드를 수정하며 k8s 파드의 노드 로그를 뚫어져라 살펴보다가 퇴근을 찍고 나서 바로 내 k8s 클러스터를 관리하는 argocd와 그라파나를 들어가 이상이 없는지 살펴보곤 한다. 가끔 내가 퇴근을 한 건지 안한 건지 헷갈릴 때가 있다.

게다가 어쩔 수 없이 회사의 클러스터와 내 클러스터를 비교하게 된다. 그 차이점 중 가장 눈에 띄는 것은 바로 베어베탈 k8s와 VM k8s다. 아무래도 CPU, GPU 자원을 풀로 활용해야 하는 회사에서는 대부분 BM 서버에 k8s 노드를 올리지만 나는 모두 VM 위에 올라가 있다.

(홈서버에 올라가 있는 Proxmox 위 VM 기반 노드 일부)
(홈서버에 올라가 있는 Proxmox 위 VM 기반 노드 일부)

VM 위에 k8s?

여기저기 이야기를 나누다보면 왜 그런 비효율적인 구성을 고집하는지 의문을 갖는 사람들이 많았다. 사실 k8s는 BM, VM, 심지어 잘 설정하면 Docker 컨테이너 위에서도 돌아가도록 만들어졌다. k8s 노드가 VM 위에 올라가는 것 자체는 기술적으로 크게 걸림돌이 없었다.

하지만 내 아키텍처를 본 사람들은 먼저 의문을 품고는 했다. 나조차 많은 고민이 필요했으니, 그 이유는 다음과 같다.

VM

나도 사람들도 제일 먼저 걱정했던 부분은, 아무래도 Hardware → Hypervisor OS → VM → k8s 라는 구조에서 오는 오버헤드였다. 사실 오래 생각할 필요도 없이 Hardware → OS → k8s 에 비하면 VM의 오버헤드가 추가되는 점은 충분히 우려할만 하다. 특히 윈도우나 맥에서 경험했던 VM은 우려스러울 정도로 느리고 비효율적인 짐덩어리였다.

사실 VirtualBox 등 윈도우, 맥 위에 올라가는 VM들은 OS 위에서 hypervisor가 돌아가는 type 2 hypervisor라, 하드웨어 위에 바로 hypervisor가 올라가는 type 1 hypervisor보다 필연적으로 느릴 수 밖에 없다. 그럼에도 KVM이라는 레이어를 추가하는 것이 성능에 좋은 영향을 미치진 않을 것이 자명하다.

Overcommit

k8s 클러스터를 구성하다보면 한 서버에 여러 VM들을 올리게 된다. 예를 들어 HA를 위해 control plane을 여러 서버에 분산 배치하기도 하고, 부족한 worker 리소스 때문에 worker VM을 더 만들어 조인하기도 한다.

그런데 여기서 문제가 생긴다. 내 홈서버의 코어는 8코어, 스레드로는 16스레드인데 CP VM만 3개가 올라가게 된다. 각 CP에 2코어씩 배분한다고 하면 벌써 6코어를 쓰게된다. 만약 4코어를 쓰는 워커 노드도 3개를 만든다면 총 18코어를 쓰게된다. 만약 dev 클러스터, prod 클러스터를 따로 구축한다면 36코어를 할당하게 된다. 여기에 argocd 클러스터도 추가한다면 실제 코어보다 훨씬 많은 코어를 VM에 할당한다!

이 상황에서 만약 spark job을 돌리는 등, 컴퓨팅 파워가 필요해진다면? 시스템 자체는 돌아가겠지만 제 성능을 못 낼 것이다.

Bandwidth

VM들은 모든 대역폭을 공유한다. 예를 들어 내 서버의 이더넷은 1Gbps의 대역폭을 갖고 있다. 만약 모든 VM들이 내부망 NAS를 사용하게 된다면, 가장 이상적인 상황에서조차 1/n 으로 그 대역폭을 사용해야 한다. 또 메모리와 OS가 설치된 SSD도 같은 이유로 대역폭이 들쭉날쭉하기 마련이다. 특히, 한 VM이 메모리 부족으로 swap 지옥에 빠지게 되어 모든 대역폭을 독점하거나 I/O 지연을 일으킨다면 그 영향은 이상 없는 다른 VM들에게 모두 전파될 것이다.

물리 인터페이스를 공유하는 특성 상, 물리 인터페이스에 대한 영향은 VM 전체에 대한 영향으로 이어지는 것이다.

그럼 왜?

그럼 굳이 무겁고 귀찮은 VM을 써야할까? 훨씬 빠르고 간단한 베어메탈 노드를 쓰면 되지 않을까?

…라고 하기에는 베어메탈 노드의 치명적인 문제가 좀 있었다. 물론, 다른 사람들은 경험하지 않을지도 모르지만 소규모 홈서버 환경에서는 중요한 문제들이었다.

Physical Machine 개수 부족

큰 집에서 돈을 펑펑쓰고 살거 아니면 많은 개수의 물리 서버를 갖기란 쉽지 않다. dev, prod 클러스터를 따로 관리한다면? 여기에 argocd와 prometheus 메트릭 등을 위한 인프라 클러스터도 구축한다면? 각 클러스터 당 최소 구성인 CP 1개, Worker 1개로만 만들어도 6개의 물리 서버가 필요해지고, HA까지 달성하려고 CP 3개, Worker 3개씩 구성한다면 18개의 물리 서버가 필요해진다. 모두 10만원짜리 알리발 미니PC를 사용한다해도 180만원이나 들어간다.

Efficiency

위의 미니PC 시나리오를 좀 더 자세히 살펴보자. 4코어를 각 물리 노드에서 쓸 수 있다고 가정하자. 모두가 같은 종류의 서버라면 CP도, Worker도 4개의 코어만 쓸 수 있다. 많은 경우 내 상황에서 CP는 Worker에 비해 많은 컴퓨팅 파워를 필요로하지 않는다. 거의 모든 상황에서 idle 상태일 것이다. 반대로 Worker는 적은 cpu 자원 때문에 resources.requests.cpu 를 많이 감당하지 못하고 오버커밋을 하다 결국 어느 순간 파드 스케줄링을 거부할 것이다(사실 메모리 자원은 CP, Worker 모두 많이 쓰는 경우를 봐서 CP와 Worker가 크게 다를 것 같지 않다).

한 마디로, CP는 놀고 Worker는 자원 부족을 겪는 것이다. 그리고 물리 서버는 이 자원을 유연하게 재할당하지 못한다. 최선의 상황은 CP 만을 위한 저사양 서버와 Worker를 위한 고사양 서버를 구축하는 것이다. 워크로드가 명확히 정의되지 않는 홈서버 특성 상, 그 결정은 이후 예측 실패로 밝혀질 확률이 매우 높다.

그러나 VM이라면 상황을 봐가며 각 노드의 자원을 유연하게 재할당할 수 있다. CP가 너무 논다? vCPU를 4 → 2로 낮춘다. Worker가 vCPU 부족으로 스테줄링이 안된다? 4 → 8로 높인다. 메모리도 쉽게 늘릴 수 있고 여차하면 디스크 크기도 쉽게 확장할 수 있다. 전체 서버의 자원을 필요하지 않은 곳에서 쉽게 회수하고 필요한 곳에 할당하는 효율성을 VM 구조에서는 얻을 수 있다.

Flexibility

위와 연결되는 이야기다. 물리 서버 노드를 A 러스터에서 제거하고 B 클러스터에 넣으려 한다고 하자. A에서 kubectl delete node로 노드 제거까지는 쉽다. 그 후 서버의 KVM에 접속해 OS를 새로 깔고 kubeadm을 설치한다. storageClass를 위해 nfs-utils를 깔고, iscsi 연결에 필요한 패키지를 설치한다. 그 후 B 클러스터에 join시킨다. 이 작업은 노드 재배치마다 수행할 것이다. OS와 패키지 버전은 매번 달라질 것이다. cloud-init이나 ansible, NixOS를 사용하면 그래도 일관성을 달성할 수 있겠지만, 또 다른 문제가 있다.

바로 서버 하드웨어는 모두 다르다는 점이다. 어떤 서버는 32코어 CPU를, 어떤 서버는 2코어 CPU를 쓸 수 있다. 홈서버 특성상 하드웨어 일관성을 갖기 어렵다. 어떤 서버는 고성능 스토리지를, 어떤 서버는 대용량 스토리지를 갖고 있을 수 있다. 아무리 OS와 패키지를 일관적으로 설치한다고 하더라도 하드웨어의 비일관성은 클러스터 관리를 매우 어렵게한다.

VM을 쓰면 이 문제가 완전히 사라진다. 모든 VM 노드들은 VM 템플릿으로 생성되어 전부 같은 하드웨어, OS, 패키지를 가진다. 추가적인 설정이 필요하다면 노드들 관리와 더불어 템플릿 버전을 올려 다음에 만들어질 노드도 편하게 관리할 수 있다.

심지어 Proxmox 같은 하이퍼바이저들은 물리 서버간 VM 마이그레이션도 지원한다. 특정 서버에 자원이 부족해지면 다른 물리 서버로 라이브 마이그레이션을 하여 어떤 경우에는 다운타임 없이 VM 자체를 옮길수도 있다. 서버를 운영하다 보면 물리 서버를 끄거나 연결을 끊어야 하는 경우가 있다(장비 업그레이드, 하드웨어 고장, 전기세 절약…). VM 마이그레이션은 이럴 때 매우 편한 기능이었다.

(VM은 스냅샷 백업도 쉽다는 장점이 있긴 하지만 노드에 종속적이지 않은 k8s 특성 상, 안 쓰는 특정 클러스터 전체를 백업을 떠 놓고 아카이브하는 경우가 아니면 딱히 필요가 없었다. 노드가 죽으면 다른 노드에 스케줄 될 뿐이다)

이런 유연성은 SE가 없는 홈서버 클러스터에서 필연적으로 VM을 선택하게 하는 이유였다.

Knowledge

학습적인 측면도 생각보다 중요하다. 사실 내가 뭐 돈 벌려고 홈서버를 가지고 있을리가 없잖는가. 배우는게 즐거워 하는건데, 실제 현장에서 돌아가는 k8s 클러스터를 내 서버에서 구현해보려면 몇 안되는 물리 서버로 꾸역꾸역 트러블슈팅하는 것보다 빠르게 VM으로 굉장히 많은 노드들을 만들어 쉽게 클러스터를 경험해볼 수 있다.

Not Only k8s

사실 어떤 서비스 인프라가 돌아가기 위해서는 k8s 외의 것들도 필요하다. 특히 외부 도메인 인증서를 관리하는 리버스 프록시나 내부망 DNS, 로드벨런서들은 물론 k8s위에서도 구축할 수 있음에도 불구하고, 대부분의 경우에는 클러스터 밖에 VM이나 LXC로 띄워 놓는게 낫다. 클러스터가 망가져도 이러한 SPOF들은 항상 띄워져 있어야 하기 때문이다.

허구한 날 깨부수는 클러스터에 내부망 DNS가 있다면, 이 얼마나 아찔한가? 클러스터 하나가 깨지는 순간, 내부망 DNS를 쓰던 공유기가 더 이상 인터넷에 연결되지 않고 내부망 모든 컴퓨터가 인터넷에 접근하지 못한다. DNS가 안되니 다른 클러스터들의 Worker들이 CP endpoint를 찾지 못한다. 그럼 다른 클러스터들도 모조리 깨진다. NAS 도메인을 찾지 못하니 PV들도 모조리 박살난다.

이런 종류의 인프라들은 클러스터 밖에 따로 관리하는게 맞다. 개인적으로 DNS 같은 것들은 k8s 클러스터보다, 직접 여러 물리 서버에 분산 배치하고 HAProxy를 사용해 로드벨런싱 및 HA를 달성하는 것이 올바르다고 생각한다.

결론

CPU intensive한 환경에서는 다른 기업에서도 관리 용이성 때문에 대부분 VM을 쓰는 것으로 알고 있다. 홈서버 환경은 거기에 더불어 훨씬 불안정하면서도 높은 유연성을 원하므로 VM 노드가 아닌 선택지가 없다고 생각한다. 추후 GPU를 사용한 고성능 노드가 필요해지면 BM 노드도 써볼 수 있겠지만… 아직은 잘 모르겠다.

이번 글에서는 홈서버 환경에서 k8s를 쓸 때 VM 노드 vs 물리 노드 논쟁을 살펴보았다. 다음 글에서는 VM 노드들이 올라가는 Proxmox 하이퍼바이저를 내가 왜 클러스터링하지 않고 각각 따로 쓰는지 소개해보려한다.

Share :