ElIauk

Service Mesh 入门:Istio 流量治理

Sidecar 之后,流量路径变了 接入 Istio 之后,一个 Pod 里的应用发出的请求不再直接出网卡:出站流量被 iptables 规则劫持到 istio-proxy 的 15001 端口,由 Envoy 按 Listener、Route、Cluster 三层决策后再转发;入站同理,先过 15

评论
Service Mesh 入门:Istio 流量治理

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 停顿。看到网格指标一切正常而业务在慢,就该回到应用侧了。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者