第10章 Kubernetes集群管理
10.1 Node的管理
10.1.1 Node的隔离与恢复
10.1.2 Node的扩容
10.2 更新资源对象的Label
10.3 Namespace:集群环境共享与隔离
10.3.1 创建Namespace
10.3.2 定义Context(运行环境)
10.3.3 设置工作组在特定Context环境下工作
10.4 Kubernetes资源管理
10.4.1 计算资源管理
10.4.2 资源配置范围管理(LimitRange)
10.4.3 资源服务质量管理(Resource QoS)
10.4.4 资源配额管理(Resource Quotas)
10.4.5 ResourceQuota和LimitRange实践
10.4.6 资源管理总结
10.5 资源紧缺时的Pod驱逐机制
10.5.1 驱逐策略
10.5.2 驱逐信号
10.5.3 驱逐阈值
10.5.4 驱逐监控频率
10.5.5 节点的状况
10.5.6 节点状况的抖动
10.5.7 回收Node级别的资源
10.5.8 驱逐用户的Pod
10.5.9 资源最少回收量
10.5.10 节点资源紧缺情况下的系统行为
10.5.11 可调度的资源和驱逐策略实践
10.5.12 现阶段的问题
10.6 Pod Disruption Budget(主动驱逐保护)
10.7 Kubernetes集群的高可用部署方案
10.7.1 手工方式的高可用部署方案
10.7.2 使用kubeadm的高可用部署方案
10.8 Kubernetes集群监控
10.8.1 通过Metrics Server监控Pod和Node的CPU和内存资源使用数据
10.8.2 Prometheus+Grafana集群性能监控平台搭建
10.9 集群统一日志管理
10.9.1 系统部署架构
10.9.2 创建Elasticsearch RC和Service
10.9.3 在每个Node上启动Fluentd
10.9.4 运行Kibana
10.10 Kubernetes的审计机制
10.11 使用Web UI(Dashboard)管理集群
10.12 Helm:Kubernetes应用包管理工具
10.12.1 Helm概述
10.12.2 Helm的主要概念
10.12.3 安装Helm
10.12.4 Helm的常见用法
10.12.5 --set的格式和限制
10.12.6 更多的安装方法
10.12.7 helm upgrade和helm rollback:应用的更新或回滚
10.12.8 helm install/upgrade/rollback命令的常用参数
10.12.9 helm delete:删除一个Release
10.12.10 helm repo:仓库的使用
10.12.11 自定义Chart
10.12.12 对Chart目录结构和配置文件的说明
10.12.13 对Chart.yaml文件的说明
10.12.14 快速制作自定义的Chart
10.12.15 搭建私有Repository


10.1 Node的管理
10.1.1 Node的隔离与恢复
在硬件升级、硬件维护等情况下,我们需要将某些Node隔离,使其脱离Kubernetes集群的调度范围。
Kubernetes提供了一种机制,既可以将Node纳入调度范围,也可以将Node脱离调度范围。
创建配置文件unschedule_node.yaml,在spec部分指定unschedulable为true:
apiVersion: v1
kind: Node
metadata:
name: k8s-node-1
labels:
kubernetes.io/hostname: k8s-node-1
spec:
unschedulable: true
通过kubectl replace命令完成对Node状态的修改:
# kubectl replace -f unschedule_node.yaml
查看Node的状态,可以观察到在Node的状态中增加了一项SchedulingDisabled:
# kubectl get nodes
这样,对于后续创建的Pod,系统将不会再向该Node进行调度。
也可以不使用配置文件,直接使用kubectl patch命令完成:
# kubectl patch node k8s-node-1 -p '{"spec":{"unschedulable":true}}'
需要注意的是,将某个Node脱离调度范围时,在其上运行的Pod并不会自动停止,管理员需要手动停止在该Node上运行的Pod。
同样,如果需要将某个Node重新纳入集群调度范围,则将unschedulable设置为false,再次执行kubectl replace或kubectl patch命令就能恢复系统对该Node的调度。
另外,使用kubectl的子命令cordon和uncordon也可以实现将Node进行隔离调度和恢复调度操作。
例如,使用kubectl cordon <node_name>对某个Node进行隔离调度操作:
# kubectl cordon k8s-node-1
使用 kubectl uncordon <node_name>对某个Node进行恢复调度操作:
# kubectl uncordon k8s-node-1

10.1.2 Node的扩容
在实际生产系统中经常会出现服务器容量不足的情况,这时就需要购买新的服务器,然后将应用系统进行水平扩展来完成对系统的扩容。
在Kubernetes集群中,一个新Node的加入是非常简单的。
在新的Node上安装Docker、kubelet和kube-proxy服务,然后配置kubelet和kube-proxy的启动参数,将Master URL指定为当前Kubernetes集群Master的地址,最后启动这些服务。
通过kubelet默认的自动注册机制,新的Node将会自动加入现有的Kubernetes集群中,如图10.1所示。
Kubernetes Master在接受了新Node的注册之后,会自动将其纳入当前集群的调度范围,之后创建容器时,就可以对新的Node进行调度了。
通过这种机制,Kubernetes实现了集群中Node的扩容。

10.2 更新资源对象的Label
Label是用户可灵活定义的对象属性,对于正在运行的资源对象,我们随时可以通过kubectl label命令进行增加、修改、删除等操作。
例如,要给已创建的Pod“redis-master-bobr0”添加一个标签role=backend:
# kubectl label pod redis-master-bobr0 role=backend
查看该Pod的Label:
# kubectl get pods -Lrole
删除一个Label时,只需在命令行最后指定Label的key名并与一个减号相连即可:
# kubectl label pod redis-master-bobr0 role-
在修改一个Label的值时,需要加上--overwrite参数:
# kubectl label pod redis-master-bobr0 role=master --overwrite

10.3 Namespace:集群环境共享与隔离
在一个组织内部,不同的工作组可以在同一个Kubernetes集群中工作,Kubernetes通过命名空间和Context的设置对不同的工作组进行区分,
使得它们既可以共享同一个Kubernetes集群的服务,也能够互不干扰,如图10.2所示。
假设在我们的组织中有两个工作组:开发组和生产运维组。
开发组在Kubernetes集群中需要不断创建、修改、删除各种Pod、RC、Service等资源对象,以便实现敏捷开发。
生产运维组则需要使用严格的权限设置来确保生产系统中的Pod、RC、Service处于正常运行状态且不会被误操作。

10.3.1 创建Namespace
为了在Kubernetes集群中实现这两个分组,首先需要创建两个命名空间:
namespace-development.yaml
apiVersion: v1
kind: Namespace
metadata:
name: development
namespace-production.yaml
apiVersion: v1
kind: Namespace
metadata:
name: production
使用kubectl create命令完成命名空间的创建:
# kubectl create -f namespace-development.yaml
# kubectl create -f namespace-production.yaml
查看系统中的命名空间:
# kubectl get namespaces

10.3.2 定义Context(运行环境)
接下来,需要为这两个工作组分别定义一个 Context,即运行环境。这个运行环境将属于某个特定的命名空间。
通过kubectl config set-context命令定义Context,并将Context置于之前创建的命名空间中:
# kubectl config set-cluster kubernetes-cluster --server=https://192.168.1.128:8080
# kubectl config set-context ctx-dev --namespace=development --cluster=kubernetes-cluster --user=dev
# kubectl config set-context ctx-dev --namespace=production --cluster=kubernetes-cluster --user=prod
使用kubectl config view命令查看已定义的Context:
# kubectl config view
注意,通过kubectl config命令在${HOME}/.kube目录下生成了一个名为config的文件,文件的内容为以kubectl config view命令查看到的内容。
所以,也可以通过手工编辑该文件的方式来设置Context。

10.3.3 设置工作组在特定Context环境下工作
使用kubectl config use-context <context_name>命令设置当前运行环境。
下面的命令将把当前运行环境设置为ctx-dev:
# kubectl config use-context ctx-dev
运行这个命令后,当前的运行环境被设置为开发组所需的环境。之后的所有操作都将在名为development的命名空间中完成。
现在,以redis-slave RC为例创建两个Pod:
查看创建好的Pod:
# kubectl get pods
可以看到容器被正确创建并运行起来了。而且,由于当前运行环境是ctx-dev,所以不会影响生产运维组的工作。
切换到生产运维组的运行环境:
# kubectl config use-context ctx-prod
查看RC和Pod:
# kubectl get rc
# kubectl get pods
结果为空,说明看不到开发组创建的RC和Pod。
现在也为生产运维组创建两个redis-slave的Pod:
# kubectl create -f edis-slave-controller.yaml
查看创建好的Pod:
# kubectl get pods
可以看到容器被正确创建并运行起来了,并且当前运行环境是ctx-prod,也不会影响开发组的工作。
至此,我们为两个工作组分别设置了两个运行环境,设置好当前运行环境时,各工作组之间的工作将不会相互干扰,并且都能在同一个Kubernetes集群中同时工作。

10.4 Kubernetes资源管理
本节先讲解Pod的两个重要参数:CPU Request与Memory Request。在
大多数情况下,我们在定义Pod时并没有定义这两个参数,此时Kubernetes会认为该Pod所需的资源很少,并可以将其调度到任何可用的Node上。
这样一来,当集群中的计算资源不很充足时,如果集群中的Pod负载突然加大,就会使某个Node的资源严重不足。
为了避免系统挂掉,该Node会选择“清理”某些Pod来释放资源,此时每个Pod都可能成为牺牲品。
但有些Pod担负着更重要的职责,比其他Pod更重要,比如与数据存储相关的、与登录相关的、与查询余额相关的,
即使系统资源严重不足,也需要保障这些Pod的存活,Kubernetes中该保障机制的核心如下。
通过资源限额来确保不同的Pod只能占用指定的资源。
允许集群的资源被超额分配,以提高集群的资源利用率。
为Pod划分等级,确保不同等级的Pod有不同的服务质量(QoS),资源不足时,低等级的Pod会被清理,以确保高等级的Pod稳定运行。
Kubernetes集群里的节点提供的资源主要是计算资源,计算资源是可计量的能被申请、分配和使用的基础资源,这使之区别于API资源(API Resources,例如Pod和Services等)。
当前Kubernetes集群中的计算资源主要包括CPU、GPU及Memory,绝大多数常规应用是用不到GPU的,因此这里重点介绍CPU与Memory的资源管理问题。
CPU与Memory是被Pod使用的,因此在配置Pod时可以通过参数CPU Request及Memory Request为其中的每个容器指定所需使用的CPU与Memory量,
Kubernetes会根据Request的值去查找有足够资源的Node来调度此Pod,如果没有,则调度失败。
我们知道,一个程序所使用的CPU与Memory是一个动态的量,确切地说,是一个范围,跟它的负载密切相关:负载增加时,CPU和Memory的使用量也会增加。
因此最准确的说法是,某个进程的CPU使用量为0.1个CPU~1个CPU,内存占用则为500MB~1GB。
对应到Kubernetes的Pod容器上,就是下面这4个参数:
spec.container[].resources.requests.cpu;
spec.container[].resources.limits.cpu;
spec.container[].resources.requests.memory;
spec.container[].resources.limits.memory。
其中,limits对应资源量的上限,即最多允许使用这个上限的资源量。
由于CPU资源是可压缩的,进程无论如何也不可能突破上限,因此设置起来比较容易。
对于Memory这种不可压缩资源来说,它的Limit设置就是一个问题了,如果设置得小了,当进程在业务繁忙期试图请求超过Limit限制的Memory时,此进程就会被Kubernetes杀掉。
因此,Memory的Request与Limit的值需要结合进程的实际需求谨慎设置。
如果不设置CPU或Memory的Limit值,会怎样呢?
在这种情况下,该Pod的资源使用量有一个弹性范围,我们不用绞尽脑汁去思考这两个Limit的合理值,但问题也来了,考虑下面的例子:
Pod A的 Memory Request被设置为1GB,Node A当时空闲的Memory为1.2GB,符合Pod A的需求,因此Pod A被调度到Node A上。
运行3天后,Pod A的访问请求大增,内存需要增加到1.5GB,此时Node A的剩余内存只有200MB,
由于Pod A新增的内存已经超出系统资源,所以在这种情况下,Pod A就会被Kubernetes杀掉。
没有设置Limit的Pod,或者只设置了CPU Limit或者Memory Limit两者之一的Pod,表面看都是很有弹性的,
但实际上,相对于4个参数都被设置的Pod,是处于一种相对不稳定的状态的,它们与4个参数都没设置的Pod相比,只是稳定一点而已。
理解了这一点,就很容易理解Resource QoS问题了。
如果我们有成百上千个不同的Pod,那么先手动设置每个Pod的这4个参数,再检查并确保这些参数的设置,都是合理的。
比如不能出现内存超过2GB或者CPU占据2个核心的Pod。
最后还得手工检查不同租户(Namespace)下的Pod的资源使用量是否超过限额。
为此,Kubernetes提供了另外两个相关对象:LimitRange及ResourceQuota,前者解决request与limit参数的默认值和合法取值范围等问题,后者则解决约束租户的资源配额问题。
本章从计算资源管理(Compute Resources)、服务质量管理(QoS)、资源配额管理(LimitRange、ResourceQuota)等方面,
对Kubernetes集群内的资源管理进行详细说明,并结合实践操作、常见问题分析和一个完整的示例,力求对Kubernetes集群资源管理相关的运维工作提供指导。

10.4.1 计算资源管理
1.详解Requests和Limits参数
以CPU为例,图10.3显示了未设置Limits和设置了Requests、Limits的CPU使用率的区别。
尽管Requests和Limits只能被设置到容器上,但是设置Pod级别的Requests和Limits能大大提高管理Pod的便利性和灵活性,
因此在Kubernetes中提供了对Pod级别的Requests和Limits的配置。
对于CPU和内存而言,Pod的Requests或Limits是指该Pod中所有容器的Requests或Limits的总和
(对于Pod中没有设置Requests或Limits的容器,该项的值被当作0或者按照集群配置的默认值来计算)。
下面对CPU和内存这两种计算资源的特点进行说明。
1)CPU
CPU的Requests和Limits是通过CPU数(cpus)来度量的。
CPU的资源值是绝对值,而不是相对值,比如0.1CPU在单核或多核机器上是一样的,都严格等于0.1 CPU core。
2)Memory
内存的Requests和Limits计量单位是字节数。
使用整数或者定点整数加上国际单位制(International System of Units)来表示内存值。
国际单位制包括十进制的E、P、T、G、M、K、m,或二进制的Ei、Pi、Ti、Gi、Mi、Ki。
KiB与MiB是以二进制表示的字节单位,常见的KB与MB则是以十进制表示的字节单位,比如:
1 KB(KiloByte)= 1000 Bytes = 8000 Bits;
1 KiB(KibiByte)= 2^10 Bytes = 1024 Bytes = 8192 Bits。
因此,128974848、129e6、129M、123Mi的内存配置是一样的。
Kubernetes的计算资源单位是大小写敏感的,因为m可以表示千分之一单位(milli unit),而M可以表示十进制的1000,二者的含义不同;同理,小写的k不是一个合法的资源单位。
以某个Pod中的资源配置为例:
...
如上所示,该Pod包含两个容器,每个容器配置的Requests都是0.25CPU和64MiB(2^26 Bytes)内存,而配置的Limits都是0.5CPU和128MiB(2^27 Bytes)内存。
这个Pod的Requests和Limits等于Pod中所有容器对应配置的总和,所以Pod的Requests是0.5CPU和128MiB(227 Bytes)内存,Limits是1CPU和256MiB(228 Bytes)内存。

2.基于Requests和Limits的Pod调度机制
当一个Pod创建成功时,Kubernetes调度器(Scheduler)会为该Pod选择一个节点来执行。
对于每种计算资源(CPU和Memory)而言,每个节点都有一个能用于运行Pod的最大容量值。
调度器在调度时,首先要确保调度后该节点上所有Pod的CPU和内存的Requests总和,不超过该节点能提供给Pod使用的CPU和Memory的最大容量值。
例如,某个节点上的CPU资源充足,而内存为4GB,其中3GB可以运行Pod,而某Pod的Memory Requests为1GB、Limits为2GB,那么在这个节点上最多可以运行3个这样的Pod。
这里需要注意:可能某节点上的实际资源使用量非常低,但是已运行Pod配置的Requests值的总和非常高,
再加上需要调度的Pod的Requests值,会超过该节点提供给Pod的资源容量上限,这时Kubernetes仍然不会将Pod调度到该节点上。
如果Kubernetes将Pod调度到该节点上,之后该节点上运行的Pod又面临服务峰值等情况,就可能导致Pod资源短缺。
接着上面的例子,假设该节点已经启动3个Pod实例,而这3个Pod的实际内存使用都不足500MB,那么理论上该节点的可用内存应该大于 1.5GB。
但是由于该节点的Pod Requests总和已经达到节点的可用内存上限,因此Kubernetes不会再将任何Pod实例调度到该节点上。

3.Requests和Limits的背后机制
kubelet在启动Pod的某个容器时,会将容器的Requests和Limits值转化为相应的容器启动参数传递给容器执行器(Docker或者rkt)。
如果容器的执行环境是Docker,那么容器的如下4个参数是这样传递给Docker的。
1)spec.container[].resources.requests.cpu
这个参数会转化为core数(比如配置的100m会转化为0.1),然后乘以1024,再将这个结果作为--cpu-shares参数的值传递给docker run命令。
在docker run命令中,--cpu-share参数是一个相对权重值(Relative Weight),这个相对权重值会决定Docker在资源竞争时分配给容器的资源比例。
举例说明--cpu-shares参数在Docker中的含义:比如将两个容器的CPU Requests分别设置为1和2,
那么容器在docker run启动时对应的--cpu-shares参数值分别为1024和2048,在主机CPU资源产生竞争时,Docker会尝试按照1∶2的配比将CPU资源分配给这两个容器使用。
这里需要区分清楚的是:这个参数对于Kubernetes而言是绝对值,主要用于Kubernetes调度和管理;同时Kubernetes会将这个参数的值传递给docker run的--cpu-shares参数。
--cpu-shares参数对于Docker而言是相对值,主要用于资源分配比例。

