본문으로 건너뛰기

최적화

대규모 클러스터에서 컨트롤러가 효율적으로 동작하도록 각 레이어별 최적화 방법을 다룹니다.

최적화 순서​

성능 문제가 발생했을 때 어디서부터 시작해야 하는지 우선순위입니다. 위에서 아래로, 효과가 크고 부작용이 적은 순서입니다.

순서작업효과위험도
1진단 — 실제 병목 확인방향 설정없음
2selector 축소 — label/field selector 추가API 서버 부하, 네트워크, 메모리 동시 감소낮음
3predicate_filter — 불필요한 reconcile 제거reconcile 호출 횟수 감소낮음 (predicate 조합 주의)
4metadata_watcher — spec/status 수신 생략메모리 사용량 감소중간 (reconciler에서 전체 객체 필요 시 get 필요)
5reflector 정리 — .modify()로 불필요한 필드 제거Store 메모리 감소낮음
6reconciler 튜닝 — debounce, concurrency, 캐시 사용API 호출 감소, 처리량 조절낮음
7샤딩 — 네임스페이스/라벨 기반 분배수평 확장높음 (운영 복잡도 증가)

1단계 진단이 가장 중요합니다. 메모리가 문제인지, reconcile 지연이 문제인지, API 서버 throttling이 문제인지에 따라 접근이 달라집니다. RUST_LOG=kube=debug로 로그를 확인하고, 모니터링의 메트릭으로 reconcile 횟수와 소요 시간을 측정합니다. 메모리가 의심되면 jemalloc 프로파일링으로 Store 크기를 확인합니다. 증상별 진단은 트러블슈팅을 참고합니다.

Watcher 최적화​

Watch 범위 축소​

label selector와 field selector로 API 서버가 필터링하게 합니다. 네트워크 트래픽과 메모리를 모두 절약합니다.

use kube::runtime::watcher;

let wc = watcher::Config::default()
.labels("app=myapp") // label selector
.fields("metadata.name=specific-one"); // field selector

metadata_watcher​

spec과 status가 필요 없고 메타데이터만 필요한 경우 metadata_watcher()를 사용합니다. PartialObjectMeta만 수신하므로 메모리 사용량이 크게 줄어듭니다.

use kube::runtime::watcher::metadata_watcher;
use kube::core::PartialObjectMeta;

let stream = metadata_watcher(api, wc).default_backoff();

큰 spec을 가진 리소스(Secret, ConfigMap 등)에서 메모리 절감이 큽니다. 단, reconciler에서 전체 객체가 필요하면 별도 get() 호출이 필요합니다.

StreamingList​

Watcher state machine에서 다룬 StreamingList 전략을 사용하면 initial list 로드 시 메모리 피크를 낮출 수 있습니다.

let wc = watcher::Config::default().streaming_lists();

Kubernetes 1.27 이상이 필요합니다. LIST 대신 WATCH로 initial list을 스트리밍하므로 전체 목록을 한 번에 메모리에 올리지 않습니다.

page_size 조절​

기본 page_size는 500입니다 (client-go와 동일).

클러스터 규모권장이유
소규모 (수백 개)더 크게 (1000+)API 호출 수 감소
대규모 (수만 개)더 작게 (100~300)메모리 피크 감소
let wc = watcher::Config::default().page_size(100);

Reflector 최적화​

불필요한 필드 제거​

Reflector와 Store에 캐시되는 객체에서 불필요한 필드를 제거하면 메모리를 절약합니다.

use kube::runtime::WatchStreamExt;

let stream = watcher(api, wc)
.default_backoff()
.modify(|obj| {
// managedFields 제거 — 상당한 메모리 절약
obj.managed_fields_mut().clear();
// last-applied-configuration annotation — SSA 이전 방식의 큰 annotation
obj.annotations_mut()
.remove("kubectl.kubernetes.io/last-applied-configuration");
});
modify는 Store에 저장되기 전에 적용됩니다

modify로 제거한 필드는 reconciler에서도 접근할 수 없습니다. reconciler에서 필요한 필드는 제거하지 않도록 주의합니다.

메모리 추정​

Store에 캐시된 객체 수와 평균 크기로 메모리를 추정합니다:

항목계산
기본 사용량객체 수 x 평균 크기
re-list 스파이크old store + new buffer + 스트림 버퍼 = 최대 2~3배

jemalloc과 MALLOC_CONF="prof:true"로 힙 프로파일링을 하면 실제 메모리 사용 패턴을 확인할 수 있습니다.

Reconciler 최적화​

불필요한 reconcile 방지​

Reconciler 패턴에서 다룬 것처럼, status 변경으로 인한 자기 trigger를 방지합니다.

use kube::runtime::{predicates, watcher, WatchStreamExt};
use kube::runtime::utils::predicate::PredicateConfig;

// watcher 스트림에 predicate_filter를 적용한 후 Controller에 주입
let (reader, writer) = reflector::store();
let stream = reflector(writer, watcher(api.clone(), wc))
.applied_objects()
.predicate_filter(predicates::generation, PredicateConfig::default());

Controller::for_stream(stream, reader)

status만 변경된 이벤트는 generation이 바뀌지 않으므로 필터링됩니다. finalizer를 사용한다면 predicates::generation.combine(predicates::finalizers)로 조합합니다.

