스왑 메모리 관리

스왑 메모리 관리 (Swap memory management)

Kubernetes는 노드에서 **스왑 메모리(swap memory)**를 사용하도록 구성될 수 있어요. 그러면 커널이 페이지를 백킹 스토리지(backing storage)로 스왑아웃해서 물리 메모리를 확보할 수 있습니다. 이는 여러 사용 사례에서 유용합니다. 예를 들어 메모리 풋프린트는 크지만 어느 순간에는 그 메모리의 일부만 접근하는 워크로드를 실행하는 노드가 있죠. 또한 메모리 압력 급증 중에 Pod가 종료되는 것을 방지하고, 노드 안정성을 위협할 수 있는 시스템 수준 메모리 급증으로부터 노드를 보호하며, 노드에서 더 유연한 메모리 관리를 가능하게 해줍니다.

출처: Kubernetes 공식 문서 — Swap memory management

운영 체제 지원 (Operating system support)

  • Linux 노드는 스왑을 지원해요. 각 노드를 구성해서 활성화해야 합니다. 기본적으로 kubelet은 스왑이 활성화된 Linux 노드에서는 시작되지 않아요.
  • Windows 노드는 스왑 공간을 요구해요. 기본적으로 kubelet은 스왑이 비활성화된 Windows 노드에서는 시작되지 않습니다.

어떻게 동작할까요? (How does it work?)

노드에서 스왑 사용을 상상할 수 있는 여러 방법이 있어요. 만약 kubelet이 이미 노드에서 실행 중이라면, 스왑이 프로비저닝된 이후에 이를 식별하려면 kubelet을 재시작해야 합니다.

스왑이 프로비저닝되어 사용 가능한 노드에서 kubelet이 시작되면(failSwapOn: false 구성), kubelet은 다음을 수행해요.

  • 이 스왑 활성화 노드에서 시작할 수 있습니다.
  • 일반적으로 컨테이너 런타임이라고 불리는 CRI(Container Runtime Interface) 구현에 Kubernetes 워크로드에 대해 기본적으로 스왑 메모리를 0으로 할당하도록 지시합니다.

노드의 스왑 구성은 KubeletConfigurationmemorySwap를 통해 클러스터 관리자에게 노출돼요. 클러스터 관리자는 memorySwap.swapBehavior를 설정해서 스왑 메모리 존재 시 노드의 동작을 지정할 수 있습니다.

스왑 동작 (Swap behaviors)

사용할 스왑 동작을 골라야 해요. 클러스터의 서로 다른 노드가 서로 다른 스왑 동작을 사용할 수 있습니다.

Linux 노드에서 선택할 수 있는 스왑 동작은 다음과 같아요.

  • NoSwap (기본값) – 이 노드에서 Pod로 실행되는 워크로드는 스왑을 사용할 수 없고, 사용하지도 않아요.
  • LimitedSwap – Kubernetes 워크로드가 스왑 메모리를 활용할 수 있어요.

참고: NoSwap 동작을 고르고 kubelet이 스왑 공간을 허용하도록 구성(failSwapOn: false)했다면, 워크로드는 어떤 스왑도 사용하지 않아요. 하지만 systemd 서비스나 (심지어 kubelet 자체도!) Kubernetes 관리 밖의 컨테이너 프로세스는 스왑을 활용할 수 있습니다.

컨테이너 런타임 통합 (Container runtime integration)

kubelet은 컨테이너 런타임 API를 사용하고, 컨테이너에 대해 원하는 스왑 구성을 가능하게 하는 방식으로 특정 구성(예: cgroup v2의 경우 memory.swap.max)을 적용하도록 컨테이너 런타임에 지시해요. cgroup을 사용하는 런타임의 경우, 컨테이너 런타임이 이러한 설정을 컨테이너 레벨 cgroup에 기록할 책임이 있습니다.

스왑 사용 관찰 가능성 (Observability for swap use)

노드 및 컨테이너 레벨 메트릭 통계

kubelet은 이제 노드 및 컨테이너 레벨 메트릭 통계를 수집하며, 이는 /metrics/resource(주로 Prometheus 같은 모니터링 도구가 사용)와 /stats/summary(주로 Autoscaler가 사용) kubelet HTTP 엔드포인트에서 접근할 수 있어요. 이렇게 하면 kubelet을 직접 요청할 수 있는 클라이언트가 LimitedSwap 사용 시 스왑 사용량과 남은 스왑 메모리를 모니터링할 수 있습니다. 또한 machine_swap_bytes 메트릭이 cadvisor에 추가되어 머신의 총 물리 스왑 용량을 보여줘요.

