ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kubernetes Controller 작성의 핵심 개념
    Kubernetes 2025. 3. 2. 22:43
    728x90

    Kubernetes 컨트롤러는 특정 리소스의 원하는 상태(Desired State)현재 상태(Current State) 를 비교하고,
    이를 동기화(Sync)하는 역할을 합니다.

     

    1️⃣ Kubernetes 컨트롤러의 기본 개념

    컨트롤러는 반복적으로 현재 상태를 확인하고, 원하는 상태로 변경하는 작업을 수행하는 과정입니다.

    기본 컨트롤러 루프

    for {
      desired := getDesiredState()   // 원하는 상태 가져오기
      current := getCurrentState()   // 현재 상태 가져오기
      makeChanges(desired, current)  // 현재 상태를 원하는 상태로 변경
    }

     

     

    • Kubernetes 컨트롤러는 실시간으로 이벤트를 감지하여 상태를 조정하는 역할을 합니다.
    • Informer를 활용하여 최적화할 수 있습니다.

    2️⃣ 컨트롤러 작성 시 주요 원칙

    1. 한 번에 하나의 리소스만 처리

    • 여러 개의 워커(worker)가 동시에 실행되지만, 동일한 리소스를 중복 처리하지 않도록 보장해야 합니다.
    • WorkQueue를 사용하면 한 번에 하나의 리소스만 처리할 수 있습니다.
    queue workqueue.RateLimitingInterface

     

    2. 여러 리소스를 감지하되, 최적화하여 처리

    • 컨트롤러는 여러 리소스(Pod, Deployment 등)의 변경을 감지해야 합니다.
    • 하지만 직접적으로 모든 리소스를 감지하는 것이 아니라, 특정 연관 관계에 따라 큐에 추가하는 방식으로 최적화해야 합니다.
      • 예: ReplicaSet 컨트롤러는 Pod 삭제 이벤트를 감지하지만, 삭제된 Pod의 ReplicaSet만 큐에 추가하여 처리.

    3. 이벤트 순서가 보장되지 않음

    • 여러 리소스에서 변경이 발생할 때, 이벤트 순서는 보장되지 않습니다.
    • 따라서 컨트롤러는 항상 현재 상태를 조회한 후, 원하는 상태와 비교하여 동작해야 합니다.

    4. Edge-driven이 아닌 Level-driven 방식 사용

    • 컨트롤러는 특정 이벤트(예: "true → false" 변경)만 감지하는 것이 아니라, 현재 상태를 지속적으로 확인하는 방식(level-driven)으로 동작해야 합니다.
    • API 서버가 중간 이벤트를 놓치더라도, 현재 상태만 확인하면 항상 일관된 동작을 수행할 수 있도록 구현해야 합니다.

    3️⃣ SharedInformer 사용 권장

    • SharedInformer를 사용하여 이벤트(추가, 업데이트, 삭제) 감지
    • 캐시를 공유하여 API 서버 부하 감소
    • factory.go의 Informer Factory를 사용하면 캐시를 여러 컨트롤러가 공유할 수 있음.
    deploymentInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
    	AddFunc: controller.enqueueFoo,
    	UpdateFunc: func(old, new interface{}) { controller.enqueueFoo(new) },
    	DeleteFunc: controller.handleDeletedFoo,
    })

     

    SharedInformer를 사용하면:

    1. API 서버의 부하를 줄일 수 있음 (캐시 공유).
    2. 여러 컨트롤러가 같은 데이터를 사용할 수 있음.
    3. 기존 Reflector, DeltaFIFO 방식보다 성능이 개선됨.

     

    4️⃣ 원본 객체 수정 금지

    • Kubernetes 캐시는 여러 컨트롤러가 공유합니다.
    • 따라서 원본 객체를 직접 수정하면 다른 컨트롤러가 비정상적으로 동작할 수 있음.
    • 항상 Deep Copy를 사용하여 수정해야 함.
    fooCopy := foo.DeepCopy()
    fooCopy.Status.AvailableReplicas = deployment.Status.AvailableReplicas
    _, err := c.sampleclientset.SamplecontrollerV1alpha1().Foos(foo.Namespace).UpdateStatus(ctx, fooCopy, metav1.UpdateOptions{})

     

    5️⃣ WaitForCacheSync를 활용하여 캐시 동기화 보장

    • 컨트롤러가 시작되기 전에 캐시가 최신 상태인지 확인해야 함.
    • WaitForCacheSync()를 사용하여, 모든 필요한 리소스가 동기화될 때까지 기다릴 수 있음.
    if !cache.WaitForCacheSync(stopCh, c.podStoreSynced) {
    	return
    }

    이렇게 하면 컨트롤러가 구식 데이터를 사용하여 잘못된 결정을 내리는 것을 방지할 수 있음.

     

    6️⃣ 다른 컨트롤러 및 프로세스와의 상호작용 고려

    • Kubernetes는 여러 컨트롤러가 동작하는 분산 시스템이므로, 자신이 수정하지 않은 리소스도 변경될 수 있음을 항상 고려해야 함.
    • 예를 들어, Garbage Collector가 리소스를 삭제할 수도 있음.
    • Owner References를 설정하여 리소스를 자동 정리하도록 만들 수도 있음.

    7️⃣ 컨트롤러에서 에러 처리 및 재시도 (Requeue)

    • 실패한 작업을 다시 시도할 수 있도록 Rate-Limited Requeue 메커니즘을 사용해야 합니다.
    • WorkQueue를 활용하여 실패한 작업을 자동으로 재시도.
    // 실패한 작업을 다시 큐에 추가하여 재시도
    c.queue.AddRateLimited(key)

     

    • 에러가 발생하면 상위로 에러를 전파해야 하며, 정상 처리된 경우는 큐에서 제거해야 합니다.
    err := c.syncHandler(key.(string))
    if err == nil {
    	c.queue.Forget(key) // 정상 처리된 경우 큐에서 제거
    	return true
    }

     

    즉, 컨트롤러는 작업이 실패하면 백오프(지수적 증가) 방식으로 다시 시도해야 함.

     

    8️⃣ ObservedGeneration을 사용하여 컨트롤러가 처리한 상태 표시

    • ObservedGeneration 필드를 사용하여 컨트롤러가 리소스를 처리했음을 API 서버에 기록할 수 있음.
    if foo.Status.ObservedGeneration != foo.Generation {
    	foo.Status.ObservedGeneration = foo.Generation
    }

    이렇게 하면 사용자는 컨트롤러가 최신 변경 사항을 반영했는지 쉽게 확인할 수 있음.

     

    9️⃣ Owner References 활용하여 자식 리소스 자동 삭제

    • ReplicaSet이 Pod를 관리하듯이, OwnerReferences를 설정하면 부모 리소스 삭제 시 자식 리소스도 자동 삭제됨.
    ObjectMeta: metav1.ObjectMeta{
    	OwnerReferences: []metav1.OwnerReference{
    		*metav1.NewControllerRef(foo, samplev1alpha1.SchemeGroupVersion.WithKind("Foo")),
    	},
    }

     

    이렇게 하면 부모 리소스가 삭제될 때 자동으로 관련된 리소스도 삭제됨.

     

    컨트롤러의 전체적인 구조

    type Controller struct {
    	podLister cache.StoreToPodLister
    	queue     workqueue.RateLimitingInterface
    }
    
    func (c *Controller) Run(threadiness int, stopCh chan struct{}) {
    	defer utilruntime.HandleCrash()
    	defer c.queue.ShutDown()
    
    	// 캐시 동기화
    	if !framework.WaitForCacheSync(stopCh, c.podStoreSynced) {
    		return
    	}
    
    	// 여러 개의 worker 실행
    	for i := 0; i < threadiness; i++ {
    		go wait.Until(c.runWorker, time.Second, stopCh)
    	}
    
    	<-stopCh
    }

    즉, 컨트롤러는 WorkQueue를 사용하여 리소스를 감시하고, 변경 사항을 큐에 추가한 후, worker가 이를 하나씩 처리하는 구조로 동작함.

     

    결론

    컨트롤러 작성 시 핵심 원칙

    1. WorkQueue를 사용하여 동일한 리소스를 중복 처리하지 않도록 보장.
    2. SharedInformer를 사용하여 API 서버 부하를 최소화.
    3. 원본 객체를 수정하지 않고, 항상 Deep Copy를 사용.
    4. 캐시 동기화 (WaitForCacheSync) 후 컨트롤러 실행.
    5. 에러 발생 시 작업을 Requeue하여 재시도.
    6. OwnerReferences를 설정하여 관련 리소스를 자동 정리.
    7. ObservedGeneration을 설정하여 컨트롤러가 최신 변경 사항을 반영했는지 확인.

    이러한 원칙을 따르면 Kubernetes 컨트롤러가 안정적으로 동작하며, 성능이 최적화된 구조로 운영될 수 있음.

     

    728x90
Designed by Tistory.