Skip to content

Split View: Live Migration 3: migration proxy, 포트, TLS, 소켓은 왜 필요한가

|

Live Migration 3: migration proxy, 포트, TLS, 소켓은 왜 필요한가

들어가며

live migration을 생각하면 흔히 "source QEMU가 target QEMU로 메모리를 복사한다"라고 끝내기 쉽다. 하지만 실제 Kubernetes 환경에서는 그 사이에 훨씬 더 복잡한 transport 문제가 있다.

  • launcher 내부 libvirt는 주로 unix socket과 로컬 자원을 쓴다
  • target Pod는 동적으로 생긴다
  • source와 target은 서로 다른 노드에 있다
  • 추가 TLS 계층이 필요하다
  • block migration일 때는 포트 요구가 달라진다

이 문제를 풀기 위해 KubeVirt는 pkg/virt-handler/migration-proxy/migration-proxy.go의 migration proxy 계층을 둔다.

proxy가 왜 필요한가

핵심 이유는 로컬 libvirt 제어 socket과 클러스터 네트워크 경로를 직접 1:1로 묶기 어렵기 때문이다.

launcher 내부에서는 libvirt와 QEMU가 unix socket, 로컬 파일, Pod namespace 기반으로 동작한다. 하지만 migration은 source 노드와 target 노드 사이의 네트워크 연결이 필요하다. 따라서 KubeVirt는 중간에 proxy를 둬서:

  • target 측에서는 TCP listener를 열고
  • source 측에서는 local unix socket을 제공하며
  • 두 쪽을 TLS 또는 plain transport로 연결

하는 방식으로 transport를 정리한다.

즉 migration proxy는 source와 target 사이의 주소 번역기이자 transport shim이다.

source와 target에서 각각 하는 일

ProxyManager 인터페이스를 보면 역할이 선명하다.

target 측

  • StartTargetListener
  • GetTargetListenerPorts
  • StopTargetListener

즉 target 노드는 외부에서 들어올 TCP 연결을 받을 listener를 연다.

source 측

  • StartSourceListener
  • GetSourceListenerFiles
  • StopSourceListener

즉 source 노드는 local unix socket 파일을 만들고, 그것을 target 주소와 포트로 포워딩한다.

이 구조 덕분에 launcher 입장에서는 익숙한 local endpoint로 보이지만, 실제 transport는 network를 타고 target까지 간다.

포트는 어떻게 쓰이는가

코드는 기본 migration 포트를 다음처럼 정의한다.

  • direct migration port: 49152
  • block migration port: 49153

GetMigrationPortsList는 block migration 여부에 따라 필요한 포트 집합을 결정한다. 즉 디스크를 추가로 옮겨야 하면 포트 요구가 늘어난다.

운영자가 여기서 알아야 할 핵심은 live migration이 "보이지 않는 내부 통신"이 아니라, 명확한 포트와 listener를 가진 네트워크 작업이라는 점이다.

target listener는 왜 random port를 쓸 수 있는가

StartTargetListener는 target unix file들에 대해 proxy를 만들고, TCP bind port를 0으로 넘겨 random port를 사용할 수 있다. 이후 GetTargetListenerPorts가 실제 바인딩된 포트를 source 쪽에 알려 준다.

이 방식의 장점은 포트 충돌을 줄이고, Pod와 노드의 동적 환경에서 더 유연하게 listener를 열 수 있다는 점이다.

즉 고정 논리 포트와 실제 bind 포트를 분리해 두는 구조다.

소켓 파일은 왜 필요한가

source 쪽의 SourceUnixFile을 보면 migration proxy는 base directory 아래에 source socket 파일을 만든다. 이유는 launcher 내부 libvirt가 여전히 local unix endpoint를 기준으로 동작하는 흐름과 잘 맞기 때문이다.

이 말은 migration proxy가 네트워크를 위해 기존 local runtime 모델을 버리는 것이 아니라, unix socket 모델을 네트워크 가능한 형태로 포장한다는 뜻이다.

TLS는 어디에 붙는가

migration-proxy.go는 server TLS config, client TLS config, migration TLS config를 따로 관리한다. 또 cluster migration configuration에 DisableTLS 옵션이 있으면 target listener에서 추가 TLS 레이어를 끌 수 있다.