예를 들어 다음 /metrics/resource가 지원됩니다.

  • node_swap_usage_bytes: 노드의 현재 스왑 사용량(바이트)이에요.
  • container_swap_usage_bytes: 컨테이너의 현재 스왑 사용량(바이트)이에요.
  • container_swap_limit_bytes: 컨테이너의 현재 스왑 제한(바이트)이에요.

kubectl top --show-swap 사용하기

메트릭을 조회하는 것은 유용하지만 다소 번거로워요. 이 메트릭들은 사람보다는 소프트웨어가 사용하도록 설계되었기 때문이에요. 더 사용자 친화적인 방식으로 이 데이터를 소비하기 위해, kubectl top 명령이 --show-swap 플래그를 사용해 스왑 메트릭을 지원하도록 확장되었습니다.

노드의 스왑 사용량 정보를 받으려면 kubectl top nodes --show-swap을 사용할 수 있어요.

kubectl top nodes --show-swap

이는 다음과 비슷한 출력을 만듭니다.

NAME    CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)   SWAP(bytes)    SWAP(%)
node1   1m           10%      2Mi             10%         1Mi            0%
node2   5m           10%      6Mi             10%         2Mi            0%
node3   3m           10%      4Mi             10%         <unknown>      <unknown>

Pod별 스왑 사용량 정보를 받으려면 kubectl top pods --show-swap을 사용할 수 있어요.

kubectl top pod -n kube-system --show-swap

이는 다음과 비슷한 출력을 만듭니다.

NAME                                      CPU(cores)   MEMORY(bytes)   SWAP(bytes)
coredns-58d5bc5cdb-5nbk4                  2m           19Mi            0Mi
coredns-58d5bc5cdb-jsh26                  3m           37Mi            0Mi
etcd-node01                               51m          143Mi           5Mi
kube-apiserver-node01                     98m          824Mi           16Mi
kube-controller-manager-node01            20m          135Mi           9Mi
kube-proxy-ffgs2                          1m           24Mi            0Mi
kube-proxy-fhvwx                          1m           39Mi            0Mi
kube-scheduler-node01                     13m          69Mi            0Mi
metrics-server-8598789fdb-d2kcj           5m           26Mi            0Mi

노드 상태의 일부로 스왑 용량 보고하기

새 노드 상태 필드 node.status.nodeInfo.swap.capacity가 추가되어 노드의 스왑 용량을 보고해요.

예를 들어 다음 명령으로 클러스터의 노드 스왑 용량을 검색할 수 있어요.

kubectl get nodes -o go-template='{{range .items}}{{.metadata.name}}: {{if .status.nodeInfo.swap.capacity}}{{.status.nodeInfo.swap.capacity}}{{else}}<unknown>{{end}}{{"\n"}}{{end}}'

이는 다음과 비슷한 출력을 만듭니다.

node1: 21474836480
node2: 42949664768
node3: <unknown>

참고: <unknown> 값은 해당 Node에 대해 .status.nodeInfo.swap.capacity 필드가 설정되지 않았음을 나타내요. 이는 노드에 스왑이 프로비저닝되지 않았거나, 덜 가능성 있지만 kubelet이 노드의 스왑 용량을 결정할 수 없음을 의미할 수 있어요.

NFD(Node Feature Discovery)를 사용한 스왑 발견

Node Feature Discovery는 하드웨어 기능과 구성을 감지하는 Kubernetes 애드온이에요. 이를 활용해 어떤 노드에 스왑이 프로비저닝되어 있는지 발견할 수 있습니다.

예를 들어 스왑이 프로비저닝된 노드를 알아내려면 다음 명령을 사용하세요.

kubectl get nodes -o jsonpath='{range .items[?(@.metadata.labels.feature\.node\.kubernetes\.io/memory-swap)]}{.metadata.name}{"\t"}{.metadata.labels.feature\.node\.kubernetes\.io/memory-swap}{"\n"}{end}'

이는 다음과 비슷한 출력을 만듭니다.

k8s-worker1: true
k8s-worker2: true
k8s-worker3: false

이 예에서 스왑은 노드 k8s-worker1k8s-worker2에는 프로비저닝되어 있지만 k8s-worker3에는 없어요.

위험 및 주의사항 (Risks and caveats)

⚠️ 주의: 스왑 공간을 암호화하는 것을 강력히 권장해요. 자세한 내용은 메모리 백킹 볼륨(memory-backed volumes)을 참고하세요.

