스터디/Istio

Istio 스터디 3주차 - <Resilience>

황동리 2025. 4. 25. 00:51
반응형

CloudNet@ 가시다님이 진행하는 Istio 스터디 1기 - 3주차 정리 내용 입니다.

 

앞서 진행했던 주차를 보고 오시면 이해가 빠릅니다.

 

이번 글에서는 Istio의 Resilience(복원력) 에 대해서 알아보고 실습 해보도록 하겠습니다.

 


✅ Resilience (복원력)

❗복원력이 중요한 이유
마이크로서비스에서는 수 많은 파드들이 존재하고 언제든 예기치 않게 실패할 수 있으며, 사람의 손으로 즉각 대응하기 어렵습니다.

따라서 이를 대처하기 위해 우리는 Istio를 사용하고,
Istio를 사용하면 타임아웃, 재시도, 서킷 브레이커 같은 복원력 기능을 코드 수정 없이 애플리케이션에 적용할 수 있어, 자동 복구 가능한 인프라를 구현하는 데 큰 도움이 됩니다.

실습을 통해 좀 더 자세히 알아보겠습니다.

🛠️ 실습 환경 구성

🔧 kind 사용하여 k8s 클러스터 구성
$ git clone https://github.com/AcornPublishing/istio-in-action
$ cd istio-in-action/book-source-code-master
$ pwd # 각자 자신의 pwd 경로
$ kind create cluster --name myk8s --image kindest/node:v1.23.17 --config - <> /root/.bashrc
$ curl -s -L https://istio.io/downloadIstio | ISTIO_VERSION=$ISTIOV sh -
$ cp istio-$ISTIOV/bin/istioctl /usr/local/bin/istioctl
$ istioctl version --remote=false
$ istioctl install --set profile=default -y
$ kubectl apply -f istio-$ISTIOV/samples/addons
$ exit

🔧 istio-proxy 로그 출력 설정 변경
$ KUBE_EDITOR="vi"  kubectl edit cm -n istio-system istio
----
  mesh: |-
    accessLogFile: /dev/stdout
----

🔧 실습을 위한 네임스페이스 설정
$ kubectl create ns istioinaction
$ kubectl label namespace istioinaction istio-injection=enabled
$ kubectl get ns --show-labels

결과
NAME                 STATUS   AGE     LABELS
istioinaction        Active   11s     istio-injection=enabled,kubernetes.io/metadata.name=istioinaction

🔧 istio-ingressgateway 서비스 Nodeport로 변경
$ kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec": {"type": "NodePort", "ports": [{"port": 80, "targetPort": 8080, "nodePort": 30000}]}}'
$ kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec": {"type": "NodePort", "ports": [{"port": 443, "targetPort": 8443, "nodePort": 30005}]}}'
$ kubectl patch svc -n istio-system istio-ingressgateway -p '{"spec":{"externalTrafficPolicy": "Local"}}'

🔧 AddOn 서비스들 NodePort 변경 및 nodeport 30001~30003으로 변경 : prometheus(30001), grafana(30002), kiali(30003), tracing(30004)
$ kubectl patch svc -n istio-system prometheus -p '{"spec": {"type": "NodePort", "ports": [{"port": 9090, "targetPort": 9090, "nodePort": 30001}]}}'
$ kubectl patch svc -n istio-system grafana -p '{"spec": {"type": "NodePort", "ports": [{"port": 3000, "targetPort": 3000, "nodePort": 30002}]}}'
$ kubectl patch svc -n istio-system kiali -p '{"spec": {"type": "NodePort", "ports": [{"port": 20001, "targetPort": 20001, "nodePort": 30003}]}}'
$ kubectl patch svc -n istio-system tracing -p '{"spec": {"type": "NodePort", "ports": [{"port": 80, "targetPort": 16686, "nodePort": 30004}]}}'

🔧 접속 테스트용 netshoot 파드 생성
$ cat <

Client-side LoadBalancing 실습

클라이언트 측 로드밸런싱 이란?

엔드포인트 간에 요청을 최적으로 분산시키기 위해 여러 엔드포인트를 클라이언트에게 알려주고 클라이언트가 특정 로드 밸런싱 알고리즘을 선택 하게 하는 방식입니다.

장점으로는,

  • 병목 현상과 장애 지점을 만들 수 있는 중앙 집중식 로드 밸런싱 의존성 부담이 줄어듭니다.
  • 클라이언트가 여러 홉을 거치지 않고 특정 엔드포인트로 바로 가기 때문에 요청 속도가 더 빠릅니다.