하지만 코드와 API 설명 모두 공통적으로 시사하는 바는 명확하다. DisableTLS는 보통 나쁜 생각이다.

왜냐하면 migration 트래픽은 guest 메모리 상태와 실행 상태를 담기 때문에, 네트워크 경로 보호가 중요하기 때문이다.

즉 migration proxy는 단순 포워더가 아니라 보안 경계이기도 하다.

migration network를 왜 따로 둘 수 있는가

KubeVirt configuration에는 migration traffic이 기본 Pod network가 아니라 별도 network를 타게 하는 옵션이 있다. 이는 migration 트래픽이 애플리케이션 Pod 네트워크와 경쟁하거나, 대역폭과 보안 측면에서 분리되어야 할 수 있기 때문이다.

이 설정이 있는 이유를 proxy 관점에서 보면 이해가 쉽다. proxy는 어차피 source와 target 사이의 transport shim이므로, 그 transport를 어느 네트워크로 보낼지 바꾸기 쉬운 구조가 된다.

block migration과 direct migration이 왜 다른가

direct migration은 주로 메모리와 실행 상태 중심이지만, block migration은 디스크 관련 데이터 경로까지 포함할 수 있다. 그래서 포트도 더 필요하고, 전체 bandwidth와 timeout 계산도 더 민감해진다.

proxy 계층이 이 둘을 구분해 포트 맵을 관리하는 이유가 여기에 있다.

운영자가 proxy를 봐야 하는 순간

다음 같은 상황에서는 migration proxy를 먼저 의심해야 한다.

  • target Pod는 살아 있는데 migration 연결이 시작되지 않음
  • source와 target 사이 TLS handshake 문제가 있음
  • block migration에서 특정 포트만 실패함
  • migration network 분리 후 연결 문제가 생김

이때는 단순히 libvirt error만 보면 안 되고, listener가 열렸는지, source socket이 만들어졌는지, target 포트 매핑이 맞는지를 함께 봐야 한다.

자주 하는 오해

오해 1: source와 target QEMU가 그냥 직접 붙는다

Kubernetes 환경에서는 중간 transport shim이 필요하다. 그 역할을 migration proxy가 한다.

오해 2: migration 포트는 항상 고정이다

논리 포트는 정해져 있어도 실제 bind 포트와 매핑은 유동적일 수 있다.

오해 3: TLS는 선택 사항이니 꺼도 큰 문제 없다

migration state는 민감하다. 보통은 추가 TLS 레이어를 유지하는 것이 맞다.

마무리

KubeVirt의 migration proxy는 live migration transport를 Kubernetes 환경에 맞게 다듬는 핵심 계층이다. target 쪽은 TCP listener를 열고, source 쪽은 unix socket을 제공하며, 그 사이를 TLS와 포트 매핑으로 연결한다. 이 구조 덕분에 launcher 내부의 로컬 libvirt 모델과 클러스터 간 네트워크 이동 모델이 깔끔하게 이어진다.

다음 글에서는 migration과 직접 연결되는 자원 문제, 즉 CPU, 메모리, NUMA, hugepages가 Pod 스케줄링과 guest 하드웨어 모델에서 어떻게 해석되는지 살펴보겠다.

Live Migration 3: Why Migration Proxy, Ports, TLS, and Sockets Are Needed

Introduction

When thinking about live migration, it is easy to simply say "source QEMU copies memory to target QEMU." But in an actual Kubernetes environment, there are far more complex transport issues in between.

  • libvirt inside the launcher primarily uses Unix sockets and local resources
  • The target Pod is dynamically created
  • Source and target are on different nodes
  • An additional TLS layer is needed
  • Port requirements differ for block migration

To solve this problem, KubeVirt uses the migration proxy layer in pkg/virt-handler/migration-proxy/migration-proxy.go.

Why a Proxy Is Needed

The core reason is that it is difficult to directly 1:1 bind the local libvirt control socket with the cluster network path.

Inside the launcher, libvirt and QEMU operate based on Unix sockets, local files, and Pod namespace. But migration requires network connectivity between source and target nodes. Therefore, KubeVirt places a proxy in between that:

  • Opens TCP listeners on the target side
  • Provides local Unix sockets on the source side
  • Connects the two sides via TLS or plain transport

