목록으로

Programming Notes

Kubernetes v1.36: 서버 측 샤딩된 List 및 Watch (Server-Side Sharded List and Watch)

Kubernetes 클러스터가 수만 개의 노드 규모로 확장됨에 따라, Pod와 같이 카디널리티(cardinality)가 높은 리소스를 모니터링하는 컨트롤러는 확장성의 한계에 직면하게 됩니다. 수평적으로 확장된 컨트롤러의 모든 복제본(replica)은 API 서버로부터 전체 이벤트 스트림을 수신하며, 자신과 관련 없는 객체를 폐기하기 위해 역직렬화(deserialization) 과정에서 CPU, 메모리 및 네트워크 비용을 지불합니다. 즉, 컨트롤러를 확장해도 복제본당 비용은 줄어들지 않고 오히려 배로 늘어납니다.

Kubernetes v1.36은 이 문제를 해결하기 위해 **서버 측 샤딩된 List 및 Watch (server-side sharded list and watch)**를 알파 기능(KEP-5866)으로 도입했습니다. 이 기능을 사용하면 API 서버가 소스 단계에서 이벤트를 필터링하므로, 각 컨트롤러 복제본은 자신이 소유한 리소스 컬렉션의 일부분(slice)만 수신하게 됩니다.

클라이언트 측 샤딩의 문제점

kube-state-metrics와 같은 일부 컨트롤러는 이미 수평 샤딩을 지원합니다. 각 복제본에 키 공간(keyspace)의 일부를 할당하고 자신에게 속하지 않은 객체는 폐기하는 방식입니다. 이 방식은 기능적으로는 작동하지만, API 서버에서 흐르는 데이터의 양을 줄이지는 못합니다.

  • N개 복제본 x 전체 이벤트 스트림: 모든 복제본이 모든 이벤트를 역직렬화하고 처리한 뒤, 필요 없는 이벤트를 버립니다.
  • 네트워크 대역폭이 샤드 크기가 아닌 복제본 수에 비례하여 늘어납니다.
  • 역직렬화에 소모되는 CPU가 버려지는 데이터 비율만큼 낭비됩니다.

서버 측 샤딩된 List 및 Watch는 필터링 작업을 API 서버로 업스트림 이동시켜 이 문제를 해결합니다. 각 복제본은 자신이 소유한 해시 범위를 API 서버에 알려주고, API 서버는 해당 범위에 일치하는 이벤트만 전송합니다.

작동 원리

이 기능은 ListOptionsshardSelector 필드를 추가합니다. 클라이언트는 shardRange() 함수를 사용하여 해시 범위를 지정할 수 있습니다.

shardRange(object.metadata.uid, '0x0000000000000000', '0x8000000000000000')

API 서버는 지정된 필드의 결정론적인 64비트 FNV-1a 해시를 계산하고, 해시 값이 [start, end) 범위에 속하는 객체만 반환합니다. 이는 List 응답과 Watch 이벤트 스트림 모두에 적용됩니다. 해시 함수는 모든 API 서버 인스턴스에서 동일한 결과를 생성하므로, 여러 API 서버 복제본을 사용하는 환경에서도 안전하게 사용할 수 있습니다.

현재 지원되는 필드 경로는 object.metadata.uidobject.metadata.namespace입니다.

컨트롤러에서 샤딩된 Watch 사용하기

컨트롤러는 일반적으로 인포머(informer)를 사용하여 리소스를 나열(List)하고 감시(Watch)합니다. 워크로드를 샤딩하려면 각 복제본이 WithTweakListOptions를 통해 인포머에서 사용하는 ListOptionsshardSelector를 주입해야 합니다.

import (
 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 "k8s.io/client-go/informers"
)

shardSelector := "shardRange(object.metadata.uid, '0x0000000000000000', '0x8000000000000000')"

factory := informers.NewSharedInformerFactoryWithOptions(client, resyncPeriod,
 informers.WithTweakListOptions(func(opts *metav1.ListOptions) {
 opts.ShardSelector = shardSelector
 }),
)

2개의 복제본으로 배포하는 경우, 셀렉터는 해시 공간을 절반으로 나눕니다.

// 복제본 0: 해시 공간의 하위 절반
"shardRange(object.metadata.uid, '0x0000000000000000', '0x8000000000000000')"

// 복제본 1: 해시 공간의 상위 절반
"shardRange(object.metadata.uid, '0x8000000000000000', '0x10000000000000000')"

단일 복제본이 || 연산자를 사용하여 연속되지 않은 범위를 담당할 수도 있습니다.

"shardRange(object.metadata.uid, '0x0000000000000000', '0x4000000000000000') || " +
 "shardRange(object.metadata.uid, '0x8000000000000000', '0xc000000000000000')"

서버 지원 확인

API 서버가 샤드 셀렉터를 적용하면, List 응답의 메타데이터에 적용된 셀렉터를 다시 보여주는 shardInfo 필드가 포함됩니다.

{
 "kind": "PodList",
 "apiVersion": "v1",
 "metadata": {
 "resourceVersion": "10245",
 "shardInfo": {
 "selector": "shardRange(object.metadata.uid, '0x0000000000000000', '0x8000000000000000')"
 }
 },
 "items": [...]
}

만약 shardInfo가 없다면, 서버가 샤드 셀렉터를 무시하고 전체 컬렉션을 필터링 없이 보냈음을 의미합니다. 이 경우 클라이언트는 할당된 샤드 범위를 벗어난 객체를 버리는 클라이언트 측 필터링을 적용하는 등, 전체 결과 세트를 처리할 준비가 되어 있어야 합니다.

참여하기

이 기능은 현재 알파 단계이며, API 서버에서 ShardedListAndWatch 기능 게이트(feature gate)를 활성화해야 합니다. 대규모 클러스터를 운영하는 운영자 및 컨트롤러 개발자분들의 피드백을 기다리고 있습니다.

질문이나 의견이 있다면 Kubernetes Slack#sig-api-machinery 채널에 참여해 주세요.