前言

正所谓每一次事故都是一次成长

事情是这样的,最近行业不景气(摆烂),由于业务收缩,所以需要对其中一个小的 k8s 集群中的节点做收缩,下掉几台不需要使用的服务器,在对 k8s node 做变更的时候出现了一个意外:coredns 在某个 node 被删除之后重启后发现无法正常启动,并且出现报错

0/7 nodes are available: 3 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn’t tolerate, 4 node(s) didn’t match Pod’s node affinity.

在救火之后,让我想到,之前没有写过和调度相关问题的博客,于是今天先来说一些最基本的规则,让我们能控制一个 pod 能被调度到整个集群的哪一个 node 上去。本文最后回过头再来解决这个问题。

其实合理的控制 pod 调度也是我们一个非常实用且必会的技能之一。

nodeName

这个很少用到,也最简单最粗暴,就是直接指定你这个 pod 只能到哪个 node 上去。

1
2
spec:
nodeName: kube-01

比如这样配置,当前 pod 就只能去 kube-01 这个节点。
由于这样配置过于粗暴,缺少灵活性,并且节点名称可能会改变,故较少使用。

nodeSelector

这个是最简单的一个设定,只需要两个步骤,就可以让我们直接控制 pod 调度到想要的 node 上。

  1. 给想要被调度的 node 贴上你喜欢的标签如:disktype=ssd
  2. 给 pod 设定 nodeSelector 为对应的 key value,如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    apiVersion: v1
    kind: Pod
    metadata:
    name: nginx
    labels:
    env: test
    spec:
    containers:
    - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
    nodeSelector:
    disktype: ssd
    这样就可以,用白话说就是贴上红标签,那就是红色的人才能去;贴上绿标签,那就是绿色的人才能去。

affinity

亲和性,这个规则的设定可以说是真的非常灵活,搭配使用可以造出各种调度策略

节点亲和性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: another-node-label-key
operator: In
values:
- another-node-label-value

其中的两个策略其实看英文名字翻译就很好懂,都是在调度过程中,而已经运行时不会生效的

  • requiredDuringSchedulingIgnoredDuringExecution 必须满足
  • preferredDuringSchedulingIgnoredDuringExecution 优先满足(能满足最好)同时可以支持设定 weight 权重,评分高的节点优先级高
    其中可以使用 In,NotIn,Exists,DoesNotExist,Gt,Lt 这些操作符

pod 亲和性

节点亲和性说白了就是约定什么样的 pod 和什么样的 node 关系比较好,可以一起玩;而 pod 亲和性其实就是,什么样的 pod 之间关系比较好能一起玩,什么样的 pod 之前关系不好,不能一起玩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S1
topologyKey: topology.kubernetes.io/zone
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2
topologyKey: topology.kubernetes.io/zone

这里的规则表示的就是:security=S1 的 pod 喜欢在一起,可以分配到同一个节点上,但是 security=S1 和 security=S2 的不喜欢在一起,会尽可能(因为是preferred)调度到不同的节点上。

在实际中可以使用 podAffinity 让相近的业务(互相之间存在访问)放在同一节点,减少网络请求开销;或者使用 podAntiAffinity 让 pod 尽可能平均分配到各个节点来保证高可用

不过具体还是需要你根据实际的集群场景以及节点数量来进行配置和选择,因为节点数量过多可能导致调度慢,或者可能有时配置的 required… 导致最终 pod 无法正常调度。

污点

一开始这个中文名称让我着实难以理解,为什么会有这样的功能,英文叫 Taint。但是联系到它的作用和功能,我觉得污点这个词也很贴切了。

污点是指你可以给一个节点打上一个 “污点”,这样 pod 发现这个节点很 “污”,就不来了。

1
kubectl taint nodes node1 key1=value1:NoSchedule

这个命令就是给 node1 添加一个 key1=value1 的污点,除非你能容忍这个污点,否则你就调度不过来了。
当然你可以选择配置为PreferNoSchedule,这样就“软”一点,也就是你最好别来。
还可以更直接 NoExecute,不仅别来,已经存在的还要给你赶出去。

这就引出了我们后面的一个概念,容忍度。

容忍度

1
2
3
4
5
6
spec:
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"

容忍度很好理解,如果当一个 pod 配置了容忍度,则表示它可以容忍你 node 的污点,即使你有污点,我也愿意到你这里去。

污点的应用其实你早就见过的:为什么你的 pod 不会被调度到 master 节点上呢?其实就是因为 master 节点被打了 node-role.kubernetes.io/master:NoSchedule 的污点,所以如果你将这个污点去掉,那么你的 pod 也就可以被调度到 master 上去了。

事故复现

事故整体的流程如下:

  1. 节点下线之后,业务的 pod 重新进行调度,业务启动之后发现无法连接相关中间件或相关服务
  2. 发现业务报错:tcp: lookup xxxxxxxxx on 172.1.1.x -> … connection refused,显然肯定和 dns 有关系
  3. 发现 coreDNS 的 pod 一直处于 pending 状态,并且出现报错 0/7 nodes are available: 3 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate, 4 node(s) didn't match Pod's node affinity.
  4. 修改 coreDNS 的 yaml 其中调度策略配置后,进行重启,最终恢复

scheduling-assign-pod-error-lookup

scheduling-assign-pod-error-failed-scheduling

问题分析

首先看到业务报错想到 dns 有问题,这个确实很容易想到,再看到 coreDNS 报错就开始分析了,本质问题就是 pod 发现自己没有任何 node 可以去

首先 master 有污点,所以不会被调度,这个没问题,然后就去 coreDNS 的配置,看它能被调度到什么样的 node 上去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: type
operator: NotIn
values:
- virtual-kubelet
- key: k8s.aliyun.com
operator: NotIn
values:
- 'true'
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: k8s-app
operator: In
values:
- kube-dns
topologyKey: kubernetes.io/hostname
weight: 100

......

nodeSelector:
dns: coredns
kubernetes.io/os: linux

一开始我以为是亲和性(affinity) 上的配置出现了问题(因为毕竟报错里面提到了affinity),但是显然这些要求都应该是满足的,一开始没注意到 nodeSelector 上的配置,后来才发现 nodeSelector 上又一个奇怪的标签是 dns: coredns, 这个标签所有的 node 上都没有,这下好,所有 node 都不满足调度要求了,故 pod 没有办法去任何一个 node。
然后删除掉这个 label 就可以正常调度了,唉,没错就这么简单。

总结

  1. 通过 nodeSelector 来指定 pod 喜欢去哪里
  2. 通过 nodeAffinity 来指定 node 喜欢什么样的 pod
  3. 通过 podAffinity 来指定 什么样的 pod 喜欢在一起或不喜欢在一起
  4. 通过 taint 来让大家都别来,通过 tolerations 来忍受 taint 强行喜欢你

在 pod 的调度上面其实很多时候可以设定各种规则来满足我们各种的调度需求,同时当出现调度问题无法正常调度的时候,也就知道需要去找对应配置来看,一一比对之后则可以查询出为何 pod 没有正常调度的原因了。但其实调度失败往往还有其他的原因,如:资源不够等,故后面我们还会继续深入研究其他方面。