In other words, the migration proxy is an address translator and transport shim between source and target.

What Source and Target Each Do

Looking at the ProxyManager interface, the roles are clear.

Target side

  • StartTargetListener
  • GetTargetListenerPorts
  • StopTargetListener

The target node opens listeners to accept incoming TCP connections from outside.

Source side

  • StartSourceListener
  • GetSourceListenerFiles
  • StopSourceListener

The source node creates local Unix socket files and forwards them to the target address and port.

Thanks to this structure, from the launcher's perspective, it sees a familiar local endpoint, but the actual transport goes over the network to the target.

How Ports Are Used

The code defines default migration ports as follows:

  • Direct migration port: 49152
  • Block migration port: 49153

GetMigrationPortsList determines the required set of ports based on whether block migration is involved. In other words, if disks need to be additionally moved, port requirements increase.

The key insight for operators is that live migration is a network operation with explicit ports and listeners, not "invisible internal communication."

Why the Target Listener Can Use Random Ports

StartTargetListener creates proxies for target Unix files and can pass TCP bind port as 0 to use random ports. Then GetTargetListenerPorts reports the actual bound ports to the source side.

The advantage of this approach is reducing port conflicts and being more flexible in opening listeners in the dynamic environment of Pods and nodes.

In other words, it is a structure that separates fixed logical ports from actual bind ports.

Why Socket Files Are Needed

Looking at SourceUnixFile on the source side, the migration proxy creates source socket files under a base directory. The reason is that this fits well with the flow where libvirt inside the launcher still operates based on local Unix endpoints.

This means the migration proxy does not abandon the existing local runtime model for networking -- it wraps the Unix socket model into a network-capable form.

Where TLS Is Applied

migration-proxy.go separately manages server TLS config, client TLS config, and migration TLS config. Also, if the cluster migration configuration has a DisableTLS option, the additional TLS layer can be turned off on the target listener.

However, both the code and API descriptions consistently imply that DisableTLS is usually a bad idea.

This is because migration traffic carries guest memory state and execution state, making network path protection important.

In other words, the migration proxy is not just a forwarder but also a security boundary.

Why a Separate Migration Network Can Be Used

The KubeVirt configuration has an option to route migration traffic through a separate network instead of the default Pod network. This is because migration traffic may compete with application Pod networks, or may need to be separated for bandwidth and security reasons.

Understanding this from the proxy perspective is straightforward. Since the proxy is already a transport shim between source and target, it is easy to change which network the transport uses.

Why Block Migration and Direct Migration Differ

Direct migration primarily involves memory and execution state, while block migration can also include disk-related data paths. Therefore, more ports are needed, and overall bandwidth and timeout calculations become more sensitive.

This is why the proxy layer manages port maps that distinguish between the two.

When Operators Should Look at the Proxy

In the following situations, the migration proxy should be the first suspect:

  • Target Pod is alive but migration connection does not start
  • TLS handshake issues between source and target
  • Only specific ports fail in block migration
  • Connection problems after migration network separation

In these cases, looking only at libvirt errors is insufficient -- you should also check whether listeners are open, whether source sockets were created, and whether target port mappings are correct.

Common Misconceptions

Misconception 1: Source and target QEMU just connect directly

In Kubernetes environments, an intermediate transport shim is needed. The migration proxy handles this role.

Misconception 2: Migration ports are always fixed

Logical ports are defined, but actual bind ports and mappings can be dynamic.

Misconception 3: TLS is optional so turning it off is not a big deal

Migration state is sensitive. Usually the additional TLS layer should be maintained.

Conclusion

KubeVirt's migration proxy is a key layer that adapts live migration transport for the Kubernetes environment. The target side opens TCP listeners, the source side provides Unix sockets, and the two are connected via TLS and port mapping. Thanks to this structure, the local libvirt model inside the launcher and the cross-cluster network migration model are cleanly bridged.

In the next post, we will look at the resource issues directly connected to migration -- how CPU, memory, NUMA, and hugepages are interpreted in Pod scheduling and the guest hardware model.