Istio의 재시도 정책과 관련 메트릭 살펴보기
서비스 메시인 Istio에는 요청이 실패했을 때 적용할 재시도 정책을 설정할 수 있다. 과거에는 서비스 디스커버리나 메시 같은 기능을 각 서비스에서 구축했다. 하지만 이 경우 서비스마다 정책과 성숙도에 차이가 난다. 그래서 이제는 대부분 인프라 차원에서 처리하는 쪽으로 움직였다. 물론 인프라는 전체에 적용되는 정책이라 보수적일 수밖에 없다. 따라서 인프라의 기본 설정에 애플리케이션의 미세 조정이 더해져야 한다.
서비스에 문제가 생기면 Istio나 애플리케이션이 재시도한다. 장애가 발생하면 재시도가 장애를 더 악화시키거나 복구 시간을 늘리는 경우도 많으므로 신중하게 다뤄야 한다.
일부 상황에서는 재시도 때문에 요청이 늘어나기도 하고 사용자가 새로고침을 하면서 요청이 늘어나기도 한다. 오랫동안 시스템을 보면서 재시도 정책을 자세히 이해하고 재시도 관련 정보를 더 파악할 방법을 찾고자 테스트해 봤다.
Istio의 defaultHttpRetryPolicy
Istio 전역 설정인 MeshConfig에는 재시도 정책을 설정하는 defaultHttpRetryPolicy가 있다.
기본 HTTP 재시도 정책을 구성합니다. 다음 오류에 대한 기본 재시도 횟수는 2회로 설정되어 있습니다:
connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes. 재시도 횟수를 0으로 설정하면 재시도 정책이 전역적으로 비활성화됩니다. 이 설정은 Virtual Service API를 사용하여 호스트별로 재정의할 수 있습니다. 현재 재시도 정책의 설정 중perTryTimeout을 제외한 모든 설정을 이 필드를 통해 전역적으로 구성할 수 있습니다.
위 설명에 나온 대로 이 설정은 Virtual Service의 retries 필드로 재정의할 수 있다.
재시도 설정은 다음과 같은 형태이다.
1meshConfig:
2 defaultHttpRetryPolicy:
3 attempts: 2
4 retryOn: connect-failure,refused-stream,unavailable,cancelled,503
attempts는 재시도 횟수다. 여기서 재시도를 2회로 설정했으므로 최초 요청 뒤 두 번 재시도하며, 최대 세 번의 요청이 발생할 수 있다.
retryOn은 재시도 조건을 지정한다. 조건은 콤마(,)로 여러 개 지정할 수 있으며, 지정할 수 있는 값은 Envoy의 retry 정책과 gRPC retry 정책을 참고하면 된다.
defaultHttpRetryPolicy라는 이름 때문에 헷갈릴 수 있지만, 이 필드는 HTTP뿐 아니라 gRPC에도 사용한다. 따라서 위 설정에서 connect-failure는 TCP 연결 실패나 타임아웃을, refused-stream은 HTTP/2 업스트림이 REFUSED STREAM로 스트림을 거절한 경우를 가리킨다. unavailable과 cancelled는 각각 gRPC의 14 UNAVAILABLE, 1 CANCELLED가 발생한 경우이며, 503은 HTTP 503 Service Unavailable이 발생한 경우다.
참고로 Destination Rule에는 maxRetries 설정이 있는데 이는 특정 시점에 허용하는 최대 재시도 수를 지정한다.
Retry 데모
저번에 기능 분석을 하면서 LLM을 이용하니 데모를 잘 만들어줘서 이번에도 비슷한 방식을 이용했다.
Kubernetes 클러스터 생성
1$ kind create cluster --config kind-config.yaml --wait 5m
2Creating cluster "istio-retry-lab" ...
3 ✓ Ensuring node image (kindest/node:v1.36.1) 🖼
4 ✓ Preparing nodes 📦 📦
5 ✓ Writing configuration 📜
6 ✓ Starting control-plane 🕹️
7 ✓ Installing CNI 🔌
8 ✓ Installing StorageClass 💾
9 ✓ Joining worker nodes 🚜
10 ✓ Waiting ≤ 5m0s for control-plane = Ready ⏳
11 • Ready after 6s 💚
12Set kubectl context to "kind-istio-retry-lab"
13You can now use your cluster with:
14
15kubectl cluster-info --context kind-istio-retry-lab
16
17Not sure what to do next? 😅 Check out https://kind.sigs.k8s.io/docs/user/quick-start/
kind를 이용해서 Kubernetes 클러스터를 생성했다.
1$ kubectl get nodes
2NAME STATUS ROLES AGE VERSION
3istio-retry-lab-control-plane Ready control-plane 51s v1.36.1
4istio-retry-lab-worker Ready <none> 37s v1.36.1
복잡한 설정이 필요한 건 아니라 워커 노드는 1개만 만들었다.
Istio 설치
Istio 오퍼레이터를 설정하면서 아까 살펴봤던 defaultHttpRetryPolicy를 설정했다. 설정하지 않아도 기본 구성이 있지만 여기서는 명시적으로 지정했다.
1apiVersion: install.istio.io/v1alpha1
2kind: IstioOperator
3spec:
4 meshConfig:
5 defaultHttpRetryPolicy:
6 attempts: 2
7 retryOn: connect-failure,refused-stream,unavailable,cancelled,503
8 defaultConfig:
9 proxyStatsMatcher:
10 inclusionRegexps:
11 - ".*upstream_rq_retry.*"
12 - ".*\\.retry\\.upstream_rq_.*"
Istio는 Envoy 통계 일부만 수집한다. proxyStatsMatcher로 추가 수집할 메트릭을 지정할 수 있다. 요청 통계뿐 아니라 재시도 관련 메트릭도 살펴보고 싶어 수집하도록 설정했다.
Envoy의 통계 관련 문서를 보면 다음과 같은 재시도 관련한 메트릭이 있는데 .*upstream_rq_retry.*는 이를 잡기 위한 설정이다.
| Name | Type | Description |
|---|---|---|
upstream_rq_retry |
Counter | 전체 요청 재시도 수 |
upstream_rq_retry_success |
Counter | 요청 재시도 성공 수 |
upstream_rq_retry_backoff_exponential |
Counter | 지수 백오프 전략을 사용한 전체 재시도 수 |
upstream_rq_retry_backoff_ratelimited |
Counter | rate-limited 백오프 전략을 사용한 전체 재시도 수 |
upstream_rq_retry_limit_exceeded |
Counter | 설정한 최대 재시도 수를 초과해 재시도하지 않은 총 요청 수 |
upstream_rq_retry_overflow |
Counter | 서킷 브레이커나 재시도 예산을 초과해서 재시도되지 않은 요청 수 |
재시도는 요청을 보내는 다운스트림 쪽에서 일어나므로 메트릭도 요청을 보내는 쪽 Envoy에 쌓인다. 위 메트릭에는 다운스트림의 최종 응답에 대한 수만 집계된다. 재시도를 유발한 응답에 대한 통계는 retry.upstream_rq_<*xx>나 retry.upstream_rq_<*>에 집계되므로 .*\\.retry\\.upstream_rq_.* 설정으로 이러한 메트릭을 수집한다. 이번 데모에서는 이 메트릭을 사용하지 않았지만, 재시도가 어떤 상황에서 발생했는지 추적하려면 retry.upstream_rq_ 메트릭이 필요해 보인다. 관련 메트릭은 더 검토해야 한다. 다만 수집에도 비용이 들어 모두 수집하기는 어렵다.
이 설정을 이용해서 Istio 1.30.3을 설치했다.
1$ istioctl x precheck
2✔ No issues found when checking the cluster. Istio is safe to install or upgrade!
3 To get started, check out https://istio.io/latest/docs/setup/getting-started/.
4
5$ istioctl install -f istio-operator.yaml --verify -y
6 |\
7 | \
8 | \
9 | \
10 /|| \
11 / || \
12 / || \
13 / || \
14 / || \
15 / || \
16/______||__________\
17____________________
18 \__ _____/
19 \_____/
20
21✔ Istio core installed ⛵️
22✔ Istiod installed 🧠
23✔ Ingress gateways installed 🛬
24✔ Installation complete
1$ istioctl version
2client version: 1.30.3
3control plane version: 1.30.3
4data plane version: 1.30.3 (1 proxies)
Retry 동작 확인
재시도 동작을 추적하기 위해 app-a, app-b라는 2개의 앱을 만들었다.