시스템에서 스왑을 사용할 수 있게 되면 예측 가능성이 줄어듭니다. 스왑은 더 많은 RAM을 확보해 성능을 향상시킬 수 있지만, 데이터를 메모리로 다시 스왑하는 것은 무거운 작업이라 때로는 수십 배 더 느릴 수 있어 예상치 못한 성능 저하를 일으킬 수 있어요. 게다가 스왑은 메모리 압력 하에서 시스템의 동작을 바꿉니다. 스왑을 활성화하면 **노이지 네이버(noisy neighbors)**의 위험이 커지는데, RAM을 자주 사용하는 Pod가 다른 Pod를 스왑하게 만들 수 있기 때문이에요. 또한 스왑이 예측 가능하게 회계 처리되지 않는 Kubernetes 워크로드의 더 큰 메모리 사용을 허용하고, 예상치 못한 패킹 구성으로 인해 스케줄러는 현재 스왑 메모리 사용량을 고려하지 않습니다. 이는 노이지 네이버의 위험을 높여요.

스왑 메모리가 활성화된 노드의 성능은 기본 물리 스토리지에 따라 달라져요. 스왑 메모리를 사용할 때, I/O throttling이 있는 클라우드 VM처럼 IOPS(초당 I/O 작업)가 제약된 환경에서는 SSD나 NVMe 같은 더 빠른 스토리지 매체에 비해 성능이 훨씬 나빠질 수 있습니다. 스왑이 I/O 압력을 유발할 수 있으므로, 시스템 중요 데몬에 더 높은 I/O 지연 우선순위를 주는 것이 권장됩니다.

메모리 백킹 볼륨 (Memory-backed volumes)

Linux 노드에서 메모리 백킹 볼륨(예: secret 볼륨 마운트, medium: Memory인 emptyDir)은 tmpfs 파일시스템으로 구현돼요. 이러한 볼륨의 내용은 항상 메모리에 남아 있어야 하므로 디스크로 스왑되어서는 안 됩니다. 그러한 볼륨의 내용이 메모리에 남도록 보장하기 위해 noswap tmpfs 옵션이 사용됩니다.

Linux 커널은 버전 6.3부터 noswap 옵션을 공식 지원해요. 하지만 여러 배포판이 이 마운트 옵션을 더 오래된 Linux 버전에도 백포트하는 경우가 많습니다.

노드가 noswap 옵션을 지원하는지 확인하기 위해 kubelet은 다음을 수행해요.

  • 커널 버전이 6.3보다 높으면 noswap 옵션이 지원된다고 가정합니다.
  • 그렇지 않으면 kubelet은 시작 시 noswap 옵션으로 더미 tmpfs를 마운트하려 시도해요. 알 수 없는 옵션을 나타내는 오류로 kubelet이 실패하면 noswap이 지원되지 않는 것으로 간주되어 사용되지 않습니다. 메모리 백킹 볼륨이 디스크로 스왑될 수 있음을 경고하는 kubelet 로그 항목이 발행돼요. 성공하면 더미 tmpfs는 삭제되고 noswap 옵션이 사용됩니다.
  • noswap 옵션이 지원되지 않으면 kubelet은 경고 로그 항목을 발행한 후 실행을 계속합니다.

스왑 설정은 kubelet의 범위 밖이며 일반적인 OS 구성 문제이므로, 암호화 스왑 처리는 관리자의 책임이에요. 이 위험을 완화하려면 관리자가 암호화 스왑을 프로비저닝해야 합니다.

축출 (Evictions)

스왑 활성화 노드에 대한 메모리 축출 임계값을 구성하는 것은 까다로울 수 있어요.

스왑이 비활성화된 상태에서는 kubelet의 축출 임계값을 노드의 메모리 용량보다 약간 낮게 설정하는 것이 합리적이에요. 그 이유는 노드 메모리가 바닥나서 OOM 킬러(OOM killer)가 호출되기 전에 Kubernetes가 Pod를 축출하기 시작하기를 원하기 때문이에요. OOM 킬러는 Kubernetes를 인식하지 못하므로 QoS, Pod 우선순위 또는 다른 Kubernetes 특정 요소를 고려하지 않습니다.

스왑이 활성화되면 상황은 더 복잡해져요. Linux에서 vm.min_free_kbytes 파라미터는 커널이 적극적으로 메모리를 재확보(스왑아웃 포함)하기 시작하는 메모리 임계값을 정의합니다. kubelet의 축출 임계값이 커널이 메모리 재확보를 시작하기 전에 축출이 일어나도록 설정되면, 워크로드가 노드 메모리 압력 중에 스왑아웃될 수 없게 될 수 있어요. 하지만 축출 임계값을 너무 높게 설정하면 노드 메모리가 바닥나 OOM 킬러를 호출할 수 있는데, 이것도 이상적이지 않습니다.

