PodTopologySpread 从扣门到放弃

330 阅读4分钟

相关文档

PodTopologySpread介绍
Kubernetes 文档 <<Pod 拓扑分布约束>>
How to evenly distribute pods across a topology without using pod anti-affinity?
Pod Topology Spread Constraints介绍
KEP-895: Pod Topology Spread
#80921 EvenPodsSpread 关于受污染节点的讨论

问题背景

因业务需要,要对服务做多可用区改造,改造后的服务满足:

  1. zone-a/zone-b两个可用区必须有都有实例.
  2. 多个实例均匀分布在集群的节点之上,即基于kubernetes.io/hostname水平打散。

技术背景

公司集群相关信息如下:

  1. 集群版本1.20.10 (部分集群1.14)
  2. 所有业务实例部署在 host.type=common的机器上。
  3. ingress部署在host.type=commonnode-role.kubernetes.io/ingress: "true"的机器上。
  4. 生产实例必须开启hpa,hpa最小实例数为2。
  5. 为了保证节点资源均衡,对业务申请的limit进行打折设置request,实现超卖,然后使用Scheduler extender实现节点直接水位平均。

当前部署设置

当前部署开启了节点亲和性,pod反亲和性

affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - preference:
          matchExpressions:
          - key: MEM-CAP
            operator: In
            values:
            - 16GB
            - 32GB
            - 48GB
            - 64GB
            - 128GB
            - 256GB
            - 384GB
            - 512GB
            - 768GB
        weight: 100
      - preference:
          matchExpressions:
          - key: sub.host.type
            operator: DoesNotExist
        weight: 100
    podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
      - podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: service-name
              operator: In
              values:
              - app1
            - key: deploy-type
              operator: In
              values:
              - prod
          topologyKey: kubernetes.io/hostname
        weight: 10
      - podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: app
              operator: In
              values:
              - app1-prod-aws-d
          topologyKey: kubernetes.io/hostname
        weight: 20
    ...
    nodeSelector:
      host.type: common

在未加topologySpreadConstraints的情况下,调度器加上了默认拓扑规则,代码片段如下:

var systemDefaultConstraints = []v1.TopologySpreadConstraint{
   {
      TopologyKey:       v1.LabelHostname,
      WhenUnsatisfiable: v1.ScheduleAnyway,
      MaxSkew:           3,
   },
   {
      TopologyKey:       v1.LabelTopologyZone,
      WhenUnsatisfiable: v1.ScheduleAnyway,
      MaxSkew:           5,
   },
}

为了实现目标将topologySpreadConstraints修改为如下配置:

      topologySpreadConstraints:
      - labelSelector:
          matchLabels:
            app: app1-prod-aws-d
        maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: ScheduleAnyway
      - labelSelector:
          matchLabels:
            app: app1-prod-aws-d
        maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: ScheduleAnyway

期望结果:

  1. 节点之间最大相差1个实例
  2. 可用区之间相差1个实例 测试环境少量节点少量实例数量符合预期。

上生产遇到的问题

1. 节点 topology.kubernetes.io/zone 值为空。

节点上线时因为上线脚本的问题,节点标签不全部分节点topology.kubernetes.io/zone值为空。 导致调度器认为是三个可用区 zone-a,zone-b,以及空值的可用区。 发布以后一直不符合期望的比例。

2. 未进行打散,单节点上实例数过多。

生产环境52台机器,两台设置node-role.kubernetes.io/NoSchedule:true为不可调度。 3台ingress机器因为是8G内存不符合节点亲和性,剩余47台机器。 当业务发布50个实例的时候,部分节点实例数超过三个。未能实现每个节点1个,有3个节点是2个实例。

3. 强制节点差值为1。

将节点均衡性的whenUnsatisfiable 改为 DoNotSchedule 想让其均匀打散。
结果发现调度器中处理节点亲和性时只处理requiredDuringSchedulingIgnoredDuringExecution.
根据nodeSelector 选择到的节点是50个。 因为没有requiredDuringSchedulingIgnoredDuringExecution,所有的节点均参与了Skew的计算, 在调度第48个pod的时候,因为有common上的实例数为0(ingress机器上的实例数)导致无论在已存在的47个节点上增加pod时Skew的值都会是2,超过1导致pending.

4. 缩容问题

锁容时并不会触发平衡性检查,即总是从最多的那个节点进行锁容。导致的问题是锁容后其比例完全不符合预期。

5. 节点污点问题

1. pod先调度到节点之上。
2. 节点出现问题,节点打污点。
3. 新增pod。  

此时调度器会略过污点的node计算,如果增加太多实例然后节点放开调度在whenUnsatisfiable: "DoNotSchedule"的情况下一样会存现问题。

6. 可用区比例问题

多可用区作为容灾方案,一般不会两边部署同样多的实例(多可用区之间流量费用)。目前我们的形式是主备方案(中间件切换成本)。但topologySpreadConstraints根本无法实现比例方式,如果maxSkew值设置的过大,实际上完全丢失了均衡性功能。

综上测试: topologySpreadConstraints无法满足我们的需求。 生产代码被回退。

最终方案

# OpenKruise v0.10.0 新特性WorkloadSpread解读
一口气将openkruise 版本从0.9.0 升级到 1.2.0

相关命令

# 统计可用区实例数量
for node in $(kubectl get po -n devops -o wide | grep -v NODE  |grep qt-java-test-aws1 | awk '{print $7}'); do kubectl get no $node -o=jsonpath='zone-{.metadata.labels.topology\.kubernetes\.io/zone}'; done | sort | uniq -c

# 查看节点type和可用区
kubectl get node -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.host\.type} {"\t"} {.metadata.labels.topology\.kubernetes\.io/zone} {"\n"}{end}'