拓扑感知路由:topologySpread、本地流量与流量亲和

系统讲解 Kubernetes 的拓扑感知能力:topologySpreadConstraints 的 maxSkew 与 whenUnsatisfiable 语义、Pod 亲和与反亲和、Service 拓扑感知路由与 topologyMode、EndpointSlice 拓扑提示、internalTrafficPolicy 本地流量策略,以及跨可用区流量成本优化与排错清单。

跨可用区流量既贵又慢:一次服务调用若被随机打到另一个可用区的 Pod,延迟增加数毫秒、云账单每月多出几千元,可用区故障时还容易被级联拖垮。拓扑感知(Topology Aware)就是让调度与路由都感知「节点在哪个故障域」:调度侧用 topologySpreadConstraints 打散副本,路由侧用 Service 拓扑感知把流量收敛到同区 Endpoint。本文覆盖两侧的配置语义、成本收益与排错方法。


目录


1. 拓扑感知的基本概念

1.1 什么是拓扑域

拓扑域(Topology Domain)由节点标签定义,常见的是 topology.kubernetes.io/zone(可用区)与 kubernetes.io/hostname(单节点)。Kubernetes 会把这些标签同步成 Node 上的 topology.kubernetes.io/* 标准标签,调度器与 kube-proxy 都依赖它们。

kubectl get nodes -L topology.kubernetes.io/zone -L topology.kubernetes.io/region

1.2 两个层次的问题

拓扑感知解决两个不同层次的问题,二者常被混为一谈:

层次组件目标
调度层kube-scheduler副本打散,避免单点故障域过载
路由层kube-proxy / Service调用收敛到就近 Endpoint,降低跨区流量

调度打散解决的是「副本是否分布均匀」,路由收敛解决的是「调用是否就近」。只做其中一个都不够:副本均匀了但调用仍跨区,成本下不来;调用就近了但副本集中在单区,可用区故障时直接全挂。

1.3 关键标签速查

kubernetes.io/hostname              节点名(最小故障域)
topology.kubernetes.io/zone         可用区
topology.kubernetes.io/region       地域
node.kubernetes.io/instance-type    实例规格

2. topologySpreadConstraints 实战

2.1 基本配置

topologySpreadConstraints 比反亲和更精确:它允许「允许一定偏斜」,而不是「绝对不允许同域」。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 6
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: api
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: api
      containers:
        - name: api
          image: registry.example.com/api:v1.2.0

2.2 字段语义

字段语义
maxSkew任意两个拓扑域间匹配 Pod 数量差的最大值,必须 ≥ 1
topologyKey拓扑域标签键
whenUnsatisfiableDoNotSchedule(硬约束)或 ScheduleAnyway(软约束)
labelSelector参与计数的 Pod 集合
minDomains最少需要多少个域才做均衡(用于扩容初期)
nodeAffinityPolicy是否把不满足 nodeAffinity 的节点计入域

maxSkew: 1 表示各域 Pod 数最多差 1。三区 6 副本时分布为 2/2/2,完全均匀。

2.3 硬约束与软约束的取舍

DoNotSchedule:不满足则 Pod 一直 Pending —— 保证均匀,但牺牲可用性
ScheduleAnyway:不满足也调度,只是打分时降权 —— 保证可用性,允许偏斜

生产建议:可用区维度用 ScheduleAnyway 兜底,节点维度用 DoNotSchedule 打散。全用硬约束会在某个区资源不足时造成大面积 Pending。

2.4 minDomains 的用途

扩容初期副本数少于域数时,maxSkew: 1 会让所有副本挤在少数域里。用 minDomains 声明期望的最小域数,调度器会把「空域」也计入偏斜计算:

- maxSkew: 1
  minDomains: 3
  topologyKey: topology.kubernetes.io/zone
  whenUnsatisfiable: DoNotSchedule
  labelSelector:
    matchLabels:
      app: api

3. Pod 亲和与反亲和

3.1 与 topologySpread 的关系

Pod 反亲和是拓扑感知的「前一代」方案,表达力弱但更严格。二者对比:

维度podAntiAffinitytopologySpreadConstraints
语义不允许/尽量不共存控制数量差上限
偏斜容忍无(硬约束下)可调 maxSkew
大集群性能差(需扫描全部 Pod)好(按域聚合)
推荐度遗留场景首选

新集群优先用 topologySpreadConstraints,反亲和只在小规模或需要「绝对不共存」时使用。

3.2 硬反亲和示例

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - topologyKey: kubernetes.io/hostname
        labelSelector:
          matchLabels:
            app: api

requiredDuringScheduling 是硬约束,preferredDuringScheduling 是软约束并带权重。

3.3 亲和性的常见误用

误用一:用 podAffinity 把同一服务的副本聚在一起 —— 与高可用目标相反
误用二:反亲和选择器过宽(匹配整个命名空间)—— 相互排斥导致无法调度
误用三:忽略 topologyKey 的语义 —— hostname 与 zone 的故障域完全不同
误用四:硬约束 + 副本数超过节点数 —— 必然 Pending

4. 服务拓扑与流量本地化

4.1 Service 的 topologyMode

从 1.22 起,Service 支持 trafficDistribution(1.31 稳定)与更早的注解 service.kubernetes.io/topology-mode,用于让 kube-proxy 优先选择同区 Endpoint。

apiVersion: v1
kind: Service
metadata:
  name: api
  annotations:
    service.kubernetes.io/topology-mode: Auto
spec:
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 8080

4.2 Auto 模式的回退逻辑

Auto 不是强制本地化,而是在满足条件时优先本地,不满足则回退到全量 Endpoint:

条件一:本区 Endpoint 数量足够(不低于按比例应得的数量)
条件二:节点带有 topology.kubernetes.io/zone 标签
条件三:本区所有 Endpoint 都处于 Ready 状态
任一不满足 → 回退到跨区负载均衡,保证可用性

这个「有条件本地化」的设计很关键:它避免了本地化导致的容量不均——如果本区只有 1 个 Pod 而其他区有 10 个,强行本地化会把本区那 1 个 Pod 打死。

4.3 trafficDistribution 新字段

spec:
  trafficDistribution: PreferClose

取值 PreferClose(优先就近)与 PreferSameZone / PreferSameNode(1.33 起的更细粒度)。相比注解,它是稳定 API,推荐新集群使用。


5. EndpointSlice 与拓扑提示

5.1 为什么用 EndpointSlice

EndpointSlice 把一个大 Service 的 Endpoint 拆成多个分片(默认每片 100 个),避免单个 Endpoints 对象过大。每个 Endpoint 都带 nodeName 与 zone 字段,这正是拓扑路由的数据基础。

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: api-abc12
  labels:
    kubernetes.io/service-name: api
addressType: IPv4
endpoints:
  - addresses: ["10.244.3.17"]
    conditions:
      ready: true
    nodeName: node-a-1
    zone: cn-north-1a
ports:
  - port: 8080
    protocol: TCP

5.2 hints 字段

EndpointSlice 支持 hints.forZones,由控制面计算并写入「哪个区应该优先用哪些 Endpoint」:

endpoints:
  - addresses: ["10.244.3.17"]
    hints:
      forZones:
        - name: cn-north-1a

kube-proxy 读取 forZones 后,只为匹配本区的 Endpoint 建立转发规则。提示由控制面算好,kube-proxy 只做消费,因此本地化逻辑统一、可观测。

5.3 排错常用命令

kubectl get endpointslice -l kubernetes.io/service-name=api -o wide
kubectl get endpointslice -l kubernetes.io/service-name=api \
  -o jsonpath='{range .items[*].endpoints[*]}{.nodeName}{" "}{.zone}{"\n"}{end}'
kubectl get svc api -o jsonpath='{.spec.trafficDistribution}'

6. 本地流量策略与 internalTrafficPolicy

6.1 三种流量策略

字段取值语义
externalTrafficPolicyCluster / Local外部流量是否只发给本节点 Pod
internalTrafficPolicyCluster / Local集群内流量是否只发给本节点 Pod
trafficDistributionPreferClose 等就近优先

internalTrafficPolicy: Local 是强本地化:只有本节点上有 Endpoint 时才转发,否则丢弃。它适合 DaemonSet 型组件(如日志采集、节点级缓存),不适合普通业务。

6.2 externalTrafficPolicy 与源 IP

spec:
  externalTrafficPolicy: Local

取 Local 时,外部流量只转发给本节点上的 Pod,保留客户端源 IP(因为不做 SNAT),但要求每个节点都有就绪 Pod,否则该节点上的请求会被丢弃。这正是 DaemonSet + NodePort 组合常见的原因。

6.3 选择建议

普通业务服务:externalTrafficPolicy=Cluster + trafficDistribution=PreferClose
需要源 IP:externalTrafficPolicy=Local + 副本覆盖所有节点
节点级组件:internalTrafficPolicy=Local(强本地,天然就近)
网关/入口:externalTrafficPolicy=Local + 反亲和打散

7. 跨可用区流量成本优化

7.1 成本来源

主流云厂商对跨可用区流量单独计费,同区流量通常免费或极低。一个每天 10 TB 东西向流量的服务,若 60% 跨区,按 0.01 美元/GB 计算每月多支出约 1800 美元。

跨区流量成本 = 跨区字节数 × 单价
优化目标     = 提高同区命中率,同时保证可用性

7.2 度量同区命中率

# 用 node 标签与 Pod 分布估算理论同区率
kubectl get pods -l app=api -o jsonpath='{range .items[*]}{.spec.nodeName}{"\n"}{end}' \
  | sort | uniq -c

真实命中率应通过服务网格或 eBPF 采集的流量数据度量,理论分布只作参考。

7.3 优化组合

手段效果代价
topologySpread 三区均匀提高就近命中概率无明显代价
trafficDistribution=PreferClose同区优先负载可能不均
本区副本数与流量成比例减少回退需要容量规划
减少跨区依赖链降低放大效应架构改造

关键洞察:就近路由的前提是「每个区都有足够的副本」。副本按区均匀分布是拓扑路由能生效的必要条件。


8. 拓扑与调度器的协同排错

8.1 Pod 一直 Pending

kubectl describe pod api-xxx | sed -n '/Events/,$p'

常见事件与原因:

事件原因
didn’t match pod anti-affinity rules反亲和硬约束无法满足
didn’t satisfy topology spread constraintmaxSkew 硬约束 + 域数不足
node(s) had untolerated taint污点未容忍
Insufficient cpu资源不足,与拓扑无关

8.2 检查域标签是否完整

kubectl get nodes -o custom-columns=\
NAME:.metadata.name,ZONE:.metadata.labels.topology\.kubernetes\.io/zone

缺少 zone 标签的节点会被视为一个独立的空域,导致偏斜计算异常。混合云或自建集群常见此问题。

8.3 本地化未生效

排查顺序:
1. EndpointSlice 的 endpoints 是否带 zone 字段
2. 本区 Ready Endpoint 数量是否达到触发阈值
3. kube-proxy 是否运行在 iptables 还是 IPVS 模式(模式影响支持度)
4. Service 是否配置了 trafficDistribution 或 topology-mode 注解
5. 是否被 NetworkPolicy 或 Mesh 侧车改写为全量负载均衡

最后一条尤其常见:Service Mesh 的 Sidecar 接管流量后,kube-proxy 的本地化策略可能被绕过,需要在 Mesh 层单独配置 locality load balancing。


9. 生产最佳实践

9.1 落地 Checklist

□ 所有节点具备 topology.kubernetes.io/zone 标签
□ 业务 Deployment 三区 topologySpread,可用区用软约束兜底
□ 关键服务开启 trafficDistribution=PreferClose
□ 每区副本数与流量大致成比例,减少回退
□ DaemonSet 类组件用 internalTrafficPolicy=Local
□ 需要源 IP 的入口用 externalTrafficPolicy=Local
□ 监控跨区流量占比与同区命中率
□ 定期审计 Pending Pod 的拓扑类事件

9.2 常见坑与对策

坑现象对策
全用硬约束域资源不足时大面积 Pending可用区维度改 ScheduleAnyway
缺 zone 标签偏斜计算异常补齐节点标准拓扑标签
本地化无副本流量回退或丢弃保证每区都有就绪副本
Mesh 绕过本地化就近策略不生效在 Mesh 层配置 locality LB
副本数少于域数分布不均用 minDomains 或提高副本数
反亲和选择器过宽相互排斥无法调度收窄 labelSelector

小结

拓扑感知的本质是把「故障域」这一概念同时注入调度与路由:调度侧用 topologySpreadConstraints 保证副本在可用区间均匀,路由侧用 trafficDistribution 或 EndpointSlice 提示把调用收敛到同区。两者是乘法关系而非加法——副本不均,就近路由就无从谈起;就近不做,均匀分布也省不下跨区成本。落地时建议先在 staging 用 PreferClose 加软约束验证,度量同区命中率与 P99 延迟,再逐步收紧约束;同时牢记本地化永远需要可用性兜底,宁可回退跨区,也不要让某个可用区的少量副本被打爆。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「云原生」更多文章

  1. 调度均衡:Descheduler 与资源碎片整理
  2. Cluster API 与声明式集群生命周期管理
  3. 运行时安全:Falco/Tetragon 与 eBPF 检测实战