Sidecar 之后,流量路径变了
接入 Istio 之后,一个 Pod 里的应用发出的请求不再直接出网卡:出站流量被 iptables 规则劫持到 istio-proxy 的 15001 端口,由 Envoy 按 Listener、Route、Cluster 三层决策后再转发;入站同理,先过 15006 再交给应用。这意味着超时、重试、熔断、mTLS 这些能力从“代码里”搬到了“数据面”,改一行 YAML 就能对全部实例生效。
代价是排查链路变长了。请求变慢时,你不知道是应用慢、Envoy 慢、还是两者之间的连接池在排队。
VirtualService 与 DestinationRule 的分工
这两个对象经常被混着写,其实职责很清楚:
VirtualService回答“请求往哪去”:匹配条件、路由权重、超时、重试、故障注入;DestinationRule回答“到了之后怎么连”:子集定义、负载均衡算法、连接池、熔断阈值、mTLS 模式。
一个典型误区是把连接池和熔断写在 VirtualService 里,怎么都不生效;它们属于 DestinationRule。反过来,把路由规则写进 DestinationRule 也不会起作用。
一份可用的灰度 + 保护规则
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: order-api
namespace: shop-prod
spec:
hosts:
- order-api
http:
- name: canary
match:
- headers:
x-gray:
exact: "canary"
route:
- destination:
host: order-api
subset: v2
- name: stable
route:
- destination:
host: order-api
subset: v1
weight: 90
- destination:
host: order-api
subset: v2
weight: 10
timeout: 800ms
retries:
attempts: 2
perTryTimeout: 300ms
retryOn: 5xx,reset,connect-failure
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: order-api
namespace: shop-prod
spec:
host: order-api
trafficPolicy:
connectionPool:
tcp:
maxConnections: 200
http:
http1MaxPendingRequests: 100
http2MaxRequests: 400
maxRequestsPerConnection: 20
outlierDetection:
consecutive5xxErrors: 5
interval: 30s
baseEjectionTime: 60s
maxEjectionPercent: 30
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
这里有个必须算清楚的关系:timeout 是整条路由的总时间预算,包含重试;perTryTimeout × attempts 不能超过它,否则重试还没跑完总超时就到了,重试等于白配。上面的配置是 300ms × 2 = 600ms,给 800ms 的总预算留了余量。
outlierDetection 是 Istio 的被动熔断:某个实例连续 5 次 5xx 就被摘出负载均衡池 60 秒,最多摘 30% 的实例。它不需要额外组件,但要注意 maxEjectionPercent 设成 100% 时,一个坏配置可能把所有实例摘光。
mTLS 与探针的两个经典冲突
严格 mTLS 打开后,只有带 Istio 证书的工作负载能访问服务。kubelet 发出的健康检查不带证书,就会被拒。Istio 的默认处理是把探针改写成经过 pilot-agent(15020 端口)转发,这一步依赖注入时自动加的注解;如果 Pod 被手工跳过了注入,或者用 exec 探针而不是 HTTP 探针,改写就不生效。
另一个冲突来自非网格客户端:Prometheus 抓取、外部负载均衡、运维脚本直连 Pod IP,在 STRICT 模式下全部失败。正确的做法是按端口或按客户端放行,而不是把整个命名空间降级成 PERMISSIVE:
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: allow-metrics-plaintext
namespace: shop-prod
spec:
selector:
matchLabels:
app: order-api
portLevelMtls:
15020:
mode: PERMISSIVE
mtls:
mode: STRICT
排查步骤
Istio 的问题有一半出在“配置根本没下发”,先确认这一层再怀疑业务:
# 1. 静态检查:语法错误、引用不存在的 subset、host 拼错
istioctl analyze -n shop-prod
# 2. 控制面到数据面的同步状态,SYNCED 才是下发了
istioctl proxy-status
# 3. 看 Envoy 实际生效的路由,这一步能直接证伪“配置没生效”
istioctl proxy-config route deploy/order-api -n shop-prod -o json \
| jq '.[] | .virtualHosts[] | {name, routes: [.routes[].route]}'
# 4. 看集群与连接池的实时指标
kubectl exec -n shop-prod deploy/order-api -c istio-proxy -- \
pilot-agent request GET stats | grep -E 'upstream_rq_(pending|timeout)|upstream_cx_overflow'
如果 proxy-status 显示 STALE,问题在控制面推送,检查 istiod 日志和配置数量;如果 SYNCED 但行为不对,用第 3 步导出实际路由,八成能看出匹配条件写错。upstream_cx_overflow 持续增长则说明连接池上限压得太低,把 maxConnections 调大或减少长连接数。
常见坑
应用层和网格层同时重试。 Spring Cloud LoadBalancer 里配了 3 次重试,Istio 又配了 3 次,最坏情况下游收到 9 次请求。接入网格之后,重试策略应该只保留一处,通常放在网格,应用只保留幂等保护。
把 Istio 的 timeout 当成应用超时。 网格超时只能切断连接,不能取消下游已经开始的数据库写操作。超过 800ms 的慢 SQL 依然在跑,只是调用方不再等它。
用 latest 标签做子集。 version 标签必须由 Deployment 显式写上,忘记加时 Pod 不属于任何 subset,路由直接 503,而报错信息里只说 “no healthy upstream”。
忽略 Envoy 访问日志的采样。 默认访问日志可能没开或只记录部分,灰度验证时没有对照数据。至少把 meshConfig.accessLogFile 打开,并把响应码、耗时、上游集群记全。
把 mesh 当成银弹。 服务网格解决的是通信治理,解决不了服务内部的慢查询、锁竞争、GC 停顿。看到网格指标一切正常而业务在慢,就该回到应用侧了。
评论