2)spec.container[].resources.limits.cpu
这个参数会转化为millicore数(比如配置的1被转化为1000,而配置的100m被转化为100),将此值乘以100000,再除以1000,然后将结果值作为--cpu-quota参数的值传递给docker run命令。
docker run命令中另外一个参数--cpu-period默认被设置为100000,表示Docker重新计量和分配CPU的使用时间间隔为100000μs(100ms)。
Docker的--cpu-quota参数和--cpu-period参数一起配合完成对容器CPU的使用限制:
比如Kubernetes中配置容器的CPU Limits为0.1,那么计算后--cpu-quota为10000,而--cpu-period为100000,
这意味着Docker在100ms内最多给该容器分配10ms×core的计算资源用量,10/100=0.1 core的结果与Kubernetes配置的意义是一致的。
注意:如果kubelet的启动参数--cpu-cfs-quota被设置为true,那么kubelet会强制要求所有Pod都必须配置CPU Limits(如果Pod没有配置,则集群提供了默认配置也可以)。
从Kubernetes 1.2版本开始,这个--cpu-cfs-quota启动参数的默认值就是true。

3)spec.container[].resources.requests.memory
这个参数值只提供给Kubernetes调度器作为调度和管理的依据,不会作为任何参数传递给Docker。

4)spec.container[].resources.limits.memory
这个参数值会转化为单位为Bytes的整数,数值会作为--memory参数传递给docker run命令。
如果一个容器在运行过程中使用了超出了其内存Limits配置的内存限制值,那么它可能会被杀掉,如果这个容器是一个可重启的容器,那么之后它会被kubelet重新启动。
因此对容器的Limits配置需要进行准确测试和评估。
与内存Limits不同的是,CPU在容器技术中属于可压缩资源,因此对CPU的Limits配置一般不会因为偶然超标使用而导致容器被系统杀掉。

4.计算资源使用情况监控
Pod的资源用量会作为Pod的状态信息一同上报给Master。
如果在集群中配置了Heapster来监控集群的性能数据,那么还可以从Heapster中查看Pod的资源用量信息。

5.计算资源相关常见问题分析
(1)Pod状态为Pending,错误信息为FailedScheduling。
如果Kubernetes调度器在集群中找不到合适的节点来运行Pod,那么这个Pod会一直处于未调度状态,直到调度器找到合适的节点为止。
每次调度器尝试调度失败时,Kubernetes都会产生一个事件,我们可以通过下面这种方式来查看事件的信息:
# kubectl describe pod frontend | grep -A 3 Events
在上面这个例子中,名为frontend的Pod由于节点的CPU资源不足而调度失败(Pod ExceedsFreeCPU),同样,如果内存不足,则也可能导致调度失败(PodExceedsFreeMemory)。
如果一个或者多个Pod调度失败且有这类错误,那么可以尝试以下几种解决方法:
添加更多的节点到集群中。
停止一些不必要的运行中的Pod,释放资源。
检查Pod的配置,错误的配置可能导致该Pod永远无法被调度执行。比如整个集群中所有节点都只有1 CPU,而Pod配置的CPU Requests为2,该Pod就不会被调度执行。
我们可以使用kubectl describe nodes命令来查看集群中节点的计算资源容量和已使用量:
# kubectl describe nodes k8s-node-1
超过可用资源容量上限(Capacity)和已分配资源量(Allocated resources)差额的Pod无法运行在该Node上。
在这个例子中,如果一个Pod的Requests超过90 millicpus或者超过1341MiB内存,就无法运行在这个节点上。
在10.4.4节中,我们还可以配置针对一组Pod的Requests和Limits总量的限制,这种限制可以作用于命名空间,通过这种方式可以防止一个命名空间下的用户将所有资源据为己有。

(2)容器被强行终止(Terminated)。
如果容器使用的资源超过了它配置的Limits,那么该容器可能会被强制终止。
我们可以通过kubectl describe pod命令来确认容器是否因为这个原因被终止:
# kubectl describe pod simmemleak-xxx
Restart Count: 5说明这个名为simmemleak的容器被强制终止并重启了5次。
我们可以在使用kubectl get pod命令时添加-o go-template=...格式参数来读取已终止容器之前的状态信息:
# kubectl get pod -o
可以看到这个容器因为reason:OOM Killed而被强制终止,说明这个容器的内存超过了限制(Out of Memory)。

6.对大内存页(Huge Page)资源的支持
在计算机发展的早期阶段,程序员是直接对内存物理地址编程的,并且需要自己管理内存,
很容易由于内存地址错误导致操作系统崩溃,而且出现了一些恶意程序对操作系统进行破坏。
后来人们将硬件和软件(操作系统)相结合,推出了虚拟地址的概念,同时推出了内存页的概念,以及CPU的逻辑内存地址与物理内存(条)地址的映射关系。
在现代操作系统中,内存是以Page(页,有时也可以称之为Block)为单位进行管理的,而不以字节为单位,包括内存的分配和回收都基于Page。
典型的Page大小为4KB,因此用户进程申请1MB内存就需要操作系统分配256个Page,而1GB内存对应26万多个Page!
为了实现快速内存寻址,CPU内部以硬件方式实现了一个高性能的内存地址映射的缓存表——TLB(Translation Lookaside Buffer),用来保存逻辑内存地址与物理内存的对应关系。
若目标地址的内存页物理地址不在TLB的缓存中或者TLB中的缓存记录失效,CPU就需要切换到低速的、以软件方式实现的内存地址映射表进行内存寻址,这将大大降低CPU的运算速度。
针对缓存条目有限的TLB缓存表,提高TLB效率的最佳办法就是将内存页增大,这样一来,一个进程所需的内存页数量会相应减少很多。
如果把内存页从默认的4KB改为2MB,那么1GB内存就只对应512个内存页了,TLB的缓存命中率会大大增加。
这是不是意味着我们可以任意指定内存页的大小,比如1314MB的内存页?
答案是否定的,因为这是由CPU来决定的,比如常见的Intel X86处理器可以支持的大内存页通常是2MB,个别型号的高端处理器则支持1GB的大内存页。
在Linux平台下,对于那些需要大量内存(1GB以上内存)的程序来说,大内存页的优势是很明显的,
因为Huge Page大大提升了TLB的缓存命中率,又因为Linux对Huge Page提供了更为简单、便捷的操作接口,所以可以把它当作文件来进行读写操作。
Linux使用Huge Page文件系统hugetlbfs支持巨页,这种方式更为灵活,我们可以设置Huge Page的大小,比如1GB、2GB甚至2.5GB,
然后设置有多少物理内存用于分配Huge Page,这样就设置了一些预先分配好的Huge Page。
可以将hugetlbfs文件系统挂载在/mnt/huge目录下,通过执行下面的指令完成设置:
# mkdir /mnt/huge
# mount -t hugetlbfs nodev /mnt/huge
在设置完成后,用户进程就可以使用mmap映射Huge Page目标文件来使用大内存页了,
Intel DPDK便采用了这种做法,测试表明应用使用大内存页比使用4KB的内存页性能提高了10%~15%。
Kubernetes 1.14版本对Linux Huge Page的支持正式更新为GA稳定版。
我们可以将Huge Page理解为一种特殊的计算资源:拥有大内存页的资源。
而拥有Huge Page资源的Node也与拥有GPU资源的Node一样,属于一种新的可调度资源节点(Schedulable Resource Node)。
Huge Page也支持ResourceQuota来实现配额限制,类似CPU或者Memory,
但不同于CPU或者内存,Huge Page资源属于不可超限使用的资源,拥有Huge Page能力的Node会将自身支持的Huge Page的能力信息自动上报给Kubernetes Master。
为此,Kubernetes引入了一个新的资源类型hugepages-<size>,来表示大内存页这种特殊的资源,比如hugepages-2Mi表示2MiB规格的大内存页资源。
一个能提供2MiB规格Huge Page的Node,会上报自己拥有Hugepages-2Mi的大内存页资源属性,供需要这种规格的大内存页资源的Pod使用,
而需要Huge Page资源的Pod只要给出相关的Huge Page的声明,就可以被正确调度到匹配的目标Node上了。相关例子如下:
...
在上面的定义中有以下几个关键点:
Huge Page需要被映射到Pod的文件系统中;
Huge Page申请的request与limit必须相同,即申请固定大小的Huge Page,不能是可变的;
在目前的版本中,Huge Page属于Pod级别的资源,未来计划成为Container级别的资源,即实现更细粒度的资源管理;
存储卷emptyDir(挂载到容器内的/hugepages目录)的后台是由Huge Page支持的,因此应用不能使用超过request声明的内存大小。
在Kubernetes未来的版本中,计划继续实现下面的一些高级特性:
支持容器级别的Huge Page的隔离能力;
支持NUMA亲和能力以提升服务的质量;
支持LimitRange配置Huge Page资源限额。

10.4.2 资源配置范围管理(LimitRange)
在默认情况下,Kubernetes不会对Pod加上CPU和内存限制,这意味着Kubernetes系统中任何Pod都可以使用其所在节点的所有可用的CPU和内存。
通过配置Pod的计算资源Requests和Limits,我们可以限制Pod的资源使用,但对于Kubernetes集群管理员而言,配置每一个Pod的Requests和Limits是烦琐的,而且很受限制。
更多时候,我们需要对集群内Requests和Limits的配置做一个全局限制。
常见的配置场景如下:
集群中的每个节点都有2GB内存,集群管理员不希望任何Pod申请超过2GB的内存:因为在整个集群中都没有任何节点能满足超过2GB内存的请求。
如果某个Pod的内存配置超过2GB,那么该Pod将永远都无法被调度到任何节点上执行。
为了防止这种情况的发生,集群管理员希望能在系统管理功能中设置禁止Pod申请超过2GB内存。
集群由同一个组织中的两个团队共享,分别运行生产环境和开发环境。
生产环境最多可以使用8GB内存,而开发环境最多可以使用512MB内存。
集群管理员希望通过为这两个环境创建不同的命名空间,并为每个命名空间设置不同的限制来满足这个需求。
用户创建Pod时使用的资源可能会刚好比整个机器资源的上限稍小,而恰好剩下的资源大小非常尴尬:不足以运行其他任务但整个集群加起来又非常浪费。
因此,集群管理员希望设置每个Pod都必须至少使用集群平均资源值(CPU和内存)的20%,这样集群能够提供更好的资源一致性的调度,从而减少了资源浪费。
针对这些需求,Kubernetes提供了LimitRange机制对Pod和容器的Requests和Limits配置进一步做出限制。
在下面的示例中,将说明如何将LimitsRange应用到一个Kubernetes的命名空间中,
然后说明LimitRange的几种限制方式,比如最大及最小范围、Requests和Limits的默认值、Limits与Requests的最大比例上限等。
下面通过LimitRange的设置和应用对其进行说明。
1.创建一个Namespace
创建一个名为limit-example的Namespace:
# kubectl create namespace limit-example
2.为Namespace设置LimitRange
为Namespace“limit-example”创建一个简单的LimitRange。
创建limits.yaml配置文件,内容如下:
apiVersion: v1
kind: LimitRange
metadata:
name: mylimits
spec:
limits:
- max:
cpu: "4"
memory: 2Gi
min:
cpu: 200m
memory: 6Mi
maxLimitRequestRatio:
cpu: 3
memory: 2
type: Pod
- default:
cpu: 300m
memory: 200Mi
defaultRequest:
cpu: 200m
memory: 100Mi
min:
cpu: "2"
memory: 1Gi
min:
cpu: 100m
memory: 3Mi
maxLimitRequestRatio:
cpu: 5
memory: 4
type: Container
创建该LimitRange:
# kubectl create -f limits.yaml --namespace=limit-example
查看namespace limit-example中的LimitRange:
# kubectl describe limits mylimits --namespace=limit-example
下面解释LimitRange中各项配置的意义和特点:
(1)不论是CPU还是内存,在LimitRange中,Pod和Container都可以设置Min、Max和Max Limit/Requests Ratio参数。
Container还可以设置Default Request和Default Limit参数,而Pod不能设置Default Request和Default Limit参数。
(2)对Pod和Container的参数解释如下。
Container的Min(上面的100m和3Mi)是Pod中所有容器的Requests值下限;
Container的Max(上面的2和1Gi)是Pod中所有容器的Limits值上限;
Container的Default Request(上面的200m和100Mi)是Pod中所有未指定Requests值的容器的默认Requests值;
Container的Default Limit(上面的300m和200Mi)是Pod中所有未指定Limits值的容器的默认Limits值。
对于同一资源类型,这4个参数必须满足以下关系:Min ≤ Default Request ≤ Default Limit ≤ Max。
Pod的Min(上面的200m和6Mi)是Pod中所有容器的Requests值的总和下限;
Pod的Max(上面的4和2Gi)是Pod中所有容器的Limits值的总和上限。
当容器未指定Requests值或者Limits值时,将使用Container的Default Request值或者Default Limit值。
Container的Max Limit/Requests Ratio(上面的5和4)限制了Pod中所有容器的Limits值与Requests值的比例上限;
而Pod的Max Limit/Requests Ratio(上面的3和2)限制了Pod中所有容器的Limits值总和与Requests值总和的比例上限。
(3)如果设置了Container的Max,那么对于该类资源而言,整个集群中的所有容器都必须设置Limits,否则无法成功创建。
Pod内的容器未配置Limits时,将使用Default Limit的值(本例中的300m CPU和200MiB内存),如果也未配置Default,则无法成功创建。
(4)如果设置了Container的Min,那么对于该类资源而言,整个集群中的所有容器都必须设置Requests。
如果创建Pod的容器时未配置该类资源的Requests,那么在创建过程中会报验证错误。
Pod里容器的Requests在未配置时,可以使用默认值defaultRequest(本例中的200m CPU和100MiB内存);
如果未配置而又没有使用默认值defaultRequest,那么会默认等于该容器的Limits;如果此时Limits也未定义,就会报错。
(5)对于任意一个Pod而言,该Pod中所有容器的Requests总和必须大于或等于6MiB,而且所有容器的Limits总和必须小于或等于1GiB;
同样,所有容器的CPU Requests总和必须大于或等于200m,而且所有容器的CPU Limits总和必须小于或等于2。
(6)Pod里任何容器的Limits与Requests的比例都不能超过Container的Max Limit/Requests Ratio;
Pod里所有容器的Limits总和与Requests的总和的比例不能超过Pod的Max Limit/Requests Ratio。

3.创建Pod时触发LimitRange限制
最后,让我们看看LimitRange生效时对容器的资源限制效果。
命名空间中LimitRange只会在Pod创建或者更新时执行检查。
如果手动修改LimitRange为一个新的值,那么这个新的值不会去检查或限制之前已经在该命名空间中创建好的Pod。
如果在创建Pod时配置的资源值(CPU或者内存)超过了LimitRange的限制,那么该创建过程会报错,在错误信息中会说明详细的错误原因。
下面通过创建一个单容器Pod来展示默认限制是如何被配置到Pod上的:
# kubectl run nginx --image=nginx --replicas=1 --namespace=limit-example
查看已创建的Pod:
# kubectl get pods --namespace=limit-example
查看该Pod的resources相关信息:
# kubectl get pods nginx-xxx --namespace=limit-example -o yaml | grep resources -C 8 resources
由于该Pod未配置资源Requests和Limits,所以使用了namespace limit-example中的默认CPU和内存定义的Requests和Limits值。
下面创建一个超出资源限制的Pod(invalid-pod.yaml 使用3 CPU):
resource:
limits:
cpu: "3"
memory: 100Mi
创建该Pod,可以看到系统报错了,并且提供的错误原因为超过资源限制:
# kubectl create -f invalid-pod.yaml --namespace=limit-example

接下来的例子(limit-test-nginx.yaml)展示了LimitRange对maxLimitRequestRatio的限制过程:
resource:
limits:
cpu: "1"
memory: 512Mi
requests:
cpu: "0.8"
memory: 250Mi
由于limit-test-nginx这个Pod的全部内存Limits总和与Requests总和的比例为512∶250,大于在LimitRange中定义的Pod的最大比率2(maxLimitRequestRatio.memory=2),因此创建失败:
# kubectl create -f limit-test-nginx.yaml --namespace=limit-example
下面的例子(valid-pod.yaml)为满足LimitRange限制的Pod:
resource:
limits:
cpu: "1"
memory: 512Mi
创建Pod将会成功:
# kubectl create -f valid-pod.yaml --namespace=limit-example
查看该Pod的资源信息:
kubectl get pods nginx-pod --namespace=limit-example -o yaml | grep resources -C 6 resources
可以看到该Pod配置了明确的Limits和Requests,因此该Pod不会使用在namespace limit-example中定义的default和defaultRequest。
需要注意的是,CPU Limits强制配置这个选项在Kubernetes集群中默认是开启的;除非集群管理员在部署kubelet时,通过设置参数--cpu-cfs-quota=false来关闭该限制:
# kubelet --help
# kubelet --cpu-cfs-quota=false
如果集群管理员希望对整个集群中容器或者Pod配置的Requests和Limits做限制,那么可以通过配置Kubernetes命名空间中的LimitRange来达到该目的。
在Kubernetes集群中,如果Pod没有显式定义Limits和Requests,那么Kubernetes系统会将该Pod所在的命名空间中定义的LimitRange的default和defaultRequests配置到该Pod上。

10.4.3 资源服务质量管理(Resource QoS)
本节对Kubernetes如何根据Pod的Requests和Limits配置来实现针对Pod的不同级别的资源服务质量控制(QoS)进行说明。
在Kubernetes的资源QoS体系中,需要保证高可靠性的Pod可以申请可靠资源,而一些不需要高可靠性的Pod可以申请可靠性较低或者不可靠的资源。
在10.4.1节中讲到了容器的资源配置分为Requests和Limits,
其中Requests是Kubernetes调度时能为容器提供的完全可保障的资源量(最低保障),而Limits是系统允许容器运行时可能使用的资源量的上限(最高上限)。
Pod级别的资源配置是通过计算Pod内所有容器的资源配置的总和得出来的。
Kubernetes中Pod的Requests和Limits资源配置有如下特点:
(1)如果Pod配置的Requests值等于Limits值,那么该Pod可以获得的资源是完全可靠的。
(2)如果Pod的Requests值小于Limits值,那么该Pod获得的资源可分成两部分:
完全可靠的资源,资源量的大小等于Requests值;
不可靠的资源,资源量最大等于Limits与Requests的差额,这份不可靠的资源能够申请到多少,取决于当时主机上容器可用资源的余量。
通过这种机制,Kubernetes可以实现节点资源的超售(Over Subscription),
比如在CPU完全充足的情况下,某机器共有32GiB内存可提供给容器使用,容器配置为Requests值1GiB,Limits值为2GiB,
那么在该机器上最多可以同时运行32个容器,每个容器最多可以使用2GiB内存,如果这些容器的内存使用峰值能错开,那么所有容器都可以正常运行。
超售机制能有效提高资源的利用率,同时不会影响容器申请的完全可靠资源的可靠性。