이를 해결하기 위해 kubelet의 축출 임계값을 vm.min_free_kbytes 값보다 약간 낮게 설정하는 것이 권장됩니다. 이렇게 하면 kubelet이 Pod를 축출하기 전에 노드가 스왑을 시작할 수 있어 워크로드가 사용하지 않는 데이터를 스왑아웃하고 축출이 발생하는 것을 방지할 수 있어요. 반면에 약간만 낮기 때문에, kubelet은 노드 메모리가 바닥나기 전에 Pod를 축출하기 시작해 OOM 킬러를 피할 가능성이 높습니다.

vm.min_free_kbytes의 값은 노드에서 다음 명령을 실행해 확인할 수 있어요.

cat /proc/sys/vm/min_free_kbytes

사용되지 않는 스왑 공간 (Unutilized swap space)

LimitedSwap 동작에서 Pod에 사용 가능한 스왑 양은 노드 총 메모리에 대한 요청된 메모리 비율에 기반해 자동으로 결정됩니다.

이 설계는 보통 Kubernetes 워크로드에 대해 제한된 상태로 남는 스왑 부분이 있을 것임을 의미해요. 예를 들어 Kubernetes 1.37은 Guaranteed QoS 클래스의 Pod에 대해 스왑 사용을 허용하지 않기 때문에, Guaranteed Pod의 메모리 요청에 비례하는 스왑 양은 Kubernetes 워크로드가 사용하지 않고 남게 됩니다.

이 동작은 스왑 대상이 아닌 Pod가 많을 때 약간의 위험을 수반해요. 반면에 Kubernetes 범위 밖의 프로세스(시스템 데몬, 심지어 kubelet 자체)가 사용할 수 있는 시스템 예약 스왑 메모리 양을 효과적으로 유지합니다.

Kubernetes 클러스터에서 스왑 사용 모범 사례 (Good practice for using swap)

시스템 중요 데몬에 대한 스왑 비활성화

테스트 단계와 사용자 피드백에서 시스템 중요 데몬과 서비스의 성능이 저하될 수 있음이 관찰됐어요. 즉 kubelet을 포함한 시스템 데몬이 평소보다 느리게 작동할 수 있습니다. 이 문제가 발생하면 system slice의 cgroup을 스왑 방지(즉, memory.swap.max=0 설정)하도록 구성하는 것이 좋아요.

I/O 지연에 대한 시스템 중요 데몬 보호

스왑은 노드의 I/O 부하를 증가시킬 수 있어요. 메모리 압력이 커널로 하여금 페이지를 빠르게 스왑 인/아웃하게 하면, I/O 작업에 의존하는 시스템 중요 데몬과 서비스가 성능 저하를 겪을 수 있습니다.

이를 완화하기 위해 systemd 사용자는 I/O 지연 측면에서 system slice에 우선순위를 주는 것이 권장됩니다. systemd를 사용하지 않는 사용자는 시스템 데몬과 프로세스를 위한 전용 cgroup을 설정하고 같은 방식으로 I/O 지연에 우선순위를 주는 것이 좋아요. 이는 system slice에 io.latency를 설정해 더 높은 I/O 우선순위를 부여함으로써 달성할 수 있어요.

스왑과 컨트롤 플레인 노드

Kubernetes 프로젝트는 스왑 공간 없이 컨트롤 플레인 노드를 실행할 것을 권장합니다. 컨트롤 플레인은 주로 Guaranteed QoS Pod를 호스팅하므로 스왑을 일반적으로 비활성화할 수 있어요. 주요 우려는 컨트롤 플레인에서 중요 서비스를 스왑하면 성능에 부정적 영향을 줄 수 있다는 것입니다.

스왑용 전용 디스크 사용

Kubernetes 프로젝트는 스왑이 활성화된 노드를 실행할 때마다 암호화 스왑을 사용할 것을 권장해요. 스왑이 파티션이나 루트 파일시스템에 있으면 워크로드가 디스크에 쓰는 시스템 프로세스와 간섭할 수 있습니다. 같은 디스크를 공유할 때 프로세스가 스왑을 압도해 kubelet, 컨테이너 런타임, systemd의 I/O를 방해하고 다른 워크로드에 영향을 줄 수 있어요. 스왑 공간이 디스크에 있으므로 의도된 사용 사례에 충분히 빠른 디스크인지 확인하는 것이 중요합니다. 또는 단일 백킹 디바이스의 서로 다른 매핑 영역 사이에 I/O 우선순위를 구성할 수도 있어요.

