Service_Mesh/Istio
[Istio-3주차] Resilience (이론, 실습)
lati-tech
2025. 4. 27. 07:56
6장 Resilience: Solving application networking challenges
별첨
- Istio DestinationRule 한줄 정리
- 서비스 또는 서비스의 부분집합(subset)에 대해 정의하여, 라우팅 이후 적용되는 로드밸런싱, 연결 풀, 서킷 브레이커 등의 세부 트래픽 전달(host: aa.com 일 시 label: v2) 규칙을 구성하여 Istio에게 힌트를 주는 정책 정의 리소스
들어가며
- 주요 사항
- 회복탄력성의 중요성 이해 Understanding the importance of resilience
- 클라이언트 측 로드 밸런싱 활용 Leveraging client-side load balancing
- 요청 시간 초과 및 재시도 구현 Implementing request timeouts and retries
- 회로 차단 및 연결 풀링 Circuit breaking and connection pooling
- 마이그레이션 Migrating from application libraries used for resilience
- 애플리케이션 오류, 네트워크 파티션, 기타 주요 문제는 이따금 예측할 수 없이 실패하여 수작업으로 전환이 어렵다.
- 이에 합리적인 대응 동작을 구축할 필요가 있다.
- Istio는 수작업으로 개발자가 코드를 변경하지 않고도 타임아웃, 재시도, 서킷 브레이커를 추가 가능하다.
6.1 Building resilience into the application
들어가며
- 마이크로서비스는 복원력이 최우선
- 서비스 장애는 모든 서비스를 중단시킬 수 있음
- 서비스 관리자는 애플리케이션 및 서비스 전반에 걸쳐 몇 가지 복원력 패턴을 일관되게 채택해야 할 필요성이 있음
- 예를 들어 서비스 B에 장애가 발생했을 때 과도한 요청 재시도로 서비스 과부하가 일어날 수 있음
- 과도한 요청 재시도로 연쇄적인 지연이 발생하면,(10초가 모이면 1분, 10분까지 진행) 심각한 연쇄 오류 발생 가능
- 해결책은 애플리케이션이 장애를 예상해 요청을 처리할 때 자동으로 복원을 시도하거나 대체 경로로 돌아갈 수 있도록 구축
- 요청 재시도
- 타임아웃
- 서킷 브레이킹 패턴
- 복원력을 올바르고 일관되게 구현할 수 있도록 함 (실습)
- 6.1.1 Building resilience into application libraries : 언어 마다 구현 상이, 운영 부담
- 트위터 Finagle(스칼라/자바/JVM, 타임아웃 재시도 서킷브레이킹) 혹은 NetflixOSS 스택: Hystrix(서킷 브레이커), Ribbon(클라이언트 측 로드 밸런싱) 과 같이 인기 있는 애플리케이션 라이브러리/프레임워크들이 있다..
- 이런 프레임워크의 한 가지 문제점은 언어, 프레임워크, 인프라 조합마다 구현 방식이 상이하다는 것이다.
- 트위터 Finagle과 NetfilxOSS는 자바 개발자에게는 훌륭하지만 Node.js, Go 언어, 파이썬 개발자는 이런 패턴의 변형을 찾거나 직접 구현해야 했다.
- 때에 따라서는 이런 라이브러리가 애플리케이션 코드에 침입해 네트워킹 코드가 여거저기 흩어지고 실제 비즈니스 로직을 가려버리는 상황이 발생하기도 했다.
- 나아가 여러 언어와 프레임워크에 걸쳐 이런 라이브러리들을 유지 관리하는 것은 마이크로서비스 운영 측면에서 부담이 된다.
(모든 조합을 동시에 패치하고 기능을 동일하게 유지해야 하기 때문)
- 6.1.2 Using Istio to solve these problems : 이스티오 서비스 프록시가 복원력 기능 제공
- Istio 서비스 프록시는 애플리케이션 옆에 위치하며 애플리케이션을 드나드는 모든 네트워크 트래픽을 처리 (그림 6.2)
- 서비스 프록시는 애플리케이션 수준 요청과 메시지(HTTP 요청 등)를 이해하므로, 프록시 안에서 복원력 기능 구현 가능
- 서비스 프록시는 서비스 인스턴스마다 배포되므로 애플리케이션마다 복원력 기능 수행 가능
Istio 서비스 프록시는 복원력 유지에 중요 기능 - ex. 서비스 호출 시 HTTP 503 오류 발생 시 3번 Retry 설정 (failed, count, timeout 각각 세부 설정 가능)
- 복원력 패턴
- 클라이언트 측 로드 밸런싱 Client-side load balancing
- 지역 인식 로드 밸런싱 Locality-aware load balancing
- 타임아웃 및 재시도 Timeouts and retries
- 서킷 브레이킹 Circuit breaking
- 6.1.3 Decentralized implementation of resilience : 복원력 패턴을 분산 구현
- Istio 사용 시 중앙집중식 게이트웨이 불필요 - Proxy가 애플리케이션 인스턴스와 같은 위치에 존재
- 중앙집중식 게이트웨이 단점: 1회 하드웨어 구성 시 변경이 어려움 - 동적이고 탄력적인 클라우드 아키텍처와 대응 및 확장에 어려움
- Istio를 사용 시 애플리케이션 별 복원력 패턴 처리 가능 - 분산 구현
- 예시: 닉 잭슨 의 Fake Service라는 프로젝트 - https://github.com/nicholasjackson/fake-service
- simple-web 서비스가 simple-backend 백엔드를 호출
6.2 Client-side load balancing 클라이언트 측 로드 밸런싱 (실습)
들어가며
- EDS, DestinationRule(Client LoadBalancer)
- 클라이언트 측 로드 밸런싱이란?: 클라이언트에게 서비스에서 사용할 수 있는 여러 엔드포인트를 알려주고 클라이언트가 특정 로드 밸런싱 알고리듬을 선택하게 하는 방식 (엔드포인트 간에 요청을 최적으로 분산시키기 위함)
- 장점
- 중앙집중식 로드 밸런싱에 의존할 필요성이 줄어듦
- 클라이언트가 다중 홉을 거칠 필요 없이 특정 엔드포인트로 직접적이면서 의도적으로 요청 가능
- 클라이언트-서비스는 확장이 용이해져 변화하는 토폴로지에 대응 가능
- 구성
- 서비스, Endpoint Discovery(EDS) 를 사용: 서비스 간 통신의 클라이언트 측 프록시에 올바른 최신 정보 제공
- istio 설정으로 클라이언트 측 로드 밸런식 설정 가능
- DestinationRule 리소스: 클라이언트가 어떤 로드 밸런싱 알고리즘을 사용할지 설정.
- Istio의 로드 밸런싱 알고리즘 (Envoy에서 지원, 일부)
- 라운드 로빈 (기본값)
- 랜덤
- 가중치를 적용한 최소 요청
- Server side load balancing vs. Client side load balancing 정의 및 비교 (장/단점)
정의
- Server Side Load Balancing(서버 사이드 로드밸런싱)
server-side lb
중앙에 위치한 로드밸런서(하드웨어 또는 소프트웨어)가 모든 클라이언트 요청을 받아, 내부 알고리즘(예: 라운드 로빈, 최소 연결 등)을 통해 여러 서버로 트래픽을 분산시키는 방식이다. 클라이언트는 로드밸런서의 주소만 알고, 실제 서버 목록이나 상태는 알 필요가 없다. - Client Side Load Balancing(클라이언트 사이드 로드밸런싱)
client-side lb
요청을 보내는 주체(클라이언트)가 직접 서버 목록을 관리하고, 자체적으로 로드밸런싱 알고리즘(예: 라운드 로빈, 랜덤 등)을 사용해 어떤 서버에 요청을 보낼지 결정하는 방식이다. 마이크로서비스 환경에서 각 서비스 인스턴스가 클라이언트 역할을 할 수 있다.
비교: 장점과 단점
구분 Server Side Load Balancing Client Side Load Balancing 구성 위치 중앙 로드밸런서(네트워크 중간) 클라이언트(서비스 또는 앱 내부) 서버 목록 관리 로드밸런서가 관리 클라이언트가 직접 관리(서비스 디스커버리 필요) 구현 복잡도 클라이언트는 단순, 로드밸런서 구축 필요 클라이언트가 복잡, 로드밸런싱 로직 내장 필요 장애 허용성 로드밸런서 장애 시 전체 영향(단일 장애점) 분산 구조, 일부 클라이언트 장애가 전체 영향 없음 성능(지연) 네트워크 홉 추가로 지연 가능 중간자 없이 직접 연결, 지연 최소화 확장성 서버 추가 시 로드밸런서 재설정 필요 클라이언트가 동적으로 서버 목록 갱신 가능 적용 사례 전통적 웹/엔터프라이즈 환경, L4/L7 로드밸런서 마이크로서비스, 클라우드 네이티브 환경 Server Side Load Balancing
장점
- 클라이언트가 단순해지고, 서버 추가/제거가 중앙에서 관리된다.
- 트래픽, 장애 조치, 정책 등을 중앙에서 일괄 관리 가능하다.
- SSL 종료, 보안 등 추가 기능 제공이 용이하다.
단점
- 로드밸런서 자체가 단일 장애점이 될 수 있다(이중화 필요).
- 네트워크 홉이 추가되어 지연이 발생할 수 있다.
- 하드웨어/소프트웨어 장비 도입·운영 비용이 발생한다.
Client Side Load Balancing
장점
- 중간 로드밸런서가 없어 네트워크 지연이 줄어든다.
- 분산 구조로, 일부 클라이언트 장애가 전체 서비스에 영향을 주지 않는다.
- 동적으로 서버 목록을 갱신할 수 있어 유연하다.
단점
- 각 클라이언트에 로드밸런싱 로직과 서버 목록 관리 기능을 구현해야 한다.
- 클라이언트가 많을수록 관리 복잡도가 증가한다.
- 서비스 디스커버리 시스템과의 연동이 필요하다.
결론
- 전통적인 대규모 웹 서비스나 엔터프라이즈 환경에는 서버 사이드 로드밸런싱이 적합하다.
- 마이크로서비스, 클라우드 네이티브 환경에서는 클라이언트 사이드 로드밸런싱이 더 유연하고 확장성 있는 선택이 될 수 있다
- Server Side Load Balancing(서버 사이드 로드밸런싱)
- 6.2.1 Getting started with client-side load balancing : DestinationRule 실습
- 실습 전 초기화
kubectl delete gw,vs,deploy,svc,destinationrule --all -n istioinaction
- 예제 서비스 2개와 gateway, virtualservice 배포
# (옵션) kiali 에서 simple-backend-1,2 버전 확인을 위해서 labels 설정 : ch6/simple-backend.yaml open ch6/simple-backend.yaml ... apiVersion: apps/v1 kind: Deployment metadata: labels: app: simple-backend version: v1 name: simple-backend-1 spec: replicas: 1 selector: matchLabels: app: simple-backend template: metadata: labels: app: simple-backend version: v1 ... apiVersion: apps/v1 kind: Deployment metadata: labels: app: simple-backend version: v2 name: simple-backend-2 spec: replicas: 2 selector: matchLabels: app: simple-backend template: metadata: labels: app: simple-backend version: v2 ... # 예제 서비스 2개 배포 kubectl apply -f ch6/simple-backend.yaml -n istioinaction kubectl apply -f ch6/simple-web.yaml -n istioinaction # 확인 kubectl get deploy,pod,svc,ep -n istioinaction -o wide NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR deployment.apps/simple-backend-1 1/1 1 1 105m simple-backend nicholasjackson/fake-service:v0.17.0 app=simple-backend deployment.apps/simple-backend-2 2/2 2 2 105m simple-backend nicholasjackson/fake-service:v0.17.0 app=simple-backend deployment.apps/simple-web 1/1 1 1 105m simple-web nicholasjackson/fake-service:v0.17.0 app=simple-web ... # gw,vs 배포 cat ch6/simple-web-gateway.yaml apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: simple-web-gateway spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - "simple-web.istioinaction.io" --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: simple-web-vs-for-gateway spec: hosts: - "simple-web.istioinaction.io" gateways: - simple-web-gateway http: - route: - destination: host: simple-web kubectl apply -f ch6/simple-web-gateway.yaml -n istioinaction # 확인 kubectl get gw,vs -n istioinaction docker exec -it myk8s-control-plane istioctl proxy-status NAME CLUSTER CDS LDS EDS RDS ECDS ISTIOD VERSION istio-ingressgateway-996bc6bb6-ztcx5.istio-system Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-7df6ffc78d-xmjbj 1.17.8 simple-backend-1-7449cc5945-d9zmc.istioinaction Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-7df6ffc78d-xmjbj 1.17.8 simple-backend-2-6876494bbf-vdttr.istioinaction Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-7df6ffc78d-xmjbj 1.17.8 simple-backend-2-6876494bbf-zn6v9.istioinaction Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-7df6ffc78d-xmjbj 1.17.8 simple-web-7cd856754-tjdv6.istioinaction Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-7df6ffc78d-xmjbj 1.17.8 # 도메인 질의를 위한 임시 설정 : 실습 완료 후에는 삭제 해둘 것 echo "127.0.0.1 simple-web.istioinaction.io" | sudo tee -a /etc/hosts cat /etc/hosts | tail -n 3 # 윈도우(Powershell) 도메인 질의 임시 설정 Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "127.0.0.1 simple-web.istioinaction.io" Get-Content $env:SystemRoot\System32\drivers\etc\hosts | Select-Object -Last 3 # 호출 curl -s http://simple-web.istioinaction.io:30000 open http://simple-web.istioinaction.io:30000 # 신규 터미널 : 반복 접속 실행 해두기 while true; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done # 로그 확인 kubectl stern -l app=simple-web -n istioinaction kubectl stern -l app=simple-web -n istioinaction -c istio-proxy kubectl stern -l app=simple-web -n istioinaction -c simple-web kubectl stern -l app=simple-backend -n istioinaction kubectl stern -l app=simple-backend -n istioinaction -c istio-proxy kubectl stern -l app=simple-backend -n istioinaction -c simple-backend # (옵션) proxy-config # proxy-config : simple-web docker exec -it myk8s-control-plane istioctl proxy-config listener deploy/simple-web.istioinaction docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/simple-web.istioinaction docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/simple-web.istioinaction | grep backend 80 simple-backend, simple-backend.istioinaction + 1 more... /* docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/simple-web.istioinaction --fqdn simple-backend.istioinaction.svc.cluster.local SERVICE FQDN PORT SUBSET DIRECTION TYPE DESTINATION RULE simple-backend.istioinaction.svc.cluster.local 80 - outbound EDS docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/simple-web.istioinaction --fqdn simple-backend.istioinaction.svc.cluster.local -o json ... "name": "outbound|80||simple-backend.istioinaction.svc.cluster.local", "type": "EDS", "edsClusterConfig": { "edsConfig": { "ads": {}, "initialFetchTimeout": "0s", "resourceApiVersion": "V3" }, "serviceName": "outbound|80||simple-backend.istioinaction.svc.cluster.local" }, "connectTimeout": "10s", "lbPolicy": "LEAST_REQUEST", ... docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/simple-web.istioinaction docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/simple-web.istioinaction --cluster 'outbound|80||simple-backend.istioinaction.svc.cluster.local' ENDPOINT STATUS OUTLIER CHECK CLUSTER 10.10.0.14:8080 HEALTHY OK outbound|80||simple-backend.istioinaction.svc.cluster.local 10.10.0.15:8080 HEALTHY OK outbound|80||simple-backend.istioinaction.svc.cluster.local 10.10.0.16:8080 HEALTHY OK outbound|80||simple-backend.istioinaction.svc.cluster.local docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/simple-web.istioinaction --cluster 'outbound|80||simple-backend.istioinaction.svc.cluster.local' -o json ...
예제 서비스 2개 배포 후 확인 Gateway, VirtualService 배포 후 확인 Istio Proxy 현황 확인 hosts 설정: wsl2 호출: curl 호출: Browser
1:2 분산인 이유: 1번 backend는 Pod가 1개고 2번 backend는 Pod가 2개라 그렇다.simple-backend fqdn에 대한 cluster 정보 확인 endpoint: EDS / lbPolicy: LEAST_REQUEST 인 점 확인 Endpoint IP 확인
- DestinationRule로 LB 정책 지정 (ROUND_ROBIN) : simple-backend 서비스 호출하는 모든 클라이언트 대상
# cat ch6/simple-backend-dr-rr.yaml apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: simple-backend-dr spec: host: simple-backend.istioinaction.svc.cluster.local trafficPolicy: loadBalancer: simple: ROUND_ROBIN # 엔드포인트 결정을 '순서대로 돌아가며'
적용 및 확인
- simple-web 은 simple-backend 를 호출하는데, simple-backend 서비스에는 복제본이 여러 개 있다.
- 이는 의도한 것으로, 런타임에 일부 엔드포인트를 수정해볼 것
# DestinationRule 적용 : ROUND_ROBIN cat ch6/simple-backend-dr-rr.yaml kubectl apply -f ch6/simple-backend-dr-rr.yaml -n istioinaction # 확인 : DestinationRule 단축어 dr kubectl get dr -n istioinaction NAME HOST AGE simple-backend-dr simple-backend.istioinaction.svc.cluster.local 11s kubectl get destinationrule simple-backend-dr -n istioinaction \ -o jsonpath='{.spec.trafficPolicy.loadBalancer.simple}{"\n"}' ROUND_ROBIN # 호출 : 이 예시 서비스 집합에서는 호출 체인을 보여주는 JSON 응답을 받느다 ## simple-web 서비스는 simple-backend 서비스를 호출하고, 우리는 궁극적으로 simple-backend-1 에서 온 응답 메시지 Hello를 보게 된다. ## 몇 번 더 반복하면 simple-backend-1 과 simple-backend-2 에게 응답을 받는다. curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body" # 반복 호출 확인 : 파드 비중은 backend-2가 2개임 for in in {1..10}; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body"; done for in in {1..50}; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body"; done | sort | uniq -c | sort -nr # 로그 확인 : backend 요청을 하면 요청을 처리할 redirect 주소를 응답 (301), 전달 받은 redirect(endpoint)로 다시 요청 kubectl stern -l app=simple-web -n istioinaction -c istio-proxy ## simpleweb → simple-backend (301) redirect 응답 수신 simple-web-7cd856754-tjdv6 istio-proxy [2025-04-20T04:22:24.317Z] "GET // HTTP/1.1" 301 - via_upstream - "-" 0 36 3 3 "172.18.0.1" "curl/8.7.1" "ee707715-7e7c-42c3-a404-d3ee22f79d11" "simple-backend:80" "10.10.0.16:8080" outbound|80||simple-backend.istioinaction.svc.cluster.local 10.10.0.17:46590 10.200.1.161:80 172.18.0.1:0 - default ## simpleweb → simple-backend (200) simple-web-7cd856754-tjdv6 istio-proxy [2025-04-20T04:22:24.324Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 278 156 156 "172.18.0.1" "curl/8.7.1" "ee707715-7e7c-42c3-a404-d3ee22f79d11" "simple-backend:80" "10.10.0.14:8080" outbound|80||simple-backend.istioinaction.svc.cluster.local 10.10.0.17:38336 10.200.1.161:80 172.18.0.1:0 - default ## simpleweb → 외부 curl 응답(200) simple-web-7cd856754-tjdv6 istio-proxy [2025-04-20T04:22:24.307Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 889 177 177 "172.18.0.1" "curl/8.7.1" "ee707715-7e7c-42c3-a404-d3ee22f79d11" "simple-web.istioinaction.io:30000" "10.10.0.17:8080" inbound|8080|| 127.0.0.6:40981 10.10.0.17:8080 172.18.0.1:0 outbound_.80_._.simple-web.istioinaction.svc.cluster.local default kubectl stern -l app=simple-backend -n istioinaction -c istio-proxy ## simple-backend → (응답) simpleweb (301) simple-backend-2-6876494bbf-zn6v9 istio-proxy [2025-04-20T04:22:45.209Z] "GET // HTTP/1.1" 301 - via_upstream - "-" 0 36 3 3 "172.18.0.1" "curl/8.7.1" "71ba286a-a45f-41bc-9b57-69710ea576d7" "simple-backend:80" "10.10.0.14:8080" inbound|8080|| 127.0.0.6:54105 10.10.0.14:8080 172.18.0.1:0 outbound_.80_._.simple-backend.istioinaction.svc.cluster.local default ## simple-backend → (응답) simpleweb (200) simple-backend-1-7449cc5945-d9zmc istio-proxy [2025-04-20T04:22:45.216Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 278 152 152 "172.18.0.1" "curl/8.7.1" "71ba286a-a45f-41bc-9b57-69710ea576d7" "simple-backend:80" "10.10.0.15:8080" inbound|8080|| 127.0.0.6:43705 10.10.0.15:8080 172.18.0.1:0 outbound_.80_._.simple-backend.istioinaction.svc.cluster.local default # docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/simple-web.istioinaction --fqdn simple-backend.istioinaction.svc.cluster.local -o json ... "name": "outbound|80||simple-backend.istioinaction.svc.cluster.local", "type": "EDS", "edsClusterConfig": { "edsConfig": { "ads": {}, "initialFetchTimeout": "0s", "resourceApiVersion": "V3" }, "serviceName": "outbound|80||simple-backend.istioinaction.svc.cluster.local" }, "connectTimeout": "10s", "lbPolicy": "LEAST_REQUEST", # RR은 기본값(?)이여서, 해당 부분 설정이 이전과 다르게 없다 ... # docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/simple-web.istioinaction --cluster 'outbound|80||simple-backend.istioinaction.svc.cluster.local' -o json
DestinationRule 적용: ROUND_ROBIN 반복 호출: 4:6 비율 호출 반복 호출: 3:7 비율 호출 로그 확인: simple-web 로그 확인 - simple-backend
- simple-web 과 함께 배포된 서비스 프록시가 모든 simple-backend 엔드포인트를 알고 있고 기본 알고리듬을 사용해 요청을 받을 엔드포인트를 결정
- 부하 생성기를 사용해 simple-backend 서비스 지연 시간을 변화시키는 어느 정도 현실적인 시나리오 (다음 실습)
- 실습 전 초기화
- 6.2.2 Setting up our scenario : Fortio 설치
- 서비스 소요 시간의 원인들
- 요청 크기 Request size
- 처리 복잡도 Processing complexity
- 데이터베이스 사용량 Database usage
- 시간이 걸리는 다른 서비스 호출 Calling other services that take time
- 서비스 외적인 이유
- 예기치 못한, 모든 작업을 멈추는 stop-the-world 가비지 컬렉션 Unexpected, stop-the-world garbage collections
- 리소스 경합 Resource contention (CPU, 네트워크 등)
- 네트워크 혼잡 Network congestion
- 서비스 지연 및 편차를 주고, 서비스를 다시 호출하여 최초 응답 시간 차이 관찰 (실습)
# 호출 3회 반복 : netshoot 에서 서비스명으로 내부 접속 kubectl exec -it netshoot -- time curl -s -o /dev/null http://simple-web.istioinaction kubectl exec -it netshoot -- time curl -s -o /dev/null http://simple-web.istioinaction kubectl exec -it netshoot -- time curl -s -o /dev/null http://simple-web.istioinaction real 0m 0.17s user 0m 0.00s sys 0m 0.00s ...
- 서비스를 호출 할 때마다 응답 시간이 달라진다.
- 로드 밸런싱은 주기적으로 혹은 예기치 못하게 지연 시간이 급증하는 엔드포인트의 영향을 줄이는 효과적인 전략이 될 수 있다.
- Fortio(CLI 부하 생성 도구) 사용 - 클라이언트 측 로드 밸런싱의 차이 관찰
- https://github.com/fortio/fortio / 프리뷰 https://demo.fortio.org/
- Fortio는 그리스어로 '부하'라는 뜻이며,포르티오 라고 읽는다.
- 부하 테스트 라이브러리, 명령줄 도구, 고급 에코 서버 및 Go(Golang)로 작성된 웹 UI이다.
- 초당 쿼리 로드를 지정하고 대기 시간 히스토그램 및 기타 유용한 통계를 기록할 수 있다.
# mac 설치 brew install fortio fortio -h fortio server open http://127.0.0.1:8080/fortio # windows 설치 1. 다운로드 https://github.com/fortio/fortio/releases/download/v1.69.3/fortio_win_1.69.3.zip 2. 압축 풀기 3. Windows Command Prompt : fortio.exe server 4. Once fortio server is running, you can visit its web UI at http://localhost:8080/fortio/
- Fortio가 우리 서비스를 호출할 수 있는지 확인
fortio curl http://simple-web.istioinaction.io:30000 14:16:29.874 r1 [INF] scli.go:122> Starting, command="Φορτίο", version="1.69.3 h1:G1cy4S0/+JKwd1fuAX+1jKdWto4fPpxAdJHtHrWzF1w= go1.24.2 arm64 darwin", go-max-procs=8 HTTP/1.1 200 OK ...
- 서비스 소요 시간의 원인들
- 6.2.3 Testing various client-side load-balancing strategies* : LB 알고리즘에 따란 지연 시간 성능 측정
그림 출처 https://netpple.github.io/docs/istio-in-action/Istio-ch6-resilience
- Fortio 사용 방안
- 60초 동안 10개의 커넥션을 통해 초당 1000개의 요청 예정
- Fortio: 각 호출 지연 시간 추적 + 지연 시간 백분위수 분석 ⇒ 히스토그램 표시
- 테스트 전: 지연 시간 1초로 늘린 simple-backend-1 서비스 투입
(엔드포인트 중 하나에 긴 가비지 컬렉션 이벤트 또는 기타 애플리케이션 지연 시간이 발생한 상황 시뮬레이션) - 로드 밸런싱 전략을 라운드 로빈, 랜덤, 최소 커넥션으로 바꿔가면서 차이점을 관찰
- 지연된 simple-backend-1 서비스 배포 - fake-service - Dockerhub , Github
# cat ch6/simple-backend-delayed.yaml ... - env: - name: "LISTEN_ADDR" value: "0.0.0.0:8080" - name: "SERVER_TYPE" value: "http" - name: "NAME" value: "simple-backend" - name: "MESSAGE" value: "Hello from simple-backend-1" - name: "TIMING_VARIANCE" value: "10ms" - name: "TIMING_50_PERCENTILE" value: "1000ms" - name: KUBERNETES_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace image: nicholasjackson/fake-service:v0.17.0 ... kubectl apply -f ch6/simple-backend-delayed.yaml -n istioinaction kubectl rollout restart deployment -n istioinaction simple-backend-1 # 확인??? kubectl exec -it deploy/simple-backend-1 -n istioinaction -- env | grep TIMING TIMING_VARIANCE=10ms TIMING_50_PERCENTILE=150ms # ??? kubectl exec -it deploy/simple-backend-2 -n istioinaction -- env | grep TIMING TIMING_VARIANCE=10ms TIMING_50_PERCENTILE=150ms # 직접 deploy 편집 수정??? KUBE_EDITOR="nano" kubectl edit deploy/simple-backend-1 -n istioinaction ... - name: TIMING_50_PERCENTILE value: 1000ms ... kubectl rollout restart deployment -n istioinaction simple-backend-1 kubectl exec -it deploy/simple-backend-1 -n istioinaction -- env | grep TIMING TIMING_VARIANCE=10ms TIMING_50_PERCENTILE=150ms # ??? # 동작 중 파드에 env 직접 수정.. kubectl exec -it deploy/simple-backend-1 -n istioinaction -- sh ----------------------------------- export TIMING_50_PERCENTILE=1000ms exit ----------------------------------- # kubectl describe pod -n istioinaction -l app=simple-backend | grep TIMING_50_PERCENTILE: TIMING_50_PERCENTILE: 1000ms TIMING_50_PERCENTILE: 150ms TIMING_50_PERCENTILE: 150ms # 테스트 curl -s http://simple-web.istioinaction.io:30000 | grep duration curl -s http://simple-web.istioinaction.io:30000 | grep duration curl -s http://simple-web.istioinaction.io:30000 | grep duration "duration": "1.058699s", "duration": "1.000934s",
1초 지연 설정 1초 지연 발생 Jagger로 트레이싱 확인 (1초 지연 요소 확인) - Fortio 인자 입력 후 확인
- Title : roundrobin
- URL : http://simple-web.istioinaction.io:30000
- QPS : 1000
- Duration : 60s
- Threads : 10
- Jitter: Check
- No Catch-up : Uncheck
- Extra Headers
- User-Agent: fortio
- Timeout : 2000ms
테스트 완료: 결과 파일이 파일시스템에 저장 + 결과 그래프도 표시- 이 라운드 로빈 밸런싱 전략의 경우 지연 시간 결과 ⇒ 75분위수에서 응답이 1초 이상 걸림.
- target 50% 0.19247
- target 75% 1.08485
- target 90% 1.26949
- target 99% 1.38028
- target 99.9% 1.39136
- 로드 밸런싱 알고리즘을 RANDOM 으로 변경하고 다시 로드 테스트
# cat ch6/simple-backend-dr-random.yaml apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: simple-backend-dr spec: host: simple-backend.istioinaction.svc.cluster.local trafficPolicy: loadBalancer: simple: RANDOM kubectl apply -f ch6/simple-backend-dr-random.yaml -n istioinaction # 확인 kubectl get destinationrule simple-backend-dr -n istioinaction \ -o jsonpath='{.spec.trafficPolicy.loadBalancer.simple}{"\n"}' # docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/simple-web.istioinaction --fqdn simple-backend.istioinaction.svc.cluster.local -o json | grep lbPolicy "lbPolicy": "RANDOM",
- RANDOM 로드 밸런싱 알고리즘 지연 시간 결과 ⇒ ⇒ 75분위수에서 응답이 1초 이상 으로 RoundRobin 과 비슷함
- target 50% 0.193694
- target 75% 1.0535
- target 90% 1.12688
- target 99% 1.17091
- target 99.9% 1.17532
- RANDOM 로드 밸런싱 알고리즘 지연 시간 결과 ⇒ ⇒ 75분위수에서 응답이 1초 이상 으로 RoundRobin 과 비슷함
- 로드 밸런싱 알고리즘을 Least connection 으로 변경하고 다시 로드 테스트
# cat ch6/simple-backend-dr-least-conn.yaml apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: simple-backend-dr spec: host: simple-backend.istioinaction.svc.cluster.local trafficPolicy: loadBalancer: simple: LEAST_CONN kubectl apply -f ch6/simple-backend-dr-least-conn.yaml -n istioinaction # 확인 kubectl get destinationrule simple-backend-dr -n istioinaction \ -o jsonpath='{.spec.trafficPolicy.loadBalancer.simple}{"\n"}' # docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/simple-web.istioinaction --fqdn simple-backend.istioinaction.svc.cluster.local -o json | grep lbPolicy "lbPolicy": "LEAST_REQUEST",
- Least connection 로드 밸런싱 알고리즘 지연 시간 결과 ⇒ 75분위수에서 응답이 200ms(0.19..) 이내 응답성능으로, RR, Random 보다 좋음!
- target 50% 0.185492
- target 75% 0.221552
- target 90% 1.17661
- target 99% 1.49254
- target 99.9% 1.52414
- Least connection 로드 밸런싱 알고리즘 지연 시간 결과 ⇒ 75분위수에서 응답이 200ms(0.19..) 이내 응답성능으로, RR, Random 보다 좋음!
- 실습 완료 후 Ctrl+C로 Fortio 서버를 종료
- Fortio 사용 방안
- 6.2.4 Understanding the different load-balancing algorithms
- 로드 테스트 종합해보면,
- 첫째, 여러 로드 밸런서는 현실적인 서비스 지연 시간 동작하에서 만들어내는 결과가 서로 다르다.
- 둘째, 히스토그램과 백분위수는 모두 다르다.
- 마지막으로, 최소 커넥션이 랜덤과 라운드 로빈보다 성능이 좋다.
- 이유
- 라운드 로빈(또는 next-in-loop)은 엔드포인트에 차례대로 요청을 전달한다. 랜덤은 엔드포인트를 무작위로 균일하게 고름
- 둘 다 비슷한 분포를 기대할 수 있는데, 이 두 전략의 과제는 로드 밸런서 풀의 엔드포인트가 일반적으로 균일하지 않다는 것
- 최소 커넥션 least-connection 로드 밸런서(엔보이에서는 최소 요청 least request으로 구현)는 특정 엔드포인트의 지연 시간을 고려한다.
- 요청을 엔드포인트로 보낼 때 대기열 깊이 queue depth 를 살펴 활성 요청 개수를 파악하고, 활성 요청이 가장 적은 엔드포인트를 고른다.
- 이런 알고리즘 유형을 사용하면, 형편없이 동작하는 엔드포인트로 요청을 보내는 것을 피하고 좀 더 빠르게 응답하는 엔드포인트를 선호할 수 있다.
- 로드 밸런서에 대한 자세한 정보는 엔보이 문서를 참고 - Docs
- 이유
6.3 Locality-aware load balancing 지역 인식 로드 밸런싱 (실습)
들어가며
- 컨트롤 플레인: 서비스 토폴로지를 이해하고 어떻게 발전할 수 있는 지 파악하는 역할도 함
- 서비스 메시에서 전체 서비스 토폴로지를 이해할 때의 이점
- 서비스와 피어 서비스의 위치 같은 휴리스틱 heuristic 을 바탕으로, 라우팅과 로드 밸런싱을 자동으로 결정할 수 있음
동일한 위치의 서비스를 호출하는 것을 선호한다 - Istio가 지원하는 로드 밸런싱 유형 중 하나는, 워크로드의 위치에 따라 루트에 가중치를 부여하고 라우팅 결정을 내리는 것
- ex. 이스티오는 특정 서비스를 배포한 리전과 가용 영역을 식별하고, 더 가까운 서비스에 우선순위를 부여 가능
- 만약 simple-backend 서비스를 여러 리전에 배포했다면(us-west, us-east, europe-west), 호출하는 방법은 여러 가지
- simple-web을 us-west 리전에 배포했다면, 우리는 simple-web이 하는 simple-backend 호출이 us-west 로컬이길 바랄 것 (그림 참고)
- 모든 엔드포인트를 동등하게 취급한다면 리전이나 영역을 넘나들면서 지연 시간이 길어지고 비용이 발생할 가능성이 높음
- 서비스와 피어 서비스의 위치 같은 휴리스틱 heuristic 을 바탕으로, 라우팅과 로드 밸런싱을 자동으로 결정할 수 있음
- 6.3.1 Hands-on with locality load balancing*
- 지역 인식 로드 밸런싱이 잘 동작하는지 살펴보기
- 쿠버네티스에 배포할 때, 리전과 영역 정보를 노드 레이블에 추가할 수 있음
- ex. failure-domain.beta.kubernetes.io/region 레이블 및 failure-domain.beta.kubernetes.io/zone 은 각각 리전과 영역을 지정 할 수 있게 해 줌
- 최근에는 쿠버네티스 API 정식 버전에서는 이 레이블들을 topology.kubernetes.io/region 과 topology.kubernetes.io/zone 으로 대체
- 이런 레이블은 구글 클라우드나 AWS 같은 클라우드 프로바이더가 자동으로 추가하는 경우가 많음
- Istio는 이런 노드 레이블을 가져와 엔보이 로드 밸런싱 엔드포인트에 지역 정보로 보강
- 현재 노드 1대(?)로 실습 하고 있으니, 다른 방식으로 지역 노드(?)로 실습 하기
- Istio에는 워크로드에 지역을 명시적으로 설정할 수 있는 방법 존재
- Pod에 istio-locality 라는 레이블을 달아 리전/영역을 지정할 수 있다.
- 이러면 지역 인식 라우팅 및 로드 밸런싱을 시연하는 것이 가능
- 예를 들어, simple-web 디플로이먼트는 다음과 같을 수 있음
# cat ch6/simple-service-locality.yaml --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: simple-web name: simple-web spec: replicas: 1 selector: matchLabels: app: simple-web template: metadata: labels: app: simple-web istio-locality: us-west1.us-west1-a ... --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: simple-backend name: simple-backend-1 spec: replicas: 1 selector: matchLabels: app: simple-backend template: metadata: labels: app: simple-backend istio-locality: us-west1.us-west1-a version: v1 # 추가해두자! ... --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: simple-backend name: simple-backend-2 spec: replicas: 2 selector: matchLabels: app: simple-backend template: metadata: labels: app: simple-backend istio-locality: us-west1.us-west1-b version: v2 # 추가해두자! ...
- simple-backend 서비스를 배포할 때 지역 레이블 지정
- simple-web과 같은 지역인 us-west1-a 에 simple-backend-1을 배포
- us-west1-b 에 simple-backend-2 를 배포 (리전은 동일하지만 Zone이 다름)
- 지역 간에 로드 밸런싱을 수행할 수 있는 Istio의 기능에는 리전, 영역, 심지어는 더 세밀한 하위 영역 subzone 도 포함함
- simple-backend 서비스를 배포할 때 지역 레이블 지정
- 다음 서비스를 배포
# kubectl apply -f ch6/simple-service-locality.yaml -n istioinaction # 확인 ## simple-backend-1 : us-west1-a (same locality as simple-web) kubectl get deployment.apps/simple-backend-1 -n istioinaction \ -o jsonpath='{.spec.template.metadata.labels.istio-locality}{"\n"}' us-west1.us-west1-a ## simple-backend-2 : us-west1-b kubectl get deployment.apps/simple-backend-2 -n istioinaction \ -o jsonpath='{.spec.template.metadata.labels.istio-locality}{"\n"}' us-west1.us-west1-b
서비스 배포 - Istio의 지역 인식 로드 밸런싱은 기본적으로 활성화되어 있음 https://istio.io/v1.17/docs/reference/config/istio.mesh.v1alpha1/
- Locality aware load balancing is enabled by default
- 비활성화: meshConfig.localityLbSetting.enabled: false
- Istio in actions 책 저자가 추천하는 이스티오의 지역 인식 로드 밸런싱 블로깅
https://karlstoney.com/locality-aware-routing/- 실습과 같이, 클러스터의 노드가 여러 가용성 영역에 배포돼 있다면 기본 지역 인식 로드 밸런싱이 항상 바람직하지는 않을 수도 있음을 고려해야 함
- 예제에서는 어느 지역에서도 simple-backend(목표 서비스) 복제본이 simple-web(호출하는 서비스)보다 적지 않음
- 그러나 실제 환경에서는 특정 지역에서 호출하는 서비스 인스턴스 보다 목표 서비스 인스턴스가 적게 배포될 수도 있음
- 이로 인해 목표 서비스가 과부하를 겪을 수 있고, 시스템 전체의 부하가 의도한 것처럼 불균형하게 분산될 수 있음
- 결론: 부하 특성과 토폴로지에 맞춰 로드 밸런싱을 튜닝하는 것이 중요
- 지역 정보가 준비되면,
us-west1-a 에 있는 simple-web 호출이
동일 Zone인 us-west1-a 에 배포된 simple-backend 서비스로 갈 것으로 기대할 수 있음동일한 위치의 서비스를 호출하는 것을 선호 - 예제에서는 simple-web의 모든 트래픽이 us-west1-a 에 있는 simple-backend-1 로 향함
- simple-backend-2 서비스는 simple-web 과 다른 영역인 us-west-1b에 배포돼 있음.
- 따라서 us-west1-a 에 있는 서비스가 실패하기 시작할 때만 simple-backend-2 로 향할 것으로 기대
- 따라서 us-west1-a 에 있는 서비스가 실패하기 시작할 때만 simple-backend-2 로 향할 것으로 기대
- 호출 테스트 1 ⇒ 지역 정보를 고려하지 않고 simple-backend 모든 엔드포인트로 트래픽이 로드 밸런싱 됨
# 신규 터미널 : 반복 접속 실행 해두기 while true; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; echo; done # 호출 : 이 예시 서비스 집합에서는 호출 체인을 보여주는 JSON 응답을 받느다 curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body" # 반복 호출 확인 : 파드 비중은 backend-2가 2개임 for in in {1..10}; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body"; done for in in {1..50}; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body"; done | sort | uniq -c | sort -nr
- 이스티오에서 지역 인식 로드밸런싱이 작동하려면 헬스 체크를 설정해야 함
- Health Check가 없으면 Istio가 다음 사항을 알지 못함
- Istio는 로드 밸런싱 풀의 어떤 엔드포인트가 정상적이지 않은지 알지 못함
- 또한 다음 지역으로 넘어가기 위해 어떤 휴리스틱을 사용해야 하는지 알지 못함
- 이상값 감지
- 엔드포인트의 동작과, 엔드포인트가 정상적으로 보이는지 여부를 수동적으로 감시
- 엔드포인트가 오류를 반환하는지 지켜보다가, 오류가 반환되면 엔드포인트를 비정상으로 표시
- simple-backend 서비스에 이상값 감지를 설정해 수동적인 헬스 체크 설정을 추가
# cat ch6/simple-backend-dr-outlier.yaml apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: simple-backend-dr spec: host: simple-backend.istioinaction.svc.cluster.local trafficPolicy: outlierDetection: consecutive5xxErrors: 1 interval: 5s baseEjectionTime: 30s maxEjectionPercent: 100 kubectl apply -f ch6/simple-backend-dr-outlier.yaml -n istioinaction # 확인 kubectl get dr -n istioinaction simple-backend-dr -o jsonpath='{.spec}' | jq # 반복 호출 확인 : 파드 비중 확인 for in in {1..10}; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body"; done for in in {1..50}; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body"; done | sort | uniq -c | sort -nr # proxy-config : simple-web 에서 simple-backend 정보 확인 docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/simple-web.istioinaction --fqdn simple-backend.istioinaction.svc.cluster.local docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/simple-web.istioinaction --fqdn simple-backend.istioinaction.svc.cluster.local -o json ... }, "outlierDetection": { "consecutive5xx": 1, "interval": "5s", "baseEjectionTime": "30s", "maxEjectionPercent": 100, "enforcingConsecutive5xx": 100, "enforcingSuccessRate": 0 }, ... docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/simple-web.istioinaction --cluster 'outbound|80||simple-backend.istioinaction.svc.cluster.local' docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/simple-web.istioinaction --cluster 'outbound|80||simple-backend.istioinaction.svc.cluster.local' -o json ... "healthStatus": { "edsHealthStatus": "HEALTHY" }, "weight": 1, "priority": 1, "locality": { "region": "us-west1", "zone": "us-west1-b" } ... # 로그 확인 kubectl logs -n istioinaction -l app=simple-backend -c istio-proxy -f kubectl stern -l app=simple-backend -n istioinaction ...
배포 및 설정 확인 반복 호출로 Pod 비중 확인 (9:1 , 6:4) 이상 탐지 항목들을 확인할 수 있다. (기본 거절 시간: 30초) locality 적용이 안되어 실패 - 호출 테스트 2 ⇒ 오동작 주입 후 확인
- 트래픽이 가용 영역을 넘어가는 것을 보기 위해 simple-backend-1 서비스를 오동작 상태로 만들어보자.
- simple-web 에서 simple-backend-1 호출하면 항상 HTTP 500 오류를 발생하게 하자
# HTTP 500 에러를 일정비율로 발생 cat ch6/simple-service-locality-failure.yaml ... - name: "ERROR_TYPE" value: "http_error" - name: "ERROR_RATE" value: "1" - name: "ERROR_CODE" value: "500" ... kubectl apply -f ch6/simple-service-locality-failure.yaml -n istioinaction # simple-backend-1- Pod 가 Running 상태로 완전히 배포된 후에 호출 확인 # 반복 호출 확인 : 파드 비중 확인 for in in {1..10}; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body"; done for in in {1..50}; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body"; done | sort | uniq -c | sort -nr # 확인 docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/simple-web.istioinaction --cluster 'outbound|80||simple-backend.istioinaction.svc.cluster.local' ENDPOINT STATUS OUTLIER CHECK CLUSTER 10.10.0.23:8080 HEALTHY OK outbound|80||simple-backend.istioinaction.svc.cluster.local 10.10.0.24:8080 HEALTHY OK outbound|80||simple-backend.istioinaction.svc.cluster.local 10.10.0.25:8080 HEALTHY FAILED outbound|80||simple-backend.istioinaction.svc.cluster.local # simple-backend-1 500에러 리턴으로 outliercheck 실패 상태로 호출에서 제외됨 docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/simple-web.istioinaction --cluster 'outbound|80||simple-backend.istioinaction.svc.cluster.local' -o json ... "healthStatus": { "failedOutlierCheck": true, "edsHealthStatus": "HEALTHY" }, ... "healthStatus": { "edsHealthStatus": "HEALTHY" },
- 이렇게 특정 지역의 서비스가 제대로 동작하지 않을 때 예상하는 지역 인식 로드 밸런싱 결과를 얻을 수 있다.
- 이 지역 인식 로드 밸런싱은 단일 클러스터 내부에서 이뤄지는 것임을 유의하자
- 다음 실습을 위해 simple-backend-1 을 정상화
kubectl apply -f ch6/simple-service-locality.yaml -n istioinaction
- Istio의 지역 인식 로드 밸런싱은 기본적으로 활성화되어 있음 https://istio.io/v1.17/docs/reference/config/istio.mesh.v1alpha1/
- 6.3.2 More control over locality load balancing with weighted distribution : 가중치 분포로 지역 인식 LB 제어 강화
- 지역 인식 로드밸런싱은 동작 방식 일부를 제어할 수도 있음
- Istio의 기본 설정: 서비스 프록시는 모든 트래픽을 동일 지역의 서비스로 보내고, 장애나 비정상 엔드포인트가 있을 때만 다른 지역으로 넘김
- 트래픽 일부를 여러 지역에 분산하고자 함: 기본 동작에 영향을 줄 수 있음. 이를 지역 가중 분포 locality weighted distribution 라고 함
- 특정 지역의 서비스가 피크 peak 시간이나 계절성 트래픽으로 인해 과부하될 것으로 예상될 경우 사용할 수 있는 방법
- ex. 특정 영역에서 리전이 처리 할 수 없는 부하 인입
- 트래픽의 70%가 최인접 지역으로 가고, 30%가 인접 지역으로 가기를 기대
- 앞선 실습처럼 simple-backend 서비스로 가는 트래픽 70%를 us-west1-a로, 30%를 us-west1-b로 보낼 것
- 실습: lb에 가중치 적용
# cat ch6/simple-backend-dr-outlier-locality.yaml apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: simple-backend-dr spec: host: simple-backend.istioinaction.svc.cluster.local trafficPolicy: loadBalancer: # 로드 밸런서 설정 추가 localityLbSetting: distribute: - from: us-west1/us-west1-a/* # 출발지 영역 to: "us-west1/us-west1-a/*": 70 # 목적지 영역 "us-west1/us-west1-b/*": 30 # 목적지 영역 connectionPool: http: http2MaxRequests: 10 maxRequestsPerConnection: 10 outlierDetection: consecutive5xxErrors: 1 interval: 5s baseEjectionTime: 30s maxEjectionPercent: 100 kubectl apply -f ch6/simple-backend-dr-outlier-locality.yaml -n istioinaction # 반복 호출 확인 : 파드 비중 확인 for in in {1..10}; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body"; done for in in {1..50}; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body"; done | sort | uniq -c | sort -nr # endpoint 에 weight 는 모두 1이다. 위 70/30 비중은 어느곳의 envoy 에 설정 되는 걸까?... docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/simple-web.istioinaction --cluster 'outbound|80||simple-backend.istioinaction.svc.cluster.local' -o json docker exec -it myk8s-control-plane istioctl proxy-config endpoint deploy/simple-web.istioinaction --cluster 'outbound|80||simple-backend.istioinaction.svc.cluster.local' ENDPOINT STATUS OUTLIER CHECK CLUSTER 10.10.0.23:8080 HEALTHY OK outbound|80||simple-backend.istioinaction.svc.cluster.local 10.10.0.24:8080 HEALTHY OK outbound|80||simple-backend.istioinaction.svc.cluster.local 10.10.0.26:8080 HEALTHY OK outbound|80||simple-backend.istioinaction.svc.cluster.local
반복 호출: 파드 비중 확인 - 트래픽 라우팅에서는 서비스의 부분집합 간에 트래픽 비중을 제어할 수 있었고, 보통 전체 서비스 그룹 내에서 종류나 버전이 여럿일 때 사용
- 위 예제에서는 서비스의 배포 토폴로지를 바탕으로 트래픽에 가중치를 부여했고, 부분집합과는 무관
- 부분집합 라우팅과 가중치 부여는 상호 배타적인 개념이 아님
- 이 둘은 중첩될 수 있으며, 5장에서 봤던 세밀한 트래픽 제어 및 라우팅을 이번 절에서 살펴본 지역 인식 로드 밸런서 위에 적용 할 수 있음
6.4 Transparent timeouts and retries (실습)
들어가며
- 네트워크에 분산된 구성 요소에 의존하는 시스템을 구축할 때 가장 큰 문제는 지연 시간과 실패
- Istio에서 로드 밸런싱과 지역을 사용해 이런 문제를 완화하는 방법
- 이 네트워크 호출이 너무 길면 어떻게 되는가?
- 또는 지연 시간이나 다른 네트워크 요인 때문에 간간이 실패한다면?
- Istio는 이런 문제를 해결하는 데 어떻게 도움이 될 수 있을까?
- Istio를 사용하면 다양한 종류의 타임아웃과 재시도를 설정해 네트워크에 내재된 신뢰성 문제를 극복할 수 있음
- 6.4.1 Timeouts: 지연 시간
- 지연 시간: 분산 환경에서 가장 다루기 어려운 시나리오 중 하나
- 처리 속도가 느려지면
→ 리소스를 오래 들고 있음
→ 서비스에서는 처리해야 할 작업이 적체
→ 연쇄 장애 상황까지 이어질 수 있음 - 예기치 못한 시나리오 방지 방안
- 커넥션이나 요청, 혹은 둘 다에서 타임아웃 구현이 필요.
- 서비스 호출 사이의 타임아웃이 서로 상호작용하는 방법 (중요)
- ex. 서비스 A가 서비스 B를 호출할 때 타임아웃은 1초
- 서비스 B가 서비스 C를 호출할 때 타임이웃은 2초
- 어떤 타임아웃이 먼저 작동하는가?
- 가장 제한적인 타임아웃이 먼저 동작하므로, 서비스 B에서 서비스 C로의 타임아웃은 발동하지 않을 것
- 일반적으로 아키텍처의 가장자리(트래픽이 들어오는 곳)에 가까울수록 타임아웃이 길고
호출 그래프의 계층이 깊을수록 타임아웃이 짧은(혹은 더 제한적인)것이 합리적 - 통상, 밖 → 안, backend에 위치할 수록 timeout 을 짧게 설정
- Istio를 사용해 타임아웃 정책을 제어 (실습)
- 환경을 재설정
kubectl apply -f ch6/simple-web.yaml -n istioinaction kubectl apply -f ch6/simple-backend.yaml -n istioinaction kubectl delete destinationrule simple-backend-dr -n istioinaction
- 호출 테스트 : simple-backend-1을 1초 delay로 응답
# 호출 테스트 : 보통 10~20ms 이내 걸림 curl -s http://simple-web.istioinaction.io:30000 | jq .code time curl -s http://simple-web.istioinaction.io:30000 | jq .code for in in {1..10}; do time curl -s http://simple-web.istioinaction.io:30000 | jq .code; done # simple-backend-1를 1초 delay로 응답하도록 배포 cat ch6/simple-backend-delayed.yaml kubectl apply -f ch6/simple-backend-delayed.yaml -n istioinaction kubectl exec -it deploy/simple-backend-1 -n istioinaction -- env | grep TIMING TIMING_VARIANCE=10ms TIMING_50_PERCENTILE=150ms # 동작 중 파드에 env 직접 수정.. kubectl exec -it deploy/simple-backend-1 -n istioinaction -- sh ----------------------------------- export TIMING_50_PERCENTILE=1000ms exit ----------------------------------- # 호출 테스트 : simple-backend-1로 로드밸런싱 될 경우 1초 이상 소요 확인 for in in {1..10}; do time curl -s http://simple-web.istioinaction.io:30000 | jq .code; done ... curl -s http://simple-web.istioinaction.io:30000 0.01s user 0.01s system 6% cpu 0.200 total jq .code 0.00s user 0.00s system 3% cpu 0.199 total 500 ...
호출 테스트 : 보통 10~20ms 이내 호출 테스트 : simple-backend-1로 로드밸런싱 될 경우 1초 이상 소요 확인 - Istio 는 지연시간 급증에 대비해 VirtualService 리소스로 요청별로 타임아웃 지정 가능
- ex. 메시 내 클라이언트에서 simple-backend 로 향하는 호출의 타임아웃을 0.5초로 지정
# cat ch6/simple-backend-vs-timeout.yaml apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: simple-backend-vs spec: hosts: - simple-backend http: - route: - destination: host: simple-backend timeout: 0.5s kubectl apply -f ch6/simple-backend-vs-timeout.yaml -n istioinaction # kubectl get vs -n istioinaction NAME GATEWAYS HOSTS AGE simple-backend-vs ["simple-backend"] 14s simple-web-vs-for-gateway ["simple-web-gateway"] ["simple-web.istioinaction.io"] 6h11m # 호출 테스트 : 0.5s 이상 걸리는 호출은 타임아웃 발생 (500응답) for in in {1..10}; do time curl -s http://simple-web.istioinaction.io:30000 | jq .code; done ... curl -s http://simple-web.istioinaction.io:30000 0.01s user 0.01s system 2% cpu 0.537 total jq .code 0.00s user 0.00s system 0% cpu 0.535 total 500 ... # istio-proxy config 에서 위 timeout 적용 envoy 설정 부분 찾아 두자.
virtualservice 로 타임아웃 지정(생성) 확인
- 환경을 재설정
- 6.4.2 Retries* : 재시도
- 설정 초기화
kubectl apply -f ch6/simple-web.yaml -n istioinaction kubectl apply -f ch6/simple-backend.yaml -n istioinaction
- 서비스를 호출할 때 간간이 네트워크 실패를 겪는다면, 애플리케이션이 요청을 재시도하길 원할 수 있다.
- 요청을 재시도하지 않으면, 서비스가 흔히 발새하고 예견할 수 있는 실패에 취약해져 사용자에게 좋지 않은 경험을 제공할 수 있다.
- 한편으로 무분별한 재시도는 연쇄 장애를 야기하는 등 시스템 상태를 저하시킬 수 있으므로 적절히 균형을 맞춰야 한다.
- 서비스가 실제로 과부화된 경우에는 재시도해봐야 문제가 악화시킬 뿐이다.
- 이스티오의 재시도 옵션을 살펴보자.
- 이스티오에서는 재시도가 기본적으로 활성화돼 있고, 두 번까지 재시도한다.
- 동작을 세밀하게 튜닝하기에 앞서 기본 동작을 이해해야 한다.
- 시작하기 위해 예제 애플리케이션에서 기본적인 재시도를 비활성화하자.
- VirtualService 리소스에서 최대 재시도를 0으로 설정하면 된다.
# docker exec -it myk8s-control-plane bash ---------------------------------------- # Retry 옵션 끄기 : 최대 재시도 0 설정 istioctl install --set profile=default --set meshConfig.defaultHttpRetryPolicy.attempts=0 y exit ---------------------------------------- # 확인 kubectl get istiooperators -n istio-system -o yaml ... meshConfig: defaultConfig: proxyMetadata: {} defaultHttpRetryPolicy: attempts: 0 enablePrometheusMerge: true ... # istio-proxy 에서 적용 부분은?
Retry 옵션 끄기: 최대 재시도 0 설정
- 에러 발생 시 재시도 실습
- 이제 주기적으로(75%) 실패하는 simple-backend 서비스 버전을 배포해보자.
- 이 경우 엔드포인트 셋 중 하나(simple-backend-1)는 그림 6.12처럼 호출 중 75%에 HTTP 503 반환한다
ERROR Type, Rate, Code 설정 및 호출테스트 50x 응답
- 기본적으로, 이스티오는 호출이 실패하면 두 번 더 시도한다. 이 기본 재시도는 특정 상황에서만 적용된다.
- 일반적으로는 이들 기본 상황에서는 재시도해도 안전하다.
- 이 상황들은 네트워크 커넥션이 수립되지 않아 첫 시도에서 요청이 전송될 수 없음을 의미하기 때문이다.
- 커넥션 수립 실패 connect-failure
- 스트림 거부됨 refused-stream
- 사용 불가 gRPC 상태 코드 14
- 취소됨 gRPC 상태 코드 1
- 재시도할 수 있는 상태 코드들 이스티오에서 기본값은 HTTP 503
- 우리는 앞서 살펴본 설정으로 기본 재시도 정책을 비활성화했다.
- 다음 VirtualService 리소스를 사용해 simple-backend 로 향하는 호출에 재시도를 2회로 명시적으로 설정해보자.
# cat ch6/simple-backend-enable-retry.yaml apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: simple-backend-vs spec: hosts: - simple-backend http: - route: - destination: host: simple-backend retries: attempts: 2 kubectl apply -f ch6/simple-backend-enable-retry.yaml -n istioinaction # docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/simple-web.istioinaction --name 80 docker exec -it myk8s-control-plane istioctl proxy-config routes deploy/simple-web.istioinaction --name 80 -o json ... "name": "simple-backend.istioinaction.svc.cluster.local:80", "domains": [ "simple-backend.istioinaction.svc.cluster.local", "simple-backend", "simple-backend.istioinaction.svc", "simple-backend.istioinaction", "10.200.1.161" ], "routes": [ { "match": { "prefix": "/" }, "route": { "cluster": "outbound|80||simple-backend.istioinaction.svc.cluster.local", "timeout": "0s", "retryPolicy": { "retryOn": "connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes", "numRetries": 2, ... # 호출테스트 : 모두 성공! # simple-backend-1 --(503, retry 후 정상 응답)--> simple-web --> curl(외부) for in in {1..10}; do time curl -s http://simple-web.istioinaction.io:30000 | jq .code; done # app, istio-proxy log 에서 503 로그 확인해보자.
- 앞에서 봤듯이, 실패는 있지만 호출자에게는 드러나지 않는다. 이스티오의 재시도 정책을 활성화해 이런 오류를 우회하게끔 했기 때문이다.
- HTTP 503은 기본적으로 재시도할 수 있는 상태 코드 중 하나다.
- 다음 VirtualService 재시도 정책은 설정할 수 있는 재시도 파라미터를 보여준다.
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: simple-backend-vs spec: hosts: - simple-backend http: - route: - destination: host: simple-backend retries: attempts: 2 # 최대 재시도 횟수 retryOn: gateway-error,connect-failure,retriable-4xx # 다시 시도해야 할 오류 perTryTimeout: 300ms # 타임 아웃 retryRemoteLocalities: true # 재시도 시 다른 지역의 엔드포인트에 시도할지 여부
- 다양한 재시도 설정으로 재시도 동작(얼마나 많이, 얼마나 깊게, 어느 엔드포인트로 재시도할 지)과 어떤 상태 코드일 때 재시도할지를 어느 정도 제어할 수 있다. 상술했듯이 모든 요청을 재시도할 수 있거나 해야 하는 것은 아니다.
- 예를 들어 HTTP 500 코드를 반환하는 simple-backend 서비스를 배포하면, 기본 재시도 동작은 실패를 잡아내지 않는다.
- 503 이외의 다른 에러 발생 시에도 retry 가 동작하는지 확인해보자.
# 500 에러 코드 리턴 cat ch6/simple-backend-periodic-failure-500.yaml ... - name: "ERROR_TYPE" value: "http_error" - name: "ERROR_RATE" value: "0.75" - name: "ERROR_CODE" value: "500" ... kubectl apply -f ch6/simple-backend-periodic-failure-500.yaml -n istioinaction # kubectl exec -it deploy/simple-backend-1 -n istioinaction -- sh --------------------------------------------------------------- export ERROR_TYPE=http_error export ERROR_RATE=0.75 export ERROR_CODE=500 exit --------------------------------------------------------------- # envoy 설정 확인 : 재시도 동작(retryOn) 에 retriableStatusCodes 는 503만 있음. docker exec -it myk8s-control-plane istioctl proxy-config route deploy/simple-web.istioinaction --name 80 -o json ... "route": { "cluster": "outbound|80||simple-backend.istioinaction.svc.cluster.local", "timeout": "0s", "retryPolicy": { "retryOn": "connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes", "numRetries": 2, "retryHostPredicate": [ { "name": "envoy.retry_host_predicates.previous_hosts", "typedConfig": { "@type": "type.googleapis.com/envoy.extensions.retry.host.previous_hosts.v3.PreviousHostsPredicate" } } ], "hostSelectionRetryMaxAttempts": "5", "retriableStatusCodes": [ 503 ] }, "maxGrpcTimeout": "0s" }, ... # 호출테스트 : Retry 동작 안함. # simple-backend-1 --(500, retry 안함)--> simple-web --(500)> curl(외부) for in in {1..10}; do time curl -s http://simple-web.istioinaction.io:30000 | jq .code; done ... curl -s http://simple-web.istioinaction.io:30000 0.01s user 0.01s system 30% cpu 0.036 total jq .code 0.00s user 0.00s system 14% cpu 0.035 total 200 curl -s http://simple-web.istioinaction.io:30000 0.00s user 0.01s system 5% cpu 0.184 total jq .code 0.00s user 0.00s system 2% cpu 0.183 total 500 ...
- HTTP 500은 재시도하는 상태 코드에 포함되지 않는다.
- 모든 HTTP 500 코드(커넥션 수립 실패 및 스트림 거부 포함)를 재시도하는 VirtualService 재시도 정책을 사용해보자.
# cat ch6/simple-backend-vs-retry-500.yaml apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: simple-backend-vs spec: hosts: - simple-backend http: - route: - destination: host: simple-backend retries: attempts: 2 retryOn: 5xx # HTTP 5xx 모두에 재시도 kubectl apply -f ch6/simple-backend-vs-retry-500.yaml -n istioinaction # 호출테스트 : 모두 성공! # simple-backend-1 --(500, retry)--> simple-web --(200)> curl(외부) for in in {1..10}; do time curl -s http://simple-web.istioinaction.io:30000 | jq .code; done
- 사용할 수 있는 retryOn 설정은 엔보이 문서를 참조하자.
https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#x-envoy-retry-on
- 타임아웃에 따른 재시도
- 각 재시도에는 자체적인 제한 시간(perTryTimeout) 이 있다.
- 이 설정에서 주의할 점은 perTryTimeout에 총 시도 횟수를 곱한 값이 전체 요청 제한 시간(이전 절에서 설명)보다 작아야 한다는 것이다.
- perTryTimeout * attempts < overall timeout
- 예를 들어, 총 제한 시간이 1초이고 시도별 제한 시간이 500ms에 3회까지 재시도하는 재시도 정책은 의도대로 동작하지 않는다.
- 재시도를 하기 전에 전체 요청 타임아웃이 발생할 것이다.
- 또 재시도 사이에는 백오프 backoff 지연이 있다는 점도 유념하자.
- perTryTimeout * attempts + backoffTime * (attempts-1) < overall timeout
- 이 백오프 시간도 전체 요청 제한 시간 계산에 포함된다.
- 작동 방식
- 요청이 이스티오 서비스를 서비스 프록시를 거쳐 흐를 때, 업스트림으로 전달되는 데 실패하면 요청을 ‘실패 failed’로 표시하고 VirtualService 리소스에 정의한 최대 재시도 횟수까지 재시도한다.
- 재시도 횟수가 2이면 실제로는 요청이 3회까지 전달되는데, 한 번은 원래 요청이고 두 번은 재시도다.
- 재시도 사이에 이스티오는 25ms 를 베이스로 재시도를 ‘백오프’ 한다.
- 재시도 및 백오프 동작을 설명하는 그림 6.13을 참조하자
요청 실패 시 재시도 요청 흐름 - 즉, 이스티오는 재시도에 시차를 주고자 연속적인 재시도에서 (25ms x 재시도 횟수)까지 백오프한다. (기다린다)
- 현재 재시도 베이스는 고정돼 있다.
- 그러나 다음 절에서 언급하겠지만, 이스티오가 노출하지 않는 엔보이는 API를 바꿀 수 있다.
- 상술했듯이 이스티오의 기본 재시도 횟수는 2회다.
- 시스템 내의 계층이 다르면 재시도 횟수도 다르도록 이 값을 재정의하고 싶을 수도 있다.
- 기본값과 같이 재시도 횟수를 무턱대고 설정하면, 심각한 재시도 ‘천둥 무리 thundering herd’ 문제가 발생할 수 있다. (그림 6.14 참조)
- 에를 들어 서비스 체인이 5단계 깊이로 연결돼 있고 각 단계가 두 번씩 재시도할 수 있다면, 들어오는 요청 하나에 대해 최대 32회의 요청이 발생할 수 있다.
- 체인 끝부분의 리소스에 과부하가 걸린 상태에서는 이 추가적인 부하가 해당 리소스를 감당할 수 없게 만들어 쓰러뜨릴수 있다.
- 이 상황을해결하는 한 가지 방법은 아키텍처 가장자리에서는 재시도 횟수를 1회 내지 0회로 제한하고, 중간 요소는 0회로 하며, 호출 스택 깊숙한 곳에서만 재시도하게 하는 것이다. 하지만 이 방법도 잘 작동하지는 않는다.
- 또 다른 전략은 전체 재시도 횟수에 상한을 두는 것이다.
- 재시도 예산 budget 를 이용해 조절할 수 있는데, 이 기능은 아직 이스티오 API에서 노출되지 않고 있다.
- 이스티오에 이런 문제에 대한 우회로가 있기는 하지만, 이 책의 범위를 벗어나는 내용이다.
- 마지막으로 재시도는 기본적으로 자기 지역의 엔드포인트에 시도한다. retryRemoteLocalities 설정은 이 동작에 영향을 준다.
- true로 설정하면, 이스티오는 재시도가 다른 지역으로 넘어갈 수 있도록 허용한다.
- 이상값 감지가 같은 지역의 엔드포인트가 오작동하고 있음을 알아내기 전에 이 설정이 유용할 수 있다.
6.4.3 Advanced retries : Istio Extension API (EnvoyFilter)