第
5 篇 讲过:CDS 按「Service × 端口」生成主
Cluster,DestinationRule
存在时还会在同一步派生出 subset Cluster。第 6
篇 讲的是路由怎么选出 Cluster
名字。本文补上中间这一层:DestinationRule
具体往 Cluster 上挂了什么、四种作用域(mesh 默认 /
DestinationRule 级 / port 级 / subset
级)怎么合并、结果落到哪些 Envoy Cluster
字段。Envoy 侧的 LB 算法细节与 outlier
驱逐算法不在本文重复,见 Envoy
Cluster/LB 与 连接池/outlier。
本文是「Istio / 网格控制面内核」系列第 7 篇。→ 系列目录
上接 第 6 篇:VirtualService/HTTPRoute → RDS;下接 第 8 篇:Authn/Authz/SDS。
版本锚定:Istio 1.30.3,源码路径对应
pilot/pkg/networking/core/cluster.go、cluster_builder.go、cluster_traffic_policy.go(函数签名以 master 分支验证,1.30.x 语义一致)。
一、DestinationRule 是「追加配置」,不是「新建服务」
DestinationRule.host
必须匹配一个已经存在的服务 hostname——它不会让 istiod
凭空生成一个 Cluster,只会在 BuildClusters
遍历到该 host 对应的 Service 时,追加调用
applyDestinationRule(pilot/pkg/networking/core/cluster_builder.go):
destRule := proxy.SidecarScope.DestinationRule(model.TrafficDirectionOutbound, proxy, service.Hostname)
subsetClusters := cb.applyDestinationRule(defaultCluster, DefaultClusterMode, service, port,
clusterKey.endpointBuilder, clusterKey.destinationRule.GetRule(), clusterKey.serviceAccounts)三个直接后果:
- host
拼错或大小写不一致时,规则会静默不生效——不会报错,只是这条
DestinationRule在SidecarScope.DestinationRule查找时命中不到对应 host,主 Cluster 照常按 mesh 默认生成。排障时先确认istioctl proxy-config cluster里是否出现了预期的 subset 名,而不是先怀疑 outlier 参数错了。 SidecarScope.DestinationRule的查找范围受 第 9 篇 的Sidecaregress 作用域约束——egress 未导入某个命名空间,该命名空间下再精确的DestinationRule也不会被这个代理看到。- 同一 host 存在多个
DestinationRule时按命名空间可见性(exportTo)和创建顺序做合并/去重,这是与 第 6 篇 里 VirtualService 冲突同构的问题——本文不重复展开。
二、TrafficPolicy 的四个组件与挂载顺序
applyTrafficPolicy(cluster_traffic_policy.go)从
TrafficPolicy 里拆出四个组件,按固定顺序挂到
Cluster 的不同字段:
flowchart TD
tp["TrafficPolicy"] --> cp["connectionPool<br/>→ circuit_breakers / max_requests"]
tp --> od["outlierDetection<br/>→ outlier_detection"]
tp --> lb["loadBalancer<br/>→ lb_policy / lb_subset_config"]
tp --> tls["tls<br/>→ transport_socket"]
cp --> cluster["Envoy Cluster proto"]
od --> cluster
lb --> cluster
tls --> cluster
| 组件 | 目标 Envoy 字段 | 备注 |
|---|---|---|
connectionPool |
circuit_breakers、连接/请求上限 |
对 inbound cluster 也生效(源码注释:「connection pool settings are applicable for both inbound and outbound clusters」) |
outlierDetection |
outlier_detection |
仅对 outbound
生效;OutlierDetectionHttpErrorCodes 额外走
applyOutlierDetectionErrorCodes |
loadBalancer |
lb_policy、lb_subset_config |
仅对 outbound 生效 |
tls |
transport_socket(经
buildUpstreamTLSSettings) |
结合 serviceMTLSMode(第五节)与
autoMTLSEnabled 判定是否自动升级为
ISTIO_MUTUAL |
applyOutlierDetection 把 DestinationRule
的字段按名字直接映射到 Envoy OutlierDetection
proto——consecutive5xxErrors→consecutive_5xx、baseEjectionTime→base_ejection_time、consecutiveGatewayErrors→consecutive_gateway_failure
等,语义与默认值细节见 Envoy
连接池/outlier 篇。Istio
这一层只做字段翻译,不改变 Envoy
的驱逐算法本身——这也是为什么调参手册应该查 Envoy 的
outlier 文档,而不是只看 DestinationRule 的字段说明。
三、Subset:一个 label selector,一份独立 Cluster
subsets[].labels
是对服务注册里端点标签的过滤器(见 第
5 篇
的端点聚合层),不是路由规则本身——官方文档明确提醒:「Policies
specified for subsets will not take effect until a route
rule explicitly sends traffic to this
subset」。也就是说,声明一个 subset 只是多生成一个
Cluster(outbound|port|subsetName|hostname),真正把流量导过去要靠
VirtualService
的
route.destination.subset——两个资源各管一段,缺一个都不会报错,只会表现成「配了却没用」。
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: reviews
spec:
host: reviews.default.svc.cluster.local
trafficPolicy:
loadBalancer:
simple: LEAST_REQUEST
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
outlierDetection:
consecutive5xxErrors: 3
interval: 5s
baseEjectionTime: 60s四、四层合并:mesh 默认 → DestinationRule 级 → port 级 → subset 级——但不是同一种继承
生产上最容易踩的坑是假设四层作用域都按同一种「深合并」规则叠加。源码和官方文档都不支持这个假设:
4.1 mesh 默认 → DestinationRule 级:整块兜底
applyDestinationRule 在解析完 port-level
合并结果后,先调用
applyDefaultTrafficPolicy(cb.req.Push.Mesh.GetDefaultTrafficPolicy(), trafficPolicy)。源码注释直接写明意图:
Seed the mesh-wide baseline so per-host clusters (including those with no matching DestinationRule) inherit it for any connectionPool / outlierDetection block the DR leaves unset. Subset / portLevelSettings still override on top via the merge below.
即:mesh 级 defaultTrafficPolicy
是最底层兜底,即便某个 Service 完全没有
DestinationRule,也会先套上 mesh 默认值,再谈后续覆盖。
4.2 DestinationRule 级 → subset 级:按整块字段覆盖,不逐字段深合并
官方 DestinationRule 参考文档对
Subset.trafficPolicy 的描述:「Subsets inherit
the traffic policies specified at the DestinationRule level.
Settings specified at the subset level will override the
corresponding settings specified at the DestinationRule
level.」——继承发生,但覆盖粒度是
TrafficPolicy
的四个顶级字段(loadBalancer/connectionPool/outlierDetection/tls)整块替换,不是逐个子字段合并。第三节示例里
v2 只重写了 loadBalancer 和
outlierDetection,connectionPool(如果顶层配了)会原样继承;但如果
v2 也想改 outlierDetection
里的某一个字段,必须把这个块里其余想保留的字段一起重写一遍——遗漏的字段不会「从
DestinationRule 级补齐」,而是回落到 Envoy 的默认值。
4.3 DestinationRule 级 → port 级:不继承,直接落到 Envoy 默认值
这是四层里最反直觉的一层。官方文档原文:「port level settings will override the destination-level settings. Traffic settings specified at the destination-level will not be inherited when overridden by port-level settings, i.e. default values will be applied to fields omitted in port-level traffic policies。」
对照 4.2 就能看出这个不对称:
| 覆盖层级 | 未显式设置的字段 | 来源 |
|---|---|---|
| subset 覆盖 DestinationRule 级 | 回落到 DestinationRule 顶层同名字段 | DestinationRule 顶层配置 |
| port 级覆盖 DestinationRule 级 | 回落到 Envoy 默认值 | Envoy 自身默认,不是 DestinationRule 顶层 |
也就是说,同一份
trafficPolicy.portLevelSettings
一旦对某个端口写了 loadBalancer,这个端口上的
connectionPool/outlierDetection
即便顶层配置得很仔细,也不会自动带过来,而是取
Envoy 内建默认值(例如熔断阈值回到 Envoy 的温和默认,见 Envoy
熔断篇)——除非在 portLevelSettings
里把这些字段也重复写一遍。这类问题的典型症状是「只在某个端口上调过一次
LB
策略,结果这个端口的熔断阈值突然变得比其他端口宽松很多」。
五、TLS mode/context:DestinationRule 管的是「客户端怎么发」
ClientTLSSettings.mode
有四档:DISABLE、SIMPLE(无客户端证书的
TLS)、MUTUAL(用户提供证书的
mTLS)、ISTIO_MUTUAL(Istio 管理证书的
mTLS,证书来自 第 8
篇 的 SDS 分发)。这里最容易混淆的一点:
DestinationRule.trafficPolicy.tls
决定的是「作为客户端连接这个上游时用什么 TLS」;PeerAuthentication
决定的是「作为服务端接受什么样的入站连接」——两者是同一条连接的两端,配置在两个不同资源里。
istiod 用
PushContext.BestEffortInferServiceMTLSMode(pilot/pkg/model/push_context.go)在
DestinationRule 没有显式设置 tls.mode 时,读取
mesh/namespace 级 PeerAuthentication 的 mTLS
模式给出「客户端是否应自动升级到
mTLS」的推断——源码注释明确这是best-effort:「因为
PeerAuthentication 是按 workload
生效的,这个函数无法在不知道 service-to-workload
绑定关系的前提下算出精确的服务级 mTLS 模式,目前只看 mesh 与
namespace 级策略,忽略 workload/port
级策略」。这就是auto
mTLS的实现基础:autoMTLSEnabled(mesh.GetEnableAutoMtls().Value)打开且没有更具体信息时,Envoy
客户端侧默认按「best-effort 推断」自动切
ISTIO_MUTUAL,不需要每个 Service 手写
DestinationRule。
两者不一致时的典型故障:PeerAuthentication
设了 STRICT(只收 mTLS),但某条
DestinationRule 显式把 tls.mode 写成
DISABLE 或压根没有 sidecar
的旧命名空间被一个宽泛 host(例如 *.local)的
DestinationRule 意外覆盖成
ISTIO_MUTUAL——前者是客户端发了明文给只收 mTLS
的服务端,后者是客户端对一个没有 sidecar
的目标发了它根本处理不了的 mTLS 握手,两种情况现网表现都是
503,且 CDS/RDS 单独看都「正确」。auto mTLS 的推断范围仅覆盖
mesh/namespace 级,workload 级
PeerAuthentication 需要运维在 DestinationRule
里显式对齐
tls.mode,不能指望自动推断兜底。
六、谱系、争论与开放问题
谱系:DestinationRule 把「服务发现产出的裸端点集合」与「怎么访问这些端点」显式拆成两个资源(Service/EndpointSlice 负责前者,DestinationRule 负责后者),呼应了 Envoy 自身把 Cluster 配置和 LB/outlier 参数放在同一层但由控制面统一管理的设计(Envoy 07)。这不是 Istio 独创,而是延续了「服务发现与访问策略解耦」这条更早的服务网格思路。
争论:port 级设置「不继承」而 subset 级「继承」,是不是一个自洽的设计——官方文档承认两者行为不同,但没有给出统一的设计理由说明为什么这两层选择了不同的默认值来源。一种工程判断是:port 级设置通常用于「个别端口需要完全不同的策略」(如 mTLS 管理端口),刻意不继承能避免顶层策略意外渗漏;subset 级更多用于「同一服务的不同版本做局部调参」,继承顶层更符合直觉。但这只是推测,源码与文档都没有明确写出这条取舍的动机。
开放问题:auto mTLS 的 best-effort
推断只覆盖 mesh/namespace 级
PeerAuthentication,明确放弃了 workload/port
级的精确对齐。随着 workload
级策略在生产中使用得越来越细,「客户端侧自动推断」与「服务端侧精确策略」之间的信息差会不会需要更强的一致性检查(例如在
istioctl analyze
里加一条规则),目前没有看到官方路线图。
七、参考资料
源码(A)
istio/istio:pilot/pkg/networking/core/cluster.go、cluster_builder.go(applyDestinationRule、BuildClusters的 subset 派生逻辑)。istio/istio:pilot/pkg/networking/core/cluster_traffic_policy.go(applyTrafficPolicy、selectTrafficPolicyComponents、applyOutlierDetection)。istio/istio:pilot/pkg/model/push_context.go(BestEffortInferServiceMTLSMode及其注释)。
规范 / 官方文档(A)
- Istio 1.30 文档,Destination Rule
参考(
TrafficPolicy、PortTrafficPolicy、Subset字段说明与继承语义原文)。
站内
- Envoy / Cluster 与负载均衡 · Envoy / 连接池、熔断与 outlier
- 第 5 篇:Service/Endpoint → CDS/EDS · 第 6 篇:VirtualService/HTTPRoute → RDS · 第 8 篇:Authn/Authz/SDS
- 系列目录
实验台账
- 本篇不粘贴
istioctl proxy-config cluster实跑输出;合并语义直接引用官方文档条款与源码注释,未在本机集群逐字段核对。
八、小结
- DestinationRule 不创建服务,只在
host匹配到已有 Service 时追加 Cluster 配置;host 拼错会静默失效。 TrafficPolicy的四个组件(connectionPool/outlierDetection/loadBalancer/tls)按固定顺序挂到 Envoy Cluster 的对应字段,Istio 只做字段翻译,不改变 Envoy 的算法语义。- Subset 只生成
Cluster,不路由流量——真正的流量分配要靠
VirtualService 的
route.destination.subset。 - 四层合并不是统一的深合并:subset 级覆盖会继承 DestinationRule 顶层同名字段兜底;port 级覆盖不继承,未写字段直接落到 Envoy 默认值——这是最容易被忽略的不对称。
- DestinationRule 的
tls.mode管客户端出站行为,与管服务端入站的 PeerAuthentication 是同一条连接的两端;auto mTLS 的 best-effort 推断只覆盖 mesh/namespace 级——见下一篇。
→ 系列目录 · 上一篇:VirtualService/HTTPRoute → RDS · 下一篇:Authn/Authz/SDS
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Istio 控制面】Service / Endpoint / Workload → CDS/EDS:istiod 如何拼出一个 Cluster
拆解 istiod 如何把 K8s Service、EndpointSlice、WorkloadEntry 聚合进内部服务模型,再经 CdsGenerator/EdsGenerator 分别生成 Cluster 与 ClusterLoadAssignment;钉住命名契约与部分推送边界,subset 留给 DestinationRule 篇。
【Istio 控制面】PeerAuthentication / AuthorizationPolicy → filter chain 与 SDS:策略怎么变成 RBAC 与证书
拆解 PeerAuthentication 如何落到入站 filter chain 的 mTLS 要求、AuthorizationPolicy 如何编译成 Envoy RBAC filter 的 permission/principal,以及 istiod CA 签发证书后 istio-agent 作为本地 SDS server 如何把 SPIFFE 身份证书喂给 Envoy;策略语义不重复展开,见零信任系列。
【Istio 控制面】控制面全景:从 CRD 到 xDS 的翻译与推送内核
定位 Istio 控制面内核相对 Envoy 消费侧、Service Mesh 税文与 Gateway API 资源模型的缺口;给出五条坐标系、16 篇地图与本系列明确不写的范围,钉住 istiod 1.30.3 为主线。
【Istio 控制面】istiod 进程模型:discovery、config、CA 与注入的边界
拆解 pilot-discovery 二进制内部的组件化启动顺序:CA 为何先于 Discovery 初始化、注入 Webhook 与配置校验为何是独立 HTTPS 服务、以及哪些组件真正位于 xDS 推送热路径上。