트래픽을 더 많이 부담할 수 있는 v2 Deployment 쪽으로 트래픽 부하가 더 많이 걸리기 때문 입니다.
기본적으로 Istio의 서비스 프록시의 기본설정이ROUND_ROBIN으로 되어있어서
자동으로 부하가 밸런싱 됩니다.
이미ROUND_ROBIN방식으로 기본 설정이 되어있지만,DestinationRule리소스를 사용하여ROUDN_ROBIN전략을 사용하는 방법도 알아보겠습니다.
🔧 DestinationRule 리소스 생성
$ 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 # 엔드포인트 결정을 '순서대로 돌아가며'
$ kubectl apply -f ch6/simple-backend-dr-rr.yaml -n istioinaction
🔧 DestinationRule 생성 확인
$ kubectl get dr -n istioinaction
결과
NAME HOST AGE
simple-backend-dr simple-backend.istioinaction.svc.cluster.local 38s
🔧 50번 간 반복 호출을 해서 부하가 정말 분산 되는지 확인해보겠습니다.
$ 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
결과
32 "Hello from simple-backend-2"
18 "Hello from simple-backend-1"
이제 부하 생성기를 사용해 simple-backend 서비스 지연 시간에 변화를 주어
이런 상황에서 Istio의 어떤 로드 밸런싱 전략이 적합한지 알아보겠습니다.
🔧 먼저 부하 생성기를 사용하기 전 서비스 응답시간 확인
$ 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.19s
user 0m 0.00s
sys 0m 0.00s
real 0m 0.16s
user 0m 0.00s
sys 0m 0.00s
real 0m 0.16s
user 0m 0.00s
sys 0m 0.00s
🔧 Fortio 설치 (Window)
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/
Window 프롬프트 창에서 아래 이미지 처럼 명령어를 실행해주면 웹 UI에 들어갈 수 있습니다.
이렇게 오류를 만든 이유는 실제 운영 환경에서 1초 정도 응답 지연이 생기는 것은 그나마 괜찮은데,
5초, 100초 등 응답 지연 시간이 길어졌을 때, 대처하는 방법을 알아보기 위함 입니다.
서비스를 호출할 때간간이 네트워크 실패를 겪는다면,애플리케이션이 요청을 재시도하길 원할 수 있습니다.
이럴 때 요청을 재시도 하지 않으면, 서비스가 흔히 발생하고 예견할 수 있는 실패에 취약해져 사용자에게 좋지 못한 경험을 제공할 수 있습니다.
그렇다고 무분별한 재시도는 연쇄 장애를 야기할 수 있습니다.
이럴 때 어떻게 대처를 해야하는 지 알아보겠습니다.
먼저 Istio에서 기본적으로 retry하는 횟수를 0으로 해주겠습니다.
🔧 k8s 클러스터 접속
$ docker exec -it myk8s-control-plane bash
----------------------------------------
🔧 Retry 옵션 끄기 : 최대 재시도 0 설정
$ istioctl install --set profile=default --set meshConfig.defaultHttpRetryPolicy.attempts=0
$ y
exit
----------------------------------------
🔧 재시도 횟수 0 인지 확인
$ kubectl get istiooperators -n istio-system -o yaml
...
meshConfig:
defaultConfig:
proxyMetadata: {}
defaultHttpRetryPolicy:
attempts: 0
enablePrometheusMerge: true
...
에러 발생 시 재시도 실습
이제 주기적으로(75%) 실패하는 simple-backend-1 서비스 버전을 배포해보겠습니다.
이 경우 엔드포인트 셋 중 하나(simple-backend-1)는 그림 6.12처럼 호출 중 75%에 HTTP 503 반환합니다.
🔧 75% 실패 하는 simple-backend Deployment를 생성해줍니다.
$ cat ch6/simple-backend-periodic-failure-503.yaml
...
- name: "ERROR_TYPE"
value: "http_error"
- name: "ERROR_RATE"
value: "0.75"
- name: "ERROR_CODE"
value: "503"
...
$ kubectl apply -f ch6/simple-backend-periodic-failure-503.yaml -n istioinaction
🔧 simple-backend-1 파드의 환경변수 확인
$ kubectl exec -it simple-backend-1-bcf648d9c-nhxxr -- env|grep ERROR
결과
ERROR_TYPE=http_error
ERROR_RATE=0.75
ERROR_CODE=503
🔧 호출테스트 : simple-backend-1 호출 시 failures (500) 발생
🔧 simple-backend-1 --(503)--> 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 6% cpu 0.200 total
jq .code 0.00s user 0.00s system 3% cpu 0.199 total
500
...
현 상황을 설명하자면,
🧩 상황 설명
지금 웹 서비스simple-web이 다른 서비스simple-backend한테 "야! 대답 좀 해줘!" 라고 요청을 보내는 상황이에요.
그런데 simple-backend가 너무 느리게 대답하거나, 아예 말이 안 통할 때가 있어요.
📸 첫 번째 그림 (503 오류):
"말은 들리는데 너무 느려서 짜증 나!" simple-backend는 대답은 할 수 있어요, 그런데 너~무 느리게 대답하고 있어서 simple-web이 기다리다 지쳐서 포기해요.
그래서 나타나는 오류가 503 Service Unavailable 이에요. 👉 "서비스가 지금 이용 불가해요" 라는 뜻이에요.
📸 두 번째 그림 (500 오류):
"말이 아예 안 통해요..." 이번엔 아예 simple-backend랑 통신 자체가 안 됐어요. 예를 들어:
네트워크가 끊겼거나,
서비스가 죽었거나,
gRPC 같은 기술에서 스트림이 거부됐거나 등등
그래서 simple-web이 "이건 내 잘못이 아니야, 진짜 못 들은 거야!" 하면서 500 Internal Server Error을 반환하는 거예요.
Istio는 이 상황에서 뭘 해주냐면? 기본적으로 1번 실패하면 2번까지 재시도해요.
다만 저희는 앞서 Istio의 기본 정책에서 재시도 횟수를0으로 설정해주었습니다.
이제 VirtualService 리소스를 사용하여 재시도 횟수를2번으로 설정해주겠습니다.
🔧 simple-backend 서비스로 트래픽을 보냈을 때 오류가 나올 시 2번 재시도 하도록 VS에서 설정
$ 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
🔧 호출 테스트
$ for in in {1..10}; do time curl -s http://simple-web.istioinaction.io:30000 | jq .code; done
결과
200 10번 모두 성공
kiail Graph에서 확인해봐도 모두 200 code가 나오는 것을 확인 할 수 있습니다.
HTTP 503은 기본적으로 재시도 할 수 있는 상태 코드 중 하나 입니다.
저희는 VirtualService 리소스를 사용하여 재시도 동작을 어떤 상태 코드 일 때, 몇번 재시도 할지 정해줄 수 있습니다.
따라서 현재는 503 코드 반환 시 재시도 동작을 시도하는데, 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 simple-backend-1-668d8fd46b-tpvcg -- env|grep ERROR
결과
ERROR_TYPE=http_error
ERROR_RATE=0.75
ERROR_CODE=500
🔧 Envoy 설정 확인: 503 코드 에러시 retry 2번 하도록 설정되어있음
$ 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"
},
🔧 호출 테스트
$ for in in {1..1000}; do time curl -s http://simple-web.istioinaction.io:30000 | jq .code; done
이제 500 코드도 재시도를 할 수 있도록 VirtualService 설정 해보도록 하겠습니다.
🔧 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
🔧 호출 테스트
$ for in in {1..1000}; do time curl -s http://simple-web.istioinaction.io:30000 | jq .code; done
그러면 200 코드가 100% 반환 되는 것을 확인 할 수 있습니다.
Advanced retries : Istio Extension API (EnvoyFilter)
EnvoyFilter는 Istio에서 Envoy Proxy의 설정을 직접 수정할 수 있게 해주는 특수한 리소스 입니다.
📌 이걸 왜 쓰냐면?
기본 VirtualService 설정만으로는 세밀한 조건 지정이 불가능하기 때문
예: 특정 HTTP 헤더가 있을 때만 재시도한다거나, 특정 gRPC 상태 코드에서만 재시도하고 싶을 때
🔧 EnvoyFilter API 예시
$ cat ch6/simple-backend-ef-retry-status-codes.yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: simple-backend-retry-status-codes
namespace: istioinaction
spec:
workloadSelector:
labels:
app: simple-web
configPatches:
- applyTo: HTTP_ROUTE
match:
context: SIDECAR_OUTBOUND
routeConfiguration:
vhost:
name: "simple-backend.istioinaction.svc.cluster.local:80"
patch:
operation: MERGE
value:
route:
retry_policy:
retry_back_off:
base_interval: 50ms # 기본 간격을 늘린다
retriable_status_codes: # 재시도할 수 있는 코드를 추가한다
- 408
- 400
- simple-web 이라는 애플리케이션이 simple-backend를 호출할 때,
- 특정 HTTP 상태 코드(408, 400)가 발생하면
- 자동으로 재시작 하도록 Envoy Proxy의 동작을 설정
다만 이렇게 되면 VirtualService를 사용해도 되는데 왜 EnvoyFilter를 사용하냐?
일반적인 트래픽 제어는VirtualService만으로도 충분합니다.
하지만,재시작할 오류 코드가 특이하거나재시도 전략을 더 정밀하게 조정해야할 때는EnvoyFilter를 사용해야합니다.
비 표준 에러코드인 408 에러코드 발생 시 재시도 할 수 있도록 실습 해보겠습니다.
🔧 EnvoyFilter 설정
$ cat ch6/simple-backend-ef-retry-status-codes.yaml
...
patch:
operation: MERGE
value:
route:
retry_policy: # 엔보이 설정에서 직접 나온다?
retry_back_off:
base_interval: 50ms # 기본 간격을 늘린다
retriable_status_codes: # 재시도할 수 있는 코드를 추가한다
- 408
- 400
$ kubectl apply -f ch6/simple-backend-ef-retry-status-codes.yaml -n istioinaction
🔧 EnvoyFilter 생성 되었는지 확인
$ kubectl get envoyfilter -n istioinaction
결과
NAME AGE
simple-backend-retry-status-codes 12s
🔧 라우팅 경로 지정을 위한 VirtualService 설정
$ cat ch6/simple-backend-vs-retry-on.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,retriable-status-codes # retryOn 항목에 retriable-status-codes 를 추가
$ kubectl apply -f ch6/simple-backend-vs-retry-on.yaml -n istioinaction
🔧 retry code 확인
$ 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": "5xx,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": [
408,
400
],
"retryBackOff": {
"baseInterval": "0.050s"
}
},
"maxGrpcTimeout": "0s"
}
🔧 호출 테스트
$ for in in {1..10}; do time curl -s http://simple-web.istioinaction.io:30000 | jq .code; done
결과
200 코드 10번 반환 됩니다.
🧠 요청 헤징(Request Hedging)이란?
요청 헤징은 요청이 타임아웃되면 다른 호스트로도 요청을 보내 원래의 타임아웃된 요청과 경쟁 시키는 것을 말합니다.
즉, 요청을 한 번 보냈는데 응답이 느려지면, → 같은 요청을 다른 서버로 한 번 더 보내서 → 더 빨리 응답 오는 쪽을 선택하는 방식입니다.
타임아웃과 재시도를 잘못 설정하면 시스템 아키텍처에 의도치 않은 동작을 증폭시켜 시스템을 과부하시키고 연쇄 장애를 일으킬 수 있습니다.
이제 마지막으로 부하를 더 늘리는 대신에 업스트림 시스템이 복구될 수 있도록(회복 시간 벌기) 부하를 잠시 제한할 수 있으며, 이러한 기능을 사용하기 위한
서킷 브레이커에 대해 알아보겠습니다.
Circuit breaking with Istio
서킷 브레이커 기능을 사용하면 부분적이거나연쇄적인 장애를 방지할 수 있습니다.
서킷 브레이커에 대해 간단하게 설명을 해보자면, ⚡️ “문제가 있는 서비스에 계속 요청하지 마! 잠깐 끊자!”
어떤 서비스가 너무 자주 실패하면
일정 시간 동안 그 서비스로 가는 요청을 아예 막아버립니다.
회복될 때까지 기다렸다가, 다시 연결을 시도합니다.
즉, 서비스 A가 서비스 B에게 계속 요청을 보내는데 서비스 B가 오류를 자주 일으키거나 응답이 매우 느리다면?
👉 A는 계속 기다리고, 결국 전체 시스템이 느려지고 병목 현상 발생!
그래서 "B가 지금 상태 안 좋으니까 잠깐 요청하지 말자" 라는 로직을 넣는 거예요. 이게 바로 서킷 브레이커입니다.
서킷 브레이커의 동작 방식으로 2가지가 있는데,
첫 번째는 특정 서비스로의커넥션및미해결 요청 개수를 얼마나허용할지 관리하는 것이다.
어떤 서비스에 진행 중인 요청이 10개이고 수신 부하가 동일한데 그 수가 계속 증가하고 있다면, 요청을 더 보내는 것은 의미가 없습니다.
요청을 더 보내면 업스트림 서비스가 압도 될 수 있습니다.
이스티오에서는DestinationRule의connectionPool설정을 사용해서비스 호출 시 누적될 수 있는 커넥션및요청 개수를 제한할 수 있습니다.
요청이 너무 많이 쌓이면, 요청을 단락(빠르게 실패)시키고클라이언트에 반환할 수 있다.
-두 번째방법은 로드 밸런싱 풀의엔드포인트에 상태를 관찰해오동작하는 엔드포인트를잠시 퇴출시키는 것 입니다.
서비스 풀의 특정 호스트에 문제가 발생하면 그 호스트로의 트래픽 전송을 건너뛸 수 있습니다.
이제 실습을 통해 알아보겠습니다.
🔧 일단 simple-backend-2 복제본을 0으로 변경
$ kubectl scale deploy simple-backend-2 --replicas=0 -n istioinaction
🔧 응답 지연(1초)을 발생하는 simple-backend-1 배포
$ kubectl apply -f ch6/simple-backend-delayed.yaml -n istioinaction
🔧 destinationrule 삭제
$ kubectl delete destinationrule --all -n istioinaction
이제 커넥션 및 요청 제한을 도입하고 어떤 일이 일어나는지 확인해보겠습니다.
🔧 DestinationRule 적용
$ cat ch6/simple-backend-dr-conn-limit.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: simple-backend-dr
spec:
host: simple-backend.istioinaction.svc.cluster.local
trafficPolicy:
connectionPool:
tcp:
maxConnections: 1 # 커넥션 총 개수 Total number of connections
http:
http1MaxPendingRequests: 1 # 대기 중인 요청 Queued requests
maxRequestsPerConnection: 1 # 커넥션당 요청 개수 Requests per connection
maxRetries: 1 # Maximum number of retries that can be outstanding to all hosts in a cluster at a given time.
http2MaxRequests: 1 # 모든 호스트에 대한 최대 동시 요청 개수 Maximum concurrent requests to all hosts
maxConnections: 커넥션 총 개수, 커넥션 오버플로 connection overflow 를 보고할 임계값이다.
이스티오 프록시(엔보이)는 이 설정에 정의된 상한까지 서비스 요청에 커넥션을 사용한다.
실제 커넥션 최댓값은 로드 밸런싱 풀의 엔드포인트 개수에 설정값을 더한 숫자다.
이 값을 넘길 때마가 엔보이는 자신의 메트릭에 그 사실을 보고한다.
http1MaxPendingRequests: 대기 중인 요청, 사용할 커넥션이 없어 보류 중인 요청을 얼마나 허용할지를 의미하는 숫자다.
http2MaxRequests: 모든 호스트에 대한 최대 동시 요청 개수, 안타깝게도 이 설정은 이스티오에서 이름을 잘못 붙였다.
내부적으로 이 숫자는 클러스터 내 모든 엔드포인트/호스트에 걸쳐 있는 병렬 요청의 최대 개수를 제어하는데, HTTP 2인지 HTTP 1.1인지는 상관없다.
🔧 Fortio를 사용하여 테스트
$ fortio load -quiet -jitter -t 30s -c 1 -qps 1 --allow-initial-errors http://simple-web.istioinaction.io:30000
결과
Sockets used: 1 (for perfect keepalive, would be 1)
Uniform: false, Jitter: true, Catchup allowed: false
IP addresses distribution:
127.0.0.1:30000: 1
Code 200 : 30 (100.0 %)
🔧 로드 테스크 도구가 2개의 커넥션에서 요청을 초당 하나씩 보내는 것으로 설정
$ fortio load -quiet -jitter -t 30s -c 2 -qps 2 --allow-initial-errors http://simple-web.istioinaction.io:30000
결과
Sockets used: 2 (for perfect keepalive, would be 2)
Uniform: false, Jitter: true, Catchup allowed: false
IP addresses distribution:
127.0.0.1:30000: 2
Code 200 : 31 (63.3 %)
Code 500 : 18 (36.7 %)