Introduction
Timeout
Timeout은 최대 응답 지연을 보장함과 동시에, 클라이언트 자원 고갈 문제를 방지하는 fast fail 기법으로, Circuit Breaker와 함께 클라이언트 자원 고갈 문제를 해결하는 주요 수단이다.
대응 문제
•
높은 지연 (
주요 대응): 클라이언트의 오랜 대기를 방지한다. 최대 응답 지연 보장 효과이다.
•
클라이언트 자원 고갈 (
주요 대응): 오랜 대기로 인한 자원 고갈 문제를 제거한다. 마이크로서비스의 클라이언트는 받은 요청 처리를 위해 서버를 호출하기 마련이다. 즉, 받은 요청 만큼 서버 호출을 위한 자원 소비량은 늘어난다.
•
연쇄적 장애 (
부분적 대응): 클라이언트 자원 고갈로 인한 장애로의 발전을 사전에 차단한다. 하지만 서버 자원 고갈로 인한 장애는 해결하지 못하기에 한계가 있다. 서버 자원 고갈 대응안은 Rate Limiting이다.
유의사항, 설정 방법, 추천 설정
•
•
설정 방법: https://istio.io/latest/docs/reference/config/networking/virtual-service/ 의 timeout field 및 예제 참고한다.
•
추천 설정: 호출 대상 API의 latency을 고려하는 것이 가장 중요하다. 참고로, 웹 사용자는 일반적으로 2~3초 내 응답을 기대한다고(AI 피셜). 또한 AWS Lambda function 역시 default timeout은 3초이다.
timeout: 3s # 무엇보다 호출 대상 API의 latency를 고려해야 한다.
YAML
복사
Retry
Retry는 일시적인 장애(네트워크 오류, 일시적 서비스 장애 등)를 극복하기 위한 수단으로, 클라이언트의 불필요한 오류 조우 및 후속 조치를 방지하는 효과를 낸다. 데이터 유실에 민감한 request일 경우 특히 유용하다.
대응 문제
•
일시적 장애 (
주요 대응): 일시적 서비스 오류 뿐 아니라 네트워크의 불안정성 특성 상, 네트워크 상에 일시적 오류가 발생 가능하며, 이때 재시도를 해보면 해결되는 경우가 상당하다.
유의 사항, 설정 방법, 추천 설정
•
•
•
•
추천 설정: 대부분의 일시적 실패(네트워크 지연, 짧은 서비스 불안정)는 2~3회 내 해결된다. 재시도 전체 기간을 Timeout 에 맞춰 횟수와 재시도 내 timeout을 고려하는 것도 유의미하다.
retries:
attempts: 2 # (attempts + 1) * perTryTimeout으로 전체 최대 지연을 고려(2는 최대 3번을 의미. 0이면 재시도 없음, 즉 1번만 호출을 의미)
perTryTimeout: 1s # 호출 대상 API의 latency 고려 필요
retryOn: "5xx, connect-failure,refused-stream,unavailable,cancelled" # default 값에 5xx 추가
YAML
복사
Circuit Breaker
Circuit Breaker(CB)는 반복적, 연쇄적 장애 방지의 대표적 장치이자 Timeout과 함께 클라이언트 관점의 안정성 유지의 주요 수단으로, 서비스의 반복적 장애를 감지해 요청을 차단한다.
대응 문제
•
반복적, 연쇄적 장애 (
주요 대응): 반복적 장애 서버 workload로의 트래픽을 제거(destination pod ejection)함으로, 클라이언트, 서버 모두에서 자원 소진을 최소화한다.
•
클라이언트 자원 고갈 (
주요 대응): 장애 pod로의 요청을 차단함으로 클라이언트 자원 소모를 방지한다.
•
서버 자원 고갈 (
부분적 대응): 서버 자원 고갈 역시 트래픽 차단으로 얻는 효과이다.
Kubernetes readiness probe에 의한 routing 대상 제외도 Circuit Breaker?
Istio는 k8s readiness probing 실패 시 해당 pod를 routing 대상에서 제외하며, 이는 기본 동작이다(kube-proxy와 동일). 따라서 ‘장애 pod의 routing 대상 제외’란 관점에서만 보자면 Circuit Breaker 전용 Istio field인 outlierDetection 기반 CB의 필요성에 의문을 갖게 한다.
Circuit Breaker 관점에서 둘 간의 차이는 동작 시점이다.
•
outlierDetection 기반 CB: 서비스 traffic 기반 즉시적 동작
•
Readiness probe 기반 CB(?): health checking 주기에 따른 주기적 동작
즉시적 동작과 주기적 동작의 차이가 크지 않아 보일 수 있는데, 설정에 따라 다르지만 일반적으로 전자는 대응 시간이 밀리 초~수 초인 반면 후자는 수십 초가 걸리는 큰 차이를 보이며, 수십 초는 장애 전파에 무시하기 어려운 시간이다(AI 피셜. 장애 전파에 ChatGPT은 수초 이내, Grok은 밀리 초 ~ 수초를 논함).
굳이 따지자면 outlierDetection 기반 CB는 선제적 장애 예방 수단이고 readiness probe 기반 CB는 장애 사후 대응 수단이 될 듯 하다.
유의 사항
•
•
Circuit Breaking 단위는 pod
즉, service(pod의 집합) 단위가 아니다(근거: 공식 문서는 field 설명 전반으로 ‘individual host’를 기준으로 논의. host는 k8s 환경에서 pod 또는 endpoint에 해당).
정확히는 pod가 아니라 endpoint 단위이다. 일반적으로 endpoint와 pod 단위가 같고 pod가 이해에 용이하기에 pod로 논한다.
이에 따라, consecutive5xxErrors 등 ejection 결정은 pod 단위로 발생하고, ejection 역시 해당 결정에 따라 해당 pod에서 발생한다. 이는 클라이언트 관점, 즉 서버 전체 관점에서는 consecutive5xxErrors 값을 한참 넘어서야 pod ejection이 발생할 수 있음을 의미하며, 서버 pod 갯수가 많을 수록 그렇다.
•
maxEjectionPercent, minHealthPercent 는 오탐으로 인한 장애 방지책
한편, maxEjectionPercent, minHealthPercent 는 service 단위이다. 각기 장애 방지를 위한 ejection pod 상한과 healthy pod 하한으로, 양자 모두 설정 중 충돌이 있을 경우 minHealthPercent 이 적용된다. 참고로 k8s에서 minHealthPercent 의 default 값이 0으로 이는 해당 기능이 꺼져있어 pod 갯수가 적을 경우를 대비한다고 공식 문서에서 언급한다. 특별한 일이 없는 이상, 혼선 방지를 위해 maxEjectionPercent 단독 사용을 추천한다.
•
◦
Ejection 판단은 통계적 heuristic 기반이기에 본질적으로 오탐 가능성이 존재하여, 이는 interval, consecutive5xxErrors 이 작을 수록 커지고, baseEjectionTime, maxEjectionPercent이 클수록 오탐 여파가 커진다.
◦
문제는 이로 인해서도 (서비스 내) 연쇄적 장애가 발생 가능하다는 점이다. 이 경우 연쇄적 장애 방지가 아니라 악화가 되어버린다.
◦
오탐 대응안
▪
Ejection을 고려한 충분한 가용 자원량 설정: 하지만 자원 낭비로 이어질 수 있다. 따라서 소규모 서비스일 경우에 적용해볼만하다(아님 돈이 많거나).
▪
Rate Limiting 설정: 설정 값을 찾는게 쉽지 않다는 단점이 있다. 하지만 설정하면 규모에 상관 없이 든든하다.
▪
HPA등의 autoscaling: autoscaling의 자체 지연 시간이 장애 전파 시간보다 짧아야 한다는 풀기 어려운 제약이 있다.
설정 방법, 추천 설정
•
추천 설정: 빠른 감지와 빠른 재투입 전략으로 ejection 효과 최대한, 오탐 부작용 최소화 전략이다. 아래는 예로 역시나 서비스마다 달라질 것이기에 서비스 별 적정 값을 찾는 것이 중요하다. 아래 AI 추천의 소규모 클러스터 경우와 유사하다.
# 오탐 대응안 마련 수준이 낮을 경우
http:
outlierDetection:
consecutive5xxErrors: 5 # default 값. 작을 수록 민감하지만 그만큼 오탐 위험도 커진다. 서비스 별 특성에 따라 적절한 값 찾는 것이 중요.
interval: 5s # default는 10s. 빠른 감지를 위해 줄였다.
baseEjectionTime: 10s # default는 30s. 오탐 여파 최소화 목적으로 줄였다. 오탐이 아니면 ejection 효과는 자연히 늘어난다(ejection time = baseEjectionTime * 연속 ejection 횟수)
maxEjectionPercent: 20 # default는 10. ejection 효과 증대를 위해 높혔다. 소규모에서는 너무 작으면 아예 ejection이 안되기에 높혀야 한다.
minHealthPercent: 0 # default 값(기능 off). maxEjectionPercent만 사용함으로 혼선 제거
# 오탐 대응안 마련 수준이 높을 경우
http:
outlierDetection:
... # 나머지는 동일
maxEjectionPercent: 50 # 100을 설정할 수도 있겠으나 오탐 시 서비스 전면 중단 위험으로 인해 줄였다. 실제 전체 장애 상황이라면 CB에 의해서가 아니더라도 ejection은 발생한다(via readiness probe).
YAML
복사
•
대규모 클러스터(대량의 pod 운용)에는?
AI는 아래와 같이 클러스터 규모에 따라 다른 기준을 제시하여, 작을 때는 빠른 감지와 빠른 재투입을, 클 때는 안정성과 오탐 방지에 초점을 두는 것을 추천한다.
대규모 클러스터에 빠른 감지/재투입 전략이 적절하지 않은 근본적 이유는, 대량의 빠른 감지/재투입 pods로 인한 Istio-proxy와 Istiod 간의 XDS(xDS API) 대규모 통신량, 운영 피로도 때문이다. 이는 부수적 장애 위험을 높인다.
◦
Istiod 과부하: 설정 동기화 지연, 신규 pod 온보딩 지연 유발 가능
◦
Istio-proxy의 대규모 endpoint 갱신(기존 연결 갱신 연산 포함): node level의 총 자원 소모 증가 → 노드 자원 고갈 유발, 타 pod로의 악영향 가능
◦
운영 피로도 증가: 모니터링 및 디버깅의 복잡성 증대
소규모(pod 2~5개) | 대규모(pod 10개 이상) | 인스턴스 수와의 관계 | |
maxEjectionPercent | 50~100% (빠른 제외) | 10~20% (오탐 방지) | 적을 수록 높게, 많을 수록 낮게 |
interval | 1~3초(빠른 감지) | 5~10초 (안정성) | 적을 수록 짧게, 많을 수록 길게 |
baseEjectionTime | 15~60초(빠른 재투입) | 120~300초 (안정성) | 적을 수록 짧게, 많을 수록 길게 |
consecutive5xxErrors | 1~3 (민감한 감지) | 3~5 (오탐 방지) | 적을 수록 낮게, 많을 수록 높게 |
Rate Limiting
Rate Limiting 은 load shedding의 주요 기법 중 하나이자 사실 상 서버 관점의 안정성 유지를 위한 최후의 보루로, 트래픽 부하가 서버 한계치에 도달 시 429 응답을 즉시 반환함으로 장애를 방지한다.
Local rate limiting vs Global rate limiting
한계치 측정 및 오류 반환 결정 위치가 부하 중인 workload와 동일할 경우 Local Rate Limiting, 다를 경우 Global Rate Limiting이라 하며, 후자는 과금을 위한 수단으로도 사용된다.
대응 문제
•
서버 자원 고갈 (
주요 대응): 서버 자원 고갈 문제의 근본적 대응안. 과부하를 근본적으로 차단한다.
•
연쇄적 장애 (
부분적 대응): 서버 자원 고갈은 연쇄적 장애 주요 원인 중 하나. 다만 자체 해결안이 아닌 서버 의존적(서버에 rate limiting이 있을 때 유효)으로 한계가 있다.
•
클라이언트 자원 고갈 (
부분적 대응): 빠른 실패(429 Too Many Requests) 응답으로 클라이언트 자원 소모를 간접적으로 줄인다.
유의 사항, 설정 방법
Rate Limiter 적용 자체는 쉽지만 정작 어려움은 자원 대비 한계 처리량 산출이 쉽지 않다는 데 있다. 해당 값 산출은 다른 어떤 해결안보다 workload 사용 패턴에 맞춘 부하 테스트를 요구하기 때문이다. 하지만 서버 과부하 대응의 대표적 exercise란 점을 고려하면 그만한 가치가 있는 활동임에 틀림 없다(Google SRE book 역시 다양한 exercise 중 이를 첫 번째로 꼽는다: This is the most important exercise you should conduct in order to prevent server overload).
•
•
설정 방법
◦
◦
참조 가이드 문서
▪
Enabling Rate Limits using Envoy (Istio)
▪
HTTP local rate limit filter (Envoy)
▪
권혁 블로그 (path 별로 처리하는 로직에 도움을 받았다).
•
적용 방법: 하기 적용 코드 중 comment 부분 만을 적절히 수정하여 적용한다. 아래는 comment에 있는 설정 fields에 대한 설명으로, path, method 별로 rate limiting 설정이 가능하고 default 설정 역시 가능하다.
◦
Path, method 매칭: header_value_match 부분에서 path, method 설정의 모습을 볼 수 있다. 해당 match에 대한 token bucket 설정은 하단에서 각 descriptor_value 값에 해당하는 섹션에서 별도 설정하는 구조이다.
◦
token buckets 설정
▪
max_tokens : 토큰 버킷이 최대 저장할 수 있는 토큰 수. 즉, 버킷의 용량.
▪
fill_interval: 버킷에 토큰이 추가되는 주기를 나타낸다.
▪
tokens_per_fill: fill_interval 마다 버킷에 추가되는 토큰 수. 즉, 리필되는 토큰량. max_tokens 보다 크면 max_tokens 갯수 외 나머지는 무시된다.
◦
기타
▪
response_headers_to_add: limiting 발생 시 x-local-rate-limit: true 를 429 response header를 추가한다.
•
적용 코드
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: local-ratelimit-dockebi-v1-frontend # (권장) local-ratelimit-{workload 이름}
namespace: service # 적용 대상 workload의 namespace
spec:
workloadSelector:
labels: # 적용 대상 pod 식별 가능한 label
app: dockebi
version: v1
mode: frontend
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.local_ratelimit
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
value:
stat_prefix: dummy # `dummy` token이 들어간 metric은 사용하지 않음
- applyTo: HTTP_ROUTE
match:
context: SIDECAR_INBOUND
routeConfiguration:
vhost:
name: "inbound|http|8080"
route:
action: ANY
patch:
operation: MERGE
value:
route:
rate_limits:
- actions:
- header_value_match: # exact match 사용 경우 예. method도 포함
descriptor_value: "stuff_exact" # 아래 token 값 설정 descriptor 섹션에 대한 label.
expect_match: true
headers:
- name: :path
string_match:
exact: "/v1/stuff"
- name: :method
string_match:
exact: "GET"
- actions:
- header_value_match: # prefix match 사용 경우
descriptor_value: "stuff_prefix"
expect_match: true
headers:
- name: :path
string_match:
prefix: "/v1/stuff/"
- actions:
- header_value_match: # regex match 사용 경우
descriptor_value: "error"
expect_match: true
headers:
- name: :path
string_match:
safe_regex:
google_re2: {}
regex: "/v1/error/.*(\\?.*)?"
typed_per_filter_config:
envoy.filters.http.local_ratelimit:
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
stat_prefix: actual # `actual` token이 들어간 rate limit metric을 사용
token_bucket: # 별도 path, method 설정이 없는 경우에 대한 rate limiting default 설정
max_tokens: 1024 # 초기 burst 허용치
tokens_per_fill: 1024 # token이 버킷에 채워진 직후의 token 갯수
fill_interval: 10s # token이 버킷에 채워지는 주기
filter_enabled:
runtime_key: test_enabled
default_value:
numerator: 100
denominator: HUNDRED
filter_enforced:
runtime_key: test_enabled
default_value:
numerator: 100
denominator: HUNDRED
response_headers_to_add: # rate limiting 발생 시 x-local-rate-limit: true header 추가 여부. 불필요하면 본 block 제거
- append: false
header:
key: x-local-rate-limit
value: "true"
descriptors:
- entries: # 상기 stuff_exact 설정에 대한 rate limiting 설정
- key: header_match
value: stuff_exact
token_bucket:
max_tokens: 5
tokens_per_fill: 5
fill_interval: 10s
- entries: # 상기 stuff_prefix 설정에 대한 rate limiting 설정
- key: header_match
value: stuff_prefix
token_bucket:
max_tokens: 3
tokens_per_fill: 3
fill_interval: 10s
- entries: # 상기 error 설정에 대한 rate limiting 설정
- key: header_match
value: error
token_bucket:
max_tokens: 2
tokens_per_fill: 2
fill_interval: 10s
YAML
복사