1.Requests和Limits对不同计算资源类型的限制机制
根据前面的内容可知,容器的资源配置满足以下两个条件:
Requests<=节点可用资源;
Requests<=Limits。
Kubernetes根据Pod配置的Requests值来调度Pod,Pod在成功调度之后会得到Requests值定义的资源来运行;
而如果Pod所在机器上的资源有空余,则Pod可以申请更多的资源,最多不能超过Limits的值。
下面看一下Requests和Limits针对不同计算资源类型的限制机制的差异。
这种差异主要取决于计算资源类型是可压缩资源还是不可压缩资源。
1)可压缩资源
Kubernetes目前支持的可压缩资源是CPU。
Pod可以得到Pod的Requests配置的CPU使用量,而能否使用超过Requests值的部分取决于系统的负载和调度。
不过由于目前Kubernetes和Docker的CPU隔离机制都是在容器级别隔离的,所以Pod级别的资源配置并不能完全得到保障;
Pod级别的cgroups正在紧锣密鼓地开发中,如果将来引入,就可以确保Pod级别的资源配置准确运行。
空闲CPU资源按照容器Requests值的比例分配。
举例说明:容器A的CPU配置为Requests 1 Limits 10,容器B的CPU配置为Request 2 Limits 8,A和B同时运行在一个节点上,初始状态下容器的可用CPU为3cores,
那么A和B恰好得到在它们的Requests中定义的CPU用量,即1CPU和2CPU。
如果A和B都需要更多的CPU资源,而恰好此时系统的其他任务释放出1.5CPU,那么这1.5CPU将按照A和B的Requests值的比例1∶2分配给A和B,即最终A可使用1.5CPU,B可使用3CPU。
如果Pod使用了超过在Limits 10中配置的CPU用量,那么cgroups会对Pod中的容器的CPU使用进行限流(Throttled);
如果Pod没有配置Limits 10,那么Pod会尝试抢占所有空闲的CPU资源(Kubernetes从1.2版本开始默认开启--cpu-cfs-quota,因此在默认情况下必须配置Limits)。
2)不可压缩资源
Kubernetes目前支持的不可压缩资源是内存。
Pod可以得到在Requests中配置的内存。
如果Pod使用的内存量小于它的Requests的配置,那么这个Pod可以正常运行(除非出现操作系统级别的内存不足等严重问题);
如果Pod使用的内存量超过了它的Requests的配置,那么这个Pod有可能被Kubernetes杀掉:
比如Pod A使用了超过Requests而不到Limits的内存量,此时同一机器上另外一个Pod B之前只使用了远少于自己的Requests值的内存,
此时程序压力增大,Pod B向系统申请的总量不超过自己的Requests值的内存,那么Kubernetes可能会直接杀掉Pod A;
另外一种情况是Pod A使用了超过Requests而不到Limits的内存量,此时Kubernetes将一个新的Pod调度到这台机器上,新的Pod需要使用内存,
而只有Pod A使用了超过了自己的Requests值的内存,那么Kubernetes也可能会杀掉Pod A来释放内存资源。
如果Pod使用的内存量超过了它的Limits设置,那么操作系统内核会杀掉Pod所有容器的所有进程中使用内存最多的一个,直到内存不超过Limits为止。

2.对调度策略的影响
Kubernetes的kubelet通过计算Pod中所有容器的Requests的总和来决定对Pod的调度。
不管是CPU还是内存,Kubernetes调度器和kubelet都会确保节点上所有Pod的Requests的总和不会超过在该节点上可分配给容器使用的资源容量上限。
3.服务质量等级(QoS Classes)
在一个超用(Over Committed,容器Limits总和大于系统容量上限)系统中,由于容器负载的波动可能导致操作系统的资源不足,最终可能导致部分容器被杀掉。
在这种情况下,我们当然会希望优先杀掉那些不太重要的容器,那么如何衡量重要程度呢?
Kubernetes将容器划分成3个QoS等级:Guaranteed(完全可靠的)、Burstable(弹性波动、较可靠的)和BestEffort(尽力而为、不太可靠的),这三种优先级依次递减,如图10.4所示。
从理论上来说,QoS级别应该作为一个单独的参数来提供API,并由用户对Pod进行配置,这种配置应该与Requests和Limits无关。
但在当前版本的Kubernetes的设计中,为了简化模式及避免引入太多的复杂性,QoS级别直接由Requests和Limits来定义。
在Kubernetes中容器的QoS级别等于容器所在Pod的QoS级别,而Kubernetes的资源配置定义了Pod的三种QoS级别,如下所述。
1)Guaranteed
如果Pod中的所有容器对所有资源类型都定义了Limits和Requests,并且所有容器的Limits值都和Requests值全部相等(且都不为0),那么该Pod的QoS级别就是Guaranteed。
注意:在这种情况下,容器可以不定义Requests,因为Requests值在未定义时默认等于Limits。
在下面这两个例子中定义的Pod QoS级别就是Guaranteed:
resource:
limits:
cpu: "1"
memory: 512Mi
在上面的例子中未定义Requests值,所以其默认等于Limits值。
而在下面这个例子中定义的Requests和Limits的值完全相同:
resource:
limits:
cpu: "1"
memory: 512Mi
requests:
cpu: "1"
memory: 512Mi

2)BestEffort
如果Pod中所有容器都未定义资源配置(Requests和Limits都未定义),那么该Pod的QoS级别就是BestEffort。
例如下面这个Pod定义:
resource:

3)Burstable
当一个Pod既不为Guaranteed级别,也不为BestEffort级别时,该Pod的QoS级别就是Burstable。Burstable级别的Pod包括两种情况。
第1种情况:Pod中的一部分容器在一种或多种资源类型的资源配置中定义了Requests值和Limits值(都不为0),且Requests值小于Limits值;
第2种情况:Pod中的一部分容器未定义资源配置(Requests和Limits都未定义)。
注意:在容器未定义Limits时,Limits值默认等于节点资源容量的上限。
下面几个例子中的Pod的QoS等级都是Burstable。
(1)容器foo的CPU Requests不等于Limits:
(2)容器bar未定义资源配置而容器foo定义了资源配置:
(3)容器foo未定义CPU,而容器bar未定义内存:
(4)容器bar未定义资源配置,而容器foo未定义Limits值:

4)Kubernetes QoS的工作特点
Pod的CPU Requests无法得到满足(比如节点的系统级任务占用过多的CPU导致无法分配足够的CPU给容器使用)时,容器得到的CPU会被压缩限流。
由于内存是不可压缩的资源,所以针对内存资源紧缺的情况,会按照以下逻辑进行处理。
(1)BestEffort Pod的优先级最低,在这类Pod中运行的进程会在系统内存紧缺时被第一优先杀掉。
当然,从另外一个角度来看,BestEffort Pod由于没有设置资源Limits,所以在资源充足时,它们可以充分使用所有的闲置资源。
(2)Burstable Pod的优先级居中,这类Pod初始时会分配较少的可靠资源,但可以按需申请更多的资源。
当然,如果整个系统内存紧缺,又没有BestEffort容器可以被杀掉以释放资源,那么这类Pod中的进程可能会被杀掉。
(3)Guaranteed Pod的优先级最高,而且一般情况下这类Pod只要不超过其资源Limits的限制就不会被杀掉。
当然,如果整个系统内存紧缺,又没有其他更低优先级的容器可以被杀掉以释放资源,那么这类Pod中的进程也可能会被杀掉。

5)OOM计分系统
OOM(Out Of Memory)计分规则包括如下内容。
OOM计分的计算方法为:计算进程使用内存在系统中占的百分比,取其中不含百分号的数值,再乘以10的结果,这个结果是进程OOM的基础分;
将进程OOM基础分的分值再加上这个进程的OOM分数调整值OOM_SCORE_ADJ的值,作为进程OOM的最终分值(除root启动的进程外)。
在系统发生OOM时,OOM Killer会优先杀掉OOM计分更高的进程。
进程的OOM计分的基本分数值范围是0~1000,如果A进程的调整值OOM_SCORE_ADJ减去B进程的调整值的结果大于1000,那么A进程的OOM计分最终值必然大于B进程,会优先杀掉A进程。
不论调整OOM_SCORE_ADJ值为多少,任何进程的最终分值范围也是0~1000。
在Kubernetes,不同QoS的OOM计分调整值规则如表10.1所示。
QoS等级 OOM_SCORE_ADJ
Guaranteed -998
BestEffort 1000
Burstable min(max(2,1000-(1000*memoryRequestBytes)/machineMemoryCapacityBytes),999)
其中:
BestEffort Pod设置OOM_SCORE_ADJ调整值为1000,因此BestEffort Pod中容器里所有进程的OOM最终分肯定是1000。
Guaranteed Pod设置OOM_SCORE_ADJ调整值为-998,因此Guaranteed Pod中容器里所有进程的OOM最终分一般是0或者1(因为基础分不可能是1000)。
Burstable Pod规则分情况说明:
如果Burstable Pod的内存Requests超过了系统可用内存的99.8%,那么这个Pod的OOM_SCORE_ADJ调整值固定为2;
否则,设置OOM_SCORE_ADJ调整值为1000-10×(% of memory requested);
如果内存Requests为0,那么OOM_SCORE_ADJ调整值固定为999。
这样的规则能确保OOM_SCORE_ADJ调整值的范围为2~999,而Burstable Pod中所有进程的OOM最终分数范围为2~1000。
Burstable Pod进程的OOM最终分数始终大于Guaranteed Pod的进程得分,因此它们会被优先杀掉。
如果一个Burstable Pod使用的内存比它的内存Requests少,那么可以肯定的是它的所有进程的OOM最终分数会小于1000,此时能确保它的优先级高于BestEffort Pod。
如果在一个Burstable Pod的某个容器中某个进程使用的内存比容器的Requests值高,那么这个进程的OOM最终分数会是1000,否则它的OOM最终分会小于1000。
假设在下面的容器中有一个占用内存非常大的进程,那么当一个使用内存超过其Requests的Burstable Pod与另外一个使用内存少于其Requests的Burstable Pod发生内存竞争冲突时,前者的进程会被系统杀掉。
如果在一个Burstable Pod内部有多个进程的多个容器发生内存竞争冲突,那么此时OOM评分只能作为参考,不能保证完全按照资源配置的定义来执行OOM Kill。
OOM还有一些特殊的计分规则,如下所述。
kubelet进程和Docker进程的调整值OOM_SCORE_ADJ为-998。
如果配置进程调整值OOM_SCORE_ADJ为-999,那么这类进程不会被OOM Killer杀掉。
6)QoS的演进
目前Kubernetes基于QoS的超用机制日趋完善,但还有一些问题需要解决。
(1)内存Swap的支持。当前的QoS策略都是假定主机不启用内存Swap。如果主机启用了Swap,那么上面的QoS策略可能会失效。
举例说明:两个Guaranteed Pod都刚好达到了内存Limits,那么由于内存Swap机制,它们还可以继续申请使用更多的内存。
如果Swap空间不足,最终这两个Pod中的进程就可能会被杀掉。由于Kubernetes和Docker尚不支持内存Swap空间的隔离机制,所以这一功能暂时还未实现。
(2)更丰富的QoS策略。
当前的QoS策略都是基于Pod的资源配置(Requests和Limits)来定义的,而资源配置本身又承担着对Pod资源管理和限制的功能。
两种不同维度的功能使用同一个参数来配置,可能会导致某些复杂需求无法满足,比如当前Kubernetes无法支持弹性的、高优先级的Pod。
自定义QoS优先级能提供更大的灵活性,完美地实现各类需求,但同时会引入更高的复杂性,而且过于灵活的设置会给予用户过高的权限,对系统管理也提出了更大的挑战。

10.4.4 资源配额管理(Resource Quotas)
如果一个Kubernetes集群被多个用户或者多个团队共享,就需要考虑资源公平使用的问题,因为某个用户可能会使用超过基于公平原则分配给其的资源量。
Resource Quotas就是解决这个问题的工具。
通过Resource Quota对象,我们可以定义资源配额,这个资源配额可以为每个命名空间都提供一个总体的资源使用的限制:
它可以限制命名空间中某种类型的对象的总数目上限,也可以设置命名空间中Pod可以使用的计算资源的总上限。
典型的资源配额使用方式如下:
不同的团队工作在不同的命名空间下,目前这是非约束性的,在未来的版本中可能会通过ACL(Access Control List,访问控制列表)来实现强制性约束。
集群管理员为集群中的每个命名空间都创建一个或者多个资源配额项。
当用户在命名空间中使用资源(创建Pod或者Service等)时,Kubernetes的配额系统会统计、监控和检查资源用量,以确保使用的资源用量没有超过资源配额的配置。
如果在创建或者更新应用时资源使用超过了某项资源配额的限制,那么创建或者更新的请求会报错(HTTP 403 Forbidden),并给出详细的出错原因说明。
如果命名空间中的计算资源(CPU和内存)的资源配额启用,那么用户必须为相应的资源类型设置Requests或Limits;否则配额系统可能会直接拒绝Pod的创建。
这里可以使用LimitRange机制来为没有配置资源的Pod提供默认资源配置。
下面的例子展示了一个非常适合使用资源配额来做资源控制管理的场景。
集群共有32GB内存和16 CPU,两个小组。A小组使用20GB内存和10 CPU,B小组使用10GB内存和2 CPU,剩下的2GB内存和2 CPU作为预留。
在名为testing的命名空间中,限制使用1 CPU和1GB内存;在名为production的命名空间中,资源使用不受限制。
在使用资源配额时,需要注意以下两点:
如果集群中总的可用资源小于各命名空间中资源配额的总和,那么可能会导致资源竞争。资源竞争时,Kubernetes系统会遵循先到先得的原则。
不管是资源竞争还是配额的修改,都不会影响已经创建的资源使用对象。
1.在Master中开启资源配额选型
资源配额可以通过在kube-apiserver的--admission-control参数值中添加ResourceQuota参数进行开启。
如果在某个命名空间的定义中存在ResourceQuota,那么对于该命名空间而言,资源配额就是开启的。
一个命名空间可以有多个ResourceQuota配置项。
1)计算资源配额(Compute Resource Quota)
资源配额可以限制一个命名空间中所有Pod的计算资源的总和。
目前支持的计算资源类型如表10.2所示。
资源名称 说明
Cpu 所有非终止状态的Pod,CPU Request的总和不能超过该值
requests.cpu 所有非终止状态的Pod,CPU Request的总和不能超过该值
Memory 所有非终止状态的Pod,内存Request的总和不能超过该值
requests.memory 所有非终止状态的Pod,内存Request的总和不能超过该值
limits.cpu 所有非终止状态的Pod,CPU Limits的总和不能超过该值
limits.memory 所有非终止状态的Pod,内存Limits的总和不能超过该值

2)存储资源配额(Volume Count Quota)
可以在给定的命名空间中限制所使用的存储资源(Storage Resources)的总量,目前支持的存储资源名称如表10.3所示。
资源名称 说明
requests.storage 所有PVC,存储请求总量不能超过此值
Persistentvolumeclaims 在该命名空间中存在的持久卷的总数上限
.storageclass.storage.k8s.io/requests.storage 和该存储类关联的所有PVC,存储请求总量不能超过此值
.storageclass.storage.k8s.io/persistentvolumeclaims 和该存储类关联的所有PVC的总数

3)对象数量配额(Object Count Quota)
指定类型的对象数量可以被限制。表10.4列出了ResourceQuota支持限制的对象类型。
资源名称 说明
Configmaps 在该命名空间中能存在的Configmap的总数上限
Pods 在该命名空间中能存在的非终止状态Pod的总数上限。Pod终止状态等价于Pod的status.phase in (Failed,Succeeded) = true
Replicationcontrollers 在该命名空间中能存在的RC的总数上限
Resourcequotas 在该命名空间中能存在的资源配额项的总数上限
Services 在该命名空间中能存在的Service的总数上限
services.loadbalancers 在该命名空间中能存在的负载均衡的总数上限
services.nodeports 在该命名空间中能存在的NodePort的总数上限
Secrets 在该命名空间中能存在的Secret的总数上限
例如,我们可以通过资源配额来限制在命名空间中能创建的Pod的最大数量。
这种设置可以防止某些用户大量创建Pod而迅速耗尽整个集群的Pod IP和计算资源。

2.配额的作用域(Quota Scopes)
每项资源配额都可以单独配置一组作用域,配置了作用域的资源配额只会对符合其作用域的资源使用情况进行计量和限制,作用域范围内超过了资源配额的请求都会报验证错误。
表10.5列出了ResourceQuota的4种作用域。
作用域 说明
Terminating 匹配所有spec.activeDeadlineSeconds不小于0的Pod
NotTerminating 匹配所有spec.activeDeadlineSeconds是nil的Pod
BestEffort 匹配所有QoS是BestEffort的Pod
NotBestEffort 匹配所有QoS不是BestEffort的Pod
其中,BestEffort作用域可以限定资源配额来追踪pods资源的使用,
Terminating、NotTerminating和NotBestEffort这三种作用域可以限定资源配额来追踪以下资源的使用:
cpu
limits.cpu
limits.memory
memory
pods
requests.cpu
requests.memory

3.在资源配额(ResourceQuota)中设置Requests和Limits
资源配额也可以设置Requests和Limits。
如果在资源配额中指定了requests.cpu或requests.memory,那么它会强制要求每个容器都配置自己的CPU Requests或CPU Limits(可使用LimitRange提供的默认值)。
同理,如果在资源配额中指定了limits.cpu或limits.memory,那么它也会强制要求每个容器都配置自己的内存Requests或内存Limits(可使用LimitRange提供的默认值)。

4.资源配额的定义
下面通过几个例子对资源配额进行设置和应用。
与LimitRange相似,ResourceQuota也被设置在Namespace中。
创建名为myspace的Namespace:
# kubectl create namespace myspace
创建ResourceQuota配置文件compute-resources.yaml,用于设置计算资源的配额:
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-resources
spec:
hard:
pods: "4"
request.cpu: "1"
request.memory: "1Gi"
limits.cpu: "2"
limits.memory: "2Gi"
创建该项资源配额:
# kubectl create -f compute-resources.yaml --namespace=myspace
创建另一个名为object-counts.yaml的文件,用于设置对象数量的配额:
apiVersion: v1
kind: ResourceQuota
metadata:
name: object-counts
spec:
hard:
configmaps: "10"
persistentvolumeclaims: "4"
replicationcontrollers: "20"
secrets: "10"
services: "10"
services.loadbalancers: "2"
创建该ResourceQuota:
# kubectl create -f object-counts.yaml --namespace=myspace
查看各ResourceQuota的详细信息:
# kubectl describe quota compute-resources --namespace=myspace
# kubectl describe quota object-counts --namespace=myspace