스왑 인지 스케줄링 (Swap-aware scheduling)

Kubernetes 1.37은 스왑 메모리 사용량을 고려해 Pod를 노드에 할당하는 것을 지원하지 않아요. 스케줄러는 일반적으로 인프라 리소스의 요청(requests)을 사용해 Pod 배치를 안내하며, Pod는 스왑 공간을 요청하지 않고 메모리만 요청합니다. 이는 스케줄러가 스케줄링 결정 시 스왑 메모리를 고려하지 않는다는 뜻이에요. 이는 활발히 작업 중인 사항이지만 아직 구현되지는 않았습니다.

관리자가 Pod가 스왑 메모리를 사용하려는 의도가 없는 한 스왑 메모리가 있는 노드에 스케줄링되지 않도록 하려면, 스왑이 있는 노드에 **테인트(taint)**를 걸어 이 문제로부터 보호할 수 있어요. 테인트는 스왑을 허용하는 워크로드가 부하 시 스왑이 없는 노드로 흘러가지 않도록 보장합니다.

최적 성능을 위한 스토리지 선택

스왑 공간용으로 지정된 스토리지 디바이스는 높은 메모리 사용 중 시스템 응답성을 유지하는 데 중요해요. 회전식 HDD는 기계적 특성 때문에 상당한 지연을 도입해 심각한 성능 저하와 시스템 스래싱(thrashing)을 일으키므로 이 작업에 부적합합니다. 현대 성능 요구에는 SSD 같은 디바이스가 스왑에 적합한 선택일 가능성이 높아요. 저지연 전자적 접근이 속도 저하를 최소화하기 때문이죠.

스왑 동작 상세 (Swap behavior details)

LimitedSwap에서 스왑 제한은 어떻게 결정될까요?

스왑 메모리 구성(제한 포함)은 상당한 도전 과제예요. 잘못 구성되기 쉬울 뿐 아니라 시스템 레벨 속성으로서, 어떤 잘못된 구성도 특정 워크로드가 아니라 노드 전체를 손상시킬 수 있습니다. 이 위험을 완화하고 노드의 건강을 보장하기 위해 제한 자동 구성을 사용한 스왑을 구현했어요.

LimitedSwap에서는 Burstable QoS 분류에 해당하지 않는 Pod(즉 BestEffort/Guaranteed QoS Pod)는 스왑 메모리를 활용할 수 없어요. BestEffort QoS Pod는 예측 불가능한 메모리 소비 패턴을 보이고 메모리 사용량에 대한 정보가 부족해 안전한 스왑 메모리 할당을 결정하기 어렵습니다. 반면 Guaranteed QoS Pod는 일반적으로 워크로드가 지정한 리소스의 정확한 할당에 의존하고 메모리가 즉시 사용 가능한 애플리케이션에 사용됩니다. 앞서 언급한 보안과 노드 건강 보장을 유지하기 위해, LimitedSwap이 적용되는 동안 이 Pod들은 스왑 메모리를 사용할 수 없어요. 또한 고우선순위 Pod는 소비하는 메모리가 항상 RAM에 남아 사용 준비가 되도록 스왑 사용이 허용되지 않습니다.

스왑 제한 계산을 자세히 설명하기 전에 다음 용어를 정의해야 해요.

  • nodeTotalMemory – 노드에서 사용 가능한 총 물리 메모리 양이에요.
  • totalPodsSwapAvailable – Pod가 사용할 수 있는 노드의 총 스왑 메모리 양이에요 (일부 스왑 메모리는 시스템용으로 예약될 수 있어요).
  • containerMemoryRequest – 컨테이너의 메모리 요청이에요.

스왑 제한은 다음과 같이 구성돼요.

( containerMemoryRequest / nodeTotalMemory ) × totalPodsSwapAvailable

즉, 컨테이너가 사용할 수 있는 스왑 양은 메모리 요청, 노드 총 물리 메모리, Pod가 사용할 수 있는 노드의 총 스왑 메모리 양에 비례합니다.

중요한 점은 Burstable QoS Pod 내 컨테이너에서 메모리 요청을 메모리 제한과 같게 지정해 스왑 사용을 선택하지 않을(opt-out) 수 있다는 점이에요. 이렇게 구성된 컨테이너는 스왑 메모리에 접근할 수 없습니다.

더 알아보기 (Learn more)