원문: https://openkruise.io/blog/learning-concurrent-reconciling/
컨트롤러 관련해서 글을 찾아서 읽어보다가 공유하면 좋을 것 같아 적어봅니다. 저의 개인적인 해석이나 정리, 생략이 포함되어 있습니다.
이 글의 내용에 대한 권리는 저에게 있지 않습니다. 문제가 된다면 삭제하도록 하겠습니다
Concurrent reconciling은 원래 단어의 맛을 해석하기가 어려워서... 부득이하게 원문을 사용하겠습니다.
goroutine의 concurrency + k8s의 reconcile ? ㅎㅎ..
3줄 요약:
1. 쿠버네티스 컨트롤러는 concurrent reconciling를 통해 큐에 쌓이는 요청을 빠르게 소모할 수 있다.
2. 동시성 기능으로 인해 발생할 수 있는 side-effect는 client-go
의 workqueue
에 의해 방지된다.
3. 하지만, 이 방지 로직으로 인해 concurrent reconciling을 사용하지 않을 경우 성능 저하가 발생할 수도 있다. (concurrent reconciling을 쓰면 쉽게 해결된다.)
쿠버네티스의 컨트롤러(controller) 컨셉은 쿠버네티스가 성공할 수 있었던 가장 큰 요인 중 하나입니다. 컨트롤러는 쿠버네티스 API가 시스템을 원하는 상태에 다다를 수 있도록 도와주는 코어 메커니즘입니다. CRD, 컨트롤러, 오퍼레이터를 통해서 다른 시스템이 쿠버네티스에 녹아드는 것이 쉬워졌습니다.
많은 개발자들이 자신이 원하는 컨트롤러를 만들기 위해 컨트롤러 런타임 라이브러리와 그 라이브러리를 사용하는 컨트롤러 개발 툴인 KubeBuilder 를 사용하고 있습니다. Kruise 프로젝트는 Kubebuilder를 통해 컨트롤러를 개발하고 있습니다. 이 포스트에서, Kruise 컨트롤러를 개발하면서 느낀점을 공유하려고 합니다. (특히 concurrent reconciling 부분에 있어서요!)
몇몇 독자분들은 이미 컨트롤러가 concurrent reconciling(동시적인 조정)을 지원하는것을 알고 있을겁니다. 컨트롤러를 생성할 때 사용할 수 있는 옵션을 확인해보세요. (source)
type Options struct {
// MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run. Defaults to 1.
MaxConcurrentReconciles int
// Reconciler reconciles an object
Reconciler reconcile.Reconciler
}
Concurrent reconciling은 컨트롤러가 바라보는 오브젝트의 상태가 수시로 바뀌어서 너무 많은 조정 요청(reconcile request)이 큐에 쌓일 경우에 유용합니다. Concurrent reconciling을 사용하면, 말 그대로 여러개의 reconcile 루프을 통해 reconcile 큐를 더 빠르게 소진할 수 있습니다. 이 방법은 따로 코드에 대단한 변화를 주지 않고도 퍼포먼스 향상에 상당한 기여를 할 수 있지만, 동시에 동시성(Concurrency)으로 인해 발생하는 문제들에 대해 고민하게 됩니다.
위의 예시에 대한 정답은 아니오
입니다. 이 “마법"은 client-go
라이브러리에 구현된 workqueue
를 통해 발현됩니다. (이 workqueue
는 컨트롤러의 reconcile queue로 사용됩니다!)
이 workqueue의 알고리즘은 다음과 같습니다.
우선 workqueue의 구성부터 알아볼까요?
workqueue는
dirty-set
, processing-set
)그림 (a)는 4개의 조정 요청을 처리하기 위한 초기 상태를 나타냅니다.
dirty set
에 존재하는지 확인합니다. 만약 없다면, dirty set
에 오브젝트를 넣고, 이미 존재하면 요청 자체를 drop 합니다.processing set
에 조정 대상인 오브젝트가 존재하지 않을 때 queue
에 오브젝트를 집어넣습니다.그림 (b)는 3개의 요청이 순차적으로 추가된 케이스를 나타냅니다.
조정 루프가 요청을 처리할 수 있는 상태가 되면, 조정 루프는 queue 의 front에서 대상 오브젝트를 가져옵니다.
이 때,
processing set
에 들어가고, dirty set
에서 제거됩니다. 그림 (c)에서는 이미 processing set
에 존재하는 오브젝트에 대한 요청이 들어오는 케이스입니다.
이 경우에
dirty set
에는 추가되지만queue
에는 추가되지 않습니다.그림 (d)는 reconcile loop에 의해 조정이 끝난 케이스 입니다.
reconcile이 종료되면,
processing set
에서 제거됩니다. dirty set
에 해당 오브젝트가 존재하면 queue
에 추가됩니다.위의 알고리즘은 다음과 같은 결과를 가져옵니다
1. 동일한 오브젝트에 대한 concurrent reconciling을 회피한다.
2. 오브젝트 처리 순서는 워킹 스레드가 하나일지라도 도착 순서에 따라 달라질 수 있다. (도착한 순서대로 처리하지 않는다는 뜻입니다.)
예를 들면,
첫 요청에 의해 오브젝트 A가 dirty set
에 들어가고, queue
에 추가됩니다.
만약 reconcile 과정이 오래걸리고, 그 동안 다른 오브젝트에 대한 요청이 많이 들어왔다면 queue
는 다른 오브젝트로 가득차게 됩니다.
첫 번째 A 오브젝트에 대한 처리가 끝나고, dirty set
에 추가되었던 두번째 A 조정 요청이 queue
의 마지막으로 추가됩니다.
이는 곧 다른 오브젝트에 대한 요청들을 모두 처리하기 전에는 두 번째 A 요청을 처리할 수 없다는 뜻입니다.
이를 해결할 수 있는 방법은 Concurrent Reconcile을 사용하는 것입니다. 따라서 MaxConcurrentReonciles
값은 항상 1보다 큰 값으로 덮어 써야 합니다.
dirty set
에 있을 경우 드랍된다는 것입니다. 이는 컨트롤러가 모든 오브젝트의 스테이트 변화를 대응하지는 않는다는 것을 의미합니다. 쿠버네티스 컨트롤러는 level-triggered
이지, edge-triggered
가 아닙니다. 즉, state를 reconcile하지, event를 reconcile 하지 않는다는 것입니다.