2.资源配额与集群资源总量的关系
资源配额与集群资源总量是完全独立的。
资源配额是通过绝对的单位来配置的,这也就意味着如果在集群中新添加了节点,那么资源配额不会自动更新,而该资源配额所对应的命名空间中的对象也不能自动增加资源上限。
在某些情况下,我们可能希望资源配额支持更复杂的策略,如下所述:
对于不同的租户,按照比例划分整个集群的资源。
允许每个租户都能按照需要来提高资源用量,但是有一个较宽容的限制,以防止意外的资源耗尽情况发生。
探测某个命名空间的需求,添加物理节点并扩大资源配额值。
这些策略可以通过将资源配额作为一个控制模块、手动编写一个控制器来监控资源使用情况,并调整命名空间上的资源配额来实现。
资源配额将整个集群中的资源总量做了一个静态划分,但它并没有对集群中的节点做任何限制:不同命名空间中的Pod仍然可以运行在同一个节点上。

10.4.5 ResourceQuota和LimitRange实践
根据前面对资源管理的介绍,这里将通过一个完整的例子来说明如何通过资源配额和资源配置范围的配合来控制一个命名空间的资源使用。
集群管理员根据集群用户的数量来调整集群配置,以达到这个目的:能控制特定命名空间中的资源使用量,最终实现集群的公平使用和成本控制。
需要实现的功能如下:
限制运行状态的Pod的计算资源用量。
限制持久存储卷的数量以控制对存储的访问。
限制负载均衡器的数量以控制成本。
防止滥用网络端口这类稀缺资源。
提供默认的计算资源Requests以便于系统做出更优化的调度。
1.创建命名空间
创建名为quota-example的命名空间,namespace.yaml文件的内容如下:
apiVersion: v1
kind: Namespace
metadata:
name: quota-example
查看命名空间:
# kubectl create -f namespace.yaml
# kubectl get namespaces

2.设置限定对象数目的资源配额
通过设置限定对象的数量的资源配额,可以控制以下资源的数量:
持久存储卷;
负载均衡器;
NodePort。
创建名为object-counts的ResourceQuota:
object-counts.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: object-counts
spec:
hard:
persistentvolumeclaims: "2"
services.loadbalancers: "2"
services.nodeports: "0"
# kubectl create -f object-counts.yaml --namespace=quota-example
配额系统会检测到资源项配额的创建,并且会统计和限制该命名空间中的资源消耗。
查看该配额是否生效:
# kubectl describe quota object-counts --namespace=quota-example
至此,配额系统会自动阻止那些使资源用量超过资源配额限定值的请求。

3.设置限定计算资源的资源配额
下面再创建一项限定计算资源的资源配额,以限制该命名空间中的计算资源的使用总量。
创建名为compute-resources的ResourceQuota:
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-resources
spec:
hard:
pods: "4"
request.cpu: "1"
request.memory: "1Gi"
limits.cpu: "2"
limits.memory: "2Gi"
# kubectl create -f compute-resources.yaml --namespace=quota-example
查看该配额是否生效:
# kubectl describe quota compute-resources --namespace=quota-example
配额系统会自动防止在该命名空间中同时拥有超过4个非“终止态”的Pod。
此外,由于该项资源配额限制了CPU和内存的Limits和Requests的总量,
因此会强制要求该命名空间下的所有容器都显式定义CPU和内存的Limits和Requests(可使用默认值,Requests默认等于Limits)。

4.配置默认Requests和Limits
在命名空间已经配置了限定计算资源的资源配额的情况下,如果尝试在该命名空间下创建一个不指定Requests和Limits的Pod,那么Pod的创建可能会失败。
下面是一个失败的例子,创建一个Nginx的Deployment:
# kubectl run nginx --image=nginx --replicas=1 --namespace=quota-example
查看创建的Pod,会发现Pod没有创建成功:
# kubectl get pods --namespace=quota-example
再查看一下Deployment的详细信息:
# kubectl describe deployment nginx --namespace=quota-example
该Deployment尝试创建一个Pod,但是失败了,查看其中ReplicaSet的详细信息:
# kubectl describe rs nginx-xxx --namespace=quota-example
可以看到Pod创建失败的原因:Master拒绝这个ReplicaSet创建Pod,因为在这个Pod中没有指定CPU和内存的Requests和Limits。
为了避免这种失败,我们可以使用LimitRange来为这个命名空间下的所有Pod都提供一个资源配置的默认值。
下面的例子展示了如何为这个命名空间添加一个指定默认资源配置的LimitRange。
创建一个名为limits的LimitRange:
limits.yaml
apiVersion: v1
kind: LimitRange
metadata:
name: limits
spec:
limits:
- default:
cpu: 200m
memory: 512Mi
default100m
memory: 256Mi
# kubectl create -f limits.yaml --namespace=quota-example
# kubectl describe limits limits --namespace=quota-example
在LimitRange创建成功后,用户在该命名空间下创建未指定资源限制的Pod的请求时,系统会自动为该Pod设置默认的资源限制。
例如,每个新建的未指定资源限制的Pod都等价于使用下面的资源限制:
kubectl run nginx --image=nginx --replicas=1 --request=cpu=100m,memory=256Mi --limits=cpu=200m,memory=512Mi --namespace=quota-example
至此,我们已经为该命名空间配置好默认的计算资源了,我们的ReplicaSet应该能够创建Pod了。
查看一下,创建Pod成功:
# kubectl get pods --namespace=quota-example
接下来可以随时查看资源配额的使用情况:
# kubectl describe quota --namespace=quota-example
可以看到每个Pod在创建时都会消耗指定的资源量,而这些使用量都会被Kubernetes准确跟踪、监控和管理。

5.指定资源配额的作用域
假设我们并不想为某个命名空间配置默认的计算资源配额,而是希望限定在命名空间内运行的QoS为BestEffort的Pod总数,
例如让集群中的部分资源运行QoS为非BestEffort的服务,而让闲置的资源运行QoS为BestEffort的服务,
即可避免集群的所有资源仅被大量的BestEffort Pod耗尽。
这可以通过创建两个资源配额来实现,如下所述。
创建一个名为quota-scopes的命名空间:
# kubectl create namespace quota-scopes
创建一个名为best-effort的ResourceQuota,指定Scope为BestEffort:
best-effort.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: best-effort
spec:
hard:
pods: "10"
scopes:
- BestEffort
# kubectl create -f best-effort.yaml --namespace=quota-scopes
再创建一个名为not-best-effort的ResourceQuota,指定Scope为NotBestEffort:
not-best-effort.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: not-best-effort
spec:
pods: "4"
request.cpu: "1"
request.memory: "1Gi"
limits.cpu: "2"
limits.memory: "2Gi"
scopes:
- NotBestEffort
# kubectl create -f not-best-effort.yaml --namespace=quota-scopes
查看创建成功的ResourceQuota:
# kubectl describe quota --namespace=quota-scopes
之后,没有配置Requests的Pod将会被名为best-effort的ResourceQuota限制;
而配置了Requests的Pod会被名为not-best-effort的ResourceQuota限制。
创建两个Deployment:
# kubectl run best-effort-nginx --image=nginx --replicas=8 --namespace=quota-scopes
# kubectl run not-best-effort-nginx --image=nginx --replicas=2 --requests=cpu=100m,memory=256Mi --limits=cpu=200m,memory=512Mi --namespace=quota-scopes
名为best-effort-nginx的Deployment因为没有配置Requests和Limits,所以它的QoS级别为BestEffort,
因此它的创建过程由best-effort资源配额项来限制,而not-best-effort资源配额项不会对它进行限制。
best-effort资源配额项没有限制Requests和Limits,因此best-effort-nginx Deployment可以成功创建8个Pod。
名为not-best-effort-nginx的Deployment因为配置了Requests和Limits,且二者不相等,所以它的QoS级别为Burstable,
因此它的创建过程由not-best-effort资源配额项限制,而best-effort资源配额项不会对它进行限制。
not-best-effort资源配额项限制了Pod的Requests和Limits的总上限,not-best-effort-nginx Deployment并没有超过这个上限,所以可以成功创建两个Pod。
查看已经创建的Pod:
# kubectl get pods --namespace=quota-scopes
可以看到10个Pod都创建成功。
再看一下两个资源配额项的使用情况:
# kubectl describe quota --namespace=quota-scopes
可以看到best-effort资源配额项已经统计了在best-effort-nginx Deployment中创建的8个Pod的资源使用信息,
not-best-effort资源配额项也已经统计了在not-best-effort-nginx Deployment中创建的两个Pod的资源使用信息。
通过这个例子我们可以看到:
资源配额的作用域(Scopes)提供了一种将资源集合分割的机制,可以使集群管理员更加方便地监控和限制不同类型的对象对各类资源的使用情况,
同时为资源分配和限制提供更大的灵活度和便利性。

10.4.6 资源管理总结
Kubernetes中资源管理的基础是容器和Pod的资源配置(Requests和Limits)。
容器的资源配置指定了容器请求的资源和容器能使用的资源上限,Pod的资源配置则是Pod中所有容器的资源配置总和上限。
通过资源配额机制,我们可以对命名空间中所有Pod使用资源的总量进行限制,也可以对这个命名空间中指定类型的对象的数量进行限制。
使用作用域可以让资源配额只对符合特定范围的对象加以限制,因此作用域机制可以使资源配额的策略更加丰富、灵活。
如果需要对用户的Pod或容器的资源配置做更多的限制,则可以使用资源配置范围(LimitRange)来达到这个目的。
LimitRange可以有效地限制Pod和容器的资源配置的最大、最小范围,也可以限制Pod和容器的Limits与Requests的最大比例上限,
此外LimitRange还可以为Pod中的容器提供默认的资源配置。
Kubernetes基于Pod的资源配置实现了资源服务质量(QoS)。
不同QoS级别的Pod在系统中拥有不同的优先级:
高优先级的Pod有更高的可靠性,可以用于运行可靠性要求较高的服务;
低优先级的Pod可以实现集群资源的超售,可以有效地提高集群资源利用率。
上面的多种机制共同组成了当前版本Kubernetes的资源管理体系。
这个资源管理体系可以满足大部分的资源管理需求。
同时,Kubernetes的资源管理体系仍然在不停地发展和进化,对于目前无法满足的更复杂、更个性化的需求,我们可以继续关注Kubernetes未来的发展和变化。

10.5 资源紧缺时的Pod驱逐机制
如何在系统硬件资源紧缺的情况下保证Node的稳定性,是kubelet需要解决的一个重要问题。
尤其对于内存和磁盘这种不可压缩的资源,紧缺就意味着不稳定。
下面对驱逐的策略、信号、阈值、监控频率和驱逐操作进行详细说明。

10.5.1 驱逐策略
kubelet持续监控主机的资源使用情况,并尽量防止计算资源被耗尽。
一旦出现资源紧缺的迹象,kubelet就会主动终止一个或多个Pod的运行,以回收紧缺的资源。
当一个Pod被终止时,其中的容器会全部停止,Pod的状态会被设置为Failed。

10.5.2 驱逐信号
在表10.6中提到了一些信号,kubelet能够利用这些信号作为决策依据来触发驱逐行为。
其中,描述列中的内容来自kubelet Summary API;每个信号都支持整数值或者百分比的表示方法,百分比的分母部分就是各个信号相关资源的总量。
表10.6 驱逐信号及其描述
驱逐信号 描述
memory.available memory.available := node.stats.capacity[memory] - node.stats.memory.workingSet
nodefs.available nodefs.available := node.stats.fs.available
nodefs.inodesFree nodefs.inodesFree := node.stats.fs.inodesFree
imagefs.available imagefs.available := node.stats.runtime.imagefs.available
imagefs.inodesFree imagefs.inodesFree := node.stats.runtime.imagefs.inodesFree
memory.available的值取自cgroupfs,而不是free -m命令,这是因为free -m不支持在容器内工作。
如果用户使用了node allocatable功能,则除了节点自身的内存需要判断,还需要利用cgroup根据用户Pod部分的情况进行判断。
下面的脚本展示了kubelet计算memory.available的过程:

kubelet假设inactive_file(不活跃LRU列表中的file-backed内存,以字节为单位)在紧缺情况下可以回收,因此对其进行了排除。
kubelet支持以下两种文件系统:
(1)nodefs:保存kubelet的卷和守护进程日志等。
(2)imagefs:在容器运行时保存镜像及可写入层。
kubelet使用cAdvisor自动监控这些文件系统。
kubelet不关注其他文件系统,不支持所有其他类型的配置,例如保存在独立文件系统中的卷和日志。
磁盘压力相关的资源回收机制正在逐渐被驱逐策略接管,未来会停止对现有垃圾收集方式的支持。

10.5.3 驱逐阈值
kubelet可以定义驱逐阈值,一旦超出阈值,就会触发kubelet进行资源回收操作。
阈值的定义方式为:
<eviction-signal> <operator> <quantity>
其中:
在表10.6中列出了驱逐信号的名称。
当前仅支持一个operator(运算符):< (小于)。
quantity需要符合Kubernetes的数量表达方式,也可以用以%结尾的百分比表示。
例如,如果一个节点有10GiB内存,我们希望在可用内存不足1GiB时进行驱逐,就可以用下面任一方式来定义驱逐阈值。
memory.available<10%
memory.available<1GiB
驱逐阈值又可以通过软阈值和硬阈值两种方式进行设置。
1.驱逐软阈值
驱逐软阈值由一个驱逐阈值和一个管理员设定的宽限期共同定义。
当系统资源消耗达到软阈值时,在这一状况的持续时间达到宽限期之前,kubelet不会触发驱逐动作。
如果没有定义宽限期,则kubelet会拒绝启动。
另外,可以定义终止Pod的宽限期。
如果定义了这一宽限期,那么kubelet会使用pod.Spec.TerminationGracePeriodSeconds和最大宽限期这两个值之间较小的数值进行宽限,
如果没有指定,则kubelet会立即杀掉Pod。
软阈值的定义包括以下几个参数:
--eviction-soft:描述驱逐阈值(例如memory.available<1.5GiB),如果满足这一条件的持续时间超过宽限期,就会触发对Pod的驱逐动作。
--eviction-soft-grace-period:驱逐宽限期(例如memory.available=1m30s),用于定义达到软阈值之后持续时间超过多久才进行驱逐。
--eviction-max-pod-grace-period:在达到软阈值后,终止Pod的最大宽限时间(单位为s)。
2.驱逐硬阈值
硬阈值没有宽限期,如果达到了硬阈值,则kubelet会立即杀掉Pod并进行资源回收。
硬阈值的定义包括参数--eviction-hard:驱逐硬阈值,一旦达到阈值,就会触发对Pod的驱逐操作。
kubelet的默认硬阈值定义如下:
--eviction-hard=memory.available<100Mi

10.5.4 驱逐监控频率
kubelet的--housekeeping-interval参数定义了一个时间间隔,kubelet每隔一个这样的时间间隔就会对驱逐阈值进行评估。

10.5.5 节点的状况
kubelet会将一个或多个驱逐信号与节点的状况对应起来。
无论触发了硬阈值还是软阈值,kubelet都会认为当前节点的压力太大,如表10.7所示为节点状况与驱逐信号的对应关系。
表10.7 节点状况与驱逐信号的对应关系
节点状况 驱逐信号 描述
MemoryPressure memory.available 节点的可用内存达到了驱逐阈值
DiskPressure nodefs.available,nodefs.inodesFree,imagefs.available,imagefs.inodesFree 节点的root文件系统或镜像文件系统的可用空间达到了驱逐阈值
kubelet会持续向Master报告节点状态的更新过程,这一频率由参数--node-statusupdate-frequency指定,默认为10s。

10.5.6 节点状况的抖动
如果一个节点的状况在软阈值的上下抖动,但是又没有超过宽限期,则会导致该节点的相应状态在True和False之间不断变换,可能会对调度的决策过程产生负面影响。
要防止这种状况,可以使用参数--eviction-pressure-transition-period(在脱离压力状态前需要等待的时间,默认值为5m0s),为kubelet设置在脱离压力状态之前需要等待的时间。
这样一来,kubelet在把压力状态设置为False之前,会确认在检测周期之内该节点没有达到驱逐阈值。

10.5.7 回收Node级别的资源
如果达到了驱逐阈值,并且也过了宽限期,kubelet就会回收超出限量的资源,直到驱逐信号量回到阈值以内。
kubelet在驱逐用户Pod之前,会尝试回收Node级别的资源。
在观测到磁盘压力的情况下,基于服务器是否为容器运行时定义了独立的imagefs,会导致不同的资源回收过程。
1.有Imagefs的情况
(1)如果nodefs文件系统达到了驱逐阈值,则kubelet会删掉死掉的Pod、容器来清理空间。
(2)如果imagefs文件系统达到了驱逐阈值,则kubelet会删掉所有无用的镜像来清理空间。
2.没有Imagefs的情况
如果nodefs文件系统达到了驱逐阈值,则kubelet会按照下面的顺序来清理空间:删除死掉的Pod、容器 --》 删除所有无用的镜像。

10.5.8 驱逐用户的Pod
如果kubelet无法通过节点级别的资源回收获取足够的资源,就会驱逐用户的Pod。
kubelet会按照下面的标准对Pod的驱逐行为进行判断:
Pod要求的服务质量。
根据Pod调度请求的被耗尽资源的消耗量。
接下来,kubelet按照下面的顺序驱逐Pod:
BestEffort:紧缺资源消耗最多的Pod最先被驱逐。
Burstable:根据相对请求来判断,紧缺资源消耗最多的Pod最先被驱逐,如果没有Pod超出它们的请求,则策略会瞄准紧缺资源消耗量最大的Pod。
Guaranteed:根据相对请求来判断,紧缺资源消耗最多的Pod最先被驱逐,如果没有Pod超出它们的请求,策略会瞄准紧缺资源消耗量最大的Pod。
Guaranteed Pod永远不会因为其他Pod的资源消费被驱逐。
如果系统进程(例如kubelet、docker、journald等)消耗了超出system-reserved或者kube-reserved的资源,
而在这一节点上只运行了Guaranteed Pod,那么为了保证节点的稳定性并降低异常消耗对其他Guaranteed Pod的影响,必须选择一个Guaranteed Pod进行驱逐。
本地磁盘是一种BestEffort资源。如有必要,kubelet会在DiskPressure的情况下,对Pod进行驱逐以回收磁盘资源。
kubelet会按照QoS进行评估。如果kubelet判定缺乏inode资源,就会通过驱逐最低QoS的Pod的方式来回收inodes。
如果kubelet判定缺乏磁盘空间,就会在相同QoS的Pod中,选择消耗最多磁盘空间的Pod进行驱逐。
下面针对有Imagefs和没有Imagefs的两种情况,说明kubelet在驱逐Pod时选择Pod的排序算法,然后按顺序对Pod进行驱逐。
1.有Imagefs的情况
如果nodefs触发了驱逐,则kubelet会根据nodefs的使用情况(以Pod中所有容器的本地卷和日志所占的空间进行计算)对Pod进行排序。
如果imagefs触发了驱逐,则kubelet会根据Pod中所有容器消耗的可写入层的使用空间进行排序。
2.没有Imagefs的情况
如果nodefs触发了驱逐,则kubelet会对各个Pod中所有容器的总体磁盘消耗(以本地卷+日志+所有容器的写入层所占的空间进行计算)进行排序。