이제 실습을 통해서 좀 더 자세히 알아보겠습니다.

🔧 Web, Backend-v1,v2 Deployment, service 배포
$ kubectl apply -f ch6/simple-backend.yaml -n istioinaction
$ kubectl apply -f ch6/simple-web.yaml -n istioinaction

🔧 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

🔧 Envoy 프록시 생성확인
$ docker exec -it myk8s-control-plane istioctl proxy-status

결과
NAME                                                  CLUSTER        CDS        LDS        EDS        RDS        ECDS         ISTIOD                      VERSION
istio-ingressgateway-996bc6bb6-plmnl.istio-system     Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-szwhn     1.17.8
simple-backend-1-7449cc5945-5qqhg.istioinaction       Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-szwhn     1.17.8
simple-backend-2-6876494bbf-hvrj9.istioinaction       Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-szwhn     1.17.8
simple-backend-2-6876494bbf-jbb2p.istioinaction       Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-szwhn     1.17.8
simple-web-7cd856754-wx9qs.istioinaction              Kubernetes     SYNCED     SYNCED     SYNCED     SYNCED     NOT SENT     istiod-7df6ffc78d-szwhn     1.17.8

🔧 도메인 질의를 위한 DNS 설정
$ echo "127.0.0.1       simple-web.istioinaction.io" | sudo tee -a /etc/hosts

🔧 호출 테스트
$ curl -s http://simple-web.istioinaction.io:30000

결과
{
  "name": "simple-web",
  "uri": "/",
  "type": "HTTP",
  "ip_addresses": [
    "10.10.0.12"
  ],
  "start_time": "2025-04-24T00:27:47.621019",
  "end_time": "2025-04-24T00:27:47.814305",
  "duration": "193.286027ms",
  "body": "Hello from simple-web!!!",
  "upstream_calls": [
    {
      "name": "simple-backend",
      "uri": "http://simple-backend:80/",
      "type": "HTTP",
      "ip_addresses": [
        "10.10.0.15"
      ],
      "start_time": "2025-04-24T00:27:47.642918",
      "end_time": "2025-04-24T00:27:47.793419",
      "duration": "150.501056ms",
      "headers": {
        "Content-Length": "281",
        "Content-Type": "text/plain; charset=utf-8",
        "Date": "Thu, 24 Apr 2025 00:27:47 GMT",
        "Server": "envoy",
        "X-Envoy-Upstream-Service-Time": "165"
      },
      "body": "Hello from simple-backend-1",
      "code": 200
    }
  ],
  "code": 200
}

🔧 트래픽 흐름을 확인하기 위해 반복 호출 시켜줍니다.
$ 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

🔧 simple-web 앱의 모든 컨테이너 로그를 istioinaction 네임스페이스에서 실시간으로 출력합니다.
$ kubectl logs -l app=simple-web -n istioinaction

🔧 simple-web 앱의 istio-proxy 사이드카 컨테이너 로그만 실시간으로 출력합니다.
$ kubectl logs -l app=simple-web -n istioinaction -c simple-web

🔧 simple-backend 앱의 모든 컨테이너 로그를 istioinaction 네임스페이스에서 실시간으로 출력합나다.
$ kubectl logs -l app=simple-backend -n istioinaction

🔧 simple-backend 앱의 istio-proxy 사이드카 컨테이너 로그만 실시간으로 출력합니다.
$ kubectl logs -l app=simple-backend -n istioinaction -c istio-proxy

🔧 simple-backend 앱의 메인 컨테이너인 simple-backend의 로그만 실시간으로 출력합니다.
$ kubectl logs -l app=simple-backend -n istioinaction -c simple-backend

kiail Graph로 트래픽 분산 비중을 확인해보면 3:7 정도로 나옵니다.

왜냐하면, simple-backend-v1,v2 Deployment를 배포했는데, v1 레플리카셋을 1개 v2 레플리카셋은 2개로 설정해두어서

트래픽을 더 많이 부담할 수 있는 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에 들어갈 수 있습니다.

그리고 URL에 http://simple-web.istioinaction.io:30000 입력
그리고 해당 도메인을 찾기 위해 resolve에 127.0.0.1 입력

🔧 Fortio에서 서비스 호출 확인

