灰度 20% 流量全挂了——Istio DestRule 一个 label 拼错引发的 503 血案
上篇讲了 Istio sidecar 启动慢是控制面配置没到,这篇我们来看另一个更沉默的陷阱:sidecar 启动好了、控制面也通了,但流量就是走不对。
场景:order-service 做灰度发布,80% v1 + 20% v2,部署上去后监控显示 v2 的 20% 流量全 503,v1 的 80% 完全正常。 路径:坐标(现象)→ 分层(Envoy access log → 控制面 XDS → 路由规则 → EDS endpoint)→ 路径(排查命令)→ 定位(误判 vs 真相)→ 标点(修复 + Check-list)
灰度上线,20% 的流量被导向新版本——监控报警了:order-service 的 /api/orders 接口有四分之一比例的请求返回 503。第一反应是 v2 Deployment 的代码有问题,回滚 v2 的 Deployment,回滚后 503 还在——不是新版代码的问题,是 Istio 的 subset 映射层出了问题。控制面和数据面都通了,但流量在终点站落不了地。
一句话总结: 灰度流量 503 的根因不是 v2 代码挂了——是 DestinationRule 的 subset label selector 没匹配到 Pod,Envoy 的 EDS(Endpoint Discovery Service)返回空 endpoint 列表导致 no_healthy_upstream。改 DestRule 的一个 label 值,503 归零。
以下排查基于 Istio 1.20,K8s v1.27,使用 sidecar 自动注入和 Istio 流量管理。
【坐标】现象:灰度 20% 流量全 503
Service 访问链
order-service 的架构很简单:客户端通过 ClusterIP order-service:8080 调用,前面挂了一个 VirtualService 做灰度路由:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- match:
- uri:
prefix: /api/orders
route:
- destination:
host: order-service
subset: v1
weight: 80
- destination:
host: order-service
subset: v2
weight: 20
配了 80% v1 + 20% v2 的灰度比例,两个 subset 由 DestinationRule 定义。
故障表现

监控曲线显示:v1 的 80% 流量全部正常(200),v2 的 20% 流量全部 503。直接把 v2 Deployment 回滚到 v1 镜像,重新部署,503 还在——这证实了问题不在 v2 代码。
# 确认 Pod 都在
$ kubectl get pods -n prod -l app=order-service
NAME READY STATUS RESTARTS
order-service-v1-f9b8d-abc12 2/2 Running 0
order-service-v1-f9b8d-abc13 2/2 Running 0
order-service-v2-7c4d2-def34 2/2 Running 0
# 确认 Endpoint
$ kubectl get endpoints order-service
NAME ENDPOINTS AGE
order-service 10.244.1.15:8080,10.244.1.16:8080,10.244.2.8:8080 12h
Endpoint 里有 3 个 Pod(v1 ×2 + v2 ×1),看起来一切正常。但 503 还在持续——每 5 个请求就有 1 个失败。如果只看 kubectl 的视角,问题不可能存在——这恰恰是 Istio 排查最迷惑的地方。
第一直觉:灰度配比有问题?
团队第一反应是 VirtualService 的 weight 或 match 条件写错了。查了一遍 YAML,prefix: /api/orders 正确,weight: 90/10 正确,subset 名称和 DestRule 定义一致。VS 没问题,那问题在哪?
【分层】四层排查:Envoy 日志 → 控制面 XDS → 路由规则 → EDS endpoint

排查 Istio 路由规则问题,不走标准的 Pod → CRI → Node → 集群四层。Istio 引入了一层新的映射——Envoy 内部的 XDS 配置链,排查时必须走完这层链路:
客户端请求 → VirtualService 匹配 → DestRule subset 解析 → EDS endpoint 查找 → Pod
↑ ↑ ↑
Layer 1(路由规则) Layer 2(控制面) Layer 3(数据面)
Layer 0(入口):Envoy access log

