Kubernetes에서 수천개의 CRD를 사용하도록 확장하기
(blog.upbound.io)Upbound에서 만든 Crossplane은 Kubernetes에서 클라우드 control plane을 제공한다. 그래서 AWS, Azure, GCP 같은 클라우드 리소스에 대응하기 위해 수백개의 CRD(Custom Resource Definition)이 있다. Crossplane에서는 MR(Managed Resource)라고 부른다.
고급 Kubernetes 사용자라고 하더라고 수십개 정도로 적절한 CR을 운영했지만 Crossplane에서는 수백개의 MR을 써야 하기 때문에 Kubernetes가 얼마나 많은 CRD를 다룰 수 있는지 한계를 살펴보기 시작했다.
이는 클라이언트측 문제와 서버측 문제로 나누어 볼 수 있다.
클라이언트측 문제
- 클라이언트에서는 discovery 과정이 문제가 된다.
- kubectl 같은 클라이언트가 어떤 API를 서버가 제공하는지 찾기 위해서 discovery하게 되믄데 이는 모든 API 엔드포인트를 한번 순회해야 한다.
- CR은 API 엔드포인트로 제공된다.
-
https://example.org/apis/rds.aws.upbound.io/v1/instances/cool-db
같은 MR을 조회하려면https://example.org/apis/
에서 지원하는 API 그룹을 찾고https://example.org/apis/rds.aws.upbound.io
에서 지원되는 버전을 찾고https://example.org/apis/rds.aws.upbound.io/v1
에서 지원되는 CR을 찾아야 한다. - AWS, Azure, GCP 클라우드 프로바이더를 제공하는 Crossplane MR은 2,000개 정도 되고 300개의 API 그룹과 버전으로 나뉘어져 있다.
- 클라이언트는 discovery에 300개의 HTTP 요청을 보내게 된다.
- 요즘 네트워크 상황에서 큰 문제는 아니지만 발견한 문제는 레이트 리밋과 캐시였다.
클라이언트 레이트 리밋
- 평균 초당 5개 요청으로 레이트 리밋이 걸려있고(100개까지 버스트) 10분마다 discovery 캐시를 무효화한다.
- 이는 레이트 리밋을 올리면 해결할 수 있는데 지금도 초당 5개 요청이지만 300개까지 올릴 수 있게 되었다.
- kubectl v1.22에서 이 제한을 올려달라는 이슈가 제기되었고 discovery 캐시도 10분이 아니라 5시간으로 조정되어 Kubernetes v1.25에서 클라이언트의 증가된 리밋을 이용할 수 있다.
클라이언트 캐시
- 레이트 리밋을 끄고 테스트해도 300개의 API 그룹을 조회하는데 20초 가까이 걸렸다.
- 처음엔 네트워크 문제인줄 알았는데 찾아보니 캐시파일을 조회하면서 생기는 문제였다.
- Kubernetes 1.25에서 수정해서 macOS에서는 25배 빨라졌고 Linux에서는 2배 빨라졌다.
차후 클라이언트 개선
- 클라이언트에서 레이트 리밋을 거는 것은 합리적이긴 하지만 사실 서버를 제대로 보호하진 못한다.
- Kubernetes 1.20에 도입된 API Priority and Fairness (AP&F)는 서버측에서 Queue와 트래픽 흘리기를 제공해서 API 서버를 보호한다.
- discovery를 위한 하나의 수집된 HTTP 엔드포인트가 KEP에서 승인되어 1.26에서 알파로 지원될 예정이다.
서버측 문제
OpenAPI 스키마 계산
- 수백개의 CRD를 등록한 후 거의 한시간 정도 API 요청이 오래 걸리는 현상을 발견했다.
- 프로파일링을 통해 이 문제가 OpenAPI v2 스키마를 계산하는 로직 때문임을 찾았다.
- CRD를 추가하거나 업데이트하면 OpenAPI 컨트롤러가 CR의 swagger 스펙을 빌드하고 모든 CR의 swagger과 합쳐서 하나의 큰 스펙으로 만든 뒤 JSON으로 직렬화해서
/openapi/v2
로 제공한다. -
/openapi/v2
를 지연 계산하도록 하고 실제 CR에 대한 엔드포인트가 요청될 때 계산되도록 수정했다. - 이 수정은 v1.24.0에 들어갔고 1.20.13, 1.21.7, 1.22.4에 백포트되었다.
etcd 클라이언트
- OpenAPI 문제를 해결하고 발견한 새로운 병목 부분이다.
- API 서버는 CRD당 4MiB의 메모리를 사용한다는 걸 알게 되었다.
- 이는 GKE, EKS같은 매니지드 Kubernetes에서 더 문제인데 API 서버의 CPU와 메모리를 제한하고 있기 때문이다. 리소스가 더 필요하면 API 서버를 알아서 확장해 주지만 안타깝게도 CRD 추가하는 확장을 결정하는 요소가 아니다. 그래서 API 서버가 반복적으로 OOM killed되지 않는 이상 화장하지 않는다.
- GKE, AKE, EKS에서 테스트했을 때 자동 힐링은 되지만 API 서버를 5초에서 1시간 정도 이용할 수 없었다. 클러스터가 완전히 멈추는 것은 아니지만 모든 rerconciliation이 멈췄다.
- 프라파일링을 통해서 로깅 라이브러리인 Zap이 메모리의 20%를 차지함을 알게 되었다.
- API 서버는 CR의 버전마다 하나의 etcd 클라이언트를 생성하고 각 etcd 클라이언트는 Zap 로거를 생성한다.
- 이 결과과 중복된 로거로 메모리가 증가했을 뿐 아니라 API 서버와 etcd 간에 불필요한 TCP 커넥션도 생기게 되었다.
- 모든 CR 엔드포인트에 하나의 etcd 클라이언트를 쓰는게 맞다고 메인테이너도 동의했지만 Kubernetes 1.25 릴리스가 임박해서 다 고치긴 어렵고 좀 더 작게 만들어서 모든 etcd 클라이언트가 하나의 로거를 공유하도록 수정했다.
-이는 1.25에 포함될 예정이고 1.22, 1.23, 1.24에 백포트 될 것이다. 메모리 사용을 20% 줄일 것이다.
차후 서버측 개선
- CR 버전마다 만들던 etcd 클라이언트를 트랜스포트 당 하나씩(etcd 클러스터 마다) 만들도록 변경할 예정이다.
- GKE, EKS, AKE 엔지니어링 팀과도 협업해서 다수의 crossplane CRD 설치를 다룰 수 있도록 작업 중이다.