결과
13:14:36.096 r66 [INF]> UI, method="GET", url="/fortio/?labels=Fortio&url=http%3A%2F%2Fsimple-web.istioinaction.io%3A30000&qps=100&t=3s&n=&c=10&log-errors=on&connection-reuse-range-min=&connection-reuse-range-max=&connection-reuse-range-value=&uniform=on&nocatchup=on&p=50%2C+75%2C+90%2C+99%2C+99.9&r=0.0001&X=&H=&payload=&runner=http%2Ftcp%2Fudp&stdclient=on&resolve=127.0.0.1&grpc-ping-delay=0&healthservice=&save=on&timeout=750ms&load=Start", host="localhost:8080", proto="HTTP/1.1", remote_addr="[::1]:14804", user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", status=200, size=8237, microsec=3412832

🖥️ Testing various client-side load-balancing strategies

위 그림에 나온 것 처럼 테스트를 진행해보도록 하겠습니다.

목표

  • Fortio를 사용해 60초 동안 10개의 커넥션을 통해 초당 1000개의 요청을 보냅니다.
  • Fortio는 각 호출의 지연 시간을 추적하고 지연 시간 백분위수 분석과 함께 히스토그램에 표시합니다.
  • 테스트를 하기 전에 지연 시간을 1초까지 늘린 simple-backend-1 서비스를 도입합니다.
  • 이는 엔드포인트 중 하나에 긴 가비지 컬렉션 이벤트 또는 기타 애플리케이션 지연 시간이 발생한 상황을 시뮬레이션 합니다.
  • 우리는 로드 밸런싱 전략을 라운드 로빈, 랜덤, 최소 커넥션으로 바꿔가면서 차이점을 관찰해보겠습니다.

이제 실습 해보겠습니다.

🔧 지연된 simple-backend-1 서비스 배포
$ 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 simple-backend-1-66d748dd67-f9w9w -n istioinaction -- env | grep TIMING

결과
TIMING_50_PERCENTILE=1000ms
TIMING_VARIANCE=10ms

🔧 호출 테스트를 통해 실제 지연시간이 1초가 걸리는 지 확인
$ curl -s http://simple-web.istioinaction.io:30000 | grep duration

결과
  "duration": "1.007351013s",
      "duration": "1.000460061s"

이제 Fortio를 사용해서 확인해보겠습니다.

웹 UI에 접속해서 아래 정보로 넣어줍니다.

웹 UI에서 넣은 정보에 대한 설명을 간략하게 하면 아래와 같습니다.

URL: http://simple-web.istioinaction.io:30000
QPS (초당 요청 수): 1000
Duration (지속 시간): 60초
동시 접속 수: 10 threads
Timeout: 2000ms
Jitter 사용: ✅ (랜덤한 요청 간격)

정보들을 모두 입력해주고 Start 누르면 아래와 같이 그래프로 나옵니다.

RANDOM 로드 밸런싱 알고리즘 지연 시간 결과
=> 75분위 수에서 응답이 1초 이상으로 RoundRobin과 비슷함

응답 시간 분포 (예시)
|────────────|──────────────|──────────────|─────────────|────────────|
P50         P75            P90           P99          P99.9
0.189s     1.010s         1.025s        1.034s       1.035s


# target 50% 0.189556
# target 75% 1.01012
# target 90% 1.02562
# target 99% 1.03492
# target 99.9% 1.03585

이제 로드 밸런싱 알고리즘을 Least Connection으로 변경하고 다시 테스트 해보겠습니다.

🔧 DestinationRule 변경
$ 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 # ROUND_ROBIN에서 변경

$ kubectl apply -f ch6/simple-backend-dr-least-conn.yaml -n istioinaction

🔧 ROUND_ROBIN -> LEAST_CONN 으로 설정이 변경되었는지 확인
$ kubectl get destinationrule simple-backend-dr -n istioinaction \
 -o jsonpath='{.spec.trafficPolicy.loadBalancer.simple}{"\n"}'
 
 결과
 LEAST_CONN

앞서 진행했던 동일한 조건으로 Fortio 테스트 진행

결과 값을 확인해보면 아래와 같이 나옵니다.

# target 50% 0.181141
# target 75% 0.194231
# target 90% 1.0082
# target 99% 1.02674
# target 99.9% 1.0286

앞서 ROUND_ROBIN 정책으로 로드 밸런싱을 했을 때 보다 응답 성능이 좋아졌습니다.

표로 살펴보면 응답 시간 및 처리 요청 수도 늘어난 것을 확인 할 수 있습니다.

ㅂㅈㄷㅂㅈㄷㅂㅈㄷ

항목 ROUND_ROBIN LEAST_CONN
처리 요청 수 1322건 2174건 ✅
QPS 21.7 35.6 ✅
평균 응답 시간 456ms 278ms ✅
P75 1.01s 0.194s ✅