跳过 Envoy access log 直接查控制面是 Istio 排查最常见的跳层。Envoy 的 access log 是第一手证据——它记录了请求进来后 Envoy 做了什么决定。
$ kubectl logs -n prod order-service-v1-f9b8d-abc12 -c istio-proxy \
--tail=50 | grep "503"
[2026-07-11T10:15:22.123Z] "GET /api/orders HTTP/1.1" 503 NR ...
cluster_not_found · "order-service|v2|v2-subset"
关键信息:cluster_not_found。Envoy 告诉你它找不到 v2 subset 对应的 cluster。不是连接超时、不是 v2 进程挂了——是 Envoy 压根不知道 v2 的 endpoint 在哪。Envoy 认为 v1 的 cluster 存在(返回 200),v2 的 cluster 找不到(返回 503)。
这不是 K8s endpoint 层面的问题——K8s 的 endpoint 记录的是 Pod IP:Port,而 Istio 在 K8s endpoint 之上还有一层 EDS 映射。Envoy 收到请求后,根据 VirtualService 找到对应 subset,再通过 EDS 从 subset 的 label selector 中查找 Pod。两个环节各有一套 endpoint 表。
Layer 1:控制面 XDS 配置是否下发
Envoy access log 定位到了 v2 subset 的 cluster 找不到,接下来查控制面——Pilot 是否把对应的 XDS 配置推给了 sidecar。
$ istioctl proxy-status order-service-v1-f9b8d-abc12
NAME CDS LDS EDS RDS PILOT
order-service-v1-f9b8d-abc12.prod SYNCED SYNCED STALE SYNCED istiod-65c8b
EDS 列为 STALE——控制面推了配置,但 Envoy 认为当前的内容过期了,正在等更新。持续 STALE 说明 EDS 层面有异常。
$ istioctl proxy-config cluster order-service-v1-f9b8d-abc12 \
--direction outbound | grep -E "v1|v2"
order-service|v1|v1-subset prod - 10.244.1.15:8080,10.244.1.16:8080
order-service|v2|v2-subset prod - <no endpoints>
Cluster 列表确认了:v1 的 cluster 有 2 个 endpoint,v2 的 cluster 显示 no endpoints。Pilot 收到了 DestRule 配置,也创建了对应的 EDS cluster,但 cluster 里一个 Pod 都没找到。
Layer 2:路由规则配置是否正确
配置已经下发、cluster 也已创建,但 cluster 为空——问题缩小到了 DestRule 的 subset label selector 与实际 Pod label 不匹配。
查看 DestRule:
$ kubectl get destinationrule order-service -n prod -o yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: order-service
spec:
host: order-service
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2_fix # ← 这里!Pod 的 label 是 version: v2,不是 v2_fix
Pod 的实际 label:
$ kubectl get pods -n prod -l app=order-service --show-labels
NAME READY LABELS
order-service-v1-f9b8d-abc12 2/2 app=order-service,version=v1
order-service-v1-f9b8d-abc13 2/2 app=order-service,version=v1
order-service-v2-7c4d2-def34 2/2 app=order-service,version=v2
version: v2 和 version: v2_fix——一个字符的差异。DestRule 的 label selector 要求匹配 version: v2_fix,但集群中没有一个 Pod 有这个 label,EDS 把 service 的 endpoint 全都枚举了一遍也找不到匹配,最终返回空列表。
Layer 3:EDS 映射链路(根因解释)
为什么 kubectl get endpoints 有 v2 的 Pod IP,但 Envoy 认为它不存在?因为这是两层不同的映射:
K8s Service → kube-proxy → iptables/IPVS(kubectl get endpoints 看到的是这层)
K8s Service → DestRule subset → Pilot → EDS → Envoy cluster(Istio 的映射层)
Istio 注入 sidecar 后,所有 Pod 间流量被 Envoy 劫持,不再走 kube-proxy 的 iptables 规则。Envoy 收到请求后,先查自己的 XDS 配置(从 Pilot 下发),再根据配置决定转发目标。
这套配置链路的完整路径是:
VirtualService → destination.subset: v2 → DestRule 查 v2 的 labels → version: v2_fix → EDS 以 app=order-service,version=v2_fix 搜索 endpoint → 0 匹配 → cluster 为 no endpoints → Envoy 返回 cluster_not_found → 503
不是 K8s 的 endpoint 不对——是 Istio 自己的 endpoint 映射层(EDS)用了一个不存在的 label 去搜索,自然搜不到任何 Pod。
【路径】核心排查命令

