Kubernetes是生产级容器编排、自动化容器部署、扩展和管理的系统;这里会告诉你如何对约束一个 Pod 只能在特定的 Node上运行,或者优先运行在特定的节点上。
1 - 实验的环境
集群环境:
里面包含的IP地址均是内部网络使用的虚拟IP;
命名空间:
默认命名空间此时只存在默认的service资源,无任何Pod在运行;
Pod部署配置:
apiVersion: v1kind: Podmetadata: name: counter namespace: default labels: app: counter release: 0.1.0spec: # 根据各实践案例,植入的调度策略配置 # ${scheduler_policy} containers: - name: count image: alpine:3.8 args: [/bin/sh, -c, 'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done'] resources: requests: cpu: 100m memory: 100Mi
如果不设置任何调度策略,直接使用以上配置,Pod将会在任意节点运行;以下所有测试均基于k8s集群1.10版本,涉及内容均为核心功能,即使后期新版也不会轻易被废弃,所有Pod调度由kube-scheduler进程管理控制。
2 - NodeName方式调度
2.1 - 调度策略
${scheduler_policy}
spec: nodeName: 10.5.119.105
2.2 - 数据结构
type PodSpec struct { ...... // Pod所在的Node节点 // 如果是运行时候,表示Pod所在的节点 // 如果是部署时候,设置为对应节点的名称,则默认调度器会忽略调度,直接将Pod部署至该节点 NodeName string ......}
2.3 - 运行示例
- 查看默认命名空间下的Pod
- 运行部署配置与查询结果
根据调度策略中的设置,Pod被调度至正确的主机上了
- 删除该Pod资源,以免影响后续实验
3 - NodeSelector方式调度
3.1 - 调度策略
${scheduler_policy}
spec: nodeSelector: a-demo-web.site/disk-type=ssd
3.2 - 数据结构
type PodSpec struct { ...... // 首先集群中各个Node均设置各自的标签 // 然后这里设置与Node对应的标签名称与值,则该Pod将会调度至该Node上 // 示例:a-demo-web.site/disk-type=ssd NodeSelector map[string]string ......}
3.3 - 运行示例
- 查看集群是否存在a-demo-web.site开头的标签,并添加调度策略部署Pod,观察状态
持续处于Pending状态,通过观察事件:
0/12 nodes are available: 12 node(s) didn't match node selector.
发现集群中没有存在符合调度的Node资源,所以Pod会一直处于Pending状态
- 为主机10.5.119.137绑定标签:a-demo-web.site/disk-type=ssd
- 通过添加标签后,另一个窗口实时监听Pod运行状态(Pod能否获得调度?)
根据调度策略中的设置,Pod被调度至正确的主机上了
- 已经运行的Pod,此时撤销对应节点标签(标签名末尾添加中划线:-)
观察Pod的变化,能否导致Pod被驱逐、下线?
已完成调度的Pod,尽管此时节点标签变化不满足运行需求,是不会导致Pod被驱逐的
- 删除该Pod资源,以免影响后续实验
3.4 - 内置标签
除了使用自定义的标签外,集群也内置了几个标签,可以设置它们进行相应调度
3.5 - 应用场景
如果我们指定了Pod的nodeSelector条件,而集群中不存在包含相关条件的节点,此时该Pod是无法被调度,即使集群其他节点是空闲状态。
所以实际上在生产环境中使用nodeSelector局限性还是比较大,可控性低,随着k8s集群版本的更新,在后续将会被废弃,由NodeAffinity代替。
4 - NodeAffinity:节点的亲和性
4.1 - 配置示例
spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: a-demo-web.site/host-type operator: Exists preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: a-demo-web.site/host-type operator: In values: - "physical" - key: a-demo-web.site/disk-type operator: In values: - "local"
自定义标签说明:
4.2 - 配置说明
存在两种表达式:
- requiredDuringSchedulingIgnoredDuringExecution
- preferredDuringSchedulingIgnoredDuringExecution
注意:
- 如果同时定义了nodeSelector和nodeAffinity,那么必须两个条件都要得到满足,Pod才能运行到指定节点
- 如果nodeAffinity指定了多个nodeSelectorTerms,那么只要其中一个匹配即可
- 如果nodeSelectorTerms中有多个matchExpressions,则一个节点必须满足所有matchExpressions才能运行该Pod
对于每个符合所有调度要求的节点,调度器将遍历该字段的元素来计算总和,并且如果节点匹配对应的MatchExpressions,则添加权重到总和,然后对各个节点评分,总分最高的节点是最优选的。
4.3 - 数据结构
Affinity
// 一组亲和性的调度规则type Affinity struct { // 节点亲和性调度策略 // json:"nodeAffinity,omitempty" NodeAffinity *NodeAffinity // Pod的亲和性调度策略 // 用于控制相同的Pod出现在同一个主机、可用区等 // json:"podAffinity,omitempty" PodAffinity *PodAffinity // Pod的互斥性调度策略 // 用于控制相同的Pod避免在同一个主机、可用区等 // json:"podAntiAffinity,omitempty" PodAntiAffinity *PodAntiAffinity}
NodeAffinity
// 节点亲和性的数据结构type NodeAffinity struct { // 硬限制;必须满足指定的规则才可以调度Pod到节点上(与nodeSelector功能一样,但语法不同) // json:"requiredDuringSchedulingIgnoredDuringExecution,omitempty" RequiredDuringSchedulingIgnoredDuringExecution *NodeSelector // 软限制;根据节点权重总分最高为最优,调度器调度Pod到该节点上 // 同时必须满足nodeSelector、requiredDuringScheduling的规则(AND关系) // json:"preferredDuringSchedulingIgnoredDuringExecution,omitempty" PreferredDuringSchedulingIgnoredDuringExecution []PreferredSchedulingTerm}
XXXXXXXIgnoredDuringExecution的含义:如果一个Pod已经运行,且它所在Node标签发生了变更,不在符合调度策略,则系统忽略,该Pod可以继续运行,也就是亲和性实际只有在调度期间才会使用(类型nodeSelector方式调度,调度成功后移除Node标签,并不会导致Pod被驱逐)
NodeSelector
// 节点选择器的数据结构// 表示一组节点上多个标签查询结果的并集type NodeSelector struct { // 在RequiredDuringScheduling调度策略下如果设置多个,则它们之间是OR的关系 // 也就是只要满足其中之一即可 // json:"nodeSelectorTerms" NodeSelectorTerms []NodeSelectorTerm}
PreferredSchedulingTerm
// 首选调度项的数据结构type PreferredSchedulingTerm struct { // 节点选择项的权重,取值范围:1至100 // json:"weight" Weight int32 // 具体节点选择项设置,关联上面的权重 // json:"preference" Preference NodeSelectorTerm}
NodeSelectorTerm
// 节点选择项的数据结构// 初始化一个策略,但不设置任何规则,则不匹配任何对象// 如果设置了多个Match规则,它们之间是AND关系,也就是说必须全匹配type NodeSelectorTerm struct { // 根据自定义的标签名称、标签值,编写表达式去匹配 // json:"matchExpressions,omitempty" MatchExpressions []NodeSelectorRequirement // 早于kubernetes1.11版本不存在 // 根据内置固定的字段调度,如:metadata.name // TODO: 待后续的案例分析详解释 // json:"matchFields,omitempty" MatchFields []NodeSelectorRequirement}
NodeSelectorRequirement
// 节点选择项规则的数据结构// 它由key、operator、values组成,不同的operator会有不一样的values结构type NodeSelectorRequirement struct { // 这个是标签名称,对应CMDB中规定的值,非顺便乱写 // json:"key" Key string // 节点选择操作符,表示对Key对应Value的操作 // 可取六个值:In、NotIn、Exists、DoesNotExist、Gt、Lt // json:"operator" Operator NodeSelectorOperator // 这里编写value均是以字符串表示 Values []string `json:"values,omitempty" protobuf:"bytes,3,rep,name=values"`}
这里Values是数组对应标签值,根据不同的Operator,可以设置0、1、多个标签值。
这里要区分注意每个主机节点设置的标签键值是1对1的,如:a-demo-web.site/disk-type=ssd,而不是可以设置数组形式,如:a-demo-web.site/disk-type=[ssd, local](没有这种写法)。
当Operator=In 或 NotIn 时,Values可以设置为数组(多个标签值),这些标签值是来自集群中各个节点的并集。
NodeSelectorOperator
// 节点选择操作符type NodeSelectorOperator stringconst ( NodeSelectorOpIn NodeSelectorOperator = "In" NodeSelectorOpNotIn NodeSelectorOperator = "NotIn" NodeSelectorOpExists NodeSelectorOperator = "Exists" NodeSelectorOpDoesNotExist NodeSelectorOperator = "DoesNotExist" NodeSelectorOpGt NodeSelectorOperator = "Gt" NodeSelectorOpLt NodeSelectorOperator = "Lt")
所以规则中只支持这六种操作符运算
4.4 - 节点选择操作符逻辑代码
switch op { // 如果 Operator=In 或 NotIn,则 Values 必须设置至少一个值 case selection.In, selection.NotIn: if len(vals) == 0 { return nil, fmt.Errorf("for 'in', 'notin' operators, values set can't be empty") } // NodeSelectorRequirement 不支持这三种操作符 case selection.Equals, selection.DoubleEquals, selection.NotEquals: if len(vals) != 1 { return nil, fmt.Errorf("exact-match compatibility requires one single value") } // 如果 Operator=Exists 或 DoesNotExist,则 Values 必须为空 case selection.Exists, selection.DoesNotExist: if len(vals) != 0 { return nil, fmt.Errorf("values set must be empty for exists and does not exist") } // 如果 Operator=Gt 或 Lt,则 Values 必须只能设置一个值,且必须为整数(不能包含小数点) case selection.GreaterThan, selection.LessThan: if len(vals) != 1 { return nil, fmt.Errorf("for 'Gt', 'Lt' operators, exactly one value is required") } for i := range vals { if _, err := strconv.ParseInt(vals[i], 10, 64); err != nil { return nil, fmt.Errorf("for 'Gt', 'Lt' operators, the value must be an integer") } } default: return nil, fmt.Errorf("operator '%v' is not recognized", op)}
5 - NodeAffinity "硬限制" 调度
5.1 - 调度策略
${scheduler_policy}
spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: a-demo-web.site/disk-type operator: In values: - local_ssd - key: a-demo-web.site/host-type operator: In values: - kvm - matchExpressions: - key: a-demo-web.site/host-type operator: In values: - vmware - physical
策略说明部署Pod至包含标签:a-demo-web.site/disk-type=local_ssd 且 a-demo-web.site/host-type=vmware 或 physciall 其中之一。
5.2 - 应用示例:多个 nodeSelectorTerms 的关系
- 确认当前集群不包含任何 a-demo-web.site 开头标签的节点
- 打开另外一个终端,开启实时监听默认命名空间下Pod的状态
- 添加调度策略,部署应用,此时观察到Pod持续处于Pending状态
- 为节点225添加标签 a-demo-web.site/host-type=vmware
此时等待一会...
可以看到Pod被调度至225节点上了,也验证了多个nodeSelectorTerms之间为OR的关系
- 移除225的标签 a-demo-web.site/host-type=vmware,已调度的Pod不会影响,仍然继续运行
- 删除该Pod资源,以免影响后续实验
5.3 - 应用示例:多个 matchExpressions 的关系
- 为避免干扰同样验证集群中需不存在 a-demo-web.site 开头的标签,然后部署应用
- 为节点224添加标签:a-demo-web.site/disk-type=local_ssd
由于并不满足所有条件,此时Pod还是维持Pending状态
- 为节点224添加标签:a-demo-web.site/host-type=kvm
- 观察原先开启的实时监听窗口
由于策略寻找到合适的节点,成功调度Pod,同时验证多个matchExpressions之间是AND的关系
- 同样,移除节点的标签,并不会导致原先已调度的Pod
- 删除该Pod资源,以免影响后续实验
6 - NodeAffinity "软限制" 调度
6.1 - 调度策略
${scheduler_policy}
spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: a-demo-web.site/host-type operator: Exists preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: a-demo-web.site/host-type operator: In values: - "physical" - key: a-demo-web.site/disk-type operator: In values: - "local" - weight: 80 preference: matchExpressions: - key: a-demo-web.site/host-type operator: In values: - "kvm" - "vmware" - key: a-demo-web.site/disk-type operator: In values: - "local" - weight: 30 preference: matchExpressions: - key: a-demo-web.site/cloud-provider operator: In values: - "aws"
由于preferredDuringSchedulingIgnoredDuringExecution是"软限制",也就是说为匹配最优节点,也会被调度并部署至其他节点,所以这里添加了requiredDuringSchedulingIgnoredDuringExecution策略,以配合实验的成功演示。
匹配 a-demo-web.site/host-type=physical 且 a-demo-web.site/disk-type=local(物理服务器本地普通磁盘)则节点权重为100
匹配 a-demo-web.site/host-type=kvm 或 vmware 且 a-demo-web.site/disk-type=local(基于kvm或vmware的虚拟机)则节点权重为80
匹配 a-demo-web.site/cloud-provider=aws(主机云服务提供商为亚马逊)则节点权重为30
6.2 - 请求示例
- 检查当前集群不包含a-demo-web.site开头的标签,并创建Pod
- 查看创建的Pod并实时监听状态的变化
不管等待多久,会持续处于Pending状态,查询事件,没有匹配的节点。
0/12 nodes are available: 12 node(s) didn't match node selector.
- 为节点135/136/137添加标签:a-demo-web.site/disk-type=local
此时只匹配到标签 a-demo-web.site/disk-type=local,对Pod并没有什么影响,还是处于Pending状态。
- 为节点135添加标签:a-demo-web.site/cloud-provider=aws
此时节点135相比其他,拥有30的权重,但还是不会吧Pod调度过来,因为前面设置了 requiredDuringSchedulingIgnoredDuringExecution 策略,如果没有责135就是最优的节点。
- 为节点136添加标签:a-demo-web.site/host-type=physical
此时节点136相比其他,拥有100的权重,同时又满足requiredDuringSchedulingIgnoredDuringExecution设置的策略,最符合调度,所以Pod被部署至该节点上。
- 删除该Pod,并为节点137添加标签:a-demo-web.site/host-type=kvm
- 重新部署nodeaffinity-preferred.yaml,观察此时会被调度至哪个节点
此时节点137拥有权重80,而136拥有权重100,仍然是136是最佳被调度节点。
- 删除该Pod,为节点135添加标签:a-demo-web.site/host-type=kvm
- 重新部署nodeaffinity-preferred.yaml,观察此时会被调度至哪个节点
此时135的权重为80+30=110,136节点的权重为100,137节点的权重为80,所以被调度部署至135节点。
7 - 最佳实践的总结
- 所有涉及的标签应该包含一个前缀,例如:a-demo-web.site/host-type;
- 所有涉及的标签应该来自一个权威数据中心,而非各集群管理员私下随意定义;
- 一般情况下不使用nodeName、nodeSelector进行Pod调度,一律通过nodeAffinity代替;
- 保障nodeAffinity中规则的简单,尽量使用“软限制"方式;