실습한 내용들을 정리해보면 다음과 같습니다.

✅ 로드 테스트 요약 정리

📌 테스트 목적

다양한 로드 밸런싱 알고리즘이 실제 서비스 환경에서 지연 시간(latency) 성능(throughput)에 어떤 영향을 미치는지를 분석하기 위해 Fortio를 사용해 테스트를 수행함.

첫번째, ROUND_ROBIN / RANDOM

매우 간단한 알고리즘

각각의 요청을 엔드포인트에 순차적으로 또는 무작위로 분배

💬 장점:

  • 구현이 쉽다.
  • 기본적인 부하 분산에는 유용함

⚠️ 단점:

  • 각 엔드포인트의 실제 상태(지연, CPU 사용량 등)를 고려하지 않음
  • 엔드포인트 간 성능이 다를 경우 느린 쪽에 요청이 쌓일 수 있음
  • 테스트 중 일부 엔드포인트에서 가비지 컬렉션(GC) 등으로 지연이 발생할 경우 성능 저하가 발생함

두번째, LEAST_CONN (최소 연결)

요청을 보낼 때, 현재 가장 적은 활성 요청 수(active connections)를 가진 엔드포인트를 선택

💬 장점:

  • 런타임 상태를 반영한 스마트한 로드 분산
  • 성능이 떨어지는 엔드포인트는 자연스럽게 덜 사용되게 됨
  • 결과적으로 더 빠르고 안정적인 응답 제공 가능

⚠️ 단점:

  • 성능 판단 기준이 단순해서 실제 부하와 일치하지 않을 수 있음
  • 짧은 시간의 지연에도 민감하게 반응해 분산이 불안정할 수 있음
  • 새로운 인스턴스에 요청이 몰리는 Cold Start 현상이 발생할 수 있음
  • 알고리즘이 복잡해서 모니터링과 디버깅이 어려움
  • choiceCount 설정을 잘못하면 부하 분산 효과가 낮아질 수 있음
  • 단순한 트래픽 패턴에선 오히려 round_robin과 성능 차이가 미미함

🔍 결론

다양한 로드 밸런싱 전략은 같은 서비스라도 전혀 다른 응답 성능을 보여줄 수 있음

지연 시간 기반 분석 (퍼센타일/히스토그램)을 통해 각 전략의 성능 차이를 명확히 확인 가능

실제 운영 환경에서는 단순한 알고리즘보다는 서비스 동작 상황을 반영하는 동적인 전략,

즉 least_conn 같은 방식이 더 효과적일 수 있음

Locality-aware load balancing 실습

🌍 지역 인식(Locality-aware) 로드 밸런싱이란?

Istio 같은 서비스 메시는 네트워크에 있는 서비스 간 통신을 중계할 때, "어디에 배포된 서비스인가?", 즉 서비스의 위치(리전/영역) 정보를 고려해서 더 가까운 서비스로 우선 라우팅할 수 있습니다.

위 그림을 보면,

simple-web에서 simple-backend 서비스를 호출할 때, 같은 리전(us-west/1a)에 있는 10.40.8.1을 우선적으로 선택하려는 경향(prefer)이 있다는 뜻입니다.

즉, 가까운 위치에 있는 서비스부터 호출하려는 로드 밸런싱 방식입니다.

이제 실습을 통해 좀 더 자세히 알아보겠습니다.

🔧 지역 별 서비스 배포
$ # 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 # 추가해두자!
...

$ kubectl apply -f ch6/simple-service-locality.yaml -n istioinaction

🔧 새롭게 배포한 Deployment 들의 resion 확인
$ kubectl get deployment.apps/simple-backend-1 -n istioinaction \
-o jsonpath='{.spec.template.metadata.labels.istio-locality}{"\n"}'

결과
us-west1.us-west1-a

$ kubectl get deployment.apps/simple-backend-2 -n istioinaction \
-o jsonpath='{.spec.template.metadata.labels.istio-locality}{"\n"}'

결과
us-west1.us-west1-b

Istio의 지역 인식 로드 밸런싱은 기본적으로 활성화가 되어있습니다.

이제 지역 정보가 준비되면, us-west1-a 에 있는 simple-web 호출이 같은 영역에 있는 simple-backend-1 으로 갈 것을 예상 됩니다.

다만, Istio에서 지역 인식 로드밸런싱이 작동하려면 헬스 체크를 설정 해야 합니다.

🔧 헬스 체크 설정
$ 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

🔧 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

결과
    50 "Hello from simple-backend-1"