10.5.9 资源最少回收量
在某些场景下,驱逐Pod可能只回收了很少的资源,这就导致了kubelet反复触发驱逐阈值。另外,回收磁盘这样的资源,是需要消耗时间的。
要缓和这种状况,kubelet可以对每种资源都定义minimum-reclaim。
kubelet一旦监测到了资源压力,就会试着回收不少于minimum-reclaim的资源数量,使得资源消耗量回到期望的范围。
例如,可以配置--eviction-minimum-reclaim如下:
--eviction-hard=memory.available<500Mi,nodefs.available<1Gi,imagefs.available<100Gi
--eviction-minimum-reclaim="memory.available<0Mi,nodefs.available<500Mi,imagefs.available<2Gi"
这样配置的效果如下:
当memory.available超过阈值触发了驱逐操作时,kubelet会启动资源回收,并保证memory.available至少有500MiB。
如果是nodefs.available超过阈值并触发了驱逐操作,则kubelet会恢复nodefs.available到至少1.5GiB。
对于imagefs.available超过阈值并触发了驱逐操作的情况,kubelet会保证imagefs.available恢复到最少102GiB。
在默认情况下,所有资源的eviction-minimum-reclaim都为0。

10.5.10 节点资源紧缺情况下的系统行为
1.调度器的行为
在节点资源紧缺的情况下,节点会向Master报告这一状况。
在Master上运行的调度器(Scheduler)以此为信号,不再继续向该节点调度新的Pod。
如表10.8所示为节点状况与调度行为的对应关系。
节点状况 调度行为
MemoryPressure 不再向这个节点调度新的BestEffort Pod
DiskPressure 不再向这个节点调度Pod

2.Node的OOM行为
如果节点在kubelet能够回收内存之前遭遇了系统的OOM(内存不足),节点则依赖oom_killer的设置进行响应(OOM评分系统详见10.4节的描述)。
kubelet根据Pod的QoS为每个容器都设置了一个oom_score_adj值,如表10.9所示。
表10.9 kubelet根据Pod的QoS为每个容器设置了一个oom_score_adj值
QoS等级 oom_score_adj值
Guaranteed -998
BestEffort 1000
Burstable min(max(2,1000-(1000*memoryRequestBytes)/machineMemoryCapacityBytes),999)
如果kubelet无法在系统OOM之前回收足够的内存,则oom_killer会根据内存使用比率来计算oom_score,将得出的结果和oom_score_adj相加,得分最高的Pod首先被驱逐。
这个策略的思路是,QoS最低且相对于调度的Request来说消耗最多内存的Pod会首先被驱逐,来保障内存的回收。
与Pod驱逐不同,如果一个Pod的容器被OOM杀掉,则是可能被kubelet根据RestartPolicy重启的。

3.对DaemonSet类型的Pod驱逐的考虑
通过DaemonSet创建的Pod具有在节点上自动重启的特性,因此我们不希望kubelet驱逐这种Pod;
然而kubelet目前并没有能力分辨DaemonSet的Pod,所以无法单独为其制定驱逐策略,
所以强烈建议不要在DaemonSet中创建BestEffort类型的Pod,避免产生驱逐方面的问题。

10.5.11 可调度的资源和驱逐策略实践
假设一个集群的资源管理需求如下:
节点内存容量:10GiB。
保留10%的内存给系统守护进程(内核、kubelet等)。
在内存使用率达到95%时驱逐Pod,以此降低系统压力并防止系统OOM。
为了满足这些需求,kubelet应该设置如下参数:
--eviction-hard=memory.available<500Mi
--system-reserved=memory=1.5Gi
在这个配置方式中隐式包含这样一个设置:系统预留内存也包括资源驱逐阈值。
如果内存占用超出这一设置,则要么是Pod占用了超过其Request的内存,要么就是系统使用了超过500MiB内存。
在这种设置下,节点一旦开始接近内存压力,调度器就不会向该节点部署Pod,并且假定这些Pod使用的资源数量少于其请求的资源数量。

10.5.12 现阶段的问题
1.kubelet无法及时观测到内存压力
kubelet目前从cAdvisor定时获取内存使用状况的统计情况。
如果内存使用在这个时间段内发生了快速增长,且kubelet无法观察到MemoryPressure,则可能会触发OOMKiller。
Kubernetes正在尝试将这一过程集成到memcg通知API中来减少这一延迟,而不是让内核首先发现这一情况。
对用户来说,一个较为可靠的处理方式就是设置驱逐阈值为大约75%,这样就降低了发生OOM的几率,提高了驱逐的标准,有助于集群状态的平衡。
2.kubelet可能会错误地驱逐更多的Pod
这也是状态搜集存在时间差导致的。
未来可能会通过按需获取根容器的统计信息来减少计算偏差(https://github.com/google/cadvisor/issues/1247)。

10.6 Pod Disruption Budget(主动驱逐保护)
在Kubernetes集群运行的过程中,许多管理操作都可能对Pod进行主动驱逐,
“主动”一词意味着这一操作可以安全地延迟一段时间,目前主要针对以下两种场景:
节点的维护或升级时(kubectl drain);
对应用的自动缩容操作(autoscaling down)。
未来,rescheduler也可能执行这个操作。
作为对比,由于节点不可用(Not Ready)导致的Pod驱逐就不能被称为主动了。
对于主动驱逐的场景来说,应用如果能够保持存活的Pod的数量,则会非常有用。
通过使用PodDisruptionBudget,应用可以保证那些会主动移除Pod的集群操作永远不会在同一时间停掉太多Pod,从而导致服务中断或者服务降级等。
例如,在对某些Node进行维护时,系统应该保证应用以不低于一定数量的Pod保障服务的正常运行。
kubectl drain操作将遵循PodDisruptionBudget的设定,如果在该节点上运行了属于同一服务的多个Pod,
则为了保证最少存活数量,系统将确保每终止一个Pod后,一定会在另一台健康的Node上启动新的Pod,再继续终止下一个Pod。
PodDisruptionBudget资源对象在Kubernetes 1.5版本时升级为Beta版本,用于指定一个Pod集合在一段时间内存活的最小实例数量或者百分比。
一个PodDisruptionBudget作用于一组被同一个控制器管理的Pod,例如ReplicaSet或RC。
与通常的Pod删除不同,驱逐Pod的控制器将使用/eviction接口对Pod进行驱逐,如果这一主动驱逐行为违反了PodDisruptionBudget的约定,就会被API Server拒绝。
PodDisruptionBudget本身无法真正保障指定数量或百分比的Pod的存活。
例如,在一个节点中包含了目标Pod中的一个,如果节点故障,就会导致Pod数量少于minAvailable。
PodDisruptionBudget对象的保护作用仅仅针对主动驱逐的场景,而非所有场景。
对PodDisruptionBudget的定义包括如下两部分:
Label Selector:用于筛选被管理的Pod。
minAvailable:指定驱逐过程需要保障的最少Pod数量。minAvailable可以是一个数字,也可以是一个百分比,例如100%就表示不允许进行主动驱逐。
PodDisruptionBudget示例如下:
(1)首先创建一个Deployment,Pod数量为3个:
# kubectl create -f deployment.yaml
创建后通过kubectl get pods命令查看Pod的创建情况。
(2)接下来创建一个PodDisruptionBudget对象:
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: nginx
spec:
minAvailable: 3
selector:
matchLabels:
name: nginx
PodDisruptionBudget使用的是和Deployment一样的Label Selector,并且设置存活Pod的数量不得少于3个。
(3)主动驱逐验证。
对Pod的主动驱逐操作将通过驱逐API(/eviction)来完成。
可以将这个API看作受策略控制的对Pod的DELETE操作。
要实现一次主动驱逐(更准确的说法是创建一个eviction),则需要POST一个JSON请求,以eviction.json文件格式表示,内容如下:
{
"apiVersion": "policy/v1beta1",
"kind": "Eviction",
"metadata": {
"name": "nginx-xxx",
"namespace": "default"
}
}
用curl命令执行eviction操作:
# curl -v -H 'Content-type: application/json' http://<k8s_master>/api/v1/namespaces/default/pods/nginx-xxx/eviction -d @eviction.json
由于PodDisruptionBudget设置存活的Pod数量不能少于3个,因此驱逐操作会失败,在返回的错误信息中会包含如下内容:
"message": "Cannot evict pod as it would violate the pod's disruption budget."
使用kubectl get pods查看Pod列表,会看到Pod的数量和名称都没有发生变化。
最后用kubectl delete pdb nginx命令删除pdb对象。
再次执行上文中的curl指令,会执行成功。
通过kubectl get pods命令查看Pod列表,会发现Pod的数量虽然没有发生变化,但是指定的Pod已经消失,取而代之的是一个新的Pod。

10.7 Kubernetes集群的高可用部署方案
Kubernetes作为容器应用的管理平台,通过对Pod的运行状况进行监控,并且根据主机或容器失效的状态将新的Pod调度到其他Node上,实现了应用层的高可用性。
针对Kubernetes集群,高可用性还应包含以下两个层面的考虑:etcd数据存储的高可用性和Kubernetes Master组件的高可用性。
下面通过两种方式对Kubernetes的高可用部署方案进行说明,一种是通过手工方式对每个组件进行配置和部署;另一种是通过kubeadm工具进行部署。

10.7.1 手工方式的高可用部署方案
1.etcd的高可用部署
etcd在整个Kubernetes集群中处于中心数据库的地位,为保证Kubernetes集群的高可用性,首先需要保证数据库不是单一故障点。
一方面,etcd需要以集群的方式进行部署,以实现etcd数据存储的冗余、备份与高可用;
另一方面,etcd存储的数据本身也应考虑使用可靠的存储设备。
etcd集群的部署可以使用静态配置,也可以通过etcd提供的REST API在运行时动态添加、修改或删除集群中的成员。
本节将对etcd集群的静态配置进行说明。关于动态修改的操作方法请参考etcd官方文档的说明。
首先,规划一个至少3台服务器(节点)的etcd集群,在每台服务器上都安装etcd。
部署一个由3台服务器组成的etcd集群,其配置如表10.10所示,其集群部署实例如图10.5所示。
表10.10 etcd集群的配置
etcd实例的名称 IP地址
etcd1 10.0.0.1
etcd2 10.0.0.2
etcd3 10.0.0.3
然后修改每台服务器上etcd的配置文件/etc/etcd/etcd.conf。
以etcd1为创建集群的实例,需要将其ETCD_INITIAL_CLUSTER_STATE设置为new。
etcd1的完整配置如下:
# [member]
ETCD_NAME=etcd1 # etcd实例的名称
ETCD_DATA_DIR="/var/lib/etcd" # etcd数据保存目录
ETCD_LISTEN_CLIENT_URLS="http://10.0.0.1:2379,http://127.0.0.1:2379" # 供外部客户端使用的URL
ETCD_ADVERTISE_CLIENT_URLS="http://10.0.0.1:2379,http://127.0.0.1:2379" # 广播给外部客户端使用的URL
# [cluster]
ETCD_LISTEN_PEER_URLS="http://10.0.0.1:2380" # 集群内部通信使用的URL
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.0.0.1:2380" # 广播给集群内其他成员访问的URL
ETCD_INITIAL_CLUSTER="etcd1=http://10.0.0.1:2380, etcd2=http://10.0.0.2:2380, etcd3=http://10.0.0.3:2380" # 初识集群成员列表
ETCD_INITIAL_CLUSTER_STATE="new" # 初始集群状态,new为新建集群
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster" # 集群名称
启动etcd1服务器上的etcd服务:
# systemctl restart etcd
启动完成后,就创建了一个名为etcd-cluster的集群。
etcd2和etcd3为加入etcd-cluster集群的实例,需要将其ETCD_INITIAL_CLUSTER_STATE设置为exist。etcd2的完整配置如下(etcd3的配置略):
# [member]
ETCD_NAME=etcd2 # etcd实例的名称
ETCD_DATA_DIR="/var/lib/etcd" # etcd数据保存目录
ETCD_LISTEN_CLIENT_URLS="http://10.0.0.2:2379,http://127.0.0.1:2379" # 供外部客户端使用的URL
ETCD_ADVERTISE_CLIENT_URLS="http://10.0.0.2:2379,http://127.0.0.1:2379" # 广播给外部客户端使用的URL
# [cluster]
ETCD_LISTEN_PEER_URLS="http://10.0.0.2:2380" # 集群内部通信使用的URL
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.0.0.2:2380" # 广播给集群内其他成员访问的URL
ETCD_INITIAL_CLUSTER="etcd1=http://10.0.0.1:2380, etcd2=http://10.0.0.2:2380, etcd3=http://10.0.0.3:2380" # 初识集群成员列表
ETCD_INITIAL_CLUSTER_STATE="exist" # 初始集群状态,new为新建集群
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster" # 集群名称
启动etcd2和etcd3服务器上的etcd服务:
# systemctl restart etcd
启动完成后,在任意etcd节点执行etcdctl cluster-health命令来查询集群的运行状态:
# etcdctl cluster-health
在任意etcd节点执行etcdctl member list命令来查询集群的成员列表:
# etcdctl member list
至此,一个etcd集群就创建成功了。
以kube-apiserver为例,将访问etcd集群的参数设置为:
--etcd-servers=http://10.0.0.1:2379,http://10.0.0.2:2379,http://10.0.0.3:2379
在etcd集群成功启动之后,如果需要对集群成员进行修改,则请参考官方文档的详细说明:
https://github.com/coreos/etcd/blob/master/Documentation/runtime-configuration.md#cluster-reconfiguration-operations。
对于在etcd中需要保存的数据的可靠性,可以考虑使用RAID磁盘阵列、高性能存储设备、共享存储文件系统,或者使用云服务商提供的存储系统等来实现。

2.Master的高可用部署
在Kubernetes系统中,Master服务扮演着总控中心的角色,
主要的三个服务kube-apiserver、kube-controller-mansger和kube-scheduler通过不断与工作节点上的kubelet和kube-proxy进行通信来维护整个集群的健康工作状态。
如果Master的服务无法访问某个Node,则会将该Node标记为不可用,不再向其调度新建的Pod。
但对Master自身则需要进行额外监控,使Master不成为集群的单故障点,所以对Master服务也需要进行高可用部署。
以Master的kube-apiserver、kube-controller-mansger和kube-scheduler三个服务作为一个部署单元,类似于etcd集群的典型部署配置。
使用至少三台服务器安装Master服务,并且需要保证任何时候总有一套Master能够正常工作。图10.6展示了一种典型的部署方式。
Kubernetes建议Master的3个组件都以容器的形式启动,启动它们的基础工具是kubelet,所以它们都将以Static Pod的形式启动并由kubelet监控和自动重启。
kubelet本身的高可用则通过操作系统来完成,例如使用Linux的Systemd系统进行管理。
注意,如果之前已运行过这3个进程,则需要先停止它们,然后启动kubelet服务,这3个主进程都将通过kubelet以容器的形式启动和运行。
接下来分别对kube-apiserver和kube-controller-manager、kube-scheduler的高可用部署进行说明。
1)kube-apiserver的高可用部署
根据第2章的介绍,为kube-apiserver预先创建所有需要的CA证书和基本鉴权文件等内容,然后在每台服务器上都创建其日志文件:
# touch /var/log/kube-apiserver.log
假设kubelet的启动参数指定--config=/etc/kubernetes/manifests,即Static Pod定义文件所在的目录,
接下来就可以创建kube-apiserver.yaml配置文件用于启动kube-apiserver了:
kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
spec:
hostNetwork: true
containers:
- name: kube-apiserver
image: gcr.io/google_containers/kube-apiserver:xxx
command:
- /bin/sh
- -c
- /usr/local/bin/kube-apiserver --etcd-servers=http://127.0.0.1:2379
--adminssion-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
--service-cluster-ip-range=169.169.0.0/16 --v=2 --allow-privileged=False 1>>/var/log/kube-apiserver.log 2>&1
ports:
- containerPort: 443
hostPort: 443
name: https
- containerPort: 7080
hostPort: 7080
name: http
- containerPort: 8080
hostPort: 8080
name: local
volumeMounts:
- mountPath: /srv/kubernetes
name: srvkube
readOnly: true
- mountPath: /var/log/kube-api-server.log
name: logfile
- mountPath: /etc/ssl
name: etcssl
readOnly: true
- mountPath: /usr/share/ssl
name: usrsharessl
readOnly: true
- mountPath: /var/ssl
name: varssl
readOnly: true
- mountPath: /usr/ssl
name: usrssl
readOnly: true
- mountPath: /usr/lib/ssl
name: usrlibssl
readOnly: true
- mountPath: /usr/local/openssl
name: usrlocalopenssl
readOnly: true
- mountPath: /etc/pki/tls
name: etcpkitls
readOnly: true
volume:
- hostPath:
path: /srv/kubernetes
name: srvkube
- hostPath:
path: /var/log/kube-api-server.log
name: logfile
- hostPath:
path: /etc/ssl
name: etcssl
- hostPath:
path: /usr/share/ssl
name: usrsharessl
- hostPath:
path: /var/ssl
name: varssl
- hostPath:
path: /usr/ssl
name: usrssl
- hostPath:
path: /usr/lib/ssl
name: usrlibssl
- hostPath:
path: /usr/local/openssl
name: usrlocalopenssl
- hostPath:
path: /etc/pki/tls
name: etcpkitls
其中:
kube-apiserver需要使用hostNetwork模式,即直接使用宿主机网络,以使客户端能够通过物理机访问其API。
镜像的tag来源于Kubernetes发布包中的kube-apiserver.docker_tag文件:kubernetes/server/kubernetes-server-linux-amd64/server/bin/kube-apiserver.docker_tag。
--etcd-servers:指定etcd服务的URL地址。
再加上其他必要的启动参数,包括--admission-control、--service-cluster-ip-range、CA证书相关配置等内容。
端口号的设置都配置了hostPort,将容器内的端口号直接映射为宿主机的端口号。
将kube-apiserver.yaml文件复制到kubelet监控的/etc/kubernetes/manifests目录下,kubelet将会自动创建在YAML文件中定义的kube-apiserver的Pod。
接下来在另外两台服务器上重复该操作,使得在每台服务器上都启动一个kube-apiserver的Pod。

