Response flag 실전 판독 — 식별 팁 5 · 계층별 카탈로그 · 조합 해석
Envoy response flag는 access log 한 줄을 짧게 유지하려고 만든 2~4자 약어라, 애초에 사람이 읽는 걸 목적으로 설계된 필드가 아니다. 그래서 “flag가 짧아 식별이 어렵다"의 해법은 약어 암기가 아니라 ① flag 옆에 이미 있는 서술형 필드(RESPONSE_CODE_DETAILS)를 읽고, ② 집계해서 보고, ③ 로그 포맷을 바꾸는 것이다. flag 하나하나의 의미·파이프라인 멘탈모델·28-flag 표의 정본은 Envoy Response Flags 운영 레퍼런스이고, 이 문서는 그 표를 실전에서 소비하는 워크플로(식별 팁 5가지)와 계층별 카탈로그·조합 판독을 다룬다.
0. 전제 교정 — NR은 404다
먼저 흔한 오분류 하나. NR을 503 계열로 묶기 쉬운데, HTTP에서 NR은 404와 함께 남는다 (정본 문서 C3에서 실측 확인). TCP나 filter chain 미매칭일 때는 응답 코드 없이 연결이 끊어지는 형태로 관측된다. 503 계열은 UH/NC/UF/UC/UR/LR/UO다.
1. 팁 1 — flag가 아니라 RESPONSE_CODE_DETAILS를 읽는다 (가장 효과 큼)
Istio의 기본 TEXT 로그 포맷에 이미 들어있다. flag 바로 다음 필드다. 대부분 이 필드 하나로 끝난다.
[2026-07-30T04:12:33.812Z] "GET /api HTTP/1.1" 503 UF,URX upstream_reset_before_response_started{connection_failure} - "TLS_error:...WRONG_VERSION_NUMBER" 0 91 12 ...
$1 $2 $3 $4 $5 $6 $7 $8 $9
code flags RESPONSE_CODE_DETAILS term_details UPSTREAM_TRANSPORT_FAILURE_REASON
대표적인 details 값:
| details | 의미 |
|---|---|
via_upstream |
정상 — upstream이 응답함 |
no_healthy_upstream |
UH의 서술형 |
no_cluster |
NC의 서술형 |
route_not_found |
NR의 서술형 |
upstream_reset_before_response_started{connection_failure} |
UF — 연결 수립 실패 |
upstream_reset_before_response_started{connection_termination} |
UC — 응답 전 끊김 |
rbac_access_denied_matched_policy[ns[x]-policy[y]-rule[0]] |
AuthorizationPolicy 거부 — flag는 - |
filter_chain_not_found |
listener에 매칭 filter chain 없음 |
upstream_max_stream_duration_reached |
UMSDR |
마지막에서 두 번째 항목이 핵심 논거다. 403 RBAC 거부는 전용 flag가 없어서 flag만 보면 -(정상)로 보인다. details를 안 읽으면 진단이 불가능한 케이스가 실제로 존재한다 (→ §8).
그리고 UF가 나왔을 때 UPSTREAM_TRANSPORT_FAILURE_REASON(9번째 필드)에 TLS 핸드셰이크 오류 원문이 담긴다. WRONG_VERSION_NUMBER가 보이면 “평문 포트에 TLS를 보냈다(또는 반대)“는 확정 신호다 — Squid 3128 평문 터널 설계에서 바로 만날 수 있는 케이스다.
2. 팁 2 — 집계해서 본다 (한 줄씩 읽지 않는다)
필드 위치가 고정이라 awk로 바로 집계된다. 여러 flag가 쉼표로 함께 나올 수 있고(UC,URX = upstream이 끊었고 retry도 소진), (코드, flag, details) 조합을 뽑아 정렬·집계하면 어떤 원인이 몇 건인지 바로 나온다.
# (코드, flag, details) 조합별 건수 — 이 한 줄이 진단의 90%
kubectl logs deploy/<app> -n <ns> -c istio-proxy --tail=5000 \
| awk '$6!="-" {print $5, $6, $7}' | sort | uniq -c | sort -rn | head -20
# 예상 출력
# 142 503 UF,URX upstream_reset_before_response_started{connection_failure}
# 17 503 UH no_healthy_upstream
# 3 404 NR route_not_found
건수 분포를 먼저 보면 “간헐 vs 전면"도 같이 판단된다.
3. 팁 3 — JSON 포맷 + 네임스페이스 단위 적용
약어 식별 문제의 근본 해결책은 필드명이 붙는 JSON 포맷이다. meshConfig.extensionProviders로 provider를 정의하고 Telemetry 리소스로 네임스페이스 단위 적용하면 mesh 전역을 안 건드려도 된다 — 무장애 전환 제약이 있는 환경에 맞는 방식이다. (mesh 전역 accessLogFormat 방식과 RESPONSE_FLAGS_LONG 등 권장 필드 구성은 정본 문서 §05 참조.)
# 1) istiod values(meshConfig) — provider 정의
meshConfig:
extensionProviders:
- name: json-log
envoyFileAccessLog:
path: /dev/stdout
logFormat:
labels:
start_time: "%START_TIME%"
method: "%REQ(:METHOD)%"
authority: "%REQ(:AUTHORITY)%"
response_code: "%RESPONSE_CODE%"
response_flags: "%RESPONSE_FLAGS%"
response_code_details: "%RESPONSE_CODE_DETAILS%"
connection_termination_details: "%CONNECTION_TERMINATION_DETAILS%"
transport_failure_reason: "%UPSTREAM_TRANSPORT_FAILURE_REASON%"
upstream_cluster: "%UPSTREAM_CLUSTER%"
upstream_host: "%UPSTREAM_HOST%"
route_name: "%ROUTE_NAME%"
requested_server_name: "%REQUESTED_SERVER_NAME%"
duration: "%DURATION%"
x_request_id: "%REQ(X-REQUEST-ID)%"
---
# 2) 적용 범위 — egress 네임스페이스만
apiVersion: telemetry.istio.io/v1
kind: Telemetry
metadata:
name: json-access-log
namespace: egress
spec:
accessLogging:
- providers:
- name: json-log
검증:
kubectl logs deploy/<app> -n egress -c istio-proxy --tail=5 | jq .
# 예상: response_flags / response_code_details 키가 붙은 JSON 객체
# JSON에서 집계
kubectl logs deploy/<app> -n egress -c istio-proxy --tail=5000 \
| jq -r 'select(.response_flags!="-") | [.response_code,.response_flags,.response_code_details]|@tsv' \
| sort | uniq -c | sort -rn
upstream_cluster를 넣은 이유가 있다. egress 설계에서 이 필드에 BlackHoleCluster가 찍히면 outboundTrafficPolicy: REGISTRY_ONLY에 막혔다는 뜻이라, ServiceEntry 누락을 flag보다 훨씬 확실하게 잡아낸다. PassthroughCluster면 반대로 mesh가 모르는 목적지로 그냥 흘러나간 것이다.
4. 팁 4 — 로컬 디코더 (istioctl에 flag 해석 커맨드는 없다)
없으니 직접 두는 게 빠르다. 기본 TEXT 포맷 전용으로 동작하는 주석기 스크립트: files/eflag.sh
#!/usr/bin/env bash
# eflag — Envoy/Istio response flag 주석기 (기본 TEXT 포맷 전용)
# usage: kubectl logs deploy/app -c istio-proxy --tail=200 | eflag
declare -A DESC=(
[UH]="endpoint 0개 (label 불일치·readiness)"
[NC]="route가 가리키는 cluster 없음 (DR/host)"
[NR]="매칭 route 없음 / filter chain 없음"
[UF]="upstream 연결 수립 실패 (refused·mTLS·NetworkPolicy)"
[UC]="응답 전 upstream이 연결 종료 (idle timeout 불일치)"
[UR]="upstream remote reset (RST 수신)"
[LR]="Envoy가 로컬에서 reset"
[UO]="circuit breaker overflow (connectionPool 한도)"
[URX]="retry 한도(HTTP)·connect 시도 한도(TCP) 초과"
[UT]="upstream 응답 타임아웃 (VS timeout)"
[DC]="downstream(client)이 먼저 끊음"
[SI]="stream idle timeout"
[UMSDR]="max stream duration 도달"
[DT]="max connection duration 초과"
[DPE]="downstream HTTP protocol error"
[UPE]="upstream HTTP protocol error"
[DF]="DNS 해석 실패"
[RL]="local rate limit"
[RLSE]="rate limit service 오류"
[UAEX]="ext_authz 거부"
[IH]="잘못된 요청 헤더 (400)"
[LH]="local health check 실패"
[OM]="overload manager 차단"
[NFCF]="filter config warming 미완료"
[DI]="fault injection delay (의도됨)"
[FI]="fault injection abort (의도됨)"
)
decode() {
local f res=""
IFS=',' read -ra fs <<< "$1"
for f in "${fs[@]}"; do res+="${f}=${DESC[$f]:-unknown}; "; done
printf '%s' "${res%; }"
}
while IFS= read -r line; do
read -ra F <<< "$line"
printf '%s\n' "$line"
[[ "${F[5]}" != "-" && -n "${F[5]}" ]] && \
printf ' \xe2\x86\xb3 [%s] %s | details=%s\n' "${F[4]}" "$(decode "${F[5]}")" "${F[6]}"
done
install -m 0755 eflag.sh ~/bin/eflag
kubectl logs deploy/<app> -n egress -c istio-proxy --tail=100 | eflag
# 예상 출력 (원본 줄 + 주석 줄)
# [2026-07-30T...] "GET / HTTP/1.1" 503 UF,URX upstream_reset_before_...{connection_failure} ...
# ↳ [503] UF=upstream 연결 수립 실패 (refused·mTLS·NetworkPolicy); URX=retry 한도... | details=upstream_reset_before_response_started{connection_failure}
5. 팁 5 — Prometheus로 추세 관측
istio_requests_total에 response_flags 라벨이 표준으로 붙는다. 로그를 안 켜도 flag 분포를 볼 수 있다.
# 어떤 목적지에서 어떤 flag가 늘고 있는지
sum by (destination_service_name, response_flags) (
rate(istio_requests_total{reporter="source", response_code=~"5.."}[5m])
)
response_flags="UH" 기준 알림(endpoint 전멸), "UO" 기준 알림(circuit breaker 발동), "UT" 비율 알림 같은 식으로 PrometheusRule을 걸어두는 패턴이 일반적이다. 로그는 개별 요청, 메트릭은 추세 — 역할이 다르다.
보조로 proxy 자체 stats도 교차 확인할 수 있다:
kubectl exec deploy/<app> -n egress -c istio-proxy -- \
pilot-agent request GET 'stats?filter=upstream_(cx_connect_fail|rq_pending_overflow|rq_retry_limit_exceeded)'
6. flag 카탈로그 — 계층별
표는 색인이고, 운영에서 실제로 헷갈리는 항목은 각 표 아래에서 메커니즘을 짚는다. (long name·전체 28-flag 표는 정본 참조.)
A. 설정·라우팅 계층 — 설정이 틀렸다
| flag | 이름 | 코드 | 발생 시점 |
|---|---|---|---|
NR |
NoRouteFound | 404 / TCP는 연결 종료 | 요청 host·path에 매칭 route 없음, 또는 downstream 연결에 매칭 filter chain 없음 |
NC |
NoClusterFound | 503 | route는 매칭됐지만 그 route가 가리키는 cluster가 CDS에 없음 |
UH |
NoHealthyUpstream | 503 | cluster는 있지만 healthy endpoint 0개 |
NR과 NC의 차이가 진단 리소스를 가른다. NR = 라우팅 테이블에 규칙 자체가 없음(VS hosts 오타, registry 미등록, gateway 미바인딩) → pc route를 본다. NC = 규칙은 있는데 목적지 cluster가 없음(DR host FQDN 불일치, subset 오타, exportTo 차단) → pc cluster를 본다. UH = cluster는 정상, label 필터 결과가 빔(Pod label 불일치, 전부 not-ready, outlier detection으로 전멸) → pc endpoint를 본다.
TCP 트래픽에서 NR은 응답 코드 없이 나타난다. filter_chain_not_found details가 같이 보이면 listener 매칭 문제다 — ServiceEntry 포트 미선언이 전형적이다.
B. 연결·전송 계층 — 연결이 안 되거나 끊겼다
| flag | 이름 | 코드 | 발생 시점 |
|---|---|---|---|
UF |
UpstreamConnectionFailure | 503 | TCP 연결 수립 실패 — refused, timeout, TLS 핸드셰이크 실패 |
UC |
UpstreamConnectionTermination | 503 | 연결은 됐으나 응답 완료 전 upstream이 정상 종료(FIN) |
UR |
UpstreamRemoteReset | 503 | upstream이 RST로 끊음 |
LR |
LocalReset | 503 | Envoy 자신이 연결을 reset |
DC |
DownstreamConnectionTermination | — | 클라이언트가 응답 완료 전에 끊음 |
DPE / UPE |
Downstream/Upstream ProtocolError | 400 / 502 | HTTP 파싱 오류 — 평문/TLS 혼선, HTTP/2 위반 |
DF |
DnsResolutionFailed | 503 | DNS 해석 실패 (ServiceEntry resolution: DNS 경로) |
UF vs UC가 실무에서 가장 자주 오진된다. UF는 연결이 처음부터 안 됨 — Pod가 그 포트를 listen하지 않거나(port×subset 오조합), mTLS 모드 불일치, NetworkPolicy 차단. UC는 연결은 됐음 — 앱의 keep-alive idle timeout이 Envoy보다 짧아서 재사용하려던 커넥션이 죽은 경우가 압도적으로 많다. 앱 idle timeout을 Envoy 쪽보다 길게 두는 게 정석 대응이다.
DC는 대개 우리 문제가 아니다 — 클라이언트 timeout이 VS timeout보다 짧으면 정상적으로 발생한다. DC를 서버 장애로 오해해 upstream을 파는 게 흔한 낭비다.
C. 정책·한도 계층 — 일부러 막았다
| flag | 이름 | 코드 | 발생 시점 |
|---|---|---|---|
UO |
UpstreamOverflow | 503 | DR connectionPool 한도 초과 (circuit breaking) |
URX |
UpstreamRetryLimitExceeded | — | HTTP retry 한도 또는 TCP connect 시도 한도 소진 |
RL |
RateLimited | 429 | local rate limit filter |
RLSE |
RateLimitServiceError | 500 | rate limit 서비스 자체 오류 |
UAEX |
UnauthorizedExternalService | 403 | ext_authz(외부 인가 서비스) 거부 |
IH |
InvalidEnvoyRequestHeaders | 400 | 엄격 검사 대상 헤더에 잘못된 값 |
OM |
OverloadManager | — | proxy 과부하 보호로 요청 차단 |
UO는 “upstream이 죽었다"가 아니라 우리가 걸어둔 한도에 막혔다는 뜻이다. pc cluster -o json의 thresholds와 :15000/stats의 upstream_rq_pending_overflow를 같이 본다. URX는 단독으로 거의 안 나온다 — 항상 원인 flag와 짝을 이룬다 (→ §7).
D. 타임아웃 계층
| flag | 이름 | 코드 | 발생 시점 |
|---|---|---|---|
UT |
UpstreamRequestTimeout | 504 | VS timeout 또는 route timeout 초과 |
SI |
StreamIdleTimeout | 408 | stream idle timeout |
UMSDR |
UpstreamMaxStreamDurationReached | — | max stream duration 도달 — gRPC 스트림 등 장수명 연결 |
DT |
DurationTimeout | — | max connection duration 초과 |
UT만 504라는 점이 식별 포인트다. 503 무리에서 벗어나 있어 코드만 봐도 구분된다.
E. 의도된 것 / 정상
| flag | 의미 |
|---|---|
- |
flag 없음 — 정상. 단, 403 RBAC 거부도 여기에 들어옴 (→ §8) |
DI / FI |
fault injection의 delay/abort — 운영에서 보이면 VS에 남은 테스트 설정 의심 |
LH |
로컬 health check 실패 |
RFCF |
cache filter가 응답 |
NFCF |
filter config warming 미완료 상태에서 요청 도착 |
7. 실전 판독 — 조합과 503 6갈래
503 UF,URX 같은 조합은 **“원인 flag + 결과 flag”**로 읽는다:
UF,URX → 연결 수립 실패가 반복되어 재시도 한도 소진 (근본 원인: UF)
UC,URX → upstream이 계속 끊어서 재시도 한도 소진 (근본 원인: UC)
UO,URX → circuit breaker에 계속 막혀 재시도 소진 (근본 원인: UO)
DC,URX → 재시도 도중 클라이언트가 포기 (근본 원인: 클라이언트 timeout)
같은 503의 갈래를 한 표로:
| flag | 연결 수립 | 누가 종료 | 1차 확인 |
|---|---|---|---|
NC |
시도 안 함 | — | pc cluster, DR host/subset/exportTo |
UH |
시도 안 함 | — | pc endpoint, Pod label·readiness |
UO |
시도 안 함 | Envoy(한도) | DR connectionPool, *_pending_overflow stat |
UF |
실패 | — | 실제 listen 포트(ss -tlnp), mTLS, NetworkPolicy |
UC |
성공 | upstream(FIN) | 앱 idle/keep-alive timeout |
UR |
성공 | upstream(RST) | 앱 crash, OOMKill, 프로토콜 불일치 |
LR |
성공 | Envoy | timeout 설정, downstream 이상 |
8. flag가 거짓말하는 순간 — 403의 flag는 -
403에 flag -가 붙은 요청은 flag만 보면 정상 트래픽과 구별되지 않는다.
kubectl logs deploy/<app> -n <ns> -c istio-proxy | awk '$5==403 {print $6, $7}' | sort | uniq -c
# 출력 예:
# 58 - rbac_access_denied_matched_policy[ns[egress]-policy[deny-all]-rule[0]]
# → flag는 "-"(정상)이지만 실제로는 AuthorizationPolicy가 차단한 것
grep -v '"-"' 같은 필터로 “flag 있는 줄만” 보는 습관이 있으면 이 58건이 통째로 사라진다. 응답 코드로 먼저 필터하고, details를 읽는 순서가 안전하다.
What you might be missing
- reporter 축 — 같은 요청이 호출자 sidecar(
reporter="source")와 목적지 sidecar(reporter="destination")에 각각 기록된다. UF/NC처럼 목적지에 도달조차 못 한 실패는 source 쪽에만 존재한다. 목적지 Pod 로그만 보고 “아무 기록 없음"이라 판단하면 원인을 놓친다 — egress gateway 2-leg 구조에서는 sidecar/egressgw 양쪽을 다 봐야 한다. - x-request-id로 leg 연결 — 2-leg 경로(sidecar → egressgw → Squid)에서 같은 요청이 최소 두 번 기록되므로,
x-request-id로 grep하면 어느 leg에서 flag가 처음 붙었는지 확정할 수 있다. egress 디버깅에서 가장 강력한 단일 기법. - flag는 Envoy 버전 의존 — OM, DF, NFCF 같은 항목은 비교적 최근 추가분이다. 처음 보는 약어는 설치된 Envoy 버전 기준의 공식 access log usage 문서로 확인할 것. 2차 자료 오류 사례: DT를 “downstream terminal"로 잘못 적은 블로그가 실재한다 — 실제로는 max connection duration 초과다.
핵심 3줄
- flag를 외우는 대신 바로 옆
RESPONSE_CODE_DETAILS(기본 포맷에 이미 있음,awk '{print $5,$6,$7}')를 읽는 게 가장 빠른 해결책. - (코드, flag, details) 조합으로 집계하고, 장기 추세는
istio_requests_total의response_flags라벨로 본다. - 같은 503도 NC/UH/UO(연결 시도 전) vs UF(수립 실패) vs UC/UR/LR(수립 후 종료)로 갈리고, URX는 결과일 뿐 원인이 아니다.
관련 문서
- Envoy Response Flags 운영 레퍼런스 — 28-flag 정본 표·파이프라인 멘탈모델·
RESPONSE_FLAGS_LONGJSON 포맷 (검증 문서). 이 문서는 그 표의 소비 워크플로 - Istio VirtualService ↔ DestinationRule — 매핑 구조와 디버깅 플레이북 — NR/NC/UH를 만드는 설정 계층의 정체 (Vol.1)
- DR subset × port 교차 생성 — 원리와 포트 스코핑 전략 — “설정은 전부 정합한데 UF” 케이스 (Vol.2)
- xDS 계층과 진단 — flag가 가리키는 계층을
pc서브커맨드로 순회하는 정본