A deep dive into Kubernetes controller

2020. 11. 27. 15:07Backend

Backgrounds

  • 쿠버네티스에서 컨트롤러는 desired state 와 observed state 의 sync 를 담당합니다.
  • 끊임없이 API 서버를 watch 하며 변화를 감지합니다.
  • 예로써 Resplica Set controller 가 있습니다. 이는 desired # of pod 이 변경되는 경우, pods 의 수를 조절해줍니다.
  • 쿠버네티스에서 각각의 컨트롤러는 특정 리소스 하나에 책임을 가집니다.
  • Own Custom Controller 또한 존재합니다.

Controller pattern

  • 컨트롤러 페턴에 대한 k8s official documentation 은 아래와 같습니다.

In applications of robotics and automation, a control loop is a non-terminating loop that regulates the state of the system. In Kubernetes, a controller is a control loop that watches the shared state of the cluster through the API server and makes changes attempting to move the current state towards the desired state. Examples of controllers that ship with Kubernetes today are the replication controller, endpoints controller, namespace controller, and serviceaccounts controller.
Kubernetes official documentation, Kube-controller-manager

  • 컨트롤러는 쉽게 말하면, 끝나지 않는 loop 를 이용해 끊임 없이 desired 와 current 의 status 변화를 sync 하는 task 들입니다.
  • 그 예로써, Replication Controller, Endpoints Controller, Namespace Controller, ServiceAccount Controller 등이 있습니다.
  • 많은 컨트롤러들이 존재하기 때문에 이를 Simplify 하기 위해, 하나의 single daemon package 가 존재하는데요.
    이를 kube-controller-manager 라 합니다.

 

이를 go 코드로 표현해보면,

for {
  desired := getDesiredState()
  current := getCurrentState()
  makeChanges(desired, current)
}

Controller components

쿠버네티스 컨트롤러에는 두개의 주 컴포넌트가 있습니다.

  1. Informer/SharedInformer
    Informer/SharedInformer 는 현재 쿠버네티스 object 의 상태를 계속 주시합니다. 그리고 이벤트를 workqueue 로 발행합니다. 그러면 worker processes 들이 workqueue 를 pop 하여 이벤트를 처리합니다. 
  2. WorkQueue

Informer in detail

  • cache exists for client-go library
  • controller also cares event related to 'CUD'
  • Listwatcher 인터페이스를 제공합니다. 이를 통해 initialize 및 특정 리소스를 watch 할 수 있습니다.
lw := cache.NewListWatchFromClient(
      client,
      &v1.Pod{},
      api.NamespaceAll,
      fieldSelector)

모든 것들은 informer 에서 컨슘됩니다. informer 의 general structure 는 아래와 같습니다.