헬스 체크를 설정해주고 kiail Graph에서도 확인해보면 v1으로만 트래픽이 가는 것을 확인 할 수 있습니다.

이번엔 v1 서비스에 오류를 넣으면 v2로 트래픽이 이동하는 지 알아보겠습니다.

🔧 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

🔧 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

결과
     50 "Hello from simple-backend-2"

kiail Graph를 확인해보면 v2로 트래픽이 가는 것을 확인 할 수 있습니다.

다음 실습을 위해 simple-backend-1 정상화 시켜줍니다.

$ kubectl apply -f ch6/simple-service-locality.yaml -n istioinaction

🖥️ 가중치 분포로 지역 인식 LB 제어 강화 실습

방금 전 지역 인식 로드 밸런싱에 대해 알아보았습니다.

이번엔 지역 인식 로드 밸런싱에 가중치를 설정하여, 트래픽을 분산해보도록 하겠습니다.

위 이미지 처럼 us-west/1a 쪽으로 70%, us-west/1b 쪽으로 30% 가중치 분산하여 트래픽을 보내보도록 하겠습니다.

🔧 DestinationRule에 리전 마다 가중치 적용
$ 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

🔧 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

결과
34 "Hello from simple-backend-1"
16 "Hello from simple-backend-2"

kiail Graph를 확인해보면 트래픽이 v1 쪽으로 70, v2 쪽으로 30 가는 것을 확인 할 수 있습니다.

🖥️ Transparent timeouts and retries 실습

✅ 배경: 네트워크 신뢰성 문제 극복 필요

분산 시스템에서는 여러 서버나 리전이 연결되어 있기 때문에, 네트워크 문제가 자주 발생할 수 있습니다. 이때 특히 지연 시간(latency) 과 요청 실패(failure) 가 주요 문제가 됩니다.

예를 들어,

  • 네트워크 지연이 너무 길어지면 어떻게 될까?
  • 다른 지역의 서버에 요청했는데, 네트워크 문제로 실패하면?

👉 이런 문제들을 해결하기 위해 Istio는 타임아웃(timeout)과 재시도(retry) 를 설정해 네트워크 신뢰성 문제를 극복하도록 돕습니다.

🔍 Timeout 지연시간 실습

🧨 문제가 되는 상황
자연 시간 문제: 서버 간 네트워크 지연 때문에 응답이 느리고, 요청이 너무 오래 걸리면 서비스 전체에 장애를 유발할 수 있음

예측하기 어려운 네트워크 상황을 대비해서, 요청 단위로 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

🔧 먼저 호출 테스트 진행 해줍니다. 평균적으로 10~20ms 걸립니다.
$ curl -s http://simple-web.istioinaction.io:30000 | jq .code

결과
200

🔧 simple-backend-1를 1초 delay로 응답하도록 배포
$ cat ch6/simple-backend-delayed.yaml
---
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
    spec:
      serviceAccountName: simple-backend
      containers:
      - 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"		# delay 1초로 설정
        - name: KUBERNETES_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        image: nicholasjackson/fake-service:v0.17.0
        imagePullPolicy: IfNotPresent
        name: simple-backend
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        securityContext:
          privileged: false
          
$ kubectl apply -f ch6/simple-backend-delayed.yaml -n istioinaction

🔧 지연시간 변경되었는지 확인
$ kubectl exec -it simple-backend-1-66d748dd67-k4746 -n istioinaction -- env | grep TIMING

결과
TIMING_VARIANCE=10ms
TIMING_50_PERCENTILE=1000ms

🔧 실제로 응답시간이 1초 이상 걸리는 지 확인
$ curl -s http://simple-web.istioinaction.io:30000

결과
{
  "name": "simple-web",
  "uri": "/",
  "type": "HTTP",
  "ip_addresses": [
    "10.10.0.24"
  ],
  "start_time": "2025-04-24T08:45:15.893244",
  "end_time": "2025-04-24T08:45:16.902499",
  "duration": "1.009254943s", ### 응답시간 1s 확인
  "body": "Hello from simple-web!!!",
  "upstream_calls": [
    {
      "name": "simple-backend",
      "uri": "http://simple-backend:80/",
      "type": "HTTP",
      "ip_addresses": [
        "10.10.0.28"
      ],
      "start_time": "2025-04-24T08:45:15.898869",
      "end_time": "2025-04-24T08:45:16.899399",
      "duration": "1.000529967s",
      "headers": {
        "Content-Length": "281",
        "Content-Type": "text/plain; charset=utf-8",
        "Date": "Thu, 24 Apr 2025 08:45:16 GMT",
        "Server": "envoy",
        "X-Envoy-Upstream-Service-Time": "1003"
      },
      "body": "Hello from simple-backend-1",
      "code": 200
    }
  ],
  "code": 200
}

