이전 포스팅에서는 트래픽이 Ingress Gateway를 거쳐 클러스터 내부로 들어오면, 요청 수준에서 트래픽을 조작하고 어디로 라우팅 할지 세밀하게 제어하는 방법을 다뤘다.
하지만 라우팅을 아무리 잘 제어한다고 해도, 현실에서는 여전히 다음과 같은 문제가 항상 발생한다.
- 애플리케이션 오류
- 네트워크 장애
- 예측할 수 없는 시스템 실패
이러한 상황은 수작업으로 대응하기엔 너무 빠르고 복잡하다. 그래서 필요한 게 바로, 문제가 발생했을 때 자동으로 대응하는 회복탄력성(resilience) 이다. Istio를 사용하면, 이 모든 복원 기능을 애플리케이션 코드를 수정하지 않고 네트워크 레벨에서 쉽게 적용할 수 있다.
이번 포스팅에서는 Istio의 다양한 회복탄력성 기법들을 어떻게 설정하고 적용하는지 살펴본다
클라이언트 측 로드 밸런싱(Client-side load balancing)
클라이언트 측 로드 밸런싱이란?
클라이언트 측 로드 밸런싱은 서비스가 제공하는 여러 엔드포인트를 클라이언트에게 직접 알려주고,
클라이언트가 알아서 로드 밸런싱 알고리즘을 선택해 요청을 보내는 방식을 의미한다
클라이언트 측 로드 밸런싱의 장점
• 중앙집중식 로드 밸런서에 의존하지 않아도 된다.
• 불필요한 네트워크 홉 없이, 직접 엔드포인트로 요청을 보낼 수 있다.
• 병목 현상과 단일 장애 지점(SPoF, Single Point of Failure)을 줄일 수 있다.
• 서비스 토폴로지가 변해도, 클라이언트가 유연하게 대응할 수 있다.
결과적으로, 시스템 전체의 확장성(Scalability)과 탄력성(Resilience)이 향상된다.
Istio에서 클라이언트 로드 밸런싱 처리 방식
Istio에서는 EDS (Endpoint Discovery Service)를 통해 클러스터 내 최신 엔드포인트 목록을 클라이언트 프록시(Envoy) 에게 지속적으로 제공한다. 서비스 운영자나 개발자는 DestinationRule 리소스를 사용해서,
어떤 로드 밸런싱 알고리즘을 사용할지 클라이언트에 지정할 수 있다.
Istio(Envoy) 지원 로드 밸런싱 알고리즘
- 라운드 로빈 (Round Robin) (기본값): 요청을 엔드포인트에 순차적으로 분산
- 랜덤 (Random): 요청을 엔드포인트 중 무작위로 선택
- 가중치 최소 요청(Weighted Least Request): 부하가 가장 적은 엔드포인트를 선호
[ 실습: 클라이언트 로드 밸런싱 적용해보기 ]
심플 서비스 배포
간단한 백엔드와 웹 서비스를 배포해보자
kubectl apply -f ch6/simple-backend.yaml -n istioinaction
kubectl apply -f ch6/simple-web.yaml -n istioinaction
kubectl apply -f ch6/simple-web-gateway.yaml -n istioinaction
kubectl get pod,gw,vs -n istioinaction
NAME READY STATUS RESTARTS AGE
pod/simple-backend-1-7449cc5945-z9jbs 2/2 Running 0 42s
pod/simple-backend-2-6876494bbf-blm8n 2/2 Running 0 42s
pod/simple-backend-2-6876494bbf-sbvs8 2/2 Running 0 42s
pod/simple-web-7cd856754-nrl4c 2/2 Running 0 42s
NAME AGE
gateway.networking.istio.io/simple-web-gateway 22s
NAME GATEWAYS HOSTS AGE
virtualservice.networking.istio.io/simple-web-vs-for-gateway ["simple-web-gateway"] ["simple-web.istioinaction.io"] 22s
Gateway, VirtualService 설정
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
통신 테스트
# 도메인 질의를 위한 임시 설정 : 실습 완료 후에는 삭제 해둘 것
echo "127.0.0.1 simple-web.istioinaction.io" | sudo tee -a /etc/hosts
cat /etc/hosts | tail -n 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
여러 backend 인스턴스에 요청이 분산되는 걸 확인할 수 있다.
기본 proxy-config 확인
현재 클라이언트(webapp)가 simple-backend 서비스를 어떤 식으로 인식하고 있는지 확인한다.
# cluster 확인
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
# endpoints 확인
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.13:8080 HEALTHY OK outbound|80||simple-backend.istioinaction.svc.cluster.local
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
DestinationRule을 통한 ROUND_ROBIN 적용
이제 명시적으로 라운드로빈 방식을 적용해 보자.
# 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
결과 확인
요청이 순차적으로 고르게 분배되는지 확인
# 반복 호출 확인 : 파드 비중은 backend-2가 2개임
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
30 "Hello from simple-backend-2"
20 "Hello from simple-backend-1"
Proxy Config와 Access Log 확인
# 로그 확인 : 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
Envoy 설정 확인
RoundRobin 방식은 기본값이기 때문에, 설정에 조회되지 않는다.
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",
...
Fortio를 활용한 로드 밸런싱 테스트
시나리오 설정: Fortio 설치
현실적인 환경에서는 서비스가 요청을 처리하는 데 시간이 걸리게 된다. 이 소요 시간은 요청 크기, 처리 복잡도, 데이터베이스 사용량, 다른 서비스 호출 등 여러 이유로 달라질 수 있다. 또한, 네트워크 혼잡이나 기타 외부적인 요인들도 응답 시간에 영향을 줄 수 있다.
# 응답 시간 차이 관찰
kubectl -n default exec -it netshoot -- time curl -s -o /dev/null http://simple-web.istioinaction
real 0m 0.21s
user 0m 0.00s
sys 0m 0.02s
이번 실습에서는 서비스 응답에 지연을 강제로 유발해서, 로드 밸런싱이 지연이 심해진 엔드포인트에 어떤 효과를 주는지 검증해 보자
이를 위해 Fortio라는 CLI 부하 생성 도구를 설치하고, 클라이언트 측 로드 밸런싱 전략에 따라 실제로 어떤 차이가 나타나는지 관찰해 본다.
Fortio 설치
# 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는 CLI 외, 웹 대시보드도 지원한다. 웹 대시보드에서는 테스트에 인자를 입력하고, 테스트를 실행하고, 결과를 시각화할 수 있다
부하 테스트 시 대시보드 확인
다양한 로드 밸런싱 전략에 따른 지연 시간 성능 비교
Fortio 부하 테스트 준비
실습 시나리오
- simple-backend-1 서비스에 응답 지연(1초)을 추가해, 현실적인 장애 상황을 유발
- Fortio를 이용해 60초 동안, 10개의 커넥션을 통해 초당 1000개의 요청
- 이후 로드 밸런싱 알고리즘(라운드 로빈 → 랜덤 → 최소 요청)을 바꿔가며 응답 지연 시간의 차이를 직접 관찰해 본다.
simple-backend-1 지연 버전 배포
simple-backend-1 서비스에 1초 지연을 주는 설정을 적용한다.
kubectl apply -f ch6/simple-backend-delayed.yaml -n istioinaction
# 환경변수 수정: 지연시간 1초 설정
kubectl exec -it deploy/simple-backend-1 -n istioinaction -- sh
-----------------------------------
export TIMING_50_PERCENTILE=1000ms
exit
실제 호출을 해보면 1초 정도의 응답 지연이 발생하는 걸 확인할 수 있다.
부하 테스트 - Round Robin 방식
별다른 변경 없이 기본 라운드 로빈 방식으로 Fortio 부하 테스트를 진행한다. 75%에서부터 응답 시간이 높아진다.
부하테스트 - Random 방식
로드 밸런싱 방식을 랜덤(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",
75 퍼센터일 때 지연 시간은 Round Robin과 비슷하거나 약간 높은 수준이다
부하테스트 - Least Request 방식
이번에는 로드 밸런싱 방식을 최소 요청(Least Request)으로 변경하고 테스트한다.
#
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",
✅ 결과적으로 75 퍼센트 이상 일 때, 지연 시간이 약 200ms 수준으로, Round Robin이나 Random에 비해 훨씬 낮은 응답 속도를 보인다.
테스트 결과 정리
테스트를 통해 확인한 사실은 다음과 같다.
• 현실적인 서비스 지연 상황에서는 로드 밸런싱 알고리듬마다 결과가 다르게 나타난다.
• Least Request(최소 요청) 전략이 Round Robin이나 Random에 비해 훨씬 뛰어난 성능을 보였다.
Round Robin과 Random은 단순해서 구현과 이해는 쉽지만, 런타임 동작(예: 엔드포인트 지연 상황)을 고려하지 않는다.
그 결과, 지연이 긴 엔드포인트에도 무작위 또는 순차적으로 요청을 보내게 된다.
반면, Least Request 방식은 각 엔드포인트의 활성 요청 개수(queue depth)를 고려해서,
부하가 가장 적은 엔드포인트를 선호한다. 그래서 지연이 심한 인스턴스를 피하고 더 빠른 응답을 받을 수 있다.
지역 인식 로드 밸런싱 실습 (Locality-aware Load Balancing) - 작성예정!
핵심 키워드 outlierDetection
- 이스티오는 특정 서비스를 배포한 리전과 가용 영역을 식별하고, 더 가까운 서비스에 우선순위를 부여
- 리전 간 문제 발생 시, 휴리스틱 heuristic을 바탕으로 라우팅과 로드 벨런싱을 자동을 결정
- 모든 엔드포인트를 동등하게 취급한다면 리전이나 영역을 넘나들면서 지연 시간이 길어지고 비용이 발생할 가능성이 크다
- 쿠버네티스 Topology Aware Hints, Traffic Distribution와의 차이는?
- 최근에는 쿠버네티스 API 정식 버전에서는 이 레이블들을 topology.kubernetes.io/region 과 topology.kubernetes.io/zone
지역 인식 로드 밸런싱 실습하기
쿠버네티스 라벨을 활용하여 리전별 워크로드를 분류istio-locality: us-west1.us-west1-a
- resion1 - simple web, backend1
- resion2 - backend2-1, backend2-2
호출 테스트 1 ⇒ 지역 정보를 고려하지 않고 simple-backend 모든 엔드포인트로 트래픽이 로드 밸런싱 됨
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
TODO 워크로드 라벨링 사진 붙여 넣기.
이스티오에서 지역 인식 로드밸런싱이 작동하려면 헬스 체크를 설정해야 한다 -> TODO outierdection이 왜 지역 인식 로드벨런싱인지 복습이 필요
수동적인 헬스 체크 설정
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 apply -f ch6/simple-backend-dr-outlier.yaml -n istioinaction
확인
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"
simple-backend-1 500 에러 리턴으로 outliercheck 실패 상태로 호출에서 제외됨
가중치 분포를 활용한 지역 인식 로드 밸런싱 세밀 제어
하지만 트래픽 일부를 여러 지역에 분산하고 싶다면 이 동작에 영향을 줄 수 있는데, 이를 지역 가중 분포 locality weighted distribution라고 한다
LB 가중치 적용
배포 및 확인
# 배포
kubectl apply -f ch6/simple-backend-dr-outlier-locality.yaml -n istioinaction
# 확인
for in in {1..100}; do curl -s http://simple-web.istioinaction.io:30000 | jq ".upstream_calls[0].body"; done | sort | uniq -c | sort -nr
68 "Hello from simple-backend-1"
32 "Hello from simple-backend-2"
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 cluster deploy/simple-web.istioinaction -o json | grep -C 3 locality
...
"localityWeightedLbConfig": {}
네트워크 신뢰성 문제 극복하기: 타임아웃과 재시도(Timeouts & Retries)
네트워크가 분산된 환경에서 네트워크 호출이 너무 오래 걸린다거나, 간헐적으로 실패하는 상황이 반복된다면 어떻게 해야 할까?
이럴 때, 이스티오는 타임아웃(timeout)과 재시도(retry) 기능을 통해 네트워크에 내재된 신뢰성 문제를 투명하게 극복할 수 있게 도와준다.
이번 실습에서는 바로 이 타임아웃과 재시도 설정 방법을 다뤄본다.
타임아웃(timeout)
분산 환경에서 가장 다루기 어려운 문제 중 하나는 지연(latency)이다. 처리 속도가 느려지면 리소스를 오랫동안 점유하게 되고, 그 결과 서비스에는 처리해야 할 작업이 점점 쌓이게 된다. 이런 상황이 지속되면 시스템 전체가 연쇄적으로 장애를 일으킬 수 있다. 이를 방지하기 위해서는 커넥션이나 요청, 또는 둘 다에 대해 적절한 timeout 설정이 필요하다.
서비스 호출 간 타임아웃이 서로 어떻게 작동하는지 이해하는 것도 중요하다. 예를 들어, 서비스 A가 서비스 B를 호출할 때 타임아웃이 1초이고, B가 서비스 C를 호출할 때 타임아웃이 2초라고 가정해 보자. 이 경우 가장 짧은 타임아웃(1초)이 먼저 적용되므로, 서비스 B가 C를 호출하는 2초 타임아웃은 발동되지 않는다.
이런 이유로 아키텍처를 설계할 때는 트래픽이 유입되는 외부에 가까울수록 타임아웃을 길게 잡고, 내부로 깊숙이 들어갈수록 타임아웃을 짧게 설정하는 것이 합리적이다. 보통 “밖에서 안으로”, backend에 가까운 서비스일수록 더 제한적인 타임아웃을 설정한다.
imple backend1 레이턴시를 1초로 설정
cat ch6/simple-backend-delayed.yaml | grep -i 1000 -B1
- name: "TIMING_50_PERCENTILE"
value: "1000ms"
kubectl apply -f ch6/simple-backend-delayed.yaml -n istioinaction
다음으로, mesh 내 클라이언트가 simple-backend를 호출할 때 타임아웃을 0.5초로 설정해 보자. 이를 위해 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
이제 호출 테스트를 진행해 보면, simple-backend의 실제 응답이 1초 걸리더라도 0.5초 안에 응답이 없으면 타임아웃이 발생해 500 응답을 받게 된다.
재시도(Retries)
분산 환경에서 서비스를 호출할 때 간헐적으로 네트워크 실패가 발생할 수 있다. 이 경우 애플리케이션이 요청을 재시도하도록 구성하면, 흔히 발생하는 일시적 장애를 극복하고 사용자 경험을 향상할 수 있다. 하지만 무분별한 재시도는 시스템 부하를 가중시켜 연쇄 장애를 초래할 수 있으므로, 재시도는 신중하게 다뤄야 한다. 특히 서비스 자체가 과부하 된 상황에서는 재시도가 오히려 문제를 악화시킬 수 있다.
Istio에서의 기본 재시도 동작
Istio는 기본적으로 재시도 기능이 활성화되어 있으며, 실패한 요청을 최대 두 번까지 재시도한다. 본격적으로 재시도 설정을 커스터마이징 하기 전에, 이 기본 동작을 정확히 이해하는 것이 중요하다.
우선, 기본 재시도 정책을 비활성화하려면 다음처럼 설정할 수 있다.
# Retry 옵션 끄기 : 최대 재시도 0 설정
istioctl install --set profile=default --set meshConfig.defaultHttpRetryPolicy.attempts=0
y
kubectl get istiooperators -n istio-system -o yaml
...
meshConfig:
defaultConfig:
proxyMetadata: {}
defaultHttpRetryPolicy:
attempts: 0
enablePrometheusMerge: true
profile: default
에러 발생 시 재시도 실습
주기적으로 실패하는 simple-backend 서비스를 배포하고, 실패 시 재시도 동작을 관찰해 보자. 이 서비스는 엔드포인트 셋 중 하나(simple-backend-1)가 요청의 약 75%에서 HTTP 503 에러를 반환하도록 설정되어 있다.
기본적으로 Istio는 네트워크 레벨의 특정 실패 상황에 대해 두 번 재시도한다. 주로 다음과 같은 상황이 이에 해당한다.
- 커넥션 수립 실패(connect-failure)
- 스트림 거부(refused-stream)
- gRPC 상태 코드 14(Unavailable)
- gRPC 상태 코드 1(Cancelled)
- HTTP 503 응답
이러한 경우에는 재시도해도 안전한 상황으로 간주되며, 사용자는 실패를 인식하지 못한 채 정상적으로 응답을 받을 수 있다.
호출테스트(retry 적용 전)
simple-backend-1 호출 시, 500 에러가 발생한다.
사용자에게 보이는 클라이언트 에러는 500이지만, 메쉬 내 백엔드에 에러코드는 503으로 설정되었으니 헷갈리지 말 것!
VirtualService 수정(retry 적용)
이 설정에서는 재시도 횟수를 2회로 명시했다.
kubectl apply -f ch6/simple-backend-enable-retry.yaml -n istioinaction
적용 후에는 호출 중 실패가 발생해도, 최종적으로는 사용자에게 성공(HTTP 200) 응답이 반환된다.
Envoy설정 확인
HTTP 500 에러에 대한 재시도 실습
기본적으로 Istio는 HTTP 503에 대해서만 재시도를 수행한다. 하지만 경우에 따라 HTTP 500 에러에도 재시도가 필요할 수 있다
# 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
VirtualService의 재시도 정책을 수정하여 500 에러에 대해서도 재시도할 수 있도록 설정한다.
수정 후에는 500 에러가 발생해도 재시도하여 정상적인 결과(HTTP 200)를 얻을 수 있게 된다.
Istio 확장 API(EnvoyFilter)를 통한 고급 재시도 설정
Istio에서 백오프(backoff) 시간이나 재시도할 수 있는 상태 코드 목록 등은 기본값이 고정되어 있지만, 이스티오 API를 통해 쉽게 조정할 수 없다. 기본적으로 백오프 간격은 25ms, 재시도 가능한 상태 코드는 HTTP 503으로 제한되어 있다.
이러한 한계를 넘어서기 위해 Istio Extension API, 특히 EnvoyFilter 리소스를 사용하면 Envoy 레벨에서 직접 설정을 수정할 수 있다. 즉, 이스티오가 기본적으로 노출하지 않는 Envoy 내부 설정을 제어할 수 있는 것이다 ( 위 호환성 문제나 예상치 못한 동작을 초래할 수 있으므로 사용을 권장하지 않는다고 한다.)
실습을 위해 먼저 HTTP 408 에러를 주기적으로 발생시키는 simple-backend 서비스를 배포
# 408 에러코드를 발생
kubectl apply -f ch6/simple-backend-periodic-failure-408.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=408
exit
---------------------------------------------------------------
적용 전, 408 에러는 retryOn: 5xx에 포함되지 않으므로 에러를 리턴.
# 호출테스트 :
# simple-backend-1 --(408)--> simple-web --(500)--> curl(외부)
for in in {1..10}; do time curl -s http://simple-web.istioinaction.io:30000 | jq .code; done
...
EnvoyFilter를 적용하여, HTTP 400번대 에러 코드(408, 400 등)에 대해서도 재시도가 가능하도록 설정
kubectl apply -f ch6/simple-backend-ef-retry-status-codes.yaml -n istioinaction
VirtualService에서도 재시도 대상 코드 목록에 추가해 준다.
kubectl apply -f ch6/simple-backend-vs-retry-on.yaml -n istioinaction
변경 후 호출 테스트를 수행하면, 408 에러가 발생해도 정상적으로 재시도가 이루어져 최종적으로 클라이언트는 200 OK를 받을 수 있다
# 호출테스트 : 성공
# simple-backend-1 --(408, retry 성공)--> simple-web --> curl(외부)
for in in {1..10}; do time curl -s http://simple-web.istioinaction.io:30000 | jq .code; done
Istio를 활용한 서킷 브레이킹 (Circuit Breaking)
서킷 브레이커(Circuit Breaker) 기능을 사용하면 시스템 내 부분적 장애나 연쇄 장애를 방지할 수 있다. 비정상 상태에 빠진 시스템에 계속 요청을 보내어 과부하를 일으키는 대신, 비정상 시스템으로 향하는 트래픽을 제한해 복구를 도울 수 있다. 예를 들어, simple-web 서비스가 simple-backend를 호출하는데 simple-backend가 연속해서 오류를 반환하면, 계속 재시도하여 부하를 주는 대신 simple-backend로의 호출을 중단하고 싶을 수 있다.
이러한 개념은 집의 전기 시스템에 있는 회로 차단기(circuit breaker)와 유사하다. 시스템에 단락이나 고장이 반복되면, 회로 차단기가 개방되어 나머지 시스템을 보호한다. 서킷 브레이커 패턴은 네트워크 호출이 실패할 수 있다는 현실을 애플리케이션이 적극적으로 처리하게 함으로써, 전체 시스템을 연쇄적인 실패로부터 보호하는 데 중요한 역할을 한다
Istio에서 서킷 브레이커를 구현하는 두 가지 방법
이스티오에는 ‘Circuit Breaker’라는 명시적 리소스는 없지만, 서킷 브레이커와 같은 효과를 낼 수 있는 두 가지 방법이 존재한다.
첫 번째 방법은 특정 서비스로 향하는 커넥션 수와 미해결 요청(outstanding requests) 개수를 제한하는 것이다. 서비스가 느려지면서 요청이 쌓이면, 추가 요청을 보내지 않고 빠르게 실패(fail fast)하게 만들어, 클라이언트가 시스템 전체의 병목에 걸리지 않도록 할 수 있다. 이스티오에서는 이를 DestinationRule 안의 connectionPool 설정을 통해 제어할 수 있다. 예를 들어, 어떤 서비스에 진행 중인 요청이 이미 10개이고 계속 증가하는 상황이라면, 추가 요청을 허용하지 않고 빠르게 실패시켜야 한다.
두 번째 방법은 로드밸런싱 풀 내 엔드포인트 상태를 관찰하여 오동작하는 엔드포인트를 퇴출시키는 것이다. 특정 호스트에 문제가 발생하면 그 호스트로의 트래픽 전송을 건너뛸 수 있다. 만약 풀에 있는 모든 호스트가 소진되면, 회로는 사실상 ‘열린(open)’ 상태가 되어 일정 시간 동안 트래픽을 차단하게 된다.
1. 커넥션 풀 제어를 통한 느린 서비스 보호
느린 서비스에 대응하기 위해 커넥션 풀 제어(connection-pool control)를 사용하는 방법을 살펴본다.
먼저, backend-service-1을 배포하고 레이턴시를 1초로 설정하여 테스트 환경을 준비한다.
# 테스트 결과, 평균 요청 시간이 약 1초로 측정된다.
fortio load -quiet -jitter -t 30s -c 1 -qps 1 http://simple-web.istioinaction.io:30000
Fortio 1.69.4 running at 1 queries per second, 12->12 procs, for 30s: http://simple-web.istioinaction.io:30000
Aggregated Function Time : count 30 avg 1.0195172 +/- 0.004495 min 1.011043625 max 1.031472166 sum 30.5855162
# target 50% 1.02091
# target 75% 1.02619
# target 90% 1.02936
# target 99% 1.03126
# target 99.9% 1.03145
커넥션 풀 제어에 사용되는 주요 설정은 다음과 같다.
maxConnections
: 한 서비스로 동시에 열 수 있는 커넥션의 총 수를 의미한다- Envoy는 이 수치까지만 커넥션을 사용하고, 초과 시 오버플로를 기록한다.
http1MaxPendingRequests
: 사용할 커넥션이 없어 대기 중인 요청을 얼마나 허용할지 설정하는 값이다.http2MaxRequests
: HTTP/2 여부와 상관없이, 동시 요청 수를 제한한다. (이름이 오해를 부를 수 있음)
보다 정확한 관찰을 위해 Istio 서비스 프록시(Envoy) 에서 더 많은 통계를 수집할 필요가 있다. 이를 위해 sidecar에 애너테이션을 추가해 Prometheus에 Envoy 메트릭을 노출한다.
# simple-web 디플로이먼트에 sidecar.istio.io/statsInclusionPrefixes="cluster.<이름>" 애너테이션 추가하자
## sidecar.istio.io/statsInclusionPrefixes: cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local
cat ch6/simple-web-stats-incl.yaml | grep statsInclusionPrefixes
sidecar.istio.io/statsInclusionPrefixes: "cluster.outbound|80||simple-backend.istioinaction.svc.cluster.local"
kubectl apply -f ch6/simple-web-stats-incl.yaml -n istioinaction
애노테이션 추가 후, istio-proxy 컨테이너의 통계 카운터를 초기화하고 새롭게 측정을 시작한다.
istio-proxy stats 카운터 초기화
kubectl exec -it deploy/simple-web -c istio-proxy -n istioinaction \
-- curl -X POST localhost:15000/reset_counters
simple-web에 istio-proxy의 stats 조회
• upstream_cx_overflow
: 설정한 maxConnections를 초과해 커넥션 오버플로가 발생한 횟수
• upstream_rq_pending_overflow
: http1MaxPendingRequests를 초과해 대기 중이던 요청이 실패한 횟수
커넥션 개수와 초당 요청 수를 2로 늘리면 어떨까? 2개의 커넥션에서 요청을 초당 하나씩 보내기 시작해 보자.
처음에는 정상적으로 요청이 진행되지만, 설정한 maxConnections 임계값을 초과하면 문제가 발생하기 시작한다.
Envoy는 연결할 수 있는 최대 커넥션 수를 초과하면 추가 요청을 즉시 실패(fail fast) 시킨다.
로그를 확인해 보면, 이때 Envoy 내부에서는 503 UO(Upstream Overflow) 에러가 발생하는데, 이는 Envoy가 더 이상 업스트림 서버로 요청을 전달할 수 없는 상태를 의미한다
503 UO란 무엇인가?
- Upstream Overflow(업스트림 오버플로우)는 Envoy가 업스트림 서버로의 요청을 처리할 수 없어 발생하는 에러다.
- 업스트림 서버가 응답이 느리거나 다운된 경우
- 트래픽 급증으로 큐에 쌓인 요청이 너무 많은 경우
- Envoy 서킷 브레이커 설정(예: max_requests, max_connections)을 초과한 경우
- Envoy 내부 버퍼나 큐가 포화 상태가 되어 요청을 수용할 수 없을 때 발생한다.
이 결과는 Envoy가 커넥션 풀 제어를 통해 느린 서비스나 장애 상황에 대해 시스템 전체를 보호하고 있음을 보여준다.
초과된 요청은 더 이상 업스트림으로 전달되지 않고, Envoy가 빠르게 실패를 반환함으로써 전체 클라이언트/서비스 체인에 가해지는 부하를 차단한다.
[ 커넥션 풀 세부 조정으로 부하 처리량 높이기 ]
현재 로드테스트는 동시 요청 2개를 발생시키고 있는데, 이를 더 잘 처리하기 위해 http2MaxRequests 값을 조정해 보았다.
먼저, http2MaxRequests 값을 1 → 2로 증가시켜 동시 요청 처리 가능 개수를 늘렸다.
# http2MaxRequests 조정: 1 → 2, '동시요청 처리개수'를 늘림
kubectl patch destinationrule simple-backend-dr -n istioinaction \
-n istioinaction --type merge --patch \
'{"spec": {"trafficPolicy": {"connectionPool": {"http": {"http2MaxRequests": 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
동시 요청 처리 수가 늘어나자 오류율이 눈에 띄게 낮아졌다.
다음으로, 대기 중인 요청(큐에 쌓인 요청)을 더 허용하기 위해 http1MaxPendingRequests 값을 1 → 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_countersC
이제 초당 2개 요청을 동시 2개 커넥션으로 보내면서 다시 부하 테스트를 진행
fortio load -quiet -jitter -t 30s -c 2 -qps 2 --allow-initial-errors http://simple-web.istioinaction.io:30000
모두 성공!
cx_overflow가 45가 발생했지만, upstream_rq_pending_overflow 는 이다!
서킷 브레이커로 인한 실패 식별 방법
우리 실습에서는 simple-web이 simple-backend로 호출을 보내고 있는데, 만약 서킷 브레이커 임계값 초과로 요청이 실패했다면,
simple-web은 이를 어떻게 감지하고 애플리케이션 문제나 네트워크 장애와 구별할 수 있을까?
이스티오 서비스 프록시(Envoy)는 서킷 브레이커로 실패했을 때, HTTP 응답에 x-envoy-overloaded 헤더를 추가한다.
이 헤더를 통해, 클라이언트(simple-web)는 단순 네트워크 오류나 서버 장애가 아니라,
서킷 브레이커에 의해 요청이 거부되었음을 명확히 인지할 수 있다.
2. 이상값 감지를 통한 비정상 서비스 격리 (Outlier Detection)
Envoy의 이상값 감지(outlier detection) 기능을 사용하면, 서비스 풀 내에서 비정상적으로 동작하는 엔드포인트를 자동으로 제거할 수 있다. 이스티오는 이 기능을 통해, 연속적인 실패를 일으키는 호스트를 감지하고, 일정 시간 동안 트래픽을 차단하여 전체 시스템의 안정성을 높인다.
우선 실습을 위해, simple-backend-1 엔드포인트에 대해 75% 확률로 HTTP 500 에러를 반환하도록 설정한다.
kubectl apply -n istioinaction -f ch6/simple-backend-periodic-failure-500.yaml
kubectl exec -it deploy/simple-backend-1 -n istioinaction -- env | grep ERROR
#
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
이후 fortio를 이용해 기존 로드 테스트를 수행했으며, 약 40%의 오류율을 확인할 수 있었다. 이는 현재 simple-web 서비스가 실패한 엔드포인트로도 트래픽을 계속 보내고 있음을 의미한다.
fortio load -quiet -jitter -t 30s -c 2 -qps 2 --allow-initial-errors http://simple-web.istioinaction.io:30000
Fortio 1.69.4 running at 2 queries per second, 12->12 procs, for 30s: http://simple-web.istioinaction.io:30000
Aggregated Function Time : count 60 avg 0.10890334 +/- 0.0761 min 0.009344583 max 0.1805755 sum 6.53420046
# target 50% 0.163529
# target 75% 0.172353
# target 90% 0.177647
# target 99% 0.180403
# target 99.9% 0.180558
Error cases : count 24 avg 0.015887967 +/- 0.004621 min 0.009344583 max 0.023096375 sum 0.381311206
# Socket and IP used for each connection:
[0] 12 socket used, resolved to 127.0.0.1:30000, connection timing : count 12 avg 0.00031974992 +/- 0.000156 min 0.000162583 max 0.000740792 sum 0.003836999
[1] 15 socket used, resolved to 127.0.0.1:30000, connection timing : count 15 avg 0.0003496918 +/- 0.0001405 min 0.000165417 max 0.0007355 sum 0.005245377
Sockets used: 27 (for perfect keepalive, would be 2)
Uniform: false, Jitter: true, Catchup allowed: true
IP addresses distribution:
127.0.0.1:30000: 27
Code 200 : 36 (60.0 %)
Code 500 : 24 (40.0 %)
All done 60 calls (plus 2 warmup) 108.903 ms avg, 2.0 qp
DestinationRule에 Outlier Detection 설정을 추가
- consecutive5xxErrors: 1 → 연속으로 5xx 에러가 1회 발생하면 엔드포인트를 퇴출
- interval: 5초 → 5초마다 이상 여부를 체크
- baseEjectionTime: 5초 → 퇴출된 엔드포인트는 5초 후 다시 감시 대상에 포함
- maxEjectionPercent: 100% → 모든 엔드포인트가 실패할 경우 전체 회로를 열어 요청을 차단
# outlierDetection 적용
kubectl apply -f ch6/simple-backend-dr-outlier-5s.yaml -n istioinaction
kubectl get dr -n istioinaction
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": "5s",
"maxEjectionPercent": 100,
"enforcingConsecutive5xx": 100,
"enforcingSuccessRate": 0
},
...
설정 적용 후 부하 테스트를 다시 진행한 결과, 오류율이 급격히 감소했다.
이는 오동작하는 엔드포인트를 로드 밸런싱 풀에서 제거했기 때문이며, 남은 정상 엔드포인트로만 트래픽이 분산되었다. 실제로 엔드포인트 감지 직전에는 3번의 실패가 발생했지만, 이후부터는 모든 요청이 성공적으로 처리되었다
퇴출된 엔드포인트는 설정된 설정한 대로 5초 후에 다시 활성화 상태가 되며 하다시 로드 밸런싱 대상이 된다
하지만, 75% 확률로 실패하는 환경에서는 여전히 일부 오류가 발생할 수 있다. 이를 추가로 보완하기 위해 VirtualService에 재시도(Retry) 설정을 추가했다.
# 명시적으로 **retries: 2**를 설정하여, HTTP 5xx 오류 발생 시 최대 두 번까지 재시도를 수행하도록 했다.
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: 5x
kubectl apply -f ch6/simple-backend-vs-retry-500.yaml -n istioinaction
# 통계 초기화
kubectl exec -it deploy/simple-web -c istio-proxy -n istioinaction \
-- curl -X POST localhost:15000/reset_counters
재시도 설정 후 통계를 확인해 보니, 재시도를 통해 최초 요청 실패를 복구하여 사용자 입장에서는 모든 요청이 정상적으로 응답되었다.
아래처럼 재시도해서 최종적으로 사용자 입장에서는 정상 응답을 확인할 수 있다.
'Kubernetes > Istio' 카테고리의 다른 글
Istio 시리즈 # 7 – Istio Security로 살펴보는 마이크로서비스 통신 보안 (1) | 2025.05.11 |
---|---|
Istio 시리즈 # 6 – 메트릭과 트레이싱으로 살펴보는 Istio Observability (1) | 2025.05.04 |
Istio 시리즈 # 4 – 세밀하게 트래픽 제어하기(Traffic Control) (0) | 2025.04.27 |
Istio 시리즈 # 3 – 외부 트래픽 진입점, Ingress Gateway 알아보기 (0) | 2025.04.20 |
Istio 시리즈 # 2 - Istio의 핵심, Envoy Proxy를 이해하자 (0) | 2025.04.20 |