공부하고 기록하는, 경제학과 출신 개발자의 노트

학습일지/kubernetes

KubeCon2023 - Building Better Controllers

inspirit941 2024. 8. 11. 15:48
반응형

https://youtu.be/GKPBQDJ2Hjk?si=DMB5DbmgD64ohT6I

 

발표자: John Howard

  • working on istio for about 5 years, mostly on control plane.
  • istio라는 big Control plane 개발하고 관리하면서 겪었던 controller 이슈를 어떤 식으로 해결했는지 소개.

What is Controller?

스크린샷 2024-08-12 오전 9 42 53스크린샷 2024-08-12 오전 9 42 59

 

Controller를 거칠게 정의하면 'input을 넣었을 때, 대응되는 output을 내뱉는 것'

  • Deployment yaml을 배포하면, Replicaset이 만들어진다.
  • Replicaset이 배포되면, pod가 만들어진다.
  • pod가 만들어지면, service에서 pod 생성이벤트 감지하고 적절한 endpoint를 배포한다.
  • Third Party controller가 붙으면, 다른 커스텀 작업도 수행 가능하다. Certificate 관리라던가..
  • ...

Problems

스크린샷 2024-08-12 오전 9 44 43

controller를 잘 만드는 작업은 진짜 어렵다. 가장 큰 이유로는 Very Low Level 로직이기 때문.

  • Main Primitive는 k8s 이벤트를 subscribing할 수 있는 Informer. (더 low level도 있지만, 요걸 메인으로 보자.)

스크린샷 2024-08-12 오전 9 44 50

 

Informer 말만 들으면 간단해보이지만, 이벤트로 발생할 수 있는 모든 case를 정의해야 하기에 생각보다 까다롭다.

  • 진짜 간단한 비즈니스 로직에도 edge case handing 다 하다보면 400줄이 넘어간다.
    • 특정 에러가 발생하는 시점 / 해당 시점에 필요한 에러 핸들링 로직을 순서에 맞게 작성해야 함.
    • controller 코드 작성하고, 수정할 때마다 거의 한 번씩은 버그를 마주한다.
  • 예시: https://github.com/kubernetes/sample-controller/blob/master/controller.go

스크린샷 2024-08-12 오전 9 54 05

 

이 문제 때문에 제보된 버그 / 해결된 것들.

  • istio: 특정 instance handler로 queue 쓰는 게 누락됨 (out of order 문제 발생)
  • istio: crd watcher에서 특정 edge case 로직이 누락됨
  • k8s core에서도 버그가 제보됨. k8s core 개발자들이 k8s 숙련도가 부족해서 생긴 버그라고 보기는 어렵다.
  • k8s scheduling 쪽 버그

Example: istio Service Mesh

스크린샷 2024-08-12 오전 9 57 08

 

istio라는 service mesh 구조를 간단히 요약하면

  • Envoy Proxy를 pod에 같이 배포하고
  • proxy configuration은 동적으로 변경될 수 있다.
  • 따라서 Control Plane은, 동적으로 바뀌는 configuration을 개별 Envoy Proxy에 전달해야 한다.

스크린샷 2024-08-12 오전 9 59 29

 

istio controller: k8s input을 넣으면, xDS output을 응답하는 것

  • 관리해야 할 Resource Type input 종류가 45개, output으로는 k8s가 아니라 Envoy의 xDS API를 사용함.
    • 관리할 리소스 Scope가 넓고,
    • intermediate state는 persist하게 저장되지 않으며 (Envoy Config 파일 크기가 크면 메모리에 저장할 수도 없다)
    • MB단위 크기로도 만들어지는 Envoy Configuration을 모든 Envoy Proxy에 동적으로 배포해야 함.

스크린샷 2024-08-12 오전 10 02 55

 

그래서, k8s가 구성한 것처럼 multi smaller controller로 동작을 분리함. in-Memory로.

  • small controller가 일종의 intermediate state를 반환
  • 다른 controller가 intermediate state 보고 다른 작업을 수행함
  • ...

이 구조는 ETCD에 read / write 부담이 늘어나는데, MB 단위의 config이 만들어지는 Envoy 결과물을 매번 ETCD에서 관리하기는 쉽지 않음

State Management

스크린샷 2024-08-12 오전 10 06 32

 