이제 VirtualService 리소스를 사용해서 0.5 초 이내에 응답이 없으면 Timeout이 발생하도록 설정해보도록 하겠습니다.

🔧 VirtualService 리소스를 사용하여 요청 별 타임아웃 지정
$ 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

이렇게 오류를 만든 이유는 실제 운영 환경에서 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)이란?

요청 헤징은 요청이 타임아웃되면 다른 호스트로도 요청을 보내 원래의 타임아웃된 요청과 경쟁 시키는 것을 말합니다.

즉, 요청을 한 번 보냈는데 응답이 느려지면,
→ 같은 요청을 다른 서버로 한 번 더 보내서
→ 더 빨리 응답 오는 쪽을 선택하는 방식입니다.

요청 헤징을 설정하려면 EnvoyFilter 리소스를 사용합니다.

🔧 요청 헤징 예시 코드
$ cat ch6/simple-backend-ef-retry-hedge.yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: simple-backend-retry-hedge
  namespace: istioinaction
spec:
  workloadSelector:
    labels:
      app: simple-web
  configPatches:
  - applyTo: VIRTUAL_HOST
    match:
      context: SIDECAR_OUTBOUND
      routeConfiguration:
        vhost:
          name: "simple-backend.istioinaction.svc.cluster.local:80"          
    patch:
      operation: MERGE
      value:
        hedge_policy:	# 헤징 Policy 설정
          hedge_on_per_try_timeout: true

정리하자면, 타임아웃과 재시도는 쉬운 설정이 아닙니다.

타임아웃과 재시도를 잘못 설정하면 시스템 아키텍처에 의도치 않은 동작을 증폭시켜 시스템을 과부하시키고 연쇄 장애를 일으킬 수 있습니다.

이제 마지막으로 부하를 더 늘리는 대신에 업스트림 시스템이 복구될 수 있도록(회복 시간 벌기) 부하를 잠시 제한할 수 있으며, 이러한 기능을 사용하기 위한

서킷 브레이커에 대해 알아보겠습니다.

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 %)

커넥션 개수와 초당 요청 수를 2로 늘리니, 요청이 실패한 것으로 반환이 되었습니다.

다만 이게 서킷 브레이커의 영향인지, 업스트림의 장애인지 확인해야합니다.

업스트림 서킷 브레이커 통계를 확장하고자 simple-web 디플로이먼트에 sidecar.istio.io/statsInclusionPrefixes 애노테이션을 사용합니다.

$ kubectl apply -f ch6/simple-web-stats-incl.yaml

🔧 simple-web 서비스에서 istio 프록시 내 모든 통계를 초기화
$ kubectl exec -it deployments/simple-web -c istio-proxy -- curl -X POST localhost:15000/reset_counters

결과
OK

🔧 http2MaxRequests 필드를 2로 늘리기
$ kubectl patch destinationrule simple-backend-dr -n istioinaction --type merge --patch '{"spec": {"trafficPolicy": {"connectionPool": {"http": {"http1MaxPendingRequests": 2}}}}}'

🔧 설정 확인
$ docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/simple-backend-1.istioinaction --fqdn simple-backend.istioinaction.svc.cluster.local -o json | grep maxRequests

결과
                    "maxRequests": 2,
                    "maxRequestsPerConnection": 1
                    

🔧 istio-proxy stats 카운터 초기화
$ kubectl exec -it deploy/simple-web -c istio-proxy -n istioinaction \
-- curl -X POST localhost:15000/reset_counters

🔧 로그 확인 : simple-web >> 아래 500(503) 발생 로그 확인
$ kubectl logs -n istioinaction -l app=simple-web -c istio-proxy -f
... 
## jaeger 에서 Tags 필터링 찾기 : guid:x-request-id=3e1789ba-2fa4-94b6-a782-cfdf0a405e13
[2025-04-22T03:55:22.424Z] "GET / HTTP/1.1" 503 UO upstream_reset_before_response_started{overflow} - "-" 0 81 0 - "172.18.0.1" "fortio.org/fortio-1.69.3" "3e1789ba-2fa4-94b6-a782-cfdf0a405e13" "simple-backend:80" "-" outbound|80||simple-backend.istioinaction.svc.cluster.local - 10.200.1.137:80 172.18.0.1:0 - -
[2025-04-22T03:55:22.410Z] "GET / HTTP/1.1" 500 - via_upstream - "-" 0 688 15 15 "172.18.0.1" "fortio.org/fortio-1.69.3" "3e1789ba-2fa4-94b6-a782-cfdf0a405e13" "simple-web.istioinaction.io:30000" "10.10.0.18:8080" inbound|8080|| 127.0.0.6:43259 10.10.0.18:8080 172.18.0.1:0 outbound_.80_._.simple-web.istioinaction.svc.cluster.local default
                    
