跨可用区流量既贵又慢:一次服务调用若被随机打到另一个可用区的 Pod,延迟增加数毫秒、云账单每月多出几千元,可用区故障时还容易被级联拖垮。拓扑感知(Topology Aware)就是让调度与路由都感知「节点在哪个故障域」:调度侧用 topologySpreadConstraints 打散副本,路由侧用 Service 拓扑感知把流量收敛到同区 Endpoint。本文覆盖两侧的配置语义、成本收益与排错方法。
目录
- 1. 拓扑感知的基本概念
- 2. topologySpreadConstraints 实战
- 3. Pod 亲和与反亲和
- 4. 服务拓扑与流量本地化
- 5. EndpointSlice 与拓扑提示
- 6. 本地流量策略与 internalTrafficPolicy
- 7. 跨可用区流量成本优化
- 8. 拓扑与调度器的协同排错
- 9. 生产最佳实践
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 | 拓扑域标签键 |
| whenUnsatisfiable | DoNotSchedule(硬约束)或 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 反亲和是拓扑感知的「前一代」方案,表达力弱但更严格。二者对比:
| 维度 | podAntiAffinity | topologySpreadConstraints |
|---|---|---|
| 语义 | 不允许/尽量不共存 | 控制数量差上限 |
| 偏斜容忍 | 无(硬约束下) | 可调 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 三种流量策略
| 字段 | 取值 | 语义 |
|---|---|---|
| externalTrafficPolicy | Cluster / Local | 外部流量是否只发给本节点 Pod |
| internalTrafficPolicy | Cluster / Local | 集群内流量是否只发给本节点 Pod |
| trafficDistribution | PreferClose 等 | 就近优先 |
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 constraint | maxSkew 硬约束 + 域数不足 |
| 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 延迟,再逐步收紧约束;同时牢记本地化永远需要可用性兜底,宁可回退跨区,也不要让某个可用区的少量副本被打爆。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。