istio의 접근법은 아래와 같다.

  • 이벤트를 받는다. (ex. pod 정보가 바뀌었다)
  • 바뀌었다는 pod 조회해서 정보 가져와서 subController의 internal state에 저장한다.
  • output으로 이벤트 반환한다. output받은 쪽에서 정보 조회가 필요하면, subcontroller에 저장된 state를 조회한다.

즉, internal State를 잘 관리해야 함.

문제는, 이 모든 작업이 imperative라는 것.

  • 하나의 리소스에 update가 이루어지면, 해당 리소스 정보가 기록된 모든 indexes / maps 등 모든 internal state 정보를 변경함.
  • 일반적인 k8s Reconcile 방식과는 맞지 않음.

Event Detection

스크린샷 2024-08-12 오전 10 49 48

 

"controller Run at Scale" 에서 문제가 되는 요인.

  • naive controller는 이벤트 변경이 이루어지면, 처음부터 연산을 다시 수행한다. Demo에서는 유효한 접근이지만 not Scalable.
  • input이 바뀌면, 바뀐 부분 때문에 변경된 output 정보만 전달하고 싶다. to do the least amount of work.
    • istio에서 특히 중요한 기능임. output computation이 굉장히 비싸기 때문.
  • k8s에서 지원하는 기능이 아니다보니 직접 만들어야 함.
    • server side apply 기능이 있지만, Deceptively hard to use correctly.
    • 변경사항이 전혀 없더라도 serverside apply k8s API 호출이 필요하다. With Full Object to the API server.

그래서 istio는 필요한 기능을 전부 자체구현했음.

  • lot of istio codebase is all about these random optimization.
  • i.e. autoscaler 관련 annotation의 추가 기능 -> entire chain에 전달되는 데이터 양이 너무 많아서, ignore the annotation 로직을 넣었다.
    • Dangerous Approach. Entire Flow를 아는 사람만 ignore 로직이 어디서 왜 빠졌는지 이해할 수 있다.
    • 수많은 버그의 원인이 될 수 있는 접근법이지만, the opposite is also just as bad. 이런 식의 최적화라도 안 하면, istio는 scale 환경에서 못 쓴다.

스크린샷 2024-08-12 오전 10 58 23

 

아래는 istio의 struct 예시들

  • 다양한 종류의 Map 데이터 타입이 있음.
  • 특정 이벤트가 발생해서 데이터 업데이트가 필요하면, 아래 정의한 map을 전부 업데이트하는 로직이 있고
  • 이 로직을 handleService 등의 메소드로 래핑해서 controller가 이벤트 발생할 때마다 호출함.

복잡도가 높아질 수밖에 없는 구조.

  • pod 하나 바뀌면, 그 pod 변경사항이 모든 controller의 Map struct에 업데이트되어야 하기 때문.
  • 업데이트를 위해서는 recompute all.
type AmbientIndex struct {
  mu sync.RUMutex
  byService map[string]map[string]*model.WorkloadInfo
  byPod map[networkAddress]*model.WorkloadInfo
  byWorkloadEntry map[networkAddress]*model.WorkloadInfo
  byUID map[string]*model.WorkloadInfo
  serviceByAddr map[networkAddress]*model.WorkloadInfo
  waypoints map[model.WaypointScope]*workloadapi.GatewayAddress
  serviceMap map[types.NamespacedName]*apiv1alpha3.ServiceEntry
}

...


// Update indexes
if isDelete {
  for _, networkAddr := range networkAddrs {
    delete(a.serviceByAddr, networkAddr)
  }
  delete(a.byService, namespacedName)
  delete(a.serviceByNamespacedHostname, si.ResourceName())
} else {
    for _, networkAddr := range networkAddrs {
      a.serviceByAddr[networkAddr] = si
    }
    a.byService[namespacedName] = wls
    a.serviceByNamespacedHostname[namespacedName] = si
}

func (a *AmbientIndex) handleService(...){
  pods := c.getPodsInService(svc)
  for _, p := range pods {
    wl := a.generateWorkload(p, c)
    for _, networkAddr := range networkAddressFromWorkload(wl) {
      a.byPod[networkAddr] = wl
    }
    a.byUID[wl.Uid] = wl
    wls[wl.Uid] = wl
  }
}

