커스텀 컨트롤러를 개발할일이 가끔가다 생기는데 이번에 새로 커스텀 컨트롤러로 개발해보고싶은게 생겼습니다. 만들어보기 전에 컨트롤러 동작방식 대해 살펴보고 개발을 해볼까 합니다. 오늘은 컨트롤러의 동작방식을 이해하기 위한 글 입니다.
오퍼레이터 패턴이란
오퍼레이터 패턴, 혹은 오퍼레이터는 시스템의 현재 상태를 원하는 상태로 유지하기 위한 자동화된 로직을 의미합니다. 즉, 현재 상태와 목표 상태가 있을 때, 이 둘을 자동으로 조정하여 일치시키는 것이 오퍼레이터의 역할입니다. 운영자가 수동으로 하던 작업을 코드로 자동화한 것이라고 생각하면 이해하기 쉽습니다.
예를 들어, 데이터베이스의 백업과 복구 작업을 오퍼레이터를 통해 자동으로 처리할 수 있습니다. CNCF 프로젝트에서는 오퍼레이터 패턴에 대한 백서를 제공하며, 이를 읽어보면 오퍼레이터 개념을 더 깊이 이해할 수 있습니다. 하지만 이 기본적인 정의만 알고 있어도 오퍼레이터 개발에 큰 어려움은 없을 것입니다.
tag-app-delivery/operator-wg/whitepaper/Operator-WhitePaper_v1-0.md at main · cncf/tag-app-delivery
📨🚚CNCF App Delivery TAG. Contribute to cncf/tag-app-delivery development by creating an account on GitHub.
github.com
컨트롤러 개발하기
오퍼레이터는 Kubernetes에서 복잡한 애플리케이션 상태를 관리하는 패턴입니다. Kubernetes를 운영할 때 만나는 다양한 컨트롤러들이 바로 오퍼레이터에 해당합니다.
처음에는 컨트롤러 개발이 매우 어렵게 느껴질 수 있지만, 다행히도 이를 쉽게 도와주는 SDK가 이미 존재합니다. 그중 하나가 Operator SDK로, CLI를 통해 프로젝트 구조를 자동으로 생성해 주는 Kubebuilder가 널리 사용됩니다. 실제 개발 과정에 대한 상세한 내용은 이후에 다룰 예정입니다.
클러스터 상태 확인과 이벤트 처리 방법
controller-runtime을 알아보기 전에, 먼저 컨트롤러가 Kubernetes 리소스를 어떻게 조회하고 이벤트가 발생했을 때 이를 어떻게 처리하는지에 대한 로직을 이해해야 합니다. 아래 그림은 client-go와 컨트롤러의 구성을 혼합하여 추상화한 그림입니다.
client-go는 Kubernetes API와 상호작용하기 위한 Go 언어 클라이언트 라이브러리입니다. 이를 사용하면 Kubernetes 리소스를 조회하고 변경하며, 리소스 상태 변화를 감시할 수 있습니다. 또한, 커스텀 컨트롤러 개발을 쉽게 할 수 있도록 Informer, Lister와 같은 유용한 기능을 제공합니다. 이 글에서는 특히 Informer와 SharedInformer의 역할을 중점적으로 다루겠습니다.
Etcd의 Watcher 인터페이스
컨트롤러의 동작방식에대해 생각해보면 현재의 상태가 바뀌었을때 내가 원하는 상태와 다르다면 두 값을 같게 해야합니다. 이 말은 쿠버네티스에 생성된 리소스들에 변화가 생긴다면 컨트롤러가 감지할 수 있어야 한다는 소리인데요. 어떻게 변경사항을 감지하고 있는지는 etcd와 kube apiserver의 통신 흐름을 보면 알 수 있습니다.
etcd에는 watcher라는 인터페이스가 존재하여, 특정 키 또는 키 범위의 변경을 감지하고 클라이언트에게 알리는 역할을 합니다. kube-apiserver는 이 인터페이스를 사용해 gRPC 프로토콜을 이용하여 etcd와 통신하고, 클러스터 리소스의 변경 사항을 실시간으로 감지하여 컨트롤러와 같은 클라이언트에 전달합니다.
Reflector의 클러스터 정보 가져오기
client-go는 kube-apiserver와 통신하여 Kubernetes 리소스를 조회하고 변경 사항을 감지합니다. 컨트롤러가 처음 실행되었을 때는 LIST 요청을 통해 전체 리소스 목록을 가져오고, 이후에는 WATCH 요청을 통해 리소스의 변경 사항만 실시간으로 받아오는 구조입니다.
이 역할을 담당하는 것이 Reflector입니다. Reflector는 새로운 리소스 정보나 변경 사항을 DeltaFIFO 큐에 넣어주며, 이를 통해 변경된 리소스를 관리합니다. Reflector는 리소스의 resourceVersion을 사용해 새로운 정보인지 판단하고 이를 처리합니다.
Delta
https://github.com/kubernetes/client-go/blob/v12.0.0/tools/cache/delta_fifo.go#L612
// Delta는 DeltaFIFO에 저장되는 타입으로,
// 어떤 변경이 발생했는지와 그 변경 후 객체의 상태를 알려줍니다.
//
// [*] 단, 변경이 삭제인 경우에는 객체가 삭제되기 전의 마지막 상태를 제공합니다.
type Delta struct {
Type DeltaType
Object interface{}
}
DeltaFifo
https://github.com/kubernetes/client-go/blob/v12.0.0/tools/cache/delta_fifo.go#L96
// DeltaFIFO는 FIFO와 유사하지만 두 가지 차이점이 있습니다.
// 하나는 주어진 객체의 키와 연결된 누적기가 해당 객체가 아니라
// 그 객체에 대한 Delta 값의 슬라이스인 Deltas라는 점입니다.
// 객체를 Deltas에 적용하는 것은 Delta를 추가하는 것을 의미합니다.
// 단, 추가하려는 Delta가 삭제(Deleted)이고, 이미 Deltas가 삭제로 끝난 경우에는
// Deltas가 더 커지지 않습니다. 이 경우 이전 삭제의 객체가 DeletedFinalStateUnknown이면
// 마지막 삭제 Delta가 새로운 삭제 Delta로 대체됩니다.
//
// 다른 차이점은 DeltaFIFO에는 누적기에 객체를 적용할 수 있는 두 가지 추가 방법이 있다는 것입니다:
// Replaced와 Sync입니다. EmitDeltaTypeReplaced가 true로 설정되지 않으면,
// 이전 호환성을 위해 교체 이벤트 시 Sync가 사용됩니다.
// Sync는 주기적인 재동기화(resync) 이벤트에 사용됩니다.
//
// DeltaFIFO는 생산자-소비자 큐로, Reflector가 생산자로 사용되며,
// 소비자는 Pop() 메서드를 호출하는 것입니다.
//
// DeltaFIFO는 다음과 같은 사용 사례를 해결합니다:
// * 각 객체 변경(delta)을 최대 한 번씩 처리하고 싶습니다.
// * 객체를 처리할 때 마지막으로 처리한 이후에 발생한 모든 변경 사항을 보고 싶습니다.
// * 일부 객체의 삭제를 처리하고 싶습니다.
// * 주기적으로 객체를 재처리하고 싶을 수 있습니다.
//
// DeltaFIFO의 Pop(), Get(), GetByKey() 메서드는 Store/Queue 인터페이스를 만족하기 위해
// interface{}를 반환하지만 항상 Deltas 타입의 객체를 반환합니다.
// List()는 FIFO에서 각 누적기의 최신 객체를 반환합니다.
//
// DeltaFIFO의 knownObjects KeyListerGetter는 Store 키를 나열하고
// Store 키로 객체를 가져오는 기능을 제공합니다.
// 해당 객체는 "알려진 객체"라고 하며, 이 객체 집합은
// Delete, Replace 및 Resync 메서드의 동작을 수정합니다 (각각 다른 방식으로).
//
// 스레딩 관련 주의 사항: 여러 스레드에서 Pop()을 병렬로 호출하면
// 약간 다른 버전의 동일한 객체를 여러 스레드에서 처리할 수 있습니다.
type DeltaFIFO struct {
// lock/cond는 'items'와 'queue'에 대한 접근을 보호합니다.
lock sync.RWMutex
cond sync.Cond
// items는 키를 Deltas에 매핑합니다.
// 각 Deltas는 최소 하나의 Delta를 가집니다.
items map[string]Deltas
// queue는 Pop()에서 소비하기 위한 FIFO 순서로 키를 유지합니다.
// queue에는 중복이 없습니다.
// 키는 items에 있는 경우에만 queue에 있습니다.
queue []string
// populated는 Replace()에 의해 삽입된 첫 번째 배치 항목이 채워졌거나
// Delete/Add/Update/AddIfNotPresent가 먼저 호출되었는지를 나타냅니다.
populated bool
// initialPopulationCount는 Replace()의 첫 번째 호출로 삽입된 항목의 수입니다.
initialPopulationCount int
// keyFunc는 큐에 삽입 및 검색을 위한 키를 만드는 데 사용되며, 결정적이어야 합니다.
keyFunc KeyFunc
// knownObjects는 "알려진" 키 목록으로, Delete(), Replace(), Resync()에 영향을 미칩니다.
knownObjects KeyListerGetter
// 큐가 닫혔는지 여부를 나타내며, 큐가 비어있을 때 컨트롤 루프가 종료할 수 있게 합니다.
// 현재로서는 CRUD 작업을 제어하는 데 사용되지 않습니다.
closed bool
// emitDeltaTypeReplaced는 Replace()가 호출될 때 Replaced 또는 Sync DeltaType을 방출할지 여부를 결정합니다
// (이전 버전 호환성을 유지하기 위함).
emitDeltaTypeReplaced bool
}
Informer
Informer는 DeltaFIFO 큐에서 변경된 오브젝트를 가져와 로컬 캐시 저장소인 Indexer에 저장하고, 적절한 EventHandler를 호출하는 역할을 합니다. 이를 통해 컨트롤러는 계속해서 kube-apiserver에 요청하지 않고, 로컬 캐시에서 리소스를 조회하여 시스템 부하를 줄일 수 있습니다.
Informer는 Kubernetes 리소스의 변경 사항을 감시하고 이를 이벤트로 처리하는 역할을 하지만, 동일한 리소스를 여러 컨트롤러가 필요로 하는 경우에는 SharedInformer를 사용해 캐시를 공유할 수 있습니다. 이를 통해 성능을 개선하고 API 서버에 대한 요청을 줄일 수 있습니다.
SharedInformer
SharedInformer는 여러 컨트롤러가 동일한 리소스 정보를 필요로 할 때 캐시를 공유하여 Kubernetes API 서버에 대한 요청 부하를 줄이고, 리소스 조회 성능을 향상시킵니다. 예를 들어, ReplicaSet 컨트롤러는 Pod 리소스를 관리하고, Job 컨트롤러 또한 Pod 리소스를 사용하여 작업을 처리합니다. 이 두 컨트롤러는 모두 Pod 리소스 정보를 필요로 하기 때문에, 각각이 API 서버에 독립적으로 요청하면 불필요한 중복이 발생합니다.
하지만 SharedInformer를 사용하면 두 컨트롤러가 동일한 Pod 리소스 정보를 공유된 캐시에서 조회할 수 있습니다. 이렇게 캐시를 공유하면 API 서버에 대한 요청을 줄일 수 있으며, 성능을 크게 향상시킬 수 있습니다. 결과적으로 같은 리소스를 사용하는 여러 컨트롤러가 동일한 캐시를 공유하여 효율적으로 Kubernetes 리소스를 관리할 수 있습니다.
Indexer
Indexer는 캐시된 리소스를 인덱싱하여, 컨트롤러가 특정 리소스를 빠르게 조회할 수 있도록 돕는 구성 요소입니다. Informer가 새로운 오브젝트 정보를 전달하면, Indexer는 resourceVersion을 확인하여 이를 저장하거나 업데이트합니다.
이 인덱싱된 데이터는 컨트롤러가 특정 리소스를 빠르게 조회할 수 있도록 하며, 이를 통해 컨트롤러는 리소스 상태에 대한 변경 사항을 효율적으로 처리할 수 있습니다. 이 캐싱 구조는 ThreadSafeStore로 구현되어 동시성 문제 없이 안전하게 동작합니다.
ThreadSafeStore
ThreadSafeStore는 Kubernetes에서 다중 스레드 환경에서도 안전하게 데이터를 저장하고 조회할 수 있도록 해주는 자료 구조입니다. Kubernetes는 여러 리소스에 대한 데이터를 캐시로 관리하는데, 이러한 캐시에서 여러 스레드가 동시에 데이터를 읽고 쓰는 상황이 발생할 수 있습니다. 이때 동시성 문제가 발생하지 않도록 데이터를 안전하게 관리하는 것이 중요한데, ThreadSafeStore가 이러한 역할을 수행합니다.
ThreadSafeStore는 내부적으로 락(lock) 메커니즘을 사용해 여러 스레드가 동일한 데이터를 동시에 읽고 쓰더라도 데이터 무결성을 유지합니다. 이를 통해 Kubernetes 컨트롤러는 데이터를 캐싱할 때, 성능 저하 없이 안정적으로 데이터를 관리할 수 있습니다.
예를 들어, 여러 컨트롤러가 동시에 리소스 정보를 업데이트하거나 조회할 때, ThreadSafeStore는 이러한 작업을 안전하게 처리하여 충돌을 방지하고, 최신 데이터를 유지할 수 있도록 합니다. 따라서, Kubernetes의 SharedInformer와 같은 구조에서 안전하게 데이터를 캐시하고 활용하는 중요한 요소입니다.
'K8S > ecosystem' 카테고리의 다른 글
Kubernetes Controller에서 Owns와 Watches의 차이점 (0) | 2024.10.25 |
---|---|
kind: 다른 네트워크 대역의 클러스터 2개 만들기 (0) | 2024.10.05 |
쿠버네티스 Metric Server 설치 및 확인 방법 (0) | 2024.09.24 |
Kubernetes KEDA: 이벤트 기반 자동 확장 플랫폼 (0) | 2024.09.24 |