Deview 2021: https://deview.kr/2021/sessions/502
영상: https://tv.naver.com/v/23651554
Google에서 Multi-Cluster Application을 제공하는 플랫폼의 형태
- 여러 Region에 배포된 클러스터를 향해 트래픽이 전달됨
- Fleet이라는 product - 물리적으로 나뉘어 있는 클러스터를 하나의 logical cluster처럼 만드는 기능
- cluster API - 하나의 API로 여러 region에 클러스터를 배포할 수 있음.
멀티 클러스터로 트래픽을 분산하는 Load balancer에는 앞단에서 사용될 수 있는 여러 routing / 인증처리와 같은 기능이 필요함.
- AWS / GCP의 경우 L7 Load balancer에서 위와 같은 기능이 제공되고 있음.
왜 Istio?
Load balancer로 사용되는 여러 SW가 있지만, 필요로 하는 여러 요구사항을 충족하지는 못함
- Envoy는 mesh로 들어오는 traffic의 proxy 역할
- istio는 proxy 설정을 On-the-fly 형태로 변경할 수 있도록 control plane역할 지원
- GCP의 LoadBalancer도 envoy를 사용중
Load balancer 클러스터 구축
- Load balancer를 위한 클러스터를 두 개의 region에 구축
- 각각의 클러스터는 2-tier Gateway architecture로 구성
- LB 클러스터에 접근하기 위한 1-tier Gateway (클러스터의 External IP)
- 각각의 서비스로 연결하기 위한 2-tier Gateway
- 1-tier와 2-tier를 연결하기 위한 VirtualService가 존재함
- 2-tier gateway와 외부 서비스의 연결을 위한 ServiceEntry / VirtualService 존재
그래서 LB 클러스터는 최종적으로 아래의 아키텍처 형태.
- 1-tier Gateway의 경우, 여러 서비스가 virtualService / ServiceEntry 형태로 연결되도록 만들었음.
- public IP 리소스 아끼기 / 가급적 많은 서비스가 1-tier Gateway 사용할 수 있도록
성능 이슈 발생
- 하나의 gateway 안에 연결된 서비스가 2~3천개 되니까 성능 이슈 발생 (2-tier gateway 개수 증가로 발생)
- VirtualService 설정을 추가할 때, 설정이 반영되는 시간이 선형적으로 증가함
- default 설정 기준으로는 2500개 넘어가면 OOM 발생
- gateway에 연결된 virtualService 개수를 모니터링하면서, 한쪽이 과도해지면 다른 쪽으로 연결해주는 migration이 필요함.
Dynamic Migration을 위한 API서버 개발
- virtualService를 동적으로 배포하고 관리하기 위한 API서버 개발, LB 클러스터에 배포.
일반적으로 API서버는 동기 방식. 하지만 k8s 클러스터에서는 대부분의 API가 비동기 방식임. 따라서 API서버도 Declarative API 형태로 개발해야 함
- 목표 상태 정의 - event 형태로 받아서 동작 수행
- 목표상태에 실패할 수 있는 상황을 감안해 Retry (Reconcile loop) 필요
- 동작 중에 변경된 목표 상태 (예컨대 생성 -> 삭제)가 들어올 경우, 변경된 요청사항 반영
- 따라서 API서버의 요구사항은 위와 같다.
- 선언형 API
- 이벤트 처리를 위한 Producer / Consumer 구조
- 재시도 로직 (Reconcilation Loop)
API 서버 모델 디자인
- Service : 정의한 다른 모델 (Routing, backend 등)을 하나의 그룹으로 묶기 위한 일종의 Container / Namespace
- DNS : DNS서버의 특정 도메인이 어떤 IP와 연결되는지 확인하는 용도
- Routing : 여러 조건에 맞게 traffic Routing 기능을 담당함
- Backend : 외부의 PM 또는 다른 클러스터의 service (pod)를 지칭함. health check / Load balancing 담당
각 모델이 실제로 생성하는 k8s Object들은 아래와 같다.
Golang으로 구현된 해당 모델의 Struct는 아래와 같다.
- Service
- 사용자가 원하는 상태를 선언하기 위한 필드.
- 해당 서비스에 어떤 Domain이 매핑되어 있는지 모델링되어 있음
- Routing
- httpRoutes struct가 따로 있고, 해당 route에 적용될 rule을 정의할 수 있다.
- RouteRule의 경우 uri나 header, query parameter와 같은 조건을 정의할 수 있다.
- Backend
- endpoint로 실제 쓰이는 address, port 지정
- backendOption의 경우 ConnectionSetting, LB setting, healthCheck 세팅 등이 포함된다.
멀티 클러스터 환경에서 DNS를 체크하기 위해 etcd는 global clustering 방식을 사용함. 다른 클러스터의 DNS 정보도 확인할 수 있어야 하므로.
- kafka같은 message Queue도 있지만, 이 경우 message Queue의 Availability / Scalability 도 보장해야만 함.
- 하나의 API server에서 데이터를 선언
- 다른 API server에서는 해당 데이터를 받아서 작업 처리.
- 요청이 들어오면, etcd 클러스터로부터 변경사항을 받아서 처리한 후에야 클라이언트에게 응답. -> 비동기 처리.
고려해야 할 장애사항
예컨대
- API 서버에서 특정 k8s Object를 삭제 요청했으나 삭제되지 않을 경우 - 재시작
- API 서버 자체에 문제가 생겨 생성 / 삭제 요청을 제대로 수행하지 못할 경우
- Backend struct를 토대로 istio Object 생성 중 일부가 실패할 경우
등등..
핵심: 생성 / 삭제와 같은 로직이 멱등성 있게 동작해야 한다.
Loop를 GoRoutine으로 구현.
- 실행해야 할 연산이 정해진 목표에 도달할 때까지 반복.
- 처리 과정에서 ETCD / k8s Object 상태 체크 -> 최신 상태와 맞는지 확인
특정 이벤트 요청이 들어왔을 때 / Bootup 시점에 Loop 동작하도록 구성. API에 멱등성이 있으므로 데이터 복구용 sync로도 사용 가능.
Reconciliation Loop
- 함수 output으로 loop연산 / 종료 연산을 반환함.
- loop key 획득 -> ACID 연산 사용
- 종료 여부 판단 -> 최신 상태 아닐 경우 재시도
- 너무 오래 재시도할 경우 timeout 처리
- 마지막 함수 input으로 실행해야 할 함수를 추가 -> 기본 로직을 공통으로 활용 가능.
동시성 처리
하나의 클러스터 내에 여러 API server가 배포되어 있음. ETCD에 접근해서 DNS 정보를 확인할 때의 동시성 처리.
- ACID txn 사용.
- 데이터 선언 시 Revision Check -> ETCD에 transaction 요청. transaction 성공한 API만 데이터 선언한 내용을 반영할 수 있음.
- 선언 완료 시 클라이언트에 응답. (비동기)
- Loop도 선언형으로 상태를 변경하는 것이므로 transcation을 확보해야 Loop 생성 가능.
- loop 과정에서 장애가 발생할 수 있으므로 etcd의 Lease 기능 포함.
- keepAlive 메시지를 일정 시간 보내지 않을 경우 장애상황으로 간주 - 작업내용 삭제.
상태 관리
다른 클러스터에서는 전부 정상적으로 동작이 완료되었는데, 하나의 클러스터에서만 재시도중인 상황
- 사용자가 API server에서 상태 조회요청을 하면, 모든 클러스터의 상태를 볼 수 있어야 한다.
- 실패했다면 어떤 이유로 실패했는지 확인이 필요
= 히스토리 관리가 필요함
히스토리 추적을 고려한 ETCD key 모델링
multi-domain / 인증서 관리
- DNS에서 사용하는 default Domain.
- 사용자는 중간의 io prefix 없이 public domain (i.e. cafe.naver.com ) 형태로 사용함
- 즉 대부분의 경우 suffix가 유사함 -> 비슷한 Service의 경우 domain Conflict 발생 가능
예컨대 두 개의 서비스가 1-tier gateway (아마도 같은 클러스터 내부)에 연결되어 있고, 같은 wildcard cname 인증서를 가지고 있을 경우
- 최초에 브라우저가 serviceA로 connection을 맺었을 경우
- serviceB 도메인으로 요청해도 내부적으로는 이미 생성된 serviceA connection을 재사용하게 됨
- 원하는 endpoint가 없으므로 404 not found.
서로 다른 인증서를 사용하더라도 cname이 같은 인증서일 경우 발생할 수 있음.
- 현재 http 2.0 spec에 따른 이슈이므로 우회하는 식으로 해결해야 함.
따라서 충돌이 나지 않도록 모델링 - Cert, Domain struct 추가. 충돌이 있을 경우 다른 Gateway로 연결되도록 변경
Health Check
클러스터에 장애가 있거나 maintenance mode로 전환하는 등 특정 클러스터를 제외해야 하는 경우.
Cluster
예컨대 특정 클러스터 (cluster4)를 모든 서비스에서 제외해야 하는 경우라면
- 모든 DNS record를 확인한 뒤 해당 클러스터의 DNS를 제외해야 한다.
- maintenance Mode 활성화 / 비활성화 API 추가, DNSRecord 모델에 zonesToExclude 필드 추가해서 제외해야 할 zone 지정 가능.
maintenance mode 기능을 활용한 health check.
- gateway의 상태를 확인
- 문제가 있을 경우 maintenance mode -> 클러스터 제외.
- 최소 두 개 이상의 health checker가 장애로 인지 / gateway 실패가 threshold 기준 초과해야 제외.
Service
연결된 서비스의 health check endpoint (i.e. /health) 확인.
Global Rate Limit
istio(envoy)의 ratelimit 기능 활용.
- 보통 service mesh 내에 여러 개의 envoy proxy가 돌고 있고, envoy proxy에서 rateLimit Service에 요청을 보내 값을 확인하는 방식.
- istio에선 ratelimit 인터페이스를 Json / gRPC 형태로 제공
- 어떤 도메인에 ratelimit을 적용할 것인지 (string domain)
- 하나의 요청에 몇 개의 limit을 증가시킬지 결정 (hit_addend)
- ratelimit service는 redis의 값을 토대로 ratelimit response를 응답한다.
자세한 설정을 위해 Descriptor 사용
- entry : kv pair
- RateLimitOverride : unit을 정의하고, 해당 unit동안 얼마의 트래픽을 허용할 것인지
Envoy 공식문서의 예제. ratelimit으로도 복잡한 시나리오를 정의할 수 있음.
모든 envoy가 요청이 들어올 때마다 ratelimit 값을 확인한 뒤 요청에 응답할지를 결정하기 때문에 성능에 영향을 미치게 됨.
- 50개 client -> 4 ratelimit pod에 요청을 보낼 시 Latency 2ms
- 400 client -> 4 ratelimit pod에 요청을 보낼 시 Latency 40ms
사용자의 요청 중간에 끼어드는 형태이므로 latency 고려가 필요함
벤치마크 결과
- 20 client : 1 pod 정도까지는 latency 1ms 정도.
- 40 client일 경우 2.5ms. 이후 linear하게 증가하는 경향.
따라서 latency 요구사항에 맞는 ratelimit pod 비율을 조정 - scale out 비율을 정의할 수 있음.
최종 아키텍처
- 사용자의 namespace마다 ratelimit service, 동적으로 service 옵션 변경을 위한 configmap 배포
- ingress gateway에서 ratelimit 활성화할 수 있도록 envoyFilter 적용
- ratelimit service에서 사용하는 redis는 system ns에 배포
- ingress gateway -> 자신의 ns에 배포된 ratelimit 서비스에 요청을 보내고 받는 구조.
단, ratelimit 자체는 하나의 클러스터에서만 동작하도록 구성되어 있음. 따라서 redis는 clustering 수행 - 여러 클러스터에서 동일한 값을 받을 수 있도록.
Logging
사용자의 namespace에 loki 배포, ingress gateway에 Promtail agent을 sidecar로 배포.
- 클러스터마다 loki 인스턴스가 존재
- 사용자의 fetch api 요청
- api에서 각 클러스터의 loki 인스턴스에 요청 전달
- 응답받은 뒤 aggregate -> 전달.
특정 서비스가 로그를 많이 생성하는 경우 -> 한 서비스가 다른 서비스에 영향을 주게 된다.
- 서비스 간 영향을 최소화하기 위해 loki를 사용자의 namespace마다 배포함.
Monitoring
사용자의 namespace에 prometheus / grafana 배포
Autoscaling
모든 컴포넌트는 HPA 기반 authoscaling
'학습일지 > Service Mesh' 카테고리의 다른 글
ServiceMeshCon 2020 - istio Service Mesh Simplified Beyond a Single Cluster (0) | 2022.11.10 |
---|---|
KubeCon 2019 - Istio Multi-Cluster Service Mesh Patterns Explained (0) | 2022.11.02 |
istio 개념 정리 (3) - mTLS Security (0) | 2022.10.29 |
istio 개념 정리 (2) - VirtualService / DestinationRule / Gateway (0) | 2022.10.10 |
istio 개념 정리 (1) - Service Mesh와 istio (0) | 2022.10.06 |