Istio Sidecar:从 Envoy 注入到可回滚的东西向流量治理
凌晨的发布刚结束,client 调用 orders 开始间歇性返回 503。应用日志里没有对应请求,Kubernetes Service 的 EndpointSlice 又是健康的;同一个 Pod 里直接访问本地应用端口成功,经 Service 名访问却失败。值班同学看到 Pod 有 istio-proxy,便认定网格已经接管流量,又因为 istiod 全部 Ready,把问题归给了业务。
继续对比才发现,新 Pod 的 Envoy 已收到包含 orders-v2 的 cluster,旧 Pod 仍连接另一个控制面 revision;一条 VirtualService 引用了不存在的 subset,使失败请求在代理本地就以 no healthy upstream 结束。这个现场同时暴露了三种不能互相替代的证据:应用是否收到请求、Envoy 实际加载了什么配置、istiod 想下发什么配置。只有把它们沿真实请求路径串起来,sidecar 才从“Pod 多了一个容器”变成可治理的数据面。
一次请求究竟经过了什么
sidecar 模式在每个应用 Pod 内运行一个 Envoy。应用仍连接 orders.mesh-lab.svc.cluster.local,但 Pod 网络命名空间中的重定向规则会把出站连接送进源 Envoy;目标节点收到流量后,目标 Pod 的规则再把连接送进目标 Envoy,最后才到 orders 容器。UDP 不在这条代理路径中;HTTP、gRPC、TLS 和原始 TCP 是否能获得七层语义,还取决于 Service 端口的协议声明和 Envoy 能否识别协议。
istiod 是控制面:它观察 Kubernetes Service、EndpointSlice、Istio 配置和安全策略,把它们转换为 xDS 资源。Envoy 是数据面:LDS 决定监听器,RDS 决定 HTTP 路由,CDS/EDS 决定上游 cluster 与 endpoint,SDS 交付证书和信任材料。控制面短暂失联时,Envoy 通常继续使用最后一次已接受的配置;但新端点、新策略和证书续期不会凭空出现。因此“旧流量还能跑”与“控制面健康”是两件事。
自动注入发生在 Pod 创建的准入阶段。namespace 标签只决定以后创建的 Pod 应由哪个 webhook 处理,既不会改写 Deployment 模板,也不会给已运行 Pod 热插入 Envoy。默认路径由 istio-init 在 Pod 网络命名空间建立重定向规则,因此发布身份必须能创建带 NET_ADMIN、NET_RAW capability 的 init container;Istio CNI 则把这份高权限集中到每节点一个 agent,由主 CNI 完成网卡配置后追加重定向。它降低了业务 Pod 的权限,却把故障域提升到了节点,绝不是 Calico、Cilium 等主 CNI 的替代品。
两条路径的取舍不是“有没有 sidecar”这么简单。每 Pod init 便于在普通集群快速验证,但会与 Pod Security、准入策略和最小权限发生冲突;节点级 CNI 更适合统一平台治理,却要求平台团队维护 DaemonSet、宿主机 CNI 目录、链式顺序和节点升级。使用 CNI 时,应用 init container 会早于 sidecar 启动,此时被重定向的网络可能没有可用代理;排除 IP/端口可以绕开启动依赖,也会让后续应用容器对同一目标永久绕过网格。任何排除项都必须进入出站资产清单,并用一个“未被 Envoy 记录但连接成功”的反向请求证明旁路范围没有被误扩大。注入标签优先级、手工注入和自定义模板可从 sidecar injection 继续核对,CNI 权限和竞态则应按 Istio CNI node agent 处理。
先固定支持组合,再安装隔离 revision
实验至少需要一个可丢弃的 Kubernetes 集群、集群管理员权限、kubectl、与目标版本配套的 istioctl,以及可以拉取目标镜像的网络。安装前在 Supported Releases 核对 Istio minor 与 Kubernetes 的受支持交集,再在 Feature Status 和目标 minor 的 upgrade notes 中核对要用的 API。tested 不能改写成 supported,滚动的 latest 也不能代替项目锁定版本。
从官方 release 下载与集群架构匹配的归档,校验发布摘要后,把其中 bin/istioctl 放入临时工具目录。先记录版本和集群事实:
istioctl version --remote=false
kubectl version
kubectl get nodes -o wide
kubectl auth can-i create customresourcedefinitions.apiextensions.k8s.io
kubectl auth can-i create mutatingwebhookconfigurations.admissionregistration.k8s.io这里需要 cluster-scoped CRD、webhook、ClusterRole 等高权限对象。个人账号不应长期持有集群管理员权限;更稳妥的做法是由受控发布身份安装控制面,应用团队只获得自己 namespace 内的 Deployment、路由和受批准策略写权限。安装清单、镜像 digest、最终 values 和执行审计应进入变更记录,不能只保存一条 shell 历史。
用不可变 revision 安装控制面,避免把实验工作负载接到已有默认控制面。default profile 是 sidecar 的生产起点;demo 会打开更重的日志与 tracing,适合展示功能,不适合容量基线。下面选择 CNI 路径;若实验明确选择每 Pod init,删除 components.cni 和 values.pilot.cni,同时接受业务 Pod 对网络 capability 的需求。
export REV=stable-a
cat > /tmp/mesh-lab-istio.yaml <<'EOF'
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
profile: default
components:
cni:
enabled: true
values:
pilot:
cni:
enabled: true
meshConfig:
accessLogFile: /dev/stdout
defaultConfig:
holdApplicationUntilProxyStarts: true
EOF
istioctl install -y --revision "$REV" -f /tmp/mesh-lab-istio.yaml
kubectl get pods -n istio-system
kubectl get daemonset -n istio-system istio-cni-node -o wide
istioctl analyze --all-namespacesrevision 把 istiod、注入 webhook 与将来要连接的数据面归入 stable-a。values.pilot.cni.enabled=true 告诉该 revision 的注入模板不要再创建特权 istio-init;漏掉它会出现“节点已有 CNI,Pod 仍带特权 init”的双重配置。accessLogFile 让实验可以直接取得代理访问证据,但会增加日志字节、采集 CPU 与敏感数据暴露面,生产环境应通过 Telemetry API 收窄作用域和字段。holdApplicationUntilProxyStarts 让应用容器等待代理就绪,可降低启动时绕过或连接失败窗口,也会把代理启动故障直接传导成 Pod 启动变慢。团队必须根据启动 SLO 决定是否使用。
CNI DaemonSet 的 Ready 也不是流量接管证据。选一个新建 Pod,确认其中没有 istio-init、存在 istio-validation,再从应用容器发请求并在 Envoy 访问日志中找到同一 request ID。反例是在隔离节点让 CNI agent 暂时不可用后创建测试 Pod:安全结果应是验证 init 阻止 Pod 正常启动,CNI repair 记录修复动作;若 Pod Ready 且请求没有经过 Envoy,说明出现了可绕过窗口。repair 的删除、打标签或原地修复权限会改变事故处置和攻击面,不能为了让 Pod 自动恢复就无条件授予宽泛的 Pod 更新、删除权限。
若采用 Helm,组件顺序至少是 istio/base 后接 istio/istiod,gateway 独立管理;各 chart 必须固定同一版本。Helm profile 只是 values 集合,不会像 istioctl install 一样替你选择并安装整套组件。GitOps 使用 helm template 或 istioctl manifest generate 时还要自行保证依赖顺序、field ownership、环境差异和 prune,因此渲染成功不等于发行路径已经验证。官方的 istioctl 安装说明 与 Helm 安装说明 应作为命令和 ownership 变化的入口。
把 mesh-lab 的两个版本接进来
先创建隔离 namespace,并只保留 revision 标签。普通 istio-injection=enabled 与 istio.io/rev 同时存在时,前者会优先,最容易造成“标签看似迁移、实际仍连旧控制面”。
kubectl create namespace mesh-lab
kubectl label namespace mesh-lab istio-injection- istio.io/rev=stable-a --overwrite在 mesh-lab 部署 client、orders-v1、orders-v2。三个 Deployment 都使用各自的 ServiceAccount;两个 orders Pod 带 app: orders 与不同 version 标签,Service selector 只选择 app: orders。应用可以是任意能返回自身版本并暴露 /healthz 的测试镜像,镜像必须由项目锁定 digest。Service 端口显式命名为 http 或设置 appProtocol: http:
apiVersion: v1
kind: Service
metadata:
name: orders
namespace: mesh-lab
spec:
selector:
app: orders
ports:
- name: http
appProtocol: http
port: 8080
targetPort: 8080appProtocol 优先于端口名。若端口被声明成 TCP,HTTP header 路由、方法级授权和七层指标不会按预期出现;若同一 Pod 端口被多个 Service 以不同协议声明,结果也可能不稳定。server-first 协议需要单独核对,不能依赖自动识别碰运气。
Deployment 创建后先检查哪个 webhook 会处理它,再逐层确认真实状态。check-inject 读取集群中已有的 Deployment;在对象创建前调用会因为找不到资源而失败:
istioctl experimental check-inject -n mesh-lab deploy/client
kubectl get pods -n mesh-lab
kubectl get pod -n mesh-lab -l app=client -o jsonpath='{.items[0].spec.containers[*].name}{"\n"}'
istioctl proxy-status
istioctl proxy-config listeners deploy/client -n mesh-lab
istioctl proxy-config clusters deploy/client -n mesh-lab | grep orders容器列表应同时出现应用与 istio-proxy;当前 CNI 路径不应出现 istio-init,但会有用于确认重定向已经建立的 istio-validation。使用非 CNI 路径时才应看到负责重定向的 istio-init。proxy-status 中对应代理应连接 stable-a 且 CDS/LDS/EDS/RDS 收敛。即使状态都是 SYNCED,仍要发真实请求:
kubectl exec -n mesh-lab deploy/client -c client -- \
sh -c 'for i in 1 2 3 4; do curl -fsS -H "x-request-id: sidecar-$i" http://orders:8080/version; done'预期请求在两个版本间按默认负载均衡分布,orders 应用日志能看到 request ID,源和目标 Envoy 访问日志也能关联同一请求。只有应用响应而代理没有记录,优先检查流量捕获、排除端口和实际调用地址;只有代理本地 503 而 orders 没日志,说明请求未到应用,不要先改业务代码。
用路由正反实验看懂配置字段
先定义 subset,再按 header 把实验请求定向到 v2:
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: orders
namespace: mesh-lab
spec:
host: orders.mesh-lab.svc.cluster.local
trafficPolicy:
connectionPool:
http:
http1MaxPendingRequests: 20
outlierDetection:
consecutive5xxErrors: 3
interval: 5s
baseEjectionTime: 15s
maxEjectionPercent: 50
subsets:
- name: v1
labels: {version: v1}
- name: v2
labels: {version: v2}
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: orders
namespace: mesh-lab
spec:
hosts: [orders]
http:
- match:
- headers:
x-orders-version: {exact: v2}
route:
- destination: {host: orders, subset: v2}
timeout: 2s
- route:
- destination: {host: orders, subset: v1}
timeout: 2sVirtualService 决定匹配、路由和请求总超时;DestinationRule 定义 subset 与目标侧连接池、负载均衡、异常摘除和 TLS。http1MaxPendingRequests 是容量保护,不是业务并发承诺;过小会在突发流量下由代理拒绝请求,过大则把延迟藏进队列。consecutive5xxErrors 与摘除时间只处理端点健康,不是应用熔断状态机,也不能代替客户端总 deadline。
正例请求应始终返回 v2,普通请求应返回 v1:
kubectl exec -n mesh-lab deploy/client -c client -- \
curl -fsS -H 'x-orders-version: v2' -H 'x-request-id: route-ok' http://orders:8080/version
kubectl exec -n mesh-lab deploy/client -c client -- \
curl -fsS -H 'x-request-id: route-default' http://orders:8080/version反例不要制造随机故障,而要只改变一个字段:把 v2 subset 的标签临时改成 version: missing,记录对象的 metadata.generation,再等待 proxy-status 与目标 Envoy 的 cluster/endpoint 配置收敛后请求。VirtualService、DestinationRule 并不提供可通用依赖的 status.observedGeneration,不能拿不存在的状态当发布闸门。预期代理返回 503,访问日志出现无健康上游相关的 response flag/details,orders 应用没有这次 request ID。随后恢复标签,确认 EDS 重新出现 v2 endpoint 且请求恢复。这个实验能区分“声明版本已变化但数据面尚未收敛”“路由已命中但 subset 无端点”和“路由根本没有下发”。
重试必须和总 deadline、幂等性一起设计。若代理、SDK 和入口各重试两次,最坏尝试数会相乘;对写请求,响应丢失并不表示上游没有完成写入。只有携带幂等键、服务端能返回既有结果且总 deadline 覆盖所有 attempt 与 backoff 时,才考虑代理重试。验证时保存每次上游 attempt 的 request ID、开始/取消时间和实际写入次数,不能用最终 200 掩盖放大。
mTLS 证明身份,不替业务做授权
Istio 默认身份通常映射为 spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>。istio-agent 向 istiod 申请短期证书,再通过 SDS 交给 Envoy;私钥、证书链、trust bundle、URI SAN 和到期时间共同构成身份材料。访问配置转储或证书输出时要按敏感资产处理,避免把真实服务拓扑、证书链或请求 Header 写入公共工单。
Auto mTLS 会对已知 mesh endpoint 尽量使用 Istio mTLS,但不等于所有入站都拒绝明文。先用 PeerAuthentication 把 orders 切到 STRICT:
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: orders-strict
namespace: mesh-lab
spec:
selector:
matchLabels: {app: orders}
mtls:
mode: STRICT随后加一条只允许 client ServiceAccount 的授权策略。Istio 授权顺序是 CUSTOM、DENY、ALLOW;目标上出现 ALLOW 后,未匹配请求会被拒绝:
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: orders-client-only
namespace: mesh-lab
spec:
selector:
matchLabels: {app: orders}
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/mesh-lab/sa/client"]
to:
- operation:
methods: ["GET"]
paths: ["/version"]正确 ServiceAccount 的 GET 应成功;换成另一个 ServiceAccount 的 Pod 应得到代理拒绝,POST 或错误 path 也应失败。再从未注入 Pod 直连 orders,STRICT 应阻止明文。证据至少包含源 principal、目标 workload、命中策略、代理拒绝码和目标应用是否收到请求。mTLS 成功只说明传输对端身份经过验证,不能推出调用者有权执行“取消订单”等业务动作;业务权限仍由应用鉴权和领域规则承担。
PeerAuthentication 管入站接受模式,DestinationRule.trafficPolicy.tls 管出站发起模式。随意把后者设为 SIMPLE、MUTUAL 或 DISABLE,可能制造 double TLS、明文或证书校验失败。排查握手问题时同时检查两端 TLS 模式、SDS 证书、URI SAN、系统时钟和 trust bundle,不能只看一张 mTLS 面板。
项目接入从依赖图开始
真实项目不要一次给整个集群打注入标签。先选一个无外部副作用、依赖清晰的 namespace,列出所有入站、出站、健康检查、数据库、消息队列、第三方 TLS 和 UDP 流量。为每条关键调用保存无网格基线:DNS 解析、端点、状态码、延迟分布、连接数和 request ID。然后按 cohort 重建 Pod,逐条比较代理前后的路径。
应用必须传播 traceparent、tracestate,兼容旧链路时再传播 B3 Header。Envoy 可以创建或上报 span,却不能替应用在业务线程和下游客户端之间传递上下文。访问日志用于单次请求到达与代理判责,指标用于窗口趋势,Trace 用于跨 hop 因果;三者互补。默认采样下查不到 Trace 可能只是未采样,应先用 request ID 和访问日志证明请求存在。
生产遥测要限制高基数。不要把用户 ID、完整 URL、订单号写成 Prometheus label;这会让 active series、内存和存储成本随业务数据基数膨胀。记录代理 CPU/RSS、每 Pod 启动时延、xDS push queue、配置收敛时间、日志字节与 Trace ingest,才能算出 sidecar 的真实单位成本。sidecar 每副本固定增加代理资源,小 Pod、多副本和短任务的相对开销尤其明显。
从现象回到责任层
排障先固定唯一 request ID,再按以下顺序推进,而不是一次收集所有日志:
kubectl get endpointslice -n mesh-lab 证明 Kubernetes 是否有目标端点。istioctl analyze -n mesh-lab 查 host、subset、端口和策略静态错误。istioctl proxy-status 查目标代理连接的 istiod 与 xDS 同步状态。
istioctl proxy-config routes|clusters|endpoints 查 Envoy 实际观察状态。源 Envoy 访问日志区分 NR、无健康上游、上游连接失败、超时和策略拒绝。目标 Envoy 与应用日志判断请求停在目标代理前还是应用内。
404 伴随 route not found 通常是 host、端口或协议识别问题;503 且没有 upstream host 常指向 subset/EDS/连接池;握手失败要查看 UPSTREAM_TRANSPORT_FAILURE_REASON;403 要关联 source principal 与授权策略。SYNCED 只表示代理确认了某版 xDS,不表示该配置符合业务意图。控制面断连实验则要分别观察既有连接、新连接、端点变化、新策略传播和证书续期,写清最后已知配置还能维持多久。
revision 升级不是移动一个标签
升级前保存 CRD、Istio CR、namespace 标签、webhook、安装 values、镜像 digest、gateway 和遥测配置,运行目标版本 istioctl x precheck --from-version=<old-minor>,并阅读源、目标 minor 的升级说明。官方推荐 canary revision 升级:先升级共享 base/CRD,再安装新 revision 的 istiod,最后按 workload cohort 重注入。
export NEW_REV=stable-b
istioctl install -y --revision "$NEW_REV" -f /tmp/mesh-lab-istio.yaml
istioctl tag set mesh-canary --revision "$NEW_REV"
istioctl tag list
kubectl label namespace mesh-lab istio-injection- istio.io/rev=mesh-canary --overwrite
kubectl rollout restart deployment -n mesh-lab
kubectl rollout status deployment -n mesh-lab
istioctl proxy-status安装新 istiod 不会改变运行中的 Envoy;移动 revision label 或 tag 也只改变以后创建 Pod 的注入目标。mesh-canary 是可移动指针,stable-b 是应保持不可变的控制面实例;只有重建 Pod 才会重新执行注入并把新 sidecar 接到新 revision。滚动重启后,应分别验证旧到旧、旧到新、新到旧、新到新的身份、路由、超时、遥测和长连接。通用 skew 约束是控制面可领先数据面一个 minor、数据面不能领先控制面;CNI 另有前后一个 minor 的兼容窗口,不能把两条规则混成“所有组件随便差一个版本”。revision 路径虽支持受控跨越两个 minor,仍要先证明中间 CRD、策略和扩展没有不可逆语义变化。
回滚首先把新建 Pod 的注入目标切回旧 revision,再滚动受影响 workload,并验证旧控制面仍能解释当前 CRD 和策略。若 gateway 曾原地升级,需要先用旧版本工具恢复 gateway;卸载新 revision 不会自动恢复它。revision tag 是可变指针,revision 实例应保持不可变,这样审计才能回答某个 Pod 究竟由哪套模板与控制面生成。
容量、所有权与长期治理
容量测试至少固定 RPS、连接数、payload、协议、sidecar worker、TLS、路由数量、策略数量、日志字段和 Trace 采样。观察 p50/p99、代理 CPU/RSS、连接与 pending queue、reset、istiod CPU/RSS、xDS push time 和 Prometheus active series。官方 Performance and Scalability 的数字适合复现实验方法,不能直接成为自己的容量承诺;硬件、拓扑、过滤器和遥测都会改变结果。
通过 Sidecar 资源缩小配置可见性可以降低每个 Envoy 的 listener、cluster 和内存,但错误的 egress.hosts 会直接让依赖不可达。每次收窄都要对配置数量、关键 endpoint 与正反请求做对照。配置风暴、证书轮换和 istiod 恢复比稳定平均负载更容易暴露长尾,因此压测应包含批量 rollout、EndpointSlice 抖动和控制面重启。
团队应给 namespace 注入标签、root namespace 策略、全局 Telemetry、revision tag、CA 和 CRD 分别指定 owner。应用团队可以维护自己的 VirtualService 与局部策略,平台团队维护跨 namespace 的信任、webhook 和升级事务;策略变更走评审与 dry-run,日志和 config dump 设置访问控制、脱敏、保留与删除期限。把所有权写清,才能避免一次全局 ALLOW、DENY 或 Telemetry 修改影响整个 mesh。
先撤数据面,再卸控制面
退出前先证明应用在无 sidecar 时仍有 DNS、TLS、授权、超时、重试和观测替代路径。移除 namespace 的 revision 标签不会改变旧 Pod,必须滚动重建,并检查实际 Pod spec 已无 istio-proxy 和重定向 init container。若 namespace 还保留 istio-injection=enabled,必须同时移除,否则默认 tag 的 webhook 仍会注入:
kubectl label namespace mesh-lab istio-injection- istio.io/rev-
kubectl rollout restart deployment -n mesh-lab
kubectl rollout status deployment -n mesh-lab
kubectl get pods -n mesh-lab -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.spec.containers[*].name}{"\n"}{end}'对关键调用重新做成功、错误身份、超时和无端点反例,确认替代告警已经接管。先删除实验 namespace 和可变 tag,再确认两个 revision 都没有连接中的 sidecar 或 gateway,最后逐个定向卸载控制面:
kubectl delete namespace mesh-lab
istioctl tag remove mesh-canary
istioctl proxy-status
istioctl uninstall --revision stable-b -y
istioctl uninstall --revision stable-a -y
rm -f /tmp/mesh-lab-istio.yamlproxy-status 仍有旧 revision 连接时不得继续卸载;先找到对应 workload 或 gateway,完成重建、迁移或回滚。不要在多 revision 或共享集群直接执行 istioctl uninstall --purge;它会删除共享的 cluster-scoped Istio 资源。Helm 卸载也不会自动删除 CRD,而手工删除 *.istio.io CRD 会级联删除所有路由和策略实例。Gateway API CRD 还可能由其他控制器共享。最终审计应确认没有注入 Pod、旧 proxy 连接、revision 标签、tag webhook、临时 RBAC、测试证书和日志保留任务,再由共享资源 owner 决定是否删除 CRD 与 istio-system。