2)为kube-apiserver配置负载均衡器
至此,我们启动了三个kube-apiserver实例,这三个kube-apiserver都可以正常工作,
我们需要通过统一的、可靠的、允许部分Master发生故障的方式来访问它们,这可以通过部署一个负载均衡器来实现。
在不同的平台下,负载均衡的实现方式不同:
在一些公用云如GCE、AWS、阿里云上都有现成的实现方案;
对于本地集群,我们可以选择硬件或者软件来实现负载均衡,比如Kubernetes社区推荐的方案HAProxy和Keepalived,
其中HAProxy负责负载均衡,而Keepalived负责对HAProxy进行监控和故障切换。
在完成API Server的负载均衡配置之后,对其访问还需要注意以下内容:
如果Master开启了安全认证机制,那么需要确保在CA证书中包含负载均衡服务节点的IP。
对于外部的访问,比如通过kubectl访问API Server,需要配置为访问API Server对应的负载均衡器的IP地址。

3)kube-controller-manager和kube-scheduler的高可用配置
不同于API Server,Master中另外两个核心组件kube-controller-manager和kube-scheduler会修改集群的状态信息,
因此对于kube-controller-manager和kube-scheduler而言,高可用不仅意味着需要启动多个实例,
还需要这些个实例能实现选举并选举出leader,以保证同一时间只有一个实例可以对集群状态信息进行读写,避免出现同步问题和一致性问题。
Kubernetes对于这种选举机制的实现是采用租赁锁(lease-lock)来实现的,
我们可以通过在kube-controller-manager和kube-scheduler的每个实例的启动参数中设置--leader-elect=true,来保证同一时间只会运行一个可修改集群信息的实例。
Scheduler和Controller Manager高可用的具体实现方式如下。
首先,在每个Master上都创建相应的日志文件:
# touch /var/log/kube-scheduler.log
# touch /var/log/kube-controller-manager.log
然后,创建kube-controller-manager和kube-scheduler的YAML配置文件。
kube-controller-manager的YAML配置文件如下:
kube-controller-manager.yaml
apiVersion: v1
kind: Pod
metadata:
name: kube-controller-manager
spec:
hostNetwork: true
containers:
- name: kube-controller-manager
image: gcr.io/google_containers/kube-controller-manager:xxx
command:
- /bin/sh
- -c
- /usr/local/bin/kube-controller-manager --master=127.0.0.1:8080 --v=2 --leader-elect=true 1>>/var/log/kube-controller-manager 2>&1
livenessProbe:
httpGet:
path: /healthz
port: 10252
initialDelaySeconds: 15
timeoutSeconds: 1
volumeMounts:
- mountPath: /srv/kubernetes
name: srvkube
readOnly: true
- mountPath: /var/log/kube-controller-manager.log
name: logfile
- mountPath: /etc/ssl
name: etcssl
readOnly: true
- mountPath: /usr/share/ssl
name: usrsharessl
readOnly: true
- mountPath: /var/ssl
name: varssl
readOnly: true
- mountPath: /usr/ssl
name: usrssl
readOnly: true
- mountPath: /usr/lib/ssl
name: usrlibssl
readOnly: true
- mountPath: /usr/local/openssl
name: usrlocalopenssl
readOnly: true
- mountPath: /etc/pki/tls
name: etcpkitls
readOnly: true
volume:
- hostPath:
path: /srv/kubernetes
name: srvkube
- hostPath:
path: /var/log/kube-controller-manager.log
name: logfile
- hostPath:
path: /etc/ssl
name: etcssl
- hostPath:
path: /usr/share/ssl
name: usrsharessl
- hostPath:
path: /var/ssl
name: varssl
- hostPath:
path: /usr/ssl
name: usrssl
- hostPath:
path: /usr/lib/ssl
name: usrlibssl
- hostPath:
path: /usr/local/openssl
name: usrlocalopenssl
- hostPath:
path: /etc/pki/tls
name: etcpkitls
其中:
kube-controller-manager需要使用hostNetwork模式,即直接使用宿主机网络。
镜像的tag来源于kubernetes发布包中的kube-controller-manager.docker_tag文件:kubernetes/server/kubernetes-server-linux-amd64/server/bin/kube-controller-manager.docker_tag。
--master:指定kube-apiserver服务的URL地址。
--leader-elect=true:使用leader选举机制。

kube-scheduler的YAML配置文件如下:
kube-scheduler.yaml
apiVersion: v1
kind: Pod
metadata:
name: kube-scheduler
spec:
hostNetwork: true
containers:
- name: kube-scheduler
image: gcr.io/google_containers/kube-scheduler:xxx
command:
- /bin/sh
- -c
- /usr/local/bin/kube-scheduler --master=127.0.0.1:8080 --v=2 --leader-elect=true 1>>/var/log/kube-scheduler 2>&1
livenessProbe:
httpGet:
path: /healthz
port: 10251
initialDelaySeconds: 15
timeoutSeconds: 1
volumeMounts:
- mountPath: /var/log/kube-scheduler.log
name: logfile
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: default-token-s8ejd
readOnly: true
volume:
- hostPath:
path: /var/log/kube-scheduler.log
name: logfile
其中:
kube-scheduler需要使用hostNetwork模式,即直接使用宿主机网络。
镜像的tag来源于kubernetes发布包中的kube-scheduler.docker_tag文件:kubernetes/server/kubernetes-server-linux-amd64/server/bin/kube-scheduler.docker_tag。
--master:指定kube-apiserver服务的URL地址。
--leader-elect=true:使用leader选举机制。
将这两个YAML文件复制到kubelet监控的/etc/kubernetes/manifests目录下,kubelet会自动创建在YAML文件中定义的kube-controller-manager和kube-scheduler的Pod。
至此,我们完成了Kubernetes Master组件的高可用配置。

10.7.2 使用kubeadm的高可用部署方案
kubeadm提供了对Master的高可用部署方案,到Kubernetes 1.13版本时Kubeadm达到GA稳定阶段,
这表明kubeadm不仅能够快速部署一个符合一致性要求的Kubernetes集群,更具备足够的弹性,能够支持多种实际生产需求。
在Kubernetes 1.14版本中又加入了方便证书传递的--experimental-upload-certs参数,减少了安装过程中的大量证书复制工作。
下面以Kubeadm 1.14版本为例,介绍在CentOS中基于kubeadm的高可用集群部署过程。
kubeadm提供了两种不同的高可用方案:
(1)堆叠方案:etcd服务和控制平面被部署在同样的节点中,对基础设施的要求较低,对故障的应对能力也较低,如图10.7所示。
(2)外置etcd方案:etcd和控制平面被分离,需要更多的硬件,也有更好的保障能力,如图10.8所示。

1.准备工作
两种部署方案都有同样的准备工作要做。
除了需要符合kubeadm部署的基本要求,因为在下面的部署过程中需要使用SCP进行传输,所以还需要操作机具备SSH到所有节点的连接能力。
为了保证高可用性,Maste服务器至少需要有三台;外置etcd的方案则需要有三台以上的etcd服务器。

2.为API Server提供负载均衡服务
从前面所示的架构图中可以看到,所有节点都需要通过负载均衡器和API Server进行通信。负载均衡器有非常多的方案,需要根据实际情况进行选择。
下面简单介绍一个基于HAProxy的简易方案。
在CentOS下使用yum安装HAProxy:
# yum install -y haproxy
接下来对HAProxy进行配置,使之为三台API Server提供负载均衡服务。编辑/etc/haproxy.cfg,输入如下内容:
listen stats 0.0.0.0:12345
mode http
log global
maxconn 10
stats enable
stats hide-version
stats refresh 30s
stats auth admin:p@ssw0rd
stats uri /stats
frontend kube-api-https
bind 0.0.0.0:12567
mode tcp
default_backend kube-api-server
backend kube-api-server
balance roundrobin
mode tcp
server kubenode1 10.211.55.30:6443 check
server kubenode2 10.211.55.31:6443 check
server kubenode3 10.211.55.32:6443 check
以上代码是非常简化的版本,假设三台Master的地址为10.211.55.30-32。
我们定义了一个代理端口,用TCP转发的方式在12567端口对工作节点提供API Server服务,并且在12345端口上开放了HAProxy的状态服务,用于观察服务状态。
运行如下命令,启动服务并将其设置为自动启动:
# systemctl enable haproxy
# systemctl start haproxy
用浏览器打开http://10.211.55.30:12345/stats,查看HAProxy的状态页面,大致会看到如图10.9所示的结果。
倒数第2行至倒数第4行代表我们的三个尚未启动的API Server都处于DOWN状态,这很正常,我们还没开始进行服务器设置。

3.启动第1组控制平面
使用如下代码创建一个YAML文件,将其保存为kubeadm-config.yaml,用于为kubeadm的安装提供配置:
kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
kubernetesVersion: stable
controlPlaneEndpoint: "10.211.55.30:12567"
然后启动kubeadm的初始化过程:
# kubeadm init --config=kubeadm-config.yaml --experimental-upload-certs
这个初始化过程和之前的情况有所不同,加入了一个新的参数--experimental-upload-certs。
这个参数专门用于高可用部署,可以将需要在不同的控制平面之间传递的证书文件上传到集群中,以Secret形式保存起来,并且使用Token进行加密。
值得注意的是,这个Secret会在两个小时后过期,
如果过期就需要使用kubeadm init phase upload-certs --experimental-upload-certs命令重新生成。
在输出内容中包含加入新的控制平面的命令、和工作节点一致的参数--toke和--discovery-token-ca-cert-hash,
以及--experimental-control-plane及--certificate-key两个参数,用于获取和使用前面生成的Secret。
打开新生成的kubeconfig文件,会发现其中的server字段使用了我们的负载均衡地址:
# cat ~/.kube/config | grep server
再次打开我们的HAProxy状态页面,界面如图10.10所示。
可以看到,kube-api-server组已经可用。

4.启用网络
这里以Weave Net为例,启用容器网络:
# kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"
在网络启动成功后,我们可以运行如下命令检查当前运行的Pod:
# kubectl get po,no --all-namespaces
可以看到,Pod都已经开始运行,并且Master已经启动。

5.加入新的控制平面
根据安装第1组控制平面时的提示,登录第2个控制平面所在的服务器,用下列命令部署新的控制平面:
kubeadm join 10.211.55.30:12567 --token xxx --discovery-token-ca-cert-hash sha256:xxx --experimental-control-plane --certificate-key xxx
可以看到提示加入新的控制平面,此时回到10.211.55.30,再次使用kubectl获取节点列表:
# kubectl get nodes
可以看到,新的控制平面已经加入。
同样,进入第3个节点,再次加入第3组控制平面。
如此一来,我们就有了3个控制平面,通过HAProxy提供的12456端口提供服务:
# kubectl get nodes
再看HAProxy的状态页面,如图10.11所示。
三组控制平面都已成功启动。

6.外置etcd方案的差异
前面几节介绍了堆叠方案的kubeadm高可用部署步骤。
外置etcd的方案与堆叠方案差异不大,进行如下设置即可:
(1)需要为kubeadm设置一组高可用的etcd集群。
(2)将访问etcd所需的证书复制到控制平面所在的服务器上。
(3)在创建kubeadm-config.yaml时需要加入etcd的访问信息,例如:
kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
kubernetesVersion: stable
controlPlaneEndpoint: "LOAD_BALANCER_DNS:LOAD_BALANCER_PORT"
etcd:
external:
endpoints:
- https://ETCD_0_IP:2379
- https://ETCD_1_IP:2379
- https://ETCD_2_IP:2379
caFile: /etc/kubernetes/pki/etcd/ca.crt
certFile: /etc/kubernetes/pki/apiserver-etcd-client.crt
keyFile: /etc/kubernetes/pki/apiserver-etcd-client.key
简单来说,将etcd所需的证书文件以配置文件的方式指派给控制平面即可。

7.补充说明
在上述内容中以Kubernetes 1.14为例简单讲解了使用kubeadm部署高可用Kubernetes集群的过程。
在完成控制平面的部署之后,就可以在其中加入工作节点了。
这是一个非常简化的过程,HAProxy成为新的单点,可能导致整体发生故障,
因此在实际工作中需要根据生产环境的具体情况,换用硬件负载均衡器方案,或者使用软件进行VIP、DNS等相关高可用保障,从而消除对单一HAProxy的依赖。

10.8 Kubernetes集群监控
Kubernetes的早期版本依靠Heapster来实现完整的性能数据采集和监控功能,
Kubernetes从1.8版本开始,性能数据开始以Metrics API的方式提供标准化接口,并且从1.10版本开始将Heapster替换为Metrics Server。
在Kubernetes新的监控体系中,Metrics Server用于提供核心指标(Core Metrics),包括Node、Pod的CPU和内存使用指标。
对其他自定义指标(Custom Metrics)的监控则由Prometheus等组件来完成。