app-a에 요청을 보내는 실험을 시작하면 app-a는 app-b에 요청을 보내게 되는데 이때 app-b는 일정 비율로 503을 응답하게 했다.

이 테스트를 진행하면서 모니터링할 수 있는 테스트 앱을 따로 만들었다. Prometheus를 연결해 두어서 테스트를 진행하면 각 메트릭을 수집해서 보여준다. 아래는 초당 30 요청에 503 비율을 40%로 설정했다.
1$ istioctl proxy-config routes app-a-6fbc5db5fb-mm8qb -n retry-lab -o json | jq '.. | .retryPolicy? // empty'
2{
3 "retryOn": "connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes",
4 "numRetries": 2,
5 "retryHostPredicate": [
6 {
7 "name": "envoy.retry_host_predicates.previous_hosts",
8 "typedConfig": {
9 "@type": "type.googleapis.com/envoy.extensions.retry.host.previous_hosts.v3.PreviousHostsPredicate"
10 }
11 }
12 ],
13 "hostSelectionRetryMaxAttempts": "5",
14 "retriableStatusCodes": [
15 503
16 ]
17}
18{
19 "retryOn": "reset-before-request",
20 "numRetries": 2
21}
실제로 app-a의 Pod에 적용된 재시도 정책은 위처럼 되어 있다. Istio 설정에서는 없었지만 Envoy에 적용된 추가 설정도 여기서 확인할 수 있다.
envoy.retry_host_predicates.previous_hosts는 재시도할 때 가능하면 이전과 같은 호스트는 피하는 설정이다. 여기서는 임의로 오류를 주입했지만 실제 환경에서는 특정 팟에 문제가 생길 수 있으므로 재시도는 가능하면 다른 Pod으로 가는게 안정성 면에서 훨씬 좋다.hostSelectionRetryMaxAttempts는 새로운 엔드 포인트를 찾을 때 host 선택을 최대 5번까지 시도한다. 위에서 다른 호스트를 찾는다고 할 때 가능하면이라고 했는데 Pod이 1개 일수도 있고 다양한 상황에서 무한대로 같은 호스트를 피할 수는 없으니 최대 시도를 5번으로 제한한다.reset-before-request는 아예 연결이 이뤄지지 않아 업스트림에 도달하지도 못한 채 리셋되면 최대 2번 재시도한다.