predicate_filter는 스트림 메서드입니다

predicate_filter()는 Controller의 메서드가 아니라 WatchStreamExt trait의 메서드입니다. for_stream()과 함께 사용해야 합니다.

debounce​

짧은 시간 내 동일 객체에 대한 중복 trigger를 흡수합니다.

use kube::runtime::Config;

Controller::new(api, wc)
.with_config(Config::default().debounce(Duration::from_secs(1)))

Deployment 업데이트 시 여러 ReplicaSet 이벤트가 연쇄적으로 발생하는 경우에 유용합니다.

concurrency 제한​

Controller::new(api, wc)
.with_config(Config::default().concurrency(10))
설정동작
0 (기본)제한 없음
N최대 N개 동시 reconcile

API 서버 부하를 제어하려면 적절한 값을 설정합니다. 같은 객체에 대한 동시 reconcile은 Controller 파이프라인에서 Runner가 자동으로 방지합니다.

reconciler 내부 최적화​

async fn reconcile(obj: Arc<MyResource>, ctx: Arc<Context>) -> Result<Action, Error> {
// 1. Store에서 읽기 (API 호출 대신 캐시 활용)
let related = ctx.store.get(&ObjectRef::new("related-name").within("ns"));

// 2. 변경 필요 없으면 patch 건너뛰기
let current_cm = cm_api.get("my-cm").await?;
if current_cm.data == desired_cm.data {
// patch 불필요 → API 호출 절약
} else {
cm_api.patch("my-cm", &pp, &patch).await?;
}

// 3. 독립적인 API 호출 병렬화
let (secret, service) = tokio::try_join!(
secret_api.get("my-secret"),
svc_api.get("my-service"),
)?;

Ok(Action::requeue(Duration::from_secs(300)))
}

대규모 클러스터 고려사항​

네임스페이스 분리​

클러스터 전체 대신 특정 네임스페이스만 watch하면 부하를 크게 줄일 수 있습니다.

// 클러스터 전체 (부하 높음)
let api = Api::<MyResource>::all(client.clone());

// 특정 네임스페이스만 (부하 낮음)
let api = Api::<MyResource>::namespaced(client.clone(), "target-ns");

여러 네임스페이스를 처리해야 하면 네임스페이스별 Controller 인스턴스를 실행할 수 있습니다.

re-list 메모리 스파이크​

객체 수평균 크기기본 메모리re-list 피크
1,00010KB10MB~30MB
10,00010KB100MB~300MB
100,00010KB1GB~3GB

완화 방법:

  • StreamingList로 피크 감소
  • metadata_watcher()로 객체 크기 축소
  • .modify()로 불필요한 필드 제거
  • label selector로 대상 축소

API 서버 부하​

owns()와 watches()를 추가할 때마다 별도 watch 연결이 생깁니다. 각 watch는 API 서버와 지속적인 HTTP 연결을 유지합니다.

가능하면 unstable-runtime feature의 shared reflector로 여러 컨트롤러가 같은 watch를 공유할 수 있습니다.

Leader election​

HA 배포에서는 여러 인스턴스 중 하나만 active로 동작해야 합니다. leader election의 메커니즘, 서드파티 크레이트, shutdown 연계에 대한 자세한 내용은 가용성에서 다룹니다.

스케일링 전략​

단일 인스턴스의 처리량이 부족할 때의 확장 전략을 다룹니다.

수직 확장​

가장 먼저 시도할 방법입니다. reconcile 자체가 병렬이므로 CPU/메모리를 늘리면 throughput이 증가합니다.

조절 항목효과
CPU request/limit 증가reconciler 동시 실행 수용량 증가
메모리 증가Store 캐시 + re-list 스파이크 수용
Config::concurrency(N) 증가동시 reconcile 수 확장

수직 확장의 한계는 watcher 하나가 처리할 수 있는 이벤트 처리량입니다. watch 연결 하나의 throughput이 병목이면 샤딩으로 전환합니다.

명시적 샤딩​

리소스를 여러 컨트롤러 인스턴스에 분배합니다. 각 인스턴스는 담당 범위만 watch합니다.

네임스페이스별 샤딩​

가장 단순한 방법입니다. 각 인스턴스가 다른 네임스페이스를 담당합니다:

// 환경변수로 담당 네임스페이스 결정
let ns = std::env::var("WATCH_NAMESPACE").unwrap_or("default".into());
let api = Api::<MyResource>::namespaced(client, &ns);

라벨 기반 샤딩​

FluxCD에서 사용하는 패턴입니다. 리소스에 샤드 라벨을 부여하고, 각 인스턴스가 해당 라벨만 watch합니다:

// 샤드별 label selector
let shard_id = std::env::var("SHARD_ID").unwrap_or("0".into());
let wc = watcher::Config::default()
.labels(&format!("controller.example.com/shard={}", shard_id));
전략장점단점
네임스페이스별구현이 간단, 격리가 자연스러움네임스페이스 수에 의존
라벨 기반유연한 분배라벨 관리 필요, 재분배 시 reconcile 중복

각 샤드에 leader election을 조합하면 HA + 수평 확장을 동시에 달성할 수 있습니다. 자세한 내용은 가용성 — Elected Shards를 참고합니다.