🔧 로그 확인 : simple-backend >> 503 에러가 발생하지 않았다??? 
$ kubectl logs -n istioinaction -l app=simple-backend -c istio-proxy -f


🔧 2개의 커넥션에서 요청을 초당 하나씩 보내기 : 동시요청 처리개수가 기존 1 에서 2로 증가되어서 거의 대부분 처리했다. >> 참고로 모두 성공 되기도함.
$ fortio load -quiet -jitter -t 30s -c 2 -qps 2 --allow-initial-errors http://simple-web.istioinaction.io:30000
...
Sockets used: 3 (for perfect keepalive, would be 2)
Code 200 : 33 (97.1 %)
Code 500 : 1 (2.9 %)
All done 34 calls (plus 2 warmup) 1789.433 ms avg, 1.1 qps
...

🔧 'cx_overflow: 40' 대비 'rq_pending_overflow: 1' 가 현저히 낮아짐을 확인
$ kubectl exec -it deploy/simple-web -c istio-proxy -n istioinaction \
 -- curl localhost:15000/stats | grep simple-backend | grep overflow
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_overflow: 40
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_pool_overflow: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_pending_overflow: 1
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_retry_overflow: 0

kubectl exec -it deploy/simple-web -c istio-proxy -n istioinaction \
 -- curl localhost:15000/stats | grep simple-backend.istioinaction.svc.cluster.local.upstream

이제 보류 대기열 깊이를 2로 늘리고 다시 실행해보겠습니다.

# http1MaxPendingRequests : 1 → 2, 'queuing' 개수를 늘립니다
kubectl patch destinationrule simple-backend-dr \
-n istioinaction --type merge --patch \
'{"spec": {"trafficPolicy": {"connectionPool": {"http": {"http1MaxPendingRequests": 2}}}}}'

#
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/simple-web.istioinaction --fqdn simple-backend.istioinaction.svc.cluster.local -o json | grep maxPendingRequests 
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/simple-backend-1.istioinaction --fqdn simple-backend.istioinaction.svc.cluster.local -o json | grep maxPendingRequests           
                    "maxPendingRequests": 2,
                    
# istio-proxy stats 카운터 초기화
kubectl exec -it deploy/simple-web -c istio-proxy -n istioinaction \
-- curl -X POST localhost:15000/reset_counters


# 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) # 큐 길이 증가 덕분에, 소켓을 2개만 사용했다.
Code 200 : 33 (100.0 %)
All done 33 calls (plus 2 warmup) 1846.745 ms avg, 1.1 qps
...

# 'cx_overflow가 45이 발생했지만, upstream_rq_pending_overflow 는 이다!
kubectl exec -it deploy/simple-web -c istio-proxy -n istioinaction \
 -- curl localhost:15000/stats | grep simple-backend | grep overflow
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_overflow: 45
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_pool_overflow: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_pending_overflow: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_retry_overflow: 0

kubectl exec -it deploy/simple-web -c istio-proxy -n istioinaction \
 -- curl localhost:15000/stats | grep simple-backend.istioinaction.svc.cluster.local.upstream

  • 서킷 브레이커가 발동되면 통계를 보고 무슨 일이 일어났는지 확인 할 수 있다. 그런데 런타임은 어떤가?
  • 우리 예제에서는 simple-web → simple-backend 를 호출한다. 그런데 서킷 브레이커 때문에 요청이 실패한다면, simple-web 은 그 사실을 어떻게 알고 애플리케이션이나 네트워크 장애 문제와 구별할 수 있는가?
  • 요청 서킷 브레이커 임계값을 넘겨 실패하면, 이스티오 서비스 프록시는 x-envoy-overloaded 헤더를 추가한다.
  • 이를 테스트하는 한 가지 방법은 커넥션 제한을 가장 엄격한 수준으로 설정하고(커넥션, 보류 요청, 최대 요청을 1로 설정함 1 for connections, pending requests, and maximum requests) 로드 테스트를 다시 수행해보는 것이다.
  • 로드 테스트를 실행하는 도중에 단일 curl 명령도 실행하면 서킷 브레이커 때문에 실패할 가능성이 높다.
