--- title: Response flag 실전 판독 — 식별 팁 5 · 계층별 카탈로그 · 조합 해석 date: 2026-08-01 type: guide domain: istio tags: [istio, envoy, response-flags, access-log, telemetry, troubleshooting, monitoring] --- > [!abstract] 이 문서가 다루는 것 > Envoy response flag는 access log 한 줄을 짧게 유지하려고 만든 2~4자 약어라, 애초에 사람이 읽는 걸 목적으로 설계된 필드가 아니다. 그래서 "flag가 짧아 식별이 어렵다"의 해법은 약어 암기가 아니라 ① flag 옆에 이미 있는 서술형 필드(`RESPONSE_CODE_DETAILS`)를 읽고, ② 집계해서 보고, ③ 로그 포맷을 바꾸는 것이다. flag 하나하나의 의미·파이프라인 멘탈모델·28-flag 표의 정본은 [Envoy Response Flags 운영 레퍼런스](/docs/istio/xds-envoy/envoy-response-flags/)이고, 이 문서는 그 표를 실전에서 소비하는 워크플로(식별 팁 5가지)와 계층별 카탈로그·조합 판독을 다룬다. ## 0. 전제 교정 — NR은 404다 먼저 흔한 오분류 하나. NR을 503 계열로 묶기 쉬운데, **HTTP에서 NR은 404와 함께 남는다** ([정본 문서 C3에서 실측 확인](/docs/istio/xds-envoy/envoy-response-flags/)). 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 평문 터널 설계](/docs/istio/egress/squid-consolidation-guide/)에서 바로 만날 수 있는 케이스다. ## 2. 팁 2 — 집계해서 본다 (한 줄씩 읽지 않는다) 필드 위치가 고정이라 awk로 바로 집계된다. 여러 flag가 쉼표로 함께 나올 수 있고(`UC,URX` = upstream이 끊었고 retry도 소진), (코드, flag, details) 조합을 뽑아 정렬·집계하면 어떤 원인이 몇 건인지 바로 나온다. ```bash # (코드, flag, details) 조합별 건수 — 이 한 줄이 진단의 90% kubectl logs deploy/ -n -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](/docs/istio/xds-envoy/envoy-response-flags/) 참조.) ```yaml # 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 ``` 검증: ```bash kubectl logs deploy/ -n egress -c istio-proxy --tail=5 | jq . # 예상: response_flags / response_code_details 키가 붙은 JSON 객체 # JSON에서 집계 kubectl logs deploy/ -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](files/eflag.sh)** ```bash #!/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 ``` ```bash install -m 0755 eflag.sh ~/bin/eflag kubectl logs deploy/ -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 분포를 볼 수 있다. ```promql # 어떤 목적지에서 어떤 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도 교차 확인할 수 있다: ```bash kubectl exec deploy/ -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 표는 [정본](/docs/istio/xds-envoy/envoy-response-flags/) 참조.) ### 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 오조합](/docs/istio/egress/dr-subset-port-scoping/)), 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만 보면 정상 트래픽과 구별되지 않는다. ```bash kubectl logs deploy/ -n -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줄 1. flag를 외우는 대신 바로 옆 `RESPONSE_CODE_DETAILS`(기본 포맷에 이미 있음, `awk '{print $5,$6,$7}'`)를 읽는 게 가장 빠른 해결책. 2. (코드, flag, details) 조합으로 집계하고, 장기 추세는 `istio_requests_total`의 `response_flags` 라벨로 본다. 3. 같은 503도 NC/UH/UO(연결 시도 전) vs UF(수립 실패) vs UC/UR/LR(수립 후 종료)로 갈리고, **URX는 결과일 뿐 원인이 아니다**. ## 관련 문서 - [Envoy Response Flags 운영 레퍼런스](/docs/istio/xds-envoy/envoy-response-flags/) — 28-flag 정본 표·파이프라인 멘탈모델·`RESPONSE_FLAGS_LONG` JSON 포맷 (검증 문서). 이 문서는 그 표의 소비 워크플로 - [Istio VirtualService ↔ DestinationRule — 매핑 구조와 디버깅 플레이북](/docs/istio/egress/vs-dr-mapping-playbook/) — NR/NC/UH를 만드는 설정 계층의 정체 (Vol.1) - [DR subset × port 교차 생성 — 원리와 포트 스코핑 전략](/docs/istio/egress/dr-subset-port-scoping/) — "설정은 전부 정합한데 UF" 케이스 (Vol.2) - [xDS 계층과 진단](/docs/istio/xds-envoy/xds-layers-and-diagnosis/) — flag가 가리키는 계층을 `pc` 서브커맨드로 순회하는 정본