Envoy access log 是第一入口
不要跳过 Envoy 日志直接查控制面。Envoy 日志告诉你每个请求的处理结果——是 cluster 找不到、连接超时、还是 upstream 挂了。
# 查 503 日志
kubectl logs <pod> -c istio-proxy | grep "503"
# 输出示例:
# "GET /api/orders" 503 NR cluster_not_found · "order-service|v2|v2-subset"
# ① 503 响应码 → 后端拒绝了请求
# ② NR(NoRoute)→ Envoy 内部路由决策的结果码
# ③ cluster_not_found → Envoy 找不到目标 cluster
# ④ order-service|v2|v2-subset → 完整的 cluster 名
三层信息要从右往左读:先看 cluster 名确认是哪条路由,再看结果码(NR / UH / UF),最后看响应码。
控制面状态检查
istioctl proxy-status 查 XDS 同步状态——全列 SYNCED 才算正常。STALE 提示配置过期,NOT SENT 表示控制面根本还没推送。
istioctl proxy-status <pod>
Cluster endpoint 确认
istioctl proxy-config cluster <pod> --direction outbound | grep <service>
# 看 endpoint 数量——有 `<no endpoints>` 说明 subset 没匹配到 Pod
这一步是把排查从"控制面配了没有"推进到"配对了没有"的关键。Envoy 收到了路由规则配置,但规则说"去 v2_subset 找 endpoint 列表",EDS 找不着——那就是规则本身的 label selector 写错了。
【定位】大多数人都误判了两个地方

❌ 误判 1:v2 代码挂了
灰度上线 + 部分流量 503 → 第一反应是"新版本有 bug"。于是回滚 deployment,结果 503 还在——浪费了 20 分钟。回滚加重新部署已经够快,但方向从根上就错了。
为什么很自然会想到代码问题?因为在传统部署中(无 Istio/无服务网格),流量路由就是 deployment 配的 Service selector 决定的。新版本 Pod 起来后不响应,Service 自然把流量转到新 Pod 上,503 说明新 Pod 有问题。
但在 Istio 的架构下,deployment 决定不了流量怎么走——是 VirtualService + DestinationRule 决定的。v2 的 Pod 本身是正常的(用端口转发直接访问 Pod IP 能返回 200),只是 Envoy 不把流量发过去。
# 绕过 sidecar 直接访问 v2 Pod
$ kubectl exec -it debug-pod -- curl order-service-v2-7c4d2-def34:8080/api/orders
{"status":"OK"} # ← v2 代码正常,是 Envoy 不把流量传过来
❌ 误判 2:VirtualService weight 配错了
排除了代码问题后,团队转向检查 VirtualService:先怀疑 weight 分布、再怀疑 match 条件、再检查 VS 和 DestRule 的 host 是否一致——全查了一遍,全对。这一步花了 15 分钟排查无关方向。
✅ 正确方向:先看 Envoy 决定了什么,再看规则写了什么
正确的排查顺序应该是:
Envoy access log(怎么看请求)→ 结果码是什么(NR/UH/UF/DC)
→ 对应哪个 cluster → cluster 有 endpoint 吗
→ DestRule subset labels 匹配实际 Pod label 吗
→ 修复 label → 再次验证 access log
排查不是在查 K8s——是在查 Istio 的配置映射链。这条链的起点是 Envoy access log,不是 K8s endpoint table。
【标点】修复 + Check-list