app-a와 app-b의 요청 수, 응답 수, 오류 응답 수를 나타낸 그래프다. 재시도가 두 번 더 일어나므로 세 번 모두 503일 때만 최종 오류 응답으로 집계된다. 따라서 app-a의 총 요청 수 29.98 req/s 가운데 오류 응답은 40%가 아니라 10%에 못 미치는 1.96 req/s가 된다.

app-a에서 실제로 재시도가 발생한 수이다. 구분해서 볼 수 있는지 제일 궁금했던 메트릭이고, 재시도로 인한 요청 증폭이 발생했을 때 이 메트릭을 활용할 수 있어 보인다.

재시도를 2번 더 하므로 시나리오를 생각하면 503 -> 200, 503 -> 503 -> 200는 최종적으로 성공이 되고 503 -> 503 -> 503인 경우만 최종적으로 실패가 된다. 재시도 후에 실제로 성공하는지 실패하는지를 알아야 시스템에서 재시도 횟수를 어느 정도로 하는 게 적당한지 판단할 수 있다. 503 -> 200, 503 -> 503 -> 200의 두 케이스도 구분해서 보고 싶었는데 이걸 구분할 수 있는 방법은 찾지 못했다.
이번 테스트는 실제로 무언가를 개선하려는 목적이 아니라, 재시도 동작 방식과 이를 추적하는 메트릭을 확인하려는 목적이었다. 그래서 특별한 결과를 얻지는 못했다. 실제 시스템에 적용하려면 고민해야 할 부분이 더 많기는 하지만, 재시도 관련 메트릭에 어떤 종류가 있는지는 알게 되었다. 이 데모 코드는 GitHub에 올려두었다.
Comments