store, controller := cache.NewInformer {
	&cache.ListWatch{},
	&v1.Pod{},
	resyncPeriod,
	cache.ResourceEventHandlerFuncs{},

실제로 infoermer 는 현재 쿠버네티스에서 많이 사용되지 않습니다( 실제로는 SharedInformer 가 많이 사용됩니다.) 그렇지만 여전히 이해해야할 중요한 개념임엔 틀림없습니다. 특히 커스텀 컨트롤러를 작성하기 위한 개념이 필요하신 분들에게는 필수적이라고 생각됩니다.

 

아래의 3개 요소가 Informer 를 생성할때 사용되는 패턴입니다.

  1. ListWatcher
  2. ResourceEventHandler :
  3. ReSyncPeriod

List Watcher : 특정 namespace 의 특정 리소스를 위한 모든 List Function 과 Watch Function 의 결합체 입니다.

이는 컨트롤러가 특정 리소스에만 집중할 수 있도록 돕습니다. 여기서 field selector 는 매칭되는 특정 필드만 뽑기 위한 filter 입니다.

cache.ListWatch {
	listFunc := func(options metav1.ListOptions) (runtime.Object, error) {
		return client.Get().
			Namespace(namespace).
			Resource(resource).
			VersionedParams(&options, metav1.ParameterCodec).
			FieldsSelectorParam(fieldSelector).
			Do().
			Get()
	}
	watchFunc := func(options metav1.ListOptions) (watch.Interface, error) {
		options.Watch = true
		return client.Get().
			Namespace(namespace).
			Resource(resource).
			VersionedParams(&options, metav1.ParameterCodec).
			FieldsSelectorParam(fieldSelector).
			Watch()
	}
}

Resource Event Handler : 컨트롤러가 특정 리소스으 변화를 위한 notification 을 처리합니다

type ResourceEventHandlerFuncs struct {
	AddFunc    func(obj interface{})
	UpdateFunc func(oldObj, newObj interface{})
	DeleteFunc func(obj interface{})
}
  • AddFunc: 새 리소스가 생성될때 호출되는 callback.
  • UpdateFunc: 리소스가 업데이트 될때 호출되는 callback, 또한 re-synchronization 이 일어나는 경우 실제로 변화가 없더라도 호출된다.
  • DeleteFunc: 리소스가 지워질때 호출되는 callback

Resync Period

  •  얼마나 자주 컨트롤러가 cache 를 fire 하고 다시 UpdateFunc 를 호출 하는가 에 대한 설정입니다.
  • Periodically
  • 커스텀 컨트롤러를 구현하는 경우, 값이 아주 짧은 경우 CPU load 에 유의해야 한다.

Shared Informer

위에서 다룬 Informer 역시도 자체 localcache 를 생성하여 여러 리소스들을 다루지만, 현실 쿠버네티스에서는 특정 리소스를 하나 이상의 컨트롤러가 Share 해야 하는 경우가 발생합니다. 

 

이러한 경우 SharedInformer 가 여러 컨트롤러를 위한 하나의 shared cache 를 생성합니다. 즉 케싱된 리소스는 중복이 없습니다. 그러므로 메모리 오버헤드도 좀 줄게 됩니다. 

각각의 SharedInformer 는 다운스트림 컨슈머의 수와 관계없이 upstream 서버로의 하나의 watch 만 생성합니다. 이것은 또한 upstream  서버가 받는 부하를 줄여줄 수 있습니다. 

 

SharedInformer 이미 특정리소스에 대한add, update, delete hook 을 가지고 있습니다.   

lw := cache.NewListWatchFromClient(…)
sharedInformer := cache.NewSharedInformer(lw, &api.Pod{}, resyncPeriod)

Workqueue

SharedInformer 은  shared 이기 때문에 각각의  controller 를 tracking 하지 못합니다, 그래서 컨트롤러는 각각의 queuing 과 retrying 메커니즘을  제공해야 하는데요.  그래서 대부분의 리소스 이벤트 핸들러는 단순히 workqueue 에 이벤트를 넣습니다.

 

resource 변경 이벤트가 발생할때마다 리소스 이벤트 핸들러는 이벤트를 key'<resource_namespace>/<resource_name>' 형태로 workqueue 에 넣습니다.

그러면 각각의 컨슈머는  순차적으로 이를 처리합니다. 또한 두 워커가 동일한 키를 동시에 처리하지 않음은 보장됩니다.

또한 ratelimiting 이 supporting 됩니다.

queue :=
workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())

workqueue workflow

질문: 언제 컨트롤러가 큐에서 프로세싱을 시작해야 할까?

controller.informer = cache.NewSharedInformer(...)
controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())

controller.informer.Run(stopCh)

if !cache.WaitForCacheSync(stopCh, controller.HasSynched)
{
	log.Errorf("Timed out waiting for caches to sync"))
}

// Now start processing
controller.runWorker()

 

'Backend' 카테고리의 다른 글

Nginx deep dive  (0) 2020.04.09
Code review (Worklog)  (0) 2020.04.09
Apache Avro  (0) 2019.11.13
Abstract Factory pattern  (0) 2019.11.12
State pattern VS strategy pattern, what is the difference?  (0) 2019.11.10