服务网格:Istio + Envoy
前面几章你应该已经发现了:微服务里真正难写的从来不是业务,而是业务旁边那一堆治理代码——超时设多少、失败要不要重试、下游挂了怎么熔断、服务之间要不要加密、调用链怎么串起来。传统做法是把这些塞进每个服务的 SDK(Spring Cloud + Sentinel + Sleuth 那一套)。服务网格(Service Mesh)换了个思路:把这些活从你的应用进程里搬出来,交给每个 Pod 旁边的一个代理进程去做。你的代码只管发 HTTP,剩下的交给底座。这一章讲清 Istio + Envoy 的机制、能力、代价,以及什么时候不值得上它。
14.1 为什么会有服务网格:治理逻辑从"库"变成"层"
论SDK 模式的三个绕不过去的坎
① 多语言就得多套 SDK。Java 用 Spring Cloud Alibaba,Go 用 go-micro,Python 用另一套——每个语言都要重新实现一遍同样的重试熔断逻辑,行为还不一致。运维看到的是"三种语言三种超时策略",排查起来要命。
② 升级 SDK = 全量改代码 + 重新发布。安全团队要求所有服务间通信加密,你得让 200 个服务全部升级依赖、重新构建、灰度上线。基础设施的诉求,被绑在了业务发布上。
③ 治理能力对应用是"有侵入"的。SDK 和业务代码跑在同一个进程里,抢内存、抢线程、版本冲突(经典的 Spring Cloud 版本地狱),一个依赖错误能让整个应用起不来。
服务网格的解法:把治理能力下沉到基础设施层——每个 Pod 里注入一个独立进程(sidecar 代理),应用的网络流量被透明地劫持到代理上,所有治理逻辑在代理里完成。对应用来说,它只是在"正常发 HTTP",根本不知道网卡旁边多了个代理。
Go:go-micro
Py:自研一套
14.2 数据面与控制面:Envoy 和 Istio 各干什么
| 概念 | 是谁 | 职责 |
|---|---|---|
| 数据面 Data Plane | Envoy(C++ 高性能 L4/L7 代理,CNCF 项目) | 真正转发每一个请求:做路由、负载均衡、超时重试、熔断限流、mTLS 加密、上报指标和访问日志。它是"干活的那一层"。 |
| 控制面 Control Plane | istiod(Istio 的守护进程) | 下发配置:把 VirtualService / DestinationRule 这些 CRD 翻译成 Envoy 能懂的配置(xDS 协议)推给每个 sidecar;同时负责签发证书。它不转发任何业务流量。 |
论流量是怎么被"偷偷"劫持进 sidecar 的
应用代码完全不知道 sidecar 的存在,这靠的是内核层的流量重定向:
① Pod 启动时,一个 init 容器(istio-init)写入 iptables 规则。
② 之后这个 Pod 里所有出站流量都被重定向到 Envoy 监听的 15001 端口,所有入站流量被重定向到 15006 端口。
③ Envoy 根据从 istiod 收到的配置决定这个请求怎么处理(转发给谁、要不要加密、超时多久),然后再真正发出去。
所以数据路径变成了:App A → iptables → Envoy A → mTLS → Envoy B → iptables → App B,一个请求穿了两层代理。这既是服务网格强大的原因,也是它增加延迟和资源开销的根本原因。(Istio 1.2x 版本中,Envoy 侧共 15000 段端口:15000 管理、15001 出站、15006 入站、15020/15021 健康与指标、15090 Prometheus。)
最基础的开关:给命名空间打标签,之后新建的 Pod 自动注入 sidecar
① 应用比 sidecar 先起来,启动时连不上任何东西。应用容器启动了,但 Envoy 还没 ready,iptables 已经把流量劫持过去了,于是应用的首次外部调用全部失败(很多框架在启动时就要连配置中心/注册中心,直接启动失败)。解法:用 holdApplicationUntilProxyStarts: true(或在新版本里用 nativeSidecar 特性)让 sidecar 先 ready;健康检查也走 15021 端口。
② Pod 内访问 localhost 的服务也失效了。如果同一个 Pod 里有多个容器通过 localhost 互相调用,默认出站劫持会把它也劫走。解法:配置 traffic.sidecar.istio.io/excludeOutboundPorts 排除这些端口。
③ Job / CronJob 跑不完。批处理任务干完活后进程退出,但 sidecar 还活着,Pod 就永远不结束。解法:给这类工作负载关掉注入(sidecar.istio.io/inject: "false"),或让它正常退出前主动调 /quitquitquit。
还有一条经验:没注入 sidecar 的 Pod 和注入了的 Pod 混在一起时,行为会不一致(一个走代理一个直连)。上线服务网格时,按命名空间整片灰度,别半个业务注一半。
14.3 流量管理:Gateway / VirtualService / DestinationRule
这是 Istio 最核心、也最常用的能力。记住一条数据流:请求从 Gateway 进来 → 交给 VirtualService 判路由 → 按规则送到某个 DestinationRule 定义的子集上。
| 资源 | 管什么 | 典型内容 |
|---|---|---|
| Gateway | 集群边缘的入口(南北向流量) | 监听端口、协议(HTTP/HTTPS/TCP)、TLS 证书、允许哪些域名进来 |
| VirtualService | 怎么路由(东西向也用它) | 按 URI/Header/权重匹配 → 转发到哪个服务的哪个子集;超时、重试、故障注入也写在这里 |
| DestinationRule | 路由到达之后的策略 | 定义子集(subset,通常按版本标签 v1/v2)、负载均衡算法、连接池、熔断(outlierDetection)、mTLS 模式 |
| ServiceEntry | 把集群外的服务纳入网格 | 让 sidecar 知道"这个外部域名/第三方 API 也是我的下游",从而能对它做超时、重试和加密 |
金丝雀发布:把 10% 的流量切给新版本(生产最常用的用法)
故障注入:不写一行代码,模拟下游变慢或报错(做混沌演练必备)
① host 写错就静默不生效。host 必须和 K8s 里的 Service 名字一致(含命名空间规则)。写成 Pod 名、写成 order-service.shop.svc.cluster.local 也可能有问题。写完一定用 istioctl proxy-config routes 确认规则真的下发了,光看 kubectl apply 成功没有意义。
② 重试会放大流量。3 个上游各自重试 2 次,下游实际承受的就是 3 倍流量。下游本来就慢/过载时,重试会把它彻底打死(经典的重试风暴)。规矩:只对幂等接口重试;perTryTimeout × attempts 必须小于或接近上层总超时;配合 outlierDetection 熔断。
③ 只配了 VirtualService 没配 DestinationRule。想按 subset 分流,必须先有 DestinationRule 定义子集;只用 VirtualService 会在 proxy-status 里显示配置被拒(RDS/CDS 报错),流量行为退回默认。
④ 忘了流量会经过"入口 Gateway + 服务间两跳"。客户端 → Gateway(一次超时配置)→ 服务 A → 服务 B(又一次超时配置)。超时要层层递减(外 3s、内 1s),否则内层还没超时外层先断开,日志看起来像"无缘无故连接被重置"。
⑤ 故障注入忘了删。演练完忘记清理 VirtualService,线上长期挂着 5% 的 503——这类事故每年都在发生。做混沌演练时一定要给资源起带日期的名字,并写好清理脚本。
14.4 安全:自动 mTLS 与基于身份的服务间鉴权
论Istio 给了每个工作负载一个"身份证"
传统微服务里,A 调 B 时要证明"我是 A",通常靠共享密钥或自签 token,密钥管理和轮换极其痛苦。
Istio 的做法是:istiod 作为内置 CA,给每个 ServiceAccount 签发一份短期证书,证书里的身份是 SPIFFE 格式的——spiffe://cluster.local/ns/shop/sa/order-service。Envoy 之间用这份证书做双向 TLS(mTLS),双方都能从证书里看到对方的命名空间和服务账号。
这就带来两个巨大的好处:① 全链路加密是自动的(不用改一行业务代码,不用管证书轮换);② 鉴权可以基于"身份"而不是 IP——"只允许 shop 命名空间的 order-service 调我的 /refund 接口",这种规则用 NetworkPolicy(只管 IP 和端口)根本表达不出来。
两步开启严格 mTLS + 一条基于身份的最小权限规则
① 一定要经过 PERMISSIVE 过渡。直接把 STRICT 打开,那些还没注入 sidecar 的服务(或者集群外的客户端、K8s 的健康检查探针)会立刻连不上。正确顺序是:先全量注入 → 观察 PERMISSIVE 下还有多少明文流量 → 清零后再切 STRICT。
② ALLOW 规则的语义容易搞反。只要某个工作负载上存在任意一条 ALLOW 策略,它就变成"默认拒绝"——不在白名单里的全部 403。所以千万别在没有测试的情况下,对核心服务一次性加上一条只允许某个来源的 ALLOW,那等于把所有其他调用方全切断。首次上线建议先用 action: ALLOW + 完整白名单,并在预发环境验证。
③ 证书签发依赖 istiod。istiod 短时间挂掉不影响已建立的连接(证书有效期通常 24 小时),但如果长时间不可用,新 Pod 起不来、证书轮换失败。istiod 必须多副本部署,别当单点。它和"控制面不转发业务流量"是两件事——控制面挂了业务还能跑,但不能再变更。
14.5 可观测:白拿的三件套
服务网格最有"赚到"感觉的地方,就是可观测性——因为所有流量都经过 Envoy,所以指标、日志、拓扑图几乎不用改代码就有了。
| 类型 | 来源 | 能回答什么问题 |
|---|---|---|
| 指标 Metrics | Envoy 自动上报,Prometheus 抓 :15090/stats/prometheus | istio_requests_total(按来源/目标/响应码/响应标志位分组)——"谁在什么时候给谁返回了多少 5xx",不用改代码就有了服务级黄金指标 |
| 访问日志 | Envoy access log,可统一输出到 stdout 被 Fluentd/Vector 采集 | 每个请求的耗时、响应码、上游地址、trace id。注意:开启后日志量非常大,生产要按采样率或只记录错误 |
| 拓扑图 | Kiali(Istio 官方可视化)读指标画图 | 服务依赖拓扑、实时 QPS/错误率、健康状态。排查"到底是谁挂了"最快的一张图 |
| 链路追踪 | Envoy 生成 span,但需要你的应用透传 trace 头 | 这是唯一需要你动手的一项:应用必须把收到的 traceparent / b3 头传给下游,否则每个服务都是独立的一段,串不成一条链。很多团队以为上了 Istio 就有全链路追踪,其实只做了代理层那一段。 |
排障常用命令(背下来,比看文档快)
论为什么"上网格"后排查故障感觉更难了
你的请求现在穿过了:Gateway → Envoy → App A → Envoy → Envoy → App B。任何一跳都可能出问题,而且现象往往是"连不上/超时/被重置"这种看不出原因的样子。最常见的三类:
① 配置根本没下发。现象是"我明明配了超时 1 秒,为什么还是等 30 秒"。查 proxy-status 和 kubectl get vs/dr -o yaml 的实际内容(注意 CRD 是缺省字段被忽略,不是报错)。
② 被 mTLS 或 AuthorizationPolicy 拦了。现象是 403 / connection reset。查 istio-proxy 容器的日志,里面会明确写 RBAC: access denied 或 TLS 握手失败。
③ 健康检查把实例摘掉了。现象是"偶尔 503,且每次都是同一个后端"。用 proxy-config endpoints 看实例状态是 HEALTHY 还是 UNHEALTHY,再回头查那个 Pod 的 readiness 探针。探针配错(比如探针走了被劫持的端口)是极高频的问题。
14.6 代价、收益与 Ambient 模式
| 维度 | 收益 | 代价 |
|---|---|---|
| 延迟 | 统一的负载均衡和熔断,长尾更稳 | 每跳多两次代理(约 +1~3ms)。深调用链(10 跳)累积起来是几十毫秒——对延迟极敏感的业务要实测 |
| 资源 | 治理逻辑不再占业务进程的 CPU/内存 | 每个 sidecar 常驻 50~100MB 内存 + 少量 CPU。1000 个 Pod 就是几十 GB 内存的额外成本 |
| 复杂度 | 规则统一、与语言无关、灰度/演练不用改代码 | 复杂度没有消失,只是转移了:从"每个服务的 SDK"变成"一套 CRD + 一个控制面"。团队要有人真正懂 Envoy 和 Istio,否则出事只能干瞪眼 |
| 发布 | 基础设施升级不再绑业务发布 | 网格自身升级(Istio 大版本)要遍历所有 Pod,且 CRD 版本会变(如 v1beta1 → v1),需要专门的升级窗口 |
论Ambient 模式:把 sidecar 从"每 Pod 一个"变成"每节点一个"
sidecar 模式最大的抱怨就是"资源贵、延迟高、运维烦"。Istio 从 1.2x 版本开始推出的 Ambient 模式换了一种架构(新版本已进入稳定阶段,具体状态以官网为准):
① ztunnel(零信任隧道):一个每节点的 L4 代理(DaemonSet),所有 Pod 的流量先到这里,负责 mTLS、身份、四层路由与遥测。不需要重启业务 Pod——把命名空间标记为 ambient 即可加入网格。
② waypoint proxy:当你需要 L7 能力(按 header 路由、重试、细粒度鉴权)时,才为某个服务(或命名空间)按需部署一个 waypoint 代理。不需要 L7 的服务就不装,这就是省资源的关键。
取舍很清楚:Ambient 显著降低了资源和运维成本,代价是架构更抽象(排障时流量路径更难理解)、L7 能力要显式开启,成熟度和生态仍在演进。如果你现在从零开始评估 Istio,值得把 Ambient 作为默认选项考察;如果团队已经在 sidecar 模式上跑得很稳,也没有必要为了省内存去迁。
① 服务数量不到十个、语言单一(全 Java)。Spring Cloud + Sentinel 的 SDK 方案你已经很熟,运维成本低得多。服务网格的价值来自"规模 × 多语言 × 强合规",规模不到就是纯负债。
② 团队没有人有精力深入网络与 Envoy。服务网格不是"装上就完事"的中间件——它需要有人会读 proxy-config、理解 mTLS 握手、能判断"是网格的问题还是应用的问题"。没有人维护的网格,会成为所有故障的第一嫌疑人。
③ 你的治理需求用服务网格满足不了的部分。服务网格只管"服务间通信",不管业务级的分布式事务、不管消息队列的可靠性、不管数据一致性——那些还得靠第 6 章的那套方案。
一句话建议:先把可观测性做好(指标 + 日志 + 链路),再考虑流量治理;流量治理的需求真的痛了,再上网格。顺序反了,你会同时拥有"看不见"和"管太多"两个问题。
① 服务网格把微服务治理从"库"变成"层":应用只发普通 HTTP,重试/熔断/加密/遥测交给 sidecar,彻底解耦多语言和发布节奏。
② Envoy 是数据面(干活),istiod 是控制面(下发 xDS 配置 + 签证书),控制面挂了业务还能跑,但无法再变更。
③ 流量管理三件套:Gateway(入口)→ VirtualService(怎么路由)→ DestinationRule(到目标后的策略),金丝雀、灰度、超时重试、故障注入都不用改代码。
④ 安全默认就好:自动 mTLS + SPIFFE 身份 + AuthorizationPolicy。上线顺序必须是"先全量注入 → PERMISSIVE 观察 → 再 STRICT"。
⑤ 可观测几乎白拿,但链路追踪需要应用透传 trace 头——这是最容易被误解的一点。
⑥ 代价真实存在:每跳 +1~3ms、每 sidecar 50~100MB。规模不够或没人维护时,别上网格;Ambient 模式是降低这笔成本的新方向。