修复:改一个 label 值
把 DestRule 中 v2 subset 的 labels 从 version: v2_fix 改为 version: v2:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: order-service
spec:
host: order-service
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2 # ← fix: 改回实际 Pod label
应用后:
$ kubectl apply -f destinationrule-fix.yaml
# 验证 EDS endpoint 恢复
$ istioctl proxy-config cluster order-service-v1-f9b8d-abc12 \
--direction outbound | grep "v2"
order-service|v2|v2-subset prod - 10.244.2.8:8080
# 确认 503 消失
$ kubectl logs order-service-v1-f9b8d-abc12 -c istio-proxy \
--tail=10 | grep "503"
# (无输出——503 归零)
效果:DestRule 应用后,Pilot 拿到新配置重新计算 EDS endpoint,v2 subset 的 cluster 从 no endpoints 变为 10.244.2.8:8080,灰度流量恢复正常。整个过程不需要重启 Pod,不需要重新部署——配置热生效。
排查 Istio 路由问题不是在查 K8s——是在查 Envoy 内部的配置映射链,起点是 access log,终点是 subset label 的实际匹配。
Check-list:同类问题排查清单
# 1. 查 Envoy access log——确认结果码和 cluster 名
kubectl logs <pod> -c istio-proxy | grep "503\|NR\|UH\|cluster_not_found"
# 2. 查控制面 XDS 同步状态
istioctl proxy-status <pod>
# 3. 查目标 cluster 的 endpoint 列表
istioctl proxy-config cluster <pod> --direction outbound | grep <service>
# 4. 查 DestRule 实际 label selector
kubectl get destinationrule <name> -n <ns> -o yaml | grep -A 3 "labels:"
# 5. 查 Pod 实际 label
kubectl get pods -n <ns> -l app=<app> --show-labels
# 6. 直接用 istioctl analyze 自动检测
istioctl analyze -n <ns>
把 DestRule 的 subset label 和 Pod label 做一次互相比对,基本 5 分钟定位。用 istioctl analyze 也能检测部分 label mismatch,但它是静态代码分析——YAML 里的 label 拼写对了但和 running Pod 不对应,它查不到,最终还是需要人工核对。
从这次故障中带走的
Istio 灰度故障排查,不要在 K8s 原生视角里找原因,要在 Envoy 的配置映射链里找原因。 kubectl get endpoints 有 Pod IP ≠ Envoy 上有对应 cluster。多了一层映射,就多了一层排查维度——而 Istio 这层的排查入口在 Envoy access log 里,不仔细看根本不知道方向偏了。
下篇我们聊一个更隐蔽的 Istio 网络问题:Gateway API 配置和 Ingress 兼容性排查。
附:完整命令清单
# Envoy access log(查 503 和结果码)
kubectl logs <pod> -c istio-proxy | grep -E "503|NR|cluster_not_found|no_healthy_upstream"
# Envoy access log(查所有请求结果码,含 200)
kubectl logs <pod> -c istio-proxy | grep "inbound\|outbound"
# 控制面 XDS 同步状态
istioctl proxy-status <pod>
# 目标 cluster endpoint 列表
istioctl proxy-config cluster <pod> --direction outbound | grep <service>
# 目标 cluster 的详细 endpoint 分布
istioctl proxy-config endpoints <pod> --cluster "outbound|8080||<service>"
# 当前 XDS 配置全量(诊断用)
istioctl proxy-config all <pod>
# 检查 DestRule
kubectl get destinationrule -n <ns> -o yaml
# 检查 Pod label
kubectl get pods -n <ns> -l app=<app> --show-labels
# 绕过 sidecar 直接访问 Pod IP(验证服务本身正常)
kubectl exec -it <debug-pod> -n <ns> -- curl <pod-ip>:<port>
# Istio 配置静态检查
istioctl analyze -n <ns>
# 查看 Pilot 配置推送日志
kubectl logs -n istio-system deploy/istiod | grep -E "push|eds|endpoint" | tail -20
📺 公众号「Ai拆代码的曹操」 🌟 知识星球「Ai拆代码的曹操」