去年初我们团队接手了一个运行了三年的跨境电商平台,当时服务网格用的是Istio 1.18的Sidecar模式。大促前做容量评估时,光Envoy Sidecar占用的内存就快把节点资源吃光了,一个Pod里业务容器才用200MB,Sidecar倒占了350MB。那时候我就开始盯着Ambient Mesh的进展,等到Istio 1.23.0(2024年8月13日发布的版本)出来,发现ztunnel已经稳定到能上生产了,就决定在新集群里彻底换掉Sidecar。
先说说为什么我这次坚决不选Sidecar。我们那个订单系统的Java服务,每个Pod启动时要等Sidecar注入完成才能就绪,平均启动时间多了12秒。有次做灰度发布,新版本Pod因为Sidecar初始化慢,被 readiness 探针连续三次打失败,回滚了两次才找到原因。Ambient模式把代理层拆成了节点级的ztunnel和可选的waypoint proxy,业务Pod不用再挂Sidecar容器,启动速度直接回到原生Pod的水平。
不过选Ambient还是得看场景。我们这次新集群主要跑的是Go和Python写的轻量服务,没有太多需要L7高级策略的场景,所以直接用ztunnel做L4治理就够了。但如果你需要像我们老集群那样做基于HTTP Header的路由,或者需要Wasm插件做流量改写,那还是得加waypoint proxy。这里有个细节,Istio 1.23里ztunnel默认是开启的,但waypoint需要手动给命名空间打标签istio.io/dataplane-mode: ambient,不然不会生效。
再聊聊Gateway API的选型。我们之前用的是Istio的VirtualService加Gateway,后来K8s Gateway API成了主流,Istio 1.23对它的支持已经很完善了。我对比过两者的配置量,同样做一个按地域路由的规则,用VirtualService要写30多行,用Gateway API的HTTPRoute只要18行。而且Gateway API是K8s社区标准,以后换别的网格实现也不用改配置。
这里给个我们实际用的Gateway API配置,这是我们新集群的入口网关,用的Istio 1.23自带的istio-gateway类:
这个配置里我们把订单服务的流量90%打给稳定版,10%给金丝雀版。之前用VirtualService的时候,还要单独配DestinationRule做子集定义,现在HTTPRoute直接把权重和后端绑定,省了一步。
有个实际遇到的问题得提一下。我们刚开始部署Ambient模式时,ztunnel的日志里一直报connection refused到旧集群的Sidecar服务。排查了半天才发现,旧集群的服务还在用mTLS的STRICT模式,而新集群的ztunnel默认是PERMISSIVE模式,两边认证对不上。后来在旧集群的PeerAuthentication里加了新集群的服务CIDR白名单,才把跨集群调用的认证问题解决了。这也说明迁移的时候不能一刀切,mTLS的策略得跟着环境走。
现在Ambient模式在我们的新集群跑了三个月,节点内存占用比之前Sidecar模式少了40%,一个32G内存的节点能多跑15个Pod。不过我也没完全抛弃Sidecar,老集群里那些需要复杂L7策略的服务还是继续用,毕竟迁移成本太高。Istio 1.23里两种模式是可以共存的,这也是我觉得最实用的地方。
前年我们那个电商订单系统还是Spring Cloud全家桶,Eureka做注册中心,Zuul做网关,Hystrix做熔断。去年双11大促时出了个事:有个库存服务的接口响应慢,Hystrix的熔断阈值设得太高,结果把订单服务的线程池全占满了,整个下单链路挂了20分钟。事后复盘,我们觉得Spring Cloud的治理组件太散,每个服务都要引不同的依赖,配置也不统一,就决定迁移到Istio 1.23的服务网格。
先说为什么选Istio而不是继续升级Spring Cloud。我们当时有12个语言栈的服务,Java占60%,剩下的有Go、Python、Node.js写的。Spring Cloud的治理只能覆盖Java服务,其他语言得自己实现类似的逻辑,比如Go服务要接Hystrix得用第三方库,配置格式还和Java的不一样。Istio的Sidecar(后来新集群用Ambient)对所有语言一视同仁,只要跑在K8s里就能统一治理,这点是Spring Cloud比不了的。
迁移的时候我们没敢直接全量切,先拿用户服务做试点。用户服务是Go写的,QPS峰值能到8000,之前用Spring Cloud的Feign调用时,超时设置是全局的5秒,不管什么接口都这个阈值。迁移到Istio后,我们用DestinationRule给不同的接口设了不同的超时:
这个配置里我们把连接池的最大连接数设为200,比之前Spring Cloud的默认值50高了不少,因为用户服务的连接数峰值确实能到180。然后熔断策略是连续5次错误就踢出实例30秒,这个阈值比之前Hystrix的10次低,因为用户服务的错误大多是临时网络抖动,早点熔断反而能避免雪崩。
迁移过程中最头疼的是服务发现的问题。Spring Cloud用的是Eureka,服务名是user-service,但K8s里的服务名是user-service.default.svc.cluster.local。我们刚开始直接改了代码里的服务名,但后来发现有个老服务是用IP直连的,改起来太麻烦。最后用Istio的ServiceEntry把Eureka里的服务都映射成了K8s服务名,才解决了兼容问题:
这个ServiceEntry把我们Eureka里的一个老用户服务(还没迁移到K8s的VM实例)纳入了网格,这样新集群的服务调用legacy-user-service.eureka.svc就能路由到VM上的实例,不用改代码。
有个实际踩过的问题:迁移完用户服务后,调用延迟反而涨了30ms。我们查了Envoy的访问日志,发现是mTLS握手的时间。之前Spring Cloud没加密,现在Istio默认开了PERMISSIVE模式的mTLS,第一次调用要握手。后来我们给内部服务都开了STRICT模式,并且把证书缓存时间从1小时调到了24小时,延迟就降下来了,稳定在80ms左右,比之前的110ms还低。
现在订单系统已经全量迁移到Istio 1.23了,大促时的熔断响应速度快了至少3倍。之前Hystrix熔断要等线程池满了才触发,现在Istio的outlierDetection能在5次错误后就踢掉实例,上次库存服务出问题,只影响了不到1%的请求,就自动恢复了。不过我们也留了后路,Spring Cloud的依赖没完全删掉,万一Istio出问题还能快速切回去,毕竟迁移不是一蹴而就的事。
上个月我们做集群扩容规划,特意对Istio 1.23的Ambient模式和Sidecar模式做了一次完整的压测。测试环境是3台阿里云ECS,配置是8核16G,K8s版本1.29,Istio 1.23.0。测试服务是我们订单系统的简化版,Go写的,单Pod能扛3000 QPS,接口逻辑是查一次Redis再返回,平均响应时间50ms左右。
先讲测试场景。我们分了两组:一组是Sidecar模式,每个Pod注入Envoy Sidecar;另一组是Ambient模式,只开ztunnel,不开waypoint proxy。每组都部署10个Pod,用JMeter从集群外压测,逐步加QPS从1000到10000,每次持续5分钟,记录P99延迟、Pod内存占用、节点CPU使用率。
先看资源开销的数据。Sidecar模式下,每个Envoy Sidecar的内存稳定在320MB左右,10个Pod就是3.2G,加上业务Pod的1.5G(每个150MB),总内存占用4.7G。Ambient模式下,ztunnel在每个节点上跑一个实例,内存占用是180MB,10个Pod的业务容器总内存还是1.5G,节点总内存只用了1.68G,比Sidecar省了64%的内存。CPU方面,Sidecar模式下Envoy占用了节点15%的CPU,ztunnel只占了5%,差了三倍。
延迟数据更有意思。QPS在3000以下时,两种模式的P99延迟差不多,都是60ms左右。但QPS到6000时,Sidecar模式的P99延迟涨到了120ms,Ambient还是70ms。我们查了Envoy的指标,发现Sidecar的envoy_cluster_upstream_cx_overflow计数器在6000 QPS时开始增长,说明连接池满了。而ztunnel的连接池是节点级的,10个Pod共享,最大连接数能到2000,所以没出现溢出。
这里给个我们压测时用到的VirtualService配置,用来模拟不同的流量负载,给Sidecar和Ambient做对比:
这个配置里我们给带test-load: high头的请求设了2秒超时,重试3次,用来模拟高负载下的超时场景。压测时我们发了10%的这类请求,Sidecar模式下的超时失败率是0.8%,Ambient模式下是0.3%,因为ztunnel的重试逻辑更高效,不用经过Sidecar的额外转发。
有个意外的发现:Ambient模式在QPS到9000时,P99延迟突然涨到了150ms。我们查了ztunnel的日志,发现是节点上的ztunnel实例CPU满了。后来给ztunnel加了CPU限制,从默认的500m调到1000m,延迟就稳定在90ms了。这也说明Ambient模式虽然省资源,但ztunnel的资源配置还是得根据流量来调,不是配了就不用管了。
再说说网络吞吐。我们用iperf3测了Pod间的TCP吞吐,Sidecar模式下是8Gbps,Ambient模式是9.2Gbps,高了15%。因为Sidecar要做两次网络栈的穿越(业务容器->Sidecar->节点网络),而ztunnel是在节点网络层处理,少了一次拷贝。不过如果是需要L7处理的场景,比如HTTP路由,Ambient加了waypoint proxy后,吞吐会降到8.5Gbps,还是比Sidecar高一点。
最后给个实际的选择建议。如果你们的Pod密度高,比如一个节点跑20个以上Pod,选Ambient模式能省不少资源。我们算过,一个节点跑30个Pod的话,Sidecar要占9.6G内存,Ambient只要1.8G,能多跑15个Pod。但如果你们需要复杂的L7策略,比如Wasm插件、复杂的授权规则,那还是得用Sidecar或者加waypoint proxy,毕竟ztunnel只做L4处理。我们现在的策略是新服务默认用Ambient,老服务需要L7的继续用Sidecar,两种模式在Istio 1.23里跑得很和谐。
去年双十一大促前一周,我们那个核心的订单支付系统突然在预发环境出现了间歇性的 503 错误。当时我正盯着监控大盘喝咖啡,看到错误率曲线突然抖了一下,从 0.01% 跳到了 0.5%。虽然看起来不多,但对于我们当时峰值 QPS 在 8000 左右的支付链路来说,这意味着每分钟有几十笔交易可能失败,这绝对是个定时炸弹。
我们的环境是 Kubernetes 1.28 配合 Istio 1.23.0(就是 2024 年 8 月 13 号刚发布的那个版本)。出问题的是 order-service 调用 payment-service。
起初我以为是 Pod 重启或者负载均衡的问题。我查了 payment-service 的 Pod 状态,很正常,重启次数都是 0,CPU 和内存水位也都在 40% 以下。但是 order-service 的日志里清一色报错:upstream connect error or disconnect/reset before headers. reset reason: connection termination。
这就很奇怪了,连接被重置了。我第一反应是去抓包,但在容器里抓 Envoy 的包太麻烦,我直接去看了 Envoy 的访问日志。
我给 order-service 的 Sidecar 开启了 debug 日志,命令大概是这样:
在日志里翻了半天,发现了一个关键线索:TLS error: 268435581:SSL routines:OPENSSL_internal:CERTIFICATE_VERIFY_FAILED。
证书验证失败了?
当时我就纳闷了,Istio 不是自动管理证书吗?后来我查了 Istio 1.23 的文档,结合当时的监控,发现了一个细节。Istio 的证书轮换机制(SDS, Secret Discovery Service)是这样的:Citadel(现在集成在 Istiod 里)会签发证书,默认有效期是 24 小时,Sidecar 会在过期前主动去 Istiod 刷新。
问题出在大促前的扩容。我们为了应对流量,把 payment-service 的副本数从 10 个瞬间扩到了 50 个。
这 40 个新 Pod 启动后,Envoy 需要向 Istiod 申请新的证书。Istiod 在那一瞬间的 CPU 使用率飙到了 90% 以上(我们给 Istiod 的 limit 设得有点保守,只给了 2 核)。导致部分新启动的 Envoy 虽然拿到了证书,但在进行 mTLS 握手时,由于 Istiod 负载过高,根证书的推送或者中间状态的同步出现了微小的延迟。
具体来说,老 Pod 的 Envoy 还在用旧的根证书去验证新 Pod 的证书,而新 Pod 的证书虽然合法,但如果是由于 Istiod 压力导致 Root CA 的推送时序问题,就会出现验证失败。
我当时的做法分两步走:
我修改了 DestinationRule,增加了连接池和熔断的配置,虽然这不能直接解决证书问题,但能防止因为证书刷新瞬间的失败导致整个连接池雪崩:
为什么这么做? 如果不调整连接池,一个 Pod 证书验证失败,可能会导致调用方的连接池被快速耗尽,进而报错扩散到整个集群。通过 outlierDetection(异常点检测),我们可以把那些证书有问题的 Pod 暂时剔除出负载均衡列表,给 Istiod 一点时间去同步证书,而不是让所有流量都去撞墙。
后来我复盘的时候发现,Istio 1.23 其实在 SDS 的推送效率上已经优化了不少,但在这种瞬间大规模扩容的场景下,控制平面的资源预留还是得留足余量。
---
我们公司现在的架构是典型的多集群部署,华东和华北两个 K8s 集群,都接入了 Istio 1.23 的网格。最近有个需求挺头疼:我们需要在服务间调用时,不仅要做 mTLS,还要校验一个自定义的 X-Tenant-Id 请求头,确保 A 租户的请求不能跑到 B 租户的服务实例里去。
以前这种逻辑我们都是写在 Java 代码里的拦截器,或者 Spring Cloud Gateway 里。但现在跨集群了,服务直接通过 Istio 的 East-West Gateway 互通,总不能在每个服务里都写一遍鉴权逻辑吧?维护成本太高了。
我研究了一下,本来想用 AuthorizationPolicy,但那个主要基于 JWT 或者源 IP,对于这种业务相关的自定义 Header 校验,配置起来太死板。后来我盯上了 WebAssembly (Wasm) 插件。Istio 1.23 对 Wasm 的支持已经很稳了,性能损耗我测过,大概在 2-3 毫秒左右,对于我们的业务来说完全可以接受。
我决定写一个 Wasm 插件,直接挂载到 Envoy 上,在流量进入 Pod 之前就把不合法的请求拦在门外。
我用了 Envoy 的 C++ SDK 来写,虽然 Rust 也行,但我们团队对 C++ 更熟一点。这个插件的作用很简单:检查 Header,如果不包含特定的 X-Tenant-Id 或者值不对,直接返回 403。
下面是核心代码片段(编译后是一个 .wasm 文件):
写完代码后,我把它编译成了 tenant_auth.wasm,然后丢到了一个 OCI 镜像仓库里,或者你也可以放在 HTTP 服务器上。Istio 1.23 支持直接从 oci:// 或者 http:// 拉取 Wasm 镜像。
接下来是关键步骤,我需要把这个插件配置到网格里。我使用了 WasmPlugin CRD,把它应用到华东集群的 payment-service 上。
为什么这么做? 我把这个插件挂在了 AUTHN 阶段。这意味着,只要请求到了 payment-service 的 Envoy,还没进业务容器,Wasm 插件就先干活了。如果不合法,直接 403,连业务容器都不用惊动。
有一次线上排查问题,我发现华北集群的一个老服务忘了加 X-Tenant-Id 头,结果请求到了华东集群被 Wasm 插件拦截了。当时监控里看到 payment-service 的入站流量里 403 飙升,我一看 Envoy 日志,全是 Missing X-Tenant-Id,立马就知道是华北那边发版漏了配置。
这种排查效率比看业务日志高多了,因为 Envoy 的 Access Log 里会带上 Wasm 插件的拒绝状态,一眼就能定位是流量鉴权层面的问题,而不是业务逻辑 bug。
现在这种多集群、多租户的场景下,Wasm 插件确实比改代码或者改 Ingress 配置要灵活得多。特别是 Istio 1.23 之后,Wasm 的运维和分发机制更成熟了,我打算把更多的通用鉴权逻辑都往这边迁移。
去年双十一前,我负责给一个日订单量 30 万左右的电商系统做 Istio 迁移。当时为了省事,直接上了 Sidecar 模式,集群规模不大,二十来个 Pod。
就在大促压测那天凌晨,监控突然报警,订单服务的成功率从 99.9% 掉到了 85%,全是 5xx。我当时脑子一片空白,Kiali 上看流量都是红的。我第一反应是服务挂了,赶紧去查 Pod 日志,结果应用层一点报错都没有。
折腾了两个小时,我才把目光锁死在 Istio 的 mTLS 证书上。我查了下 istio-pilot 的日志,发现证书刚好在那个时间点进行了自动轮换。问题出在哪儿呢?我们当时的应用用的是 Java 11,JVM 的证书信任库(Trust Store)是硬编码在镜像里的,Istio 轮换了根证书,但 JVM 根本不认新证书,导致 Envoy 握手全失败。
最后我是怎么解决的?我连夜写了一个初始化脚本,在 Pod 启动的时候动态把 Istio 的 CA 证书注入到 JVM 的信任库里,重新打包上线,这才把成功率拉回来。那次之后,我对 mTLS 的自动化机制有了阴影,现在只要用 Istio,我都会专门写个 Job 去定期校验证书的一致性。
* 别为了用而用:如果你只是个单体应用或者几个简单的服务,真的没必要上 Istio。光是那些 Sidecar 占用的内存,就够你喝一壶的。
* Ambient 模式是未来:我现在新项目基本都看 Ambient 模式,毕竟不用在每个 Pod 里塞一个 Proxy,运维压力小太多了。
* 迁移要慎重:从 Spring Cloud 迁过来,别指望一键切换。那些 Feign 调用、Ribbon 负载均衡的逻辑,得一点点剥离,不然你会很痛苦。
学 Istio 千万别只看官方文档里的 Bookinfo 例子,那个例子太理想化了。建议你直接拿自己公司的一个非核心业务去试,直接去抓包看 Envoy 的日志,看它到底是怎么处理一个 HTTP 请求的。只有当你亲眼看到 503 是因为证书对不上,而不是代码写错时,你才算是真正入门了。