前两篇默认了一件事:「一个代理连上 istiod,订阅一套 xDS
资源」。这个默认在 sidecar 模式下成立,但 Istio 1.30.3 的
pilot/pkg/xds 至少要同时服务四类连接:per-pod
的 sidecar、per-node 的 ztunnel(Rust 实现)、per-namespace
的 waypoint(Envoy 实现)、以及作为独立 L4/L7 路由器的
Gateway。它们连上同一个
DiscoveryServer,但订阅的资源类型、承载的身份语义、甚至证书申请方式都不同。把它们当成「同一种
Envoy,只是部署位置不同」去排障,会在 Ambient
环境下直接卡住——因为 ztunnel 根本不理解
Listener/Cluster
这两个类型。本文钉住:身份从哪来、身份如何决定订阅范围、四类客户端具体订阅什么。
本文是「Istio / 网格控制面内核」系列第 3 篇(共 16 篇)。→ 系列目录
上接 第 2 篇 · istiod 进程模型;下接 第 4 篇 · 推送图。
版本锚定:Istio 1.30.3。源码:
pkg/model/proxy.go(NodeType/Proxy)、pilot/pkg/model/context.go(ParseServiceNodeWithMetadata)、pilot/pkg/bootstrap/discovery.go(InitGenerators)、pilot/pkg/xds/workload.go;architecture/ambient/ztunnel.md官方设计文档。
一、身份从哪来:节点 ID、元数据、验证后的 SPIFFE 身份
一次 ADS 连接建立时,istiod 从 gRPC 请求里的
core.Node 解析出两样东西:一个四段式节点
ID,一段结构化元数据。ParseServiceNodeWithMetadata(pilot/pkg/model/context.go,1.30.3)按
~ 切分节点 ID:
// pilot/pkg/model/context.go(Istio 1.30.3,节选)
const serviceNodeSeparator = "~"
func ParseServiceNodeWithMetadata(nodeID string, metadata *NodeMetadata) (*Proxy, error) {
parts := strings.Split(nodeID, serviceNodeSeparator)
// parts[0] = NodeType, parts[1] = IP(元数据里没有时的兜底),
// parts[2] = ID, parts[3] = DNSDomain
if !pm.IsApplicationNodeType(NodeType(parts[0])) {
return out, fmt.Errorf("invalid node type ...")
}
out.Type = NodeType(parts[0])
...
}一个典型 sidecar 的节点 ID
长这样:sidecar~10.42.1.7~reviews-v1-6c77b-x9k2q.default~default.svc.cluster.local——四段分别对应类型、IP、工作负载实例
ID、DNS
域。这个格式本身就是身份的最小单元:istiod
不relies于连接来源 IP
判断身份,而是要求客户端在协议层显式声明「我是谁、什么类型」。
除了节点
ID,NodeMetadata(pkg/model/proxy.go)携带更丰富的信息:Namespace、ServiceAccount、ClusterID、Network、IstioVersion、Generator(自定义生成器插槽)等。这些字段共同构成
model.Proxy 结构体:
// pkg/model/proxy.go(Istio 1.30.3,节选)
type Proxy struct {
Type NodeType
IPAddresses []string
ID string
Metadata *NodeMetadata
SidecarScope *SidecarScope
VerifiedIdentity *spiffe.Identity
XdsResourceGenerator XdsResourceGenerator
WatchedResources map[string]*WatchedResource
...
}VerifiedIdentity *spiffe.Identity
是身份链上最关键的一环:节点 ID 和 NodeMetadata
都是客户端自己声明的,只有在 mTLS 或 JWT
认证通过之后(pilot/pkg/xds/ads.go
里注册的 security.Authenticator 链,默认包含
authenticate.ClientCertAuthenticator),istiod
才会把证书里的 SPIFFE
URI(spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>)写进
VerifiedIdentity。后续任何依赖身份做授权判断的逻辑(例如该连接能否请求某个命名空间的资源),都应该以
VerifiedIdentity 为准,而不是节点 ID 里自称的
Namespace——节点 ID 和普通
NodeMetadata
字段在协议层是未经验证的自述,只有过了认证器的部分才可信。
二、身份如何决定订阅范围:先看资源类型,再看代理类型
一个容易想当然的假设是「model.Proxy.Type
直接决定这个连接能订阅哪些 xDS
类型」。源码里更准确的说法是:订阅范围首先由客户端请求的资源类型
URL 决定,Type
主要影响这一类型内部该怎么生成、要不要推送。InitGenerators(pilot/pkg/bootstrap/discovery.go,1.30.3)把类型
URL 和生成器一一注册:
// pilot/pkg/bootstrap/discovery.go(Istio 1.30.3,节选)
generators[v3.ClusterType] = &xds.CdsGenerator{ConfigGenerator: cg}
generators[v3.ListenerType] = &xds.LdsGenerator{ConfigGenerator: cg}
generators[v3.RouteType] = &xds.RdsGenerator{ConfigGenerator: cg}
generators[v3.EndpointType] = edsGen
generators[v3.SecretType] = xds.NewSecretGen(...)
generators[v3.ExtensionConfigurationType] = ecdsGen // ECDS
generators[v3.NameTableType] = &xds.NdsGenerator{...} // NDS,DNS 名表
generators[v3.ProxyConfigType] = &xds.PcdsGenerator{...} // 动态 ProxyConfig 下发
workloadGen := &xds.WorkloadGenerator{Server: s}
generators[v3.AddressType] = workloadGen
generators[v3.WorkloadType] = workloadGen
generators[v3.WorkloadAuthorizationType] = &xds.WorkloadRBACGenerator{Server: s}一个标准 Envoy(sidecar/Router/waypoint)在 bootstrap
里配置的是 ADS 上的
Cluster/Listener/RouteConfiguration/ClusterLoadAssignment/Secret
这几种类型 URL,天然落到
CdsGenerator/LdsGenerator/RdsGenerator/EdsGenerator/SecretGen;ztunnel
请求的类型 URL 是
Address/Workload/WorkloadAuthorization,天然落到
WorkloadGenerator/WorkloadRBACGenerator——两条路径在
s.Generators
这张表里从不相交,不需要靠
if proxy.Type == Ztunnel 这种分支去阻止 ztunnel
拿到
Cluster。findGenerator(pilot/pkg/xds/xdsgen.go)的查找顺序是
Metadata.Generator+"/"+typeURL →
string(proxy.Type)+"/"+typeURL → 纯
typeURL,前两级主要是给
grpc(proxyless gRPC
客户端)、api(MCP
风格消费者)这类需要按客户端族群重载同一类型的场景用的。
但 Type
确实在该不该推这一步起作用。xdsNeedsPush(pilot/pkg/xds/xdsgen.go)显式写了一条捷径:
// pilot/pkg/xds/xdsgen.go(Istio 1.30.3,节选)
func xdsNeedsPush(req *model.PushRequest, proxy *model.Proxy) (needsPush, definitive bool) {
if proxy.Type == model.Ztunnel {
// Not supported for ztunnel
return false, true
}
...
}ztunnel 走的是完全独立的推送判定路径(基于 ambient 索引的
Address 变化,而不是通用的
ConfigsUpdated
匹配);waypointNeedsPush
则是另一条专属逻辑:waypoint 的 Listener/Cluster
构建依赖「哪些 Service/Workload 挂载到了这个
waypoint」(istio.io/use-waypoint
标签),因此除了常规的 VirtualService/DestinationRule
变化,ambient 索引里的 kind.Address
更新也会触发 waypoint 重新计算——这是 waypoint
虽然用标准 LDS/RDS/CDS,却仍需要一条 Ambient
专属推送判定分支的原因。
三、四类客户端逐个看
| 维度 | Sidecar | Router(Gateway) | Waypoint | Ztunnel |
|---|---|---|---|---|
NodeType 常量 |
SidecarProxy |
Router |
Waypoint |
Ztunnel |
| 代理实现 | Envoy(istio-proxy) | Envoy(istio-proxy) | Envoy(istio-proxy) | Rust(独立 istio/ztunnel
仓库) |
| 部署粒度 | 每 Pod 一个 | 每个 Gateway 部署独立若干副本 | 每命名空间 / 每 ServiceAccount 一组 | 每节点一个(DaemonSet) |
| 订阅的 xDS 类型 | LDS/RDS/CDS/EDS/SDS | 同 Sidecar | 同 Sidecar(LDS/RDS/CDS/EDS/SDS) | 仅
Address、Authorization(自定义类型) |
| 身份粒度 | 一个证书对应自己的 ServiceAccount | 同 Sidecar | 一个证书对应 waypoint 自己的 ServiceAccount | 多证书:为节点上每个工作负载的身份各申请一份 |
| 典型故障表现 | 黑洞/旧路由(Envoy 11) | 同 Sidecar,但影响面是整个 Gateway | HBONE main_internal 监听未就绪 |
Address/Authorization 未同步,L4 授权判断错误 |
Sidecar 是默认路径,本篇不重复;下一篇(第 4 篇)的推送图就是以它为基准画的。
Router(Istio 里代表独立部署的
ingress/egress gateway)与 Sidecar
共用同一套生成器,区别在输入:Sidecar 的 Listener
集合来自「这个 Pod 的 inbound 端口 + Sidecar 资源限定的
outbound 范围」,Router 的 Listener 集合来自它所服务的
Gateway 资源声明的端口。两者殊途同归,都落在
LdsGenerator/CdsGenerator 上。
Waypoint 是 Envoy
二进制,但运行位置和触发条件都不同于 Sidecar:官方
Ambient overview 明确写道「waypoint proxy is a
deployment of the Envoy proxy; the same engine that Istio
uses for its sidecar data plane mode」,通过 Kubernetes
Gateway
API(gatewayClassName: istio-waypoint)部署,默认按命名空间共享,也支持按
ServiceAccount 更细粒度部署。它监听 HBONE
端口(15008),用一个 Envoy internal
listener(main_internal)在同进程内转发已终止
HBONE 的流量给标准 HTTP filter chain 执行
AuthorizationPolicy/RequestAuthentication。因为它本质是标准
Envoy,第 5–9 篇讲的 CDS/EDS/RDS
翻译规则对它同样适用;差异只在于第二节提到的推送触发条件多了
ambient 附着关系这一层。
Ztunnel
是最激进的分叉。architecture/ambient/ztunnel.md(1.30.3)记录了官方在
Rust、Go、Envoy 三种实现之间做过实测评估后选择 Rust
的决定,理由是性能与可控的资源footprint;同一份文档给出了不用标准
xDS 类型的量化理由:用 Envoy 通用类型表达一条 mTLS
语义要几十行 Protobuf,而 ztunnel 自定义的
Address/Authorization
类型把同样信息塞进”实测下体积、内存分配、CPU
时间都有约十倍优势”的紧凑表示(原文用词为「give
custom types a 10x edge」,这是 Istio
团队自己的工程实测结论,不是本站实测,也没有给出具体测试环境细节,属于
B
级来源,只能支持”设计取舍成立”,不能当作跨版本、跨环境都成立的精确倍数)。身份模型上
ztunnel 同样特殊:它以自己的身份向 CA
认证,却代表节点上其他工作负载去请求它们各自的证书;CA
必须验证「这个 ztunnel 有权代表这个身份」——这个校验基于
Kubernetes ServiceAccount JWT 里编码的 Pod
信息完成,本质是把「谁能请求谁的证书」这件事从「连接方=证书归属方」放宽成「连接方=已验证的节点代理,证书归属方=该节点上确有此工作负载」。
Agentgateway(NodeType
里最新加入的一类,配合
features.EnableAgentgateway
特性开关)代表另一条分叉:一个 AI/MCP 原生的 Rust
网关,可以替代 Envoy waypoint 承担 ingress/egress/waypoint
角色,专门处理 LLM 推理、MCP 工具调用、agent-to-agent
流量的路由与治理。它在 DiscoveryServer
里不完全复用经典的 Generators 表,而是通过
s.Collections(基于 KRT,Kubernetes Resource
Tree
的内部表示)单独注册(InitCollections)——这是继「标准
xDS 类型」「ztunnel
自定义类型」之后的第三条生成器路径,目前仍在快速演进中,本系列不展开其内部机制。
四、争论:通用 xDS 类型 vs 领域特化协议
| 通用 xDS 类型(LDS/RDS/CDS/EDS) | 领域特化类型(ztunnel 的 Address/Authorization) | |
|---|---|---|
| 代表实现 | Sidecar、Waypoint、Router | Ztunnel |
| 优点 | 一套生成器、一套调试工具(istioctl proxy-config)覆盖所有
Envoy 类客户端 |
单条资源信息密度更高,客户端解析成本更低 |
| 代价 | 表达简单语义(如一条 mTLS 身份要求)也要走完整 Protobuf 结构 | 需要为每类客户端单独维护一套生成器与调试路径 |
| 官方立场 | 隐含在 Envoy 数据面与传统 xDS 生态的设计里(Envoy 09) | 显式记录在
architecture/ambient/ztunnel.md,是针对「资源footprint」这一具体约束的定向优化 |
这不是「哪种设计更正确」的问题,而是协议通用性与客户端资源约束之间的权衡:Sidecar/Waypoint
本身就是完整的 Envoy,多解析几行 Protobuf
不构成瓶颈;ztunnel
的定位是「每节点一个、要托管该节点上所有工作负载的连接」,客户端本身的
CPU/内存预算被压缩到官方文档明确写出的「远低于传统
sidecar」的水平,这时候协议表达的边际成本才会被放大成设计决策的核心变量。开放问题:agentgateway
引入的第三条生成器路径(Collections/KRT)是否会与经典
Generators
表长期并存,还是会成为下一次「合并」的对象——目前没有官方路线图明确回答,这需要观察后续版本的架构文档更新,本文不做预测。
五、参考资料
官方文档(A)
- Istio 1.30 文档,Ambient / Overview、Ambient / Architecture / Data plane(waypoint 是 Envoy、ztunnel 是节点级 L4 代理的产品定位)。
源码 / 设计文档(A)
istio/istiotag 1.30.3:pkg/model/proxy.go(NodeType、Proxy结构体、VerifiedIdentity)。istio/istiotag 1.30.3:pilot/pkg/model/context.go(ParseServiceNodeWithMetadata、serviceNodeSeparator)。istio/istiotag 1.30.3:pilot/pkg/bootstrap/discovery.go(InitGenerators,类型 URL 到生成器的映射表)。istio/istiotag 1.30.3:pilot/pkg/xds/xdsgen.go(findGenerator、xdsNeedsPush、waypointNeedsPush)、pilot/pkg/xds/workload.go(WorkloadGenerator)。istio/istiotag 1.30.3:architecture/ambient/ztunnel.md(Rust 选型评估、自定义Address/Authorization类型的量化理由、CA 节点级授权模型)。
项目主页 / B 级来源
agentgateway项目主页与 GitHub 仓库(Rust 实现、MCP/A2A 原生、可作为 waypoint/ingress/egress 部署,2025 年由 Linux Foundation 托管)。
站内对照
实验台账
- 本篇不粘贴
istioctl proxy-config或 ztunnel 调试输出;ztunnel 的”十倍”体积/CPU 优势引用自官方设计文档,未在本机复现,已在正文标注为 B 级来源与适用边界。
六、小结
- 身份分两层:节点 ID 与
NodeMetadata是客户端自述,只有经过 mTLS/JWT 认证后写入的VerifiedIdentity(SPIFFE)才可信。 - 订阅范围首先由请求的类型 URL 决定:标准 Envoy 类客户端请求 LDS/RDS/CDS/EDS/SDS,ztunnel 请求自定义的 Address/Authorization,两条路径在生成器表里天然不相交。
NodeType影响的是”该不该推”而不是”能不能订阅”:xdsNeedsPush对 ztunnel 整体走独立判定路径,waypointNeedsPush让 waypoint 额外感知 ambient 附着关系。- 四类客户端 + 正在成型的 agentgateway,代表了「一切经 Envoy」到「按客户端资源约束定制协议」的一条持续分化路径,第 13 篇会在 Ambient 整体场景下继续展开。
→ 系列目录 · 上一篇:istiod 进程模型 · 下一篇:推送图
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Istio 控制面】Ambient:ztunnel 与 waypoint 作为两类 xDS 客户端
从 istiod 的 NodeType 与推送过滤代码出发,说明 ztunnel(Rust L4、HBONE、per-node)与 waypoint(Envoy L7、per-namespace)是两类订阅完全不同的 xDS 客户端,并指出 ambient 推送优化仍是源码里明写的 TODO。
【Istio 控制面】控制面全景:从 CRD 到 xDS 的翻译与推送内核
定位 Istio 控制面内核相对 Envoy 消费侧、Service Mesh 税文与 Gateway API 资源模型的缺口;给出五条坐标系、16 篇地图与本系列明确不写的范围,钉住 istiod 1.30.3 为主线。
Istio / 网格控制面内核:从 CRD 到 xDS
补齐 Envoy 数据面 xDS 消费侧与 Mesh 选型叙事之间的控制面内核:istiod 推送图、CRD/HTTPRoute 翻译、Ambient 多客户端、NACK/warming 责任与排障对齐。
【Istio 控制面】推送图:watch 到 DiscoveryServer.pushXds 的完整链路
画出 istiod 从 K8s watch 事件到某个代理收到 DiscoveryResponse 的完整推送图:ConfigUpdate、debounce、PushQueue、pushXds 各自的边界与代码依据,钉住 VersionInfo/Nonce 的生成规则,并说明为什么控制面的推送成功不等于 Envoy 在服务。