# 
kubectl patch destinationrule simple-backend-dr \
-n istioinaction --type merge --patch \
'{"spec": {"trafficPolicy": {"connectionPool": {"http": {"http1MaxPendingRequests": 1}}}}}'

kubectl patch destinationrule simple-backend-dr -n istioinaction \
-n istioinaction --type merge --patch \
'{"spec": {"trafficPolicy": {"connectionPool": {"http": {"http2MaxRequests": 1}}}}}'

# 설정 적용 확인
docker exec -it myk8s-control-plane istioctl proxy-config cluster deploy/simple-backend-1.istioinaction --fqdn simple-backend.istioinaction.svc.cluster.local -o json

# istio-proxy stats 카운터 초기화
kubectl exec -it deploy/simple-web -c istio-proxy -n istioinaction \
-- curl -X POST localhost:15000/reset_counters

# 로드 테스트
fortio load -quiet -jitter -t 30s -c 2 -qps 2 --allow-initial-errors http://simple-web.istioinaction.io:30000

# 로드 테스트 하는 상태에서 아래 curl 접속 
curl -v http://simple-web.istioinaction.io:30000
{
  "name": "simple-web",
  "uri": "/",
  "type": "HTTP",
  "ip_addresses": [
    "10.10.0.18"
  ],
  "start_time": "2025-04-22T04:23:50.468693",
  "end_time": "2025-04-22T04:23:50.474941",
  "duration": "6.247ms",
  "body": "Hello from simple-web!!!",
  "upstream_calls": [
    {
      "uri": "http://simple-backend:80/",
      "headers": {
        "Content-Length": "81",
        "Content-Type": "text/plain",
        "Date": "Tue, 22 Apr 2025 04:23:50 GMT",
        "Server": "envoy",
        "X-Envoy-Overloaded": "true" # Header indication
      },
      "code": 503,
      "error": "Error processing upstream request: http://simple-backend:80//, expected code 200, got 503"
    }
  ],
  "code": 500
}

이제 이상 값 감지로 비정상 서비스에 대응해보도록 하겠습니다.

🔧 시작 전 백지 상태로 돌리기
$ kubectl apply -f ch6/simple-backend.yaml
$ kubectl delete dr --all

🔧 simple-backend 클러스터에 대한 통계가 확장된 simple-web 디플로이먼트 배포
$ kubectl apply -f ch6/simple-web-stats-incl.yaml

🔧 istio-proxy stats 카운터 초기화
$ kubectl exec -it deploy/simple-web -c istio-proxy -n istioinaction \
-- curl -X POST localhost:15000/reset_counters

🔧 호출 테스트
$ fortio load -quiet -jitter -t 30s -c 2 -qps 2 --allow-initial-errors http://simple-web.istioinaction.io:30000

🔧 확인 
kubectl exec -it deploy/simple-web -c istio-proxy -n istioinaction \
 -- curl localhost:15000/stats | grep simple-backend.istioinaction.svc.cluster.local.upstream

결과
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_active: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_close_notify: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_connect_attempts_exceeded: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_connect_fail: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_connect_timeout: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_connect_with_0_rtt: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_destroy: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_destroy_local: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_destroy_local_with_active_rq: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_destroy_remote: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_destroy_remote_with_active_rq: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_destroy_with_active_rq: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_http1_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_http2_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_http3_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_idle_timeout: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_max_duration_reached: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_max_requests: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_none_healthy: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_overflow: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_pool_overflow: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_protocol_error: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_rx_bytes_buffered: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_rx_bytes_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_tx_bytes_buffered: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_tx_bytes_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_flow_control_backed_up_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_flow_control_drained_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_flow_control_paused_reading_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_flow_control_resumed_reading_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_http3_broken: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_internal_redirect_failed_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_internal_redirect_succeeded_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_0rtt: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_active: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_cancelled: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_completed: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_maintenance_mode: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_max_duration_reached: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_pending_active: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_pending_failure_eject: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_pending_overflow: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_pending_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_per_try_idle_timeout: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_per_try_timeout: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_retry: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_retry_backoff_exponential: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_retry_backoff_ratelimited: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_retry_limit_exceeded: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_retry_overflow: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_retry_success: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_rx_reset: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_timeout: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_total: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_rq_tx_reset: 0
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_connect_ms: No recorded values
cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local.upstream_cx_length_ms: No recorded values

이상 입니다.

반응형