10.8.1 通过Metrics Server监控Pod和Node的CPU和内存资源使用数据
Metrics Server在部署完成后,将通过Kubernetes核心API Server的“/apis/metrics.k8s.io/v1beta1”路径提供Pod和Node的监控数据。
Metrics Server源代码和部署配置可以在GitHub代码库(https://github.com/kubernetes-incubator/metrics-server)找到。
首先,部署Metrics Server实例,在下面的YAML配置中包含ServiceAccount、Deployment和Service的定义:
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: metrics-server
namespace: kube-system
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: metrics-server
namespace: kube-system
labels:
k8s-app: metrics-server
spec:
selector:
matchLabels:
k8s-app: metrics-server
template:
metadata:
name: metrics-server
labels:
k8s-app: metrics-server
spec:
serviceAccountName: metrics-server
containers:
- name: metrics-server
image: k8s.gcr.io/metrics-server-amd64:v0.3.1
imagePullPolicy: IfNotPresent
volumeMounts:
- name: tmp-dir
mountPath: /tmp
volumes:
- name: tmp-dir
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: metrics-server
namespace: kube-system
lables:
kubernetes.io/name: "Metrics-server"
spec:
selector:
k8s-app: metrics-server
ports:
- port: 443
protocol: TCP
targetPort: 443
然后,创建metrics-server所需的RBAC权限配置,此处略。
最后,创建APIService资源,将监控数据通过“/apis/metrics.k8s.io/v1beta1”路径提供:
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
name: v1beta1.metrics.k8s.io
spec:
service:
name: metrics-server
namespace: kube-system
group: metrics.k8s.io
version: v1beta1
insecureSkipTLSVerify: true
groupPriorityMinimum: 100
versionPriority: 100
在部署完成后确保metrics-server的Pod启动成功:
# kubectl -n kube-system get pod -l k8s-app=metrics-server
使用kubectl top nodes和kubectl top pods命令监控CPU和内存资源的使用情况:
# kubectl top nodes
# kubectl top pods --all-namespaces
Metrics Server提供的数据也可以供HPA控制器使用,以实现基于CPU使用率或内存使用值的Pod自动扩缩容功能。

10.8.2 Prometheus+Grafana集群性能监控平台搭建
Prometheus是由SoundCloud公司开发的开源监控系统,是继Kubernetes之后CNCF第2个毕业的项目,在容器和微服务领域得到了广泛应用。
Prometheus的主要特点如下:
使用指标名称及键值对标识的多维度数据模型。
采用灵活的查询语言PromQL。
不依赖分布式存储,为自治的单节点服务。
使用HTTP完成对监控数据的拉取。
支持通过网关推送时序数据。
支持多种图形和Dashboard的展示,例如Grafana。
Prometheus生态系统由各种组件组成,用于功能的扩充:
Prometheus Server:负责监控数据采集和时序数据存储,并提供数据查询功能。
客户端SDK:对接Prometheus的开发工具包。
Push Gateway:推送数据的网关组件。
第三方Exporter:各种外部指标收集系统,其数据可以被Prometheus采集。
AlertManager:告警管理器。
其他辅助支持工具。
Prometheus的核心组件Prometheus Server的主要功能包括:
从Kubernetes Master获取需要监控的资源或服务信息;
从各种Exporter抓取(Pull)指标数据,然后将指标数据保存在时序数据库(TSDB)中;
向其他系统提供HTTP API进行查询;
提供基于PromQL语言的数据查询;
可以将告警数据推送(Push)给AlertManager,等等。
Prometheus的系统架构如图10.12所示。
在部署Prometheus时可以直接基于官方提供的镜像进行,也可以通过Operator模式进行。
本文以直接部署为例,Operator模式的部署案例可以参考3.12.2节的示例。
下面对部署Prometheus服务的过程进行说明。
首先,创建一个ConfigMap用于保存Prometheus的主配置文件prometheus.yml,其中可以配置需要监控的Kubernetes集群的资源对象或服务(如Service、Pod、Node等):
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
namespace: kube-system
labels:
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: EnsureExists
data:
prometheus.yml: |
global:
scrape_interval: 30s
scrape_configs:
- job_name: prometheus
static_configs:
- targets:
- localhost: 9090
- job_name: kubernetes-apiservers
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- action: keep
regex: default;kubernetes;https
source_labels:
- __meta_kubernetes_namespace
- __meta_kubernetes_service_name
- __meta_kubernetes_endpoint_port_name
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
insecure_skip_verify: true
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
- job_name: kubernetes-nodes-kubelet
......
- job_name: kubernetes-service-endpoints
......
- job_name: kubernetes-services
......
- job_name: kubernetes-pods
......
接下来部署Prometheus Deployment和Service:
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: prometheus
namespace: kube-system
labels:
k8s-app: prometheus
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
spec:
replicas: 1
selector:
matchLabels:
k8s-app: prometheus
template:
metadata:
labels:
k8s-app: prometheus
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ''
spec:
priorityClassName: system-cluster-critical
initContainers:
- name: "init-chown-data"
image: "busybox:latest"
imagePullPolicy: "IfNotPresent"
command: ["chown","-R","65534:65534","/data"]
volumeMounts:
- name: storage-volume
mountPath: /data
subPath: ""
containers:
- name: prometheus-server-configmap-reload
image: "jimmidyson/configmap-reload:v0.1"
imagePullPolicy: "IfNotPresent"
args:
- --volume-dir=/etc/config
- --webhook-url=http://localhost:9090/-/reload
volumeMounts:
- name: config-volume
mountPath: /etc/config
readOnly: true
- name: prometheus-server
image: "prom/prometheus:v2.8.0"
imagePullPolicy: "IfNotPresent"
args:
- --config.file=/etc/config/prometheus.yml
- --storage.tsdb.path=/data
- --web.console.libraries=/etc/prometheus/console_libraries
- --web.console.templates=/etc/prometheus/consoles
- --web.enable-lifecycle
ports:
- containerPort: 9090
readinessProbe:
httpGet:
path: /-/ready
port: 9090
initialDelaySeconds: 30
timeoutSeconds: 30
liveinessProbe:
httpGet:
path: /-/healthy
port: 9090
initialDelaySeconds: 30
timeoutSeconds: 30
volumeMounts:
- name: config-volume
mountPath: /etc/config
- name: storage-volume
mountPath: /data
subPath: ""
terminationGracePeriodSeconds: 300
volumes:
- name: config-volume
configMap:
name: prometheus-config
- name: storage-volume
hostPath:
path: /prometheus-data
type: Directory
# Prometheus 的关键启动参数包括:
# --config.file 指定配置文件prometheus.yml的路径
# --storage.tsdb.path 指定数据的存储路径
---
apiVersion: v1
kind: Service
metadata:
name: prometheus
namespace: kube-system
labels:
kubernetes.io/name: "Prometheus"
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
spec:
type: NodePort
ports:
- name: http
port: 9090
nodePort: 9090
protocol: TCP
targetPort: 9090
selector:
k8s-app: prometheus
在部署完成后,确保Prometheus运行成功:
# kubectl -n kube-system get pods -l k8s-app=prometheus
Prometheus提供了一个简单的Web页面用于查看已采集的监控数据,
上面的Service定义了NodePort为9090,我们可以通过访问Node的9090端口访问这个页面,如图10.13所示。
在Prometheus提供的Web页面上,可以输入PromQL查询语句对指标数据进行查询,也可以选择一个指标进行查看,
例如选择container_network_receive_bytes_total指标查看容器的网络接收字节数,如图10.14所示。
单击Graph标签,可以查看该指标的时序图,如图10.15所示。
接下来可以针对各种系统和服务部署各种Exporter进行指标数据的采集。
目前Prometheus支持多种开源软件的Exporter,包括数据库、硬件系统、消息系统、存储系统、HTTP服务器、日志服务,等等,
可以从Prometheus的官网https://prometheus.io/docs/instrumenting/exporters/获取各种Exporter的信息。
下面以官方维护的node_exporter为例进行部署。
node_exporter主要用于采集主机相关的性能指标数据,其官网为https://github.com/prometheus/node_exporter。
node_exporter的YAML配置文件如下:
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: node_exporter
namespace: kube-system
labels:
k8s-app: node_exporter
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
version: v0.17.0
spec:
updateStrategy:
type: OnDelete
template:
metadata:
labels:
k8s-app: node_exporter
version: v0.17.0
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ''
spec:
priorityClassName: system-node-critical
containers:
- name: prometheus-node_exporter
image: "prom/node_exporter:v0.17.0"
imagePullPolicy: "IfNotPresent"
args:
- --path.procfs=/host/proc
- --path.sysfs=/host/sys
ports:
- name: metrics
containerPort: 9100
hostPort: 9100
volumeMounts:
- name: proc
mountPath: /host/proc
readOnly: true
- name: sys
mountPath: /host/sys
readOnly: true
resources:
limits:
cpu: 1
memory: 512Mi
requests:
cpu: 100m
memory: 50Mi
hostNetwork: true
hostPID: true
volumes:
- name: proc
hostPath:
path: /proc
- name: sys
hostPath:
path: /sys
# node_exporter 将读取宿主机上/proc和/sys目录下的内容,获取主机级别的性能指标数据
---
apiVersion: v1
kind: Service
metadata:
name: node_exporter
namespace: kube-system
labels:
kubernetes.io/name: "NodeExporter "
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
annotations:
prometheus.io/scrape: "true"
spec:
clusterIP: None
ports:
- name: metrics
port: 9100
protocol: TCP
targetPort: 9100
selector:
k8s-app: node_exporter
在部署完成后,在每个Node上都运行了一个node-exporter Pod:
#kubectl -n kube-system get pods -l k8s-app: node-exporter
从Prometheus的Web页面就可以查看node-exporter采集的Node指标数据了,如图10.16所示。
最后,部署Grafana用于展示专业的监控页面,其YAML配置文件如下:
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: grafana
namespace: kube-system
labels:
k8s-app: grafana
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
spec:
replicas: 1
selector:
matchLabels:
k8s-app: grafana
template:
metadata:
labels:
k8s-app: grafana
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ''
spec:
priorityClassName: system-cluster-critical
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
- key: "CriticalAddonsOnly"
operator: "Exists"
containers:
- name: grafana
image: grafana/grafana:6.0.1
imagePullPolicy: IfNotPresent
resources:
limits:
cpu: 1
memory: 1Gi
requests:
cpu: 100m
memory: 100Mi
env:
- name: GF_AUTH_BASIC_ENABLED
value: "false"
- name: GF_AUTH_ANONYMOUS_ENABLED
value: "ture"
- name: GF_AUTH_ANONYMOUS_ORG_ROLE
value: Admin
- name: GF_SERVER_ROOT_URL
value: /api/v1/namespaces/kube-system/services/services/grafana/proxy/
ports:
- name: ui
containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: kube-system
labels:
kubernetes.io/name: "Grafana"
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
spec:
ports:
- port: 80
protocol: TCP
targetPort: ui
selector:
k8s-app: grafana
部署完成后,通过Kubernetes Master的URL访问Grafana页面,
例如http://192.168.18.3:8080/api/v1/namespaces/kube-system/services/grafana/proxy。
在Grafana的设置页面添加类型为Prometheus的数据源,
输入Prometheus服务的URL(如http://prometheus:9090)进行保存,如图10.17所示。
在Grafana的Dashboard控制面板导入预置的Dashboard模板,以显示各种监控图表。
Grafana官网(https://grafana.com/dashboards)提供了许多针对Kubernetes集群监控的Dashboard模板,可以下载、导入并使用。
图10.18显示了一个可以监控集群CPU、内存、文件系统、网络吞吐率的Dashboard。
至此,基于Prometheus+Grafana的Kubernetes集群监控系统就搭建完成了。

10.9 集群统一日志管理
在Kubernetes集群环境中,一个完整的应用或服务都会涉及为数众多的组件运行,各组件所在的Node及实例数量都是可变的。
日志子系统如果不做集中化管理,则会给系统的运维支撑造成很大的困难,因此有必要在集群层面对日志进行统一收集和检索等工作。
在容器中输出到控制台的日志,都会以*-json.log的命名方式保存在/var/lib/docker/containers/目录下,这就为日志采集和后续处理奠定了基础。
Kubernetes推荐采用Fluentd+Elasticsearch+Kibana完成对系统和容器日志的采集、查询和展现工作。
部署统一的日志管理系统,需要以下两个前提条件:
API Server正确配置了CA证书。
DNS服务启动、运行。

10.9.1 系统部署架构
该系统的逻辑架构如图10.19所示。
图10.19 Fluentd+Elasticsearch+Kibana系统的逻辑架构图
在各Node上都运行了一个Fluentd容器,采集本节点/var/log和/var/lib/docker/containers两个目录下的日志进程,将其汇总到Elasticsearch集群,最终通过Kibana完成和用户的交互工作。
这里有一个特殊的需求:Fluentd必须在每个Node上运行。
为了满足这一需求,我们通过以下几种方式部署Fluentd:
直接在Node主机上部署Fluentd。
利用kubelet的--config参数,为每个Node都加载Fluentd Pod。
利用DaemonSet让Fluentd Pod在每个Node上运行。

10.9.2 创建Elasticsearch RC和Service
Elasticsearch的RC和Service定义如下:
elasticsearch-rc-svc.yaml
---
apiVersion: v1
kind: ReplicationController
metadata:
name: elasticsearch-logging-v1
namespace: kube-system
labels:
k8s-app: elasticsearch-logging
version: v1
kubernetes.io/cluster-service: "true"
spec:
replicas: 2
selector:
k8s-app: elasticsearch-logging
version: v1
template:
metadata:
labels:
k8s-app: elasticsearch-logging
version: v1
kubernetes.io/cluster-service: "true"
spec:
containers:
- image: gcr.io/google_containers/elasticsearch:1.8
name: elasticsearch-logging
resources:
# keep request = limit to keep this container in guaranteed class
limits:
cpu: 100m
requests:
cpu: 100m
port:
- containerPort: 9200
name: db
protocol: TCP
- containerPort: 9300
name: transport
protocol: TCP
volumeMounts:
- name: es-persistent-storage
mountPath: /data
volumes:
- name: es-persistent-storage
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch-logging
namespace: kube-system
labels:
k8s-app: elasticsearch-logging
kubernetes.io/cluster-service: "true"
kubernetes.io/name: "Elasticsearch"
spec:
ports:
-port: 9200
protocol: TCP
targetPort: db
selector:
k8s-app: elasticsearch-logging
执行kubectl create -f elastic-search.yml命令完成创建。
在命令成功执行后,首先验证Pod的运行情况。
通过kubectl get pods --namespaces=kube-system获取运行中的Pod:
# kubectl get pods --namespaces=kube-system
接下来通过Elasticsearch页面验证其功能。
首先,执行# kubectl cluster-info命令获取Elasticsearch服务的地址:
# kubectl cluster-info
然后,使用# kubectl proxy命令对API Server进行代理,在成功执行后输出如下内容:
# kubectl proxy
这样就可以在浏览器上访问URL地址http://192.168.18.3:8001/api/v1/proxy/namespaces/kube-system/services/elasticsearch-logging,
来验证Elasticsearch的运行情况了,返回的内容是一个JSON文档{“status”: 200,...}。

10.9.3 在每个Node上启动Fluentd
Fluentd的DaemonSet定义如下:
fluentd-ds.yaml
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: fluentd-cloud-logging
namespace: kube-system
labels:
k8s-app: fluentd-cloud-logging
spec:
template:
metadata:
namespace: kube-system
labels:
k8s-app: fluentd-cloud-logging
spec:
containers:
- name: fluentd-cloud-logging
image: gcr.io/google_containers/fluentd-elasticsearch:1.17
resources:
limits:
cpu: 100m
memory: 200Mi
env:
- name: FLUENTD_ARGS
values: -q
volumeMountes:
- name: varlog
mountPath: /var/log
readOnly: false
- name: containers
mountPath: /var/lib/docker/containers
readOnly: false
volumes:
- name: containers
hostPath:
path: /var/lib/docker/containers
- name: varlog
hostPath:
path: /var/log
通过kubectl create命令创建Fluentd容器:
# kubectl create -f fluentd-ds.yaml
查看创建的结果:
# kubectl get daemonset
# kubectl get pods
结果显示Fluentd DaemonSet正常运行,还启动了3个Pod,与集群中的Node数量一致。
接下来使用# kubectl logs fluentd-cloud-logging-7tw9z命令查看Pod的日志,在Elasticsearch正常工作的情况下,我们会看到类似下面这样的日志内容:
# kubectl logs fluentd-cloud-logging-7tw9z
说明Fluentd与Elasticsearch已经正确建立了连接。

10.9.4 运行Kibana
至此已经运行了Elasticsearch和Fluentd,数据的采集和汇聚已经完成,接下来使用Kibana展示和操作数据。
Kibana的RC和Service定义如下:
kibana-rc-svc.yaml
---
apiVersion: v1
kind: ReplicationController
metadata:
name: kibana-logging-v1
namespace: kube-system
labels:
k8s-app: kibana-logging
version: v1
kubernetes.io/cluster-service: "true"
spec:
replicas: 1
selector:
k8s-app: kibana-logging
version: v1
template:
metadata:
labels:
k8s-app: kibana-logging
version: v1
kubernetes.io/cluster-service: "true"
spec:
containers:
- image: gcr.io/google_containers/kibana:1.3
name: kibana-logging
resources:
# keep request = limit to keep this container in guaranteed class
limits:
cpu: 100m
requests:
cpu: 100m
env:
- name: "ELASTICSEARCH_URL"
value: "http://elasticsearch-logging:9200"
port:
- containerPort: 5601
name: ui
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: kibana-logging
namespace: kube-system
labels:
k8s-app: kibana-logging
kubernetes.io/cluster-service: "true"
kubernetes.io/name: "Kibana"
spec:
ports:
-port: 5601
protocol: TCP
targetPort: ui
selector:
k8s-app: kibana-logging
通过kubectl create -f kibana-rc-svc.yml命令创建Kibana的RC和Service:
# kubectl create -f kibana-rc-svc.yml
查看Kibana的运行情况:
# kubectl get pods
# kubectl get svc
# kubectl get rc
结果表明运行均已成功。
通过kubectl cluster-info命令获取Kibana服务的URL地址:
# kubectl cluster-info
# kubectl proxy
同样通过kubectl proxy命令启动代理,在出现“Starting to serve on 127.0.0.1:8001”字样之后,
用浏览器访问URL地址http://192.168.18.3:8001/api/v1/proxy/namespaces/kube-system/services/kibana-logging即可访问Kibana页面。
第1次进入页面时需要进行一些设置,如图10.20所示,选择所需选项后单击create按钮。
然后单击discover按钮,就可以正常查询日志了,如图10.21所示。
在搜索栏输入“error”关键字,可以搜索出包含该关键字的日志记录,如图10.22所示。
同时,通过左边菜单中Fields相关的内容对查询的内容进行限定,如图10.23所示。
至此,Kubernetes集群范围内的统一日志收集和查询系统就搭建完成了。

10.10 Kubernetes的审计机制
Kubernetes为了加强对集群操作的安全监管,从1.4版本开始引入审计机制,主要体现为审计日志(Audit Log)。
审计日志按照时间顺序记录了与安全相关的各种事件,这些事件有助于系统管理员快速、集中了解以下问题:
发生了什么事情?
作用于什么对象?
在什么时间发生?
谁(从哪儿)触发的?
在哪儿观察到的?
活动的后继处理行为是怎样的?
下面是两条Pod操作的审计日志示例:
第1条:
2017-03-21T03:57:09.106841886-04:00 AUDIT:
id="xxx" ip="xxx" method="xxx" user="xxx" groups="xxx" as="xxx" asgroups="xxx" namespace="xxx" uri="xxx"
第2条:
2017-03-21T03:57:09.106841886-04:00 AUDIT:
id="xxx" response="200"

API Server把客户端的请求(Request)的处理流程视为一个“链条”,这个链条上的每个“节点”就是一个状态(Stage),从开始到结束的所有Request Stage如下:
RequestReceived:在Audit Handler收到请求后生成的状态。
ResponseStarted:响应的Header已经发送但Body还没有发送的状态,仅对长期运行的请求(Long-running Requests)有效,例如Watch。
ResponseComplete:Body已经发送完成。
Panic:严重错误(Panic)发生时的状态。
Kubernets从1.7版本开始引入高级审计特性(AdvancedAuditing),可以自定义审计策略(选择记录哪些事件)和审计存储后端(日志和Webhook)等,
开启方法为增加kube-apiserver的启动参数--feature-gates=AdvancedAuditing=true。
注意:在开启AdvancedAuditing后,日志的格式有一些修改,例如新增了上述Stage信息;从Kubernets 1.8版本开始,该参数默认为true。
如图10.24所示,kube-apiserver在收到一个请求后(如创建Pod的请求),会根据Audit Policy(审计策略)对此请求做出相应的处理。
我们可以将Audit Policy视作一组规则,这组规则定义了有哪些事件及数据需要记录(审计)。
当一个事件被处理时,规则列表会依次尝试匹配该事件。
第1个匹配的规则会决定审计日志的级别(Audit Level)。
目前定义的几种级别如下(按级别从低到高排列):
None:不生成审计日志。
Metadata:只记录Request请求的元数据如requesting user、timestamp、resource、verb等,但不记录请求及响应的具体内容。
Request:记录Request请求的元数据及请求的具体内容。
RequestResponse:记录事件的元数据,以及请求与应答的具体内容。
None以上的级别会生成相应的审计日志并将审计日志输出到后端,当前的后端实现如下:
(1)Log backend:以本地日志文件记录保存,为JSON日志格式,我们需要对API Server的启动命令设置下列参数。
--audit-log-path:指定日志文件的保存路径。
--audit-log-maxage:设定审计日志文件保留的最大天数。
--audit-log-maxbackup:设定审计日志文件最多保留多少个。
--audit-log-maxsize:设定审计日志文件的单个大小,单位为MB,默认为100MB。
审计日志文件以audit-log-maxsize设置的大小为单位,在写满后,kube-apiserver将以时间戳重命名原文件,然后继续写入audit-log-path指定的审计日志文件;
audit-log-maxbackup和audit-log-maxage参数则用于kube-apiserver自动删除旧的审计日志文件。
(2)Webhook backend:回调外部接口进行通知,审计日志以JSON格式发送(POST方式)给Webhook Server,支持batch和blocking这两种通知模式,相关配置参数如下。
--audit-webhook-config-file:指定Webhook backend的配置文件。
--audit-webhook-mode:确定采用哪种模式回调通知,包括以下选项:
batch:批量模式,缓存事件并以异步批量方式通知,是默认的工作模式。
blocking:阻塞模式,事件按顺序逐个处理,这种模式会阻塞API Server的响应,可能导致性能问题。
blocking-strict:与阻塞模式类似,不同的是当一个Request在RequestReceived阶段发生审计失败时,整个Request请求会被认为失败。
--audit-webhook-initial-backoff:指定回调失败后第1次重试的等待时间,后继重试等待时间则呈指数级递增。
Webhook backend的配置文件采用了kubeconfig格式,主要内容包括远程审计服务的地址和相关鉴权参数,配置示例如下:
clusters:
- name: name-of-remote-audit-service
cluster:
certificate-authority: /path/to/ca.pem #远程审计服务的CA证书
server: https://audit.example.com/audit #远程审计服务URL,必须是HTTPS API server的Webhook配置
users:
- name: name-of-api-server
user:
client-certificate: /path/to/cert.pem #Webhook插件使用的证书文件
client-key: /path/to/key.pem #与证书匹配的私钥文件
current-context: webhook
contexts:
- context:
cluster: name -of-remote-audit-service
user: name-of-api-server
name: webhook
(3)Batching Dynamic backend:一种动态配置的Webhook backend,是通过AuditSink API 动态配置的,在Kubernetes 1.13版本中引入。
需要注意的是,开启审计功能会增加API Server的内存消耗量,
因为此时需要额外的内存来存储每个请求的审计上下文数据,而增加的内存量与审计功能的配置有关,比如更详细的审计日志所需的内存更多。
我们可以通过kube-apiserver中的--audit-policy-file参数指定一个Audit Policy文件名来开启API Server的审计功能。
如下Audit Policy文件可作参考:
apiVersion: audit.k8s.io/v1
kind: Policy
# 对于RequestReceived状态的请求不做审计日志记录
omitStages:
- "RequestReceived"
rules:
# 记录对Pod请求的审计日志,输出级别为RequestResponse
- level: RequestResponse
resources:
- group: "" # core API group
resources: ["pods"]
# 记录对pods/log与pods/status请求的审计日志,输出级别为Metadata
- level: Metadata
resources:
- group: ""
resources: ["pods/log","pods/status"]
# 记录对核心API与扩展API的所有请求,输出级别为Request
- level: Request
resources:
- group: "" # core API group
- group: "extensions" # Version of group should NOT be included
通常审计日志可以以本地日志文件方式保存,然后使用Fluentd作为Agent采集该日志并存储到Elasticsearch,用Kibana等UI界面对日志进行展示和查询。

10.11 使用Web UI(Dashboard)管理集群
Kubernetes的Web UI网页管理工具kubernetes-dashboard可提供部署应用、资源对象管理、容器日志查询、系统监控等常用的集群管理功能。
为了在页面上显示系统资源的使用情况,要求部署Metrics Server,详见10.8节的说明。
可通过https://rawgit.com/kubernetes/dashboard/master/src/deploy/kubernetes-dashboard.yaml页面下载部署kubernetes-dashboard的YAML配置文件。
该配置文件的内容如下,其中包含Deployment和Service的定义:
kubernetes-dashboard.yaml
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: kubernetes-dashboard
namespace: kube-system
labels:
app: kubernetes-dashboard
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: kubernetes-dashboard
template:
metadata:
labels:
app: kubernetes-dashboard
annotations:
scheduler.alpha.kubernetes.io/tolerations: |
[
{
“key”: "dedicated",
“operator”: "Equal",
“value”: "master",
“effect”: "NoSchedule"
}
]
spec:
containers:
- name: kubernetes-dashboard
image: gcr.io/google_containers/kubernetes-dashboard-amd64:v1.6.0
imagePullPolicy: Always
port:
- containerPort: 9090
protocol: TCP
args:
livenessProbe:
httpGet:
path: /
port: 9090
initialDelaySeconds: 30
timeoutSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: kubernetes-dashboard
namespace: kube-system
labels:
app: kubernetes-dashboard
spec:
type: NodePort
ports:
-port: 80
targetPort: 9090
nodePort: 9090
selector:
app: kubernetes-dashboard
这里,Service设置了NodePort映射到物理机的端口号,用于客户端浏览器访问。
使用kubectl create命令进行部署:
# kubectl create -f kubernetes-dashboard.yaml
打开浏览器,输入某Node的IP和9090端口号,例如http://192.168.18.3:9090,就能访问Dashboard的页面了,如图10.25所示。
主页会默认显示RC和Pod列表,并显示各Pod的CPU和内存的性能指标。
单击右上角的“+ CREATE”按钮,将跳转到部署应用的页面。
在这个页面可以通过设置相关参数或者直接通过YAML或JSON文件部署应用,如图10.26所示。
通过左侧的菜单,可以查看Admin、Workloads、Service、Storage、Config等各类资源对象的列表和详细信息。
例如,查看Service列表,如图10.27所示。
在Pod列表中,在各Pod右侧可以查看容器应用的日志,如图10.28所示。

10.12 Helm:Kubernetes应用包管理工具
随着容器技术逐渐被企业接受,在Kubernetes上已经能便捷地部署简单的应用了。
但对于复杂的应用中间件,在Kubernetes上进行容器化部署并非易事,
通常需要先研究Docker镜像的运行需求、环境变量等内容,并能为这些容器定制存储、网络等设置,
最后设计和编写Deployment、ConfigMap、Service及Ingress等相关YAML配置文件,再提交给Kubernetes部署。
这些复杂的过程将逐步被Helm应用包管理工具实现。

10.12.1 Helm概述
Helm是一个由CNCF孵化和管理的项目,用于对需要在Kubernetes上部署的复杂应用进行定义、安装和更新。
Helm以Chart的方式对应用软件进行描述,可以方便地创建、版本化、共享和发布复杂的应用软件。

10.12.2 Helm的主要概念
Helm的主要概念如下:
Chart:一个Helm包,其中包含运行一个应用所需要的工具和资源定义,还可能包含Kubernetes集群中的服务定义,类似于Homebrew中的formula、APT中的dpkg或者Yum中的RPM文件。
Release:在Kubernetes集群上运行的一个Chart实例。
在同一个集群上,一个Chart可以被安装多次。每次安装都会生成新的Release,会有独立的Release名称。
例如有一个MySQL Chart,如果想在服务器上运行两个MySQL数据库,就可以基于这个Chart安装两次。
Repository:用于存放和共享Chart仓库。
简单来说,Helm整个系统的主要任务就是,在仓库中查找需要的Chart,然后将Chart以Release的形式安装到Kubernetes集群中。

10.12.3 安装Helm
Helm由HelmClient和TillerServer两个组件组成,下面讲解这两个组件。
1.HelmClient
HelmClient是一个客户端,拥有对Repository、Chart、Release等对象的管理能力,可以通过二进制文件或脚本进行安装。
通过二进制文件安装时,从https://github.com/kubernetes/helm/releases下载二进制文件,将其解压并复制到执行目录即可。
通过脚本安装的代码如下:
# curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get | bash

2.TillerServer
TillerServer负责客户端指令和Kubernetes集群之间的交互,根据Chart定义,生成和管理各种Kubernetes的资源对象。
对TillerServer的安装可以使用helm init命令进行(官方推荐),
这一命令会在kubectl当前context指定集群内的kube-system命名空间创建一个Deployment和一个Service,运行TillerServer服务。
在Deployment中使用的镜像是gcr.io/kubernetes-helm/tiller:v[helm-version],可以通过helm version命令获得其Helm版本。
如果无法连接互联网获取该镜像,则可以先通过一台能够联网的服务器下载这个镜像并保存到镜像私库,
利用helm init子命令的--tiller-image参数来指定私库中的镜像执行初始化过程。
安装结束之后,用helm version命令验证安装情况,一切正常的话,会分别显示Tiller和Helm的版本信息。
一个常见的问题是,Tiller部分显示一个错误信息“uid:unable to do port forwarding: socat not found”,这是因为所在节点没有socat,无法进行端口转发,这时在主机上安装socat软件即可。
对TillerServer的安装还可以在本地进行,在服务器本地直接运行Tiller,这种安装方式需要让Helm指定要连接的服务地址,有以下两种方法:
使用--host指定Tiller运行的监听地址。
设置HELM_HOST环境变量。
需要注意的是,Tiller仍会使用kubectl配置中的context连接Kubernetes集群。

10.12.4 Helm的常见用法
下面介绍Helm的常见用法,包括搜索Chart、安装Chart、自定义Chart配置、更新或回滚Release、删除Release、创建自定义Chart、搭建私有仓库等。

1.helm search:搜索可用的Chart
Helm在初始化完成之后,默认配置为使用官方的Kubernetes Chart仓库。
官方仓库包含大量的经过组织和持续维护的Chart,这个仓库通常被命名为stable。
使用helm search命令查找可安装的Chart:
# helm search
在没有过滤的情况下,通过helm search命令会显示所有可用的Chart,可以使用参数进行过滤:
# helm search mysql
可以通过helm inspect <chart_name>命令查看Chart的详细信息:
# helm inspect stable/mysql
在找到需要安装的Chart之后,就可以进行安装了。

2.helm install:安装Chart
使用helm install命令安装应用,最简单的参数是Chart的名称。以MariaDB为例:
# helm install stable/mariadb
至此,MariaDB就安装完成了。
可以看到系统创建了一个新的名为womping-bobcat的Release对象,可以在helm install命令中使用--name参数修改其名称。
在安装过程中,Helm客户端会输出一些有用的信息,例如Release的状态及额外的配置步骤等。
Helm不会等待所有创建过程的完成,这是因为有些Chart的Docker镜像较大,会消耗很长的时间进行下载和创建。
在helm install命令的执行过程中,可以使用helm status命令跟踪Release的状态:
# helm status womping-bobcat
在成功安装Chart后,系统会在kube-system命名空间内创建一个ConfigMap用于保存Release对象的数据:
# kubectl get configmap

3.自定义Chart的配置
前面介绍的安装过程使用的是Chart的默认配置。然而在很多情况下,我们希望使用自定义的配置部署应用。
首先,通过helm inspect命令查看Chart的可配置内容:
# helm inspect stable/mariadb
用户可以编写一个YAML配置文件来覆盖上面这些设置,然后利用这一文件来给安装过程提供配置。
例如,我们可以自定义额外的两个配置文件config.yaml和config2.yaml,
用于在执行helm install命令安装MariaDB之后,在MariaDB启动时自动创建名为firstdb和seconddb的数据库,并且设置root用户的密码:
# echo 'mariadbDatabase: firstdb' > config.yaml
# echo 'mariadbRootPassword: abcdefgh' > config2.yaml
# echo 'mariadbDatabase: seconddb' > config2.yaml
# helm install stable/mariadb -f config.yaml -f config2.yaml
在安装完成之后,可以登录MariaDB Pod查看数据库是否创建成功。
自定义Chart的配置有两种方法:
--values或者-f:使用YAML配置文件进行参数配置,可以设置多个文件,最后一个优先生效。
多个文件中的重复value会进行覆盖操作,不同的value会叠加生效。上面的例子使用的就是这种方式。
--set:在命令行中直接设置参数。
如果同时使用两个参数,则--set会以高优先级合并到--values中。

10.12.5 --set的格式和限制
--set参数可以使用0个或多个名称/值的组合,最简单的方式是--set name=value,YAML配置文件中的等效描述是:
name:value
多个值可以使用逗号进行分隔,例如--set a=b,c=d的YAML配置等效于下面的描述:
a:b
c:d
还可以用来表达多层结构的变量--set outer.inner=value:
outer:
inner: value
大括号({})可以用来表达列表数据,例如--set name={a,b,c}会被翻译成:
name:
- a
- b
- c
有时需要在--set时使用一些特殊字符,这里可以使用斜线进行转义,比如--set name=value1\,value2。
类似地,可以对点符号“.”进行转义,这样Chart使用toYaml函数解析注解、标签或者node selector时就很方便了,例如:--set nodeSelector."kubernetes\.io/role"=master。
尽管如此,--set语法的表达能力依然无法和YAML语言相提并论,尤其是在处理集合时。
目前没有方法能够实现“把列表中第3个元素设置为XXX”这样的语法。

10.12.6 更多的安装方法
使用helm install命令时,可以通过多种安装源进行安装:
上面用到的Chart仓库。
本地的Chart压缩包(helm install foo-0.1.1.tgz)。
一个Chart目录(helm install path/to/foo)。
一个完整的URL(helm install https://example.com/charts/foo-1.2.3.tgz)。

10.12.7 helm upgrade和helm rollback:应用的更新或回滚
当一个Chart发布新版本或者需要修改一个Release的配置时,就需要使用helm upgrade命令了。
helm upgrade命令会利用用户提供的更新信息来对Release进行更新。
因为Kubernetes Chart可能会有很大的规模或者相对复杂的关系,所以Helm会尝试进行最小影响范围的更新,只更新相对于上一个Release来说发生变化的内容。
例如,我们要更新一个Release的资源限制,则可以创建config3.yaml配置文件,内容如下:
resources:
requests:
memory: 256Mi
cpu: 500m
使用upgrade命令完成更新:
# helm upgrade -f config3.yaml nomadic-terrier stable/mariadb
看到更新提示之后,我们可以用Helm的list指令查看Release的信息,会发现revision一列发生了变化。
接下来通过kubectl get pods指令可以看到Pod已经更新;通过kubectl describe deploy指令还会看到Deployment的更新过程和一系列的ScalingReplicaSet事件。
如果对更新后的Release不满意,则可以使用helm rollback命令对Release进行回滚,例如:
# helm rollback nomadic-terrier 2
这个命令将把名为nomadic-terrier的Release回滚到版本2。
在执行命令之后,同样可以使用前面提到的几个查询指令,会看到类似的结果。
最后,可以使用helm history <release_name>命令查看一个Release的变更历史。

10.12.8 helm install/upgrade/rollback命令的常用参数
Helm 有很多参数可以帮助用户指导命令的行为。
本节介绍一些常用参数,用户可以使用helm <command> help命令获取所有参数的列表。
--timeout:等待Kubernetes命令完成的时间,单位是s,默认值为300,也就是5min。
--wait:等待Pod,直到其状态变为ready,PVC才完成绑定。
Deployment完成其最低就绪要求的Pod创建,并且服务有了IP地址,才认为Release创建成功。
这一等待过程会一直持续到超过--timeout,超时后这一Release被标记为FAILED。
注意:当Deployment的replicas被设置为1,同时滚动更新策略的maxUnavailable不为0时,--wait才会因为最小就绪Pod数量达成而返回ready状态。
--no-hooks:该命令会跳过Hook执行。
--recreate-pods:会引起所有Pod的重建(Deployment所属的Pod除外)。

10.12.9 helm delete:删除一个Release
通过helm delete命令可以删除一个Release,例如通过helm delete happy-panda命令可以从集群中删除名为happy-panda的Release。
可以通过helm list命令列出在集群中部署的Release。
如果给list加上--deleted参数,则会列出所有删除的Release;--all参数会列出所有的Release,包含删除的、现存的及失败的Release。
正因为Helm会保存所有被删除Release的信息,所以Release的名字是不可复用的,这样被删除的Release也可以被回滚,甚至重新激活。
(如果坚持复用,则可以使用--replace参数,这一操作不建议在生产环境中使用)

10.12.10 helm repo:仓库的使用
我们在前面使用的Chart来自stable仓库。
我们也可以对Helm进行配置,让其使用其他仓库。
Helm在helm repo命令中提供了很多仓库相关的工具。
helm repo list:列出所有仓库。
helm repo add:添加仓库,例如从repo_url中添加名为dev的仓库helm repo add dev http://<repo_url>/dev-charts。
helm repo update:更新仓库中的Chart信息。

10.12.11 自定义Chart
用户可以将自己的应用定义为Chart并进行打包部署,本节对其进行简单介绍,详细的开发指南参见https://docs.helm.sh/developing-charts。
自定义Chart时需要使用符合Helm规范的一组目录和配置文件来完成。

10.12.12 对Chart目录结构和配置文件的说明
Chart是一个包含一系列文件的目录。
目录的名称就是Chart的名称(不包含版本信息),例如一个WordPress的Chart就被会存储在wordpress目录下。
该目录的文件结构如下:
wordpress/
Chart.yaml #用于描述Chart信息的YAML文件
LICENSE #可选:Chart的许可信息
README.md #可选:README文件
values.yaml #默认的配置值
charts/ #可选:包含该Chart所依赖的其他Chart,依赖管理推荐采用requirements.yaml文件进行
templates/ #可选:结合values.yaml,能够生成kubernetes的manifest文件
templates/NOTES.txt #可选:文本文件,用于描述
charts/子目录和requirements.yaml的区别在于,前者需要提供整个Chart文件,后者仅需要注明依赖Chart的仓库信息,
例如一个requirements.yaml可以被定义为:
dependencies:
- name: apache
version: 1.2.3
repository: http://example.com/charts
- name: mysql
version: .7.27
repository: http://another.example.com/charts

10.12.13 对Chart.yaml文件的说明
Chart.yaml文件(注意首字母大写)是个必要文件,包含如下内容。
name:Chart的名称,必选。
version:SemVer 2规范的版本号,必选。
description:项目的描述,可选。
keywords:一个用于描述项目的关键字列表,可选。
home:项目的主页,可选。
sources:一个URL列表,说明项目的源代码位置,可选。
maintainers:维护者列表,可选。
name:管理员的名字,必选。
email:管理员的邮件,必选。
engine:模板引擎的名称,默认是gotpl,可选。
icon:一个指向svg或png图像的URL,作为Chart的图标,可选。
appVersion:在Chart中包含的应用的版本,无须遵循SemVer规范,可选。
deprecated:布尔值,该Chart是否标注为“弃用”,可选。
tillerVersion:可选,该Chart所需的Tiller版本。取值应该是一个SemVer的范围,例如“>2.0.0”。

10.12.14 快速制作自定义的Chart
同其他软件开发过程一样,快速制作一个简单Chart的方法,就是从其他项目中复制并修改。
例如,我们要简单地改写前面MariaDB的Chart,令其使用本地的私有镜像仓库,就可以按照如下步骤进行。
下载Chart:使用helm fetch stable/mariadb命令下载这一Chart的压缩包。
编辑Chart。
利用tar解压之后,我们将目录重新命名为mymariadb。
修改templates中的deployment.yaml,简单地将其中的image字段硬编码为需要的镜像(当然不推荐这种用法,可以继续以变量的方式在values中进行设置)。
将Chart.yaml中的版本号修改为0.1.1,name为mymariadb。
使用helm package mymariadb打包Chart,会生成一个名为mymariadb-0.1.1.tgz的压缩包。
安装Chart:通过helm install mymariadb-0.1.1.tgz命令即可将我们“新”生成的Chart安装到集群中。

10.12.15 搭建私有Repository
在自建Chart之后自然需要搭建私有仓库。
下面使用Apache搭建一个简单的Chart私有仓库,并将刚才新建的mymariadb Chart保存到私有仓库中,详情可参考https://docs.helm.sh/developing-charts/#chart-repo-guide。
Chart仓库主要由前面提到的Chart压缩包和索引文件构成,通过HTTP/HTTPS对外提供服务。这里使用一个Apache应用来提供仓库服务。
Apache的设置如下:
Apache使用/var/web/repo目录进行仓库的存储。
使用http://127.0.0.1/repo网址提供访问服务。
将前面生成的mymariadb-0.1.1.tgz文件复制到仓库的/var/web/repo目录下。
接下来使用helm repo index /var/web/repo --url http://127.0.0.1/repo命令,Helm将自动根据目录下的内容创建索引。
在命令执行完毕后,可以看到在目录下多出一个index.yaml文件。
最后启动Web Server。
为了能够使用这个私有仓库,需要将这个新的仓库地址加入Helm配置中:
# helm repo add localhost http://127.0.0.1/repo
再次运行helm search mysql命令,会看到在Chart列表中多出localhost/mymariadb项目,也就是我们的新仓库中的Chart。
现在可以通过helm install localhost/mymariadb命令安装私有仓库中的Chart了。

posted on 2020-04-24 16:04  Brad Miller  阅读(678)  评论(0编辑  收藏  举报