Goals

스크린샷 2024-08-12 오후 1 20 05스크린샷 2024-08-12 오후 1 16 54

 

Envoy Proxy 관련해서 필요한 로직을 k8s에 Declarative로 선언하도록 만든 건 좋았지만, 내부 로직이 전부 imperative한 상황.

istio Controller를 이런 방식으로 구현하면 Not Sustainable. 그래서 구조를 바꿔야 했다.

  • Easy to Write Correctly / Efficiently.
    • 지금은 이게 불가능할 만큼 복잡하다.
  • High Level로 구현
    • low level의 state change를 직접 코드로 만들지 않는다.
  • Composable
    • building blocks를 조합해서 만들 수 있도록.

Implementation

스크린샷 2024-08-12 오후 1 21 54스크린샷 2024-08-12 오후 1 21 47

 

인터페이스를 간단하게 해보자

  • Collection of Resources. you can Watch / Get / List. (informer와 비슷한)
  • 이걸 여러 controller에서 쉽게 사용할 수 있는 ecosystem

Source: Bunch of ways to get data into this interface.

  • informers
  • files (istio에 필요한 옵션)
  • in-memory object (for test)
  • fetch from External State (Database, Object storage, etc...)

물론, external State의 경우 K8s 내장기능이 아니므로, informer가 없다. Own Abstraction / Own interface로, 직접 만들어야 한다.

Transformation: input 받아서, 어떻게 output을 만들 것인지.

  • index: look up things efficiently
  • transformation: deployment를 replicaset으로 만드는 것처럼, 원하는 output 형태로 변환.
  • Complex composition: service에서 pod 정보 보고 endpoint 만드는 것처럼, 여러 리소스가 얽힌 경우의 로직
    • istio의 경우 10여개가 넘는 리소스 읽어서 one state로 변환해야 함.

Output

  • Write / deploy to k8s
  • Send over xDS
  • Write to Cloud API (Load balancer라던가..)

Collection Create

스크린샷 2024-08-12 오후 1 33 59

 

Configmap 데이터 변경 이벤트를 탐지하는 informer Collection 설정하기.

  • informer 타입을 받는 collection list 정의하고
  • 여러 개의 configmap 정보를 merge.

이렇게 되면, ConfigMap이라는 Collection 객체는 merge로 포함한 configmap 정보가 바뀔 때마다 automatically updated.

  • fetch하라고 명시적으로 코드 쓸 필요 없음.
  • 데이터 업데이트가 아닌 annotation 변경의 경우 config output 변경이 없다. 따라서 변경 이벤트가 발생하지 않고, dependency로 전파되지 않는다.

따라서 개별 controller에 하나하나 로직 넣을 필요가 없어진다.

Index Creation

스크린샷 2024-08-12 오후 1 45 32

 

istio는 pod ip주소로 Pod Object를 조회하는 로직이 있다. indexing 필요.

  • informer에서도 index 설정할 수 있지만, informer는 k8s object에만 설정 가능하다는 단점이 있음.

스크린샷 2024-08-12 오후 1 47 21

 

indexing이 잘 되어 있다면, 위와 같이 로직을 단순화할 수 있다.

  • pod의 Desired State를 정의한다
  • pod의 Actual State를 불러온다
  • SyncWithApply 수행한다.

Debugging / Testing

스크린샷 2024-08-12 오후 1 48 23

 

위 다이어그램은 go code를 토대로 autogenerated된 것.

  • System Overview 확인이 쉬워진다. 어떤 로직이 어디에 영향을 주는지

스크린샷 2024-08-12 오후 1 53 24

 

controller 간 통신에 tracing 붙여서 확인할 수도 있음.

스크린샷 2024-08-12 오후 1 55 05

 

istio의 경우 testing은 비즈니스 로직 검증보다는 State Management가 많았다.

  • 하지만, 지금과 같은 구조는 state management를 라이브러리에서 해결하므로, 비즈니스 로직 테스트만 작성하면 된다.

스크린샷 2024-08-12 오후 1 57 24

 

다만, 지금은 prototype 단계.

  • Istio의 경우 현재까지 약 50% 이상이 위와 같은 구조로 전환되었음.
  • Deploying to production is another matter...
반응형