1.4 Kubernetes基本概念和术语
Kubernetes中的大部分概念如Node、Pod、Replication Controller、Service等都可以看作一种“资源对象”,几乎所有的资源对象都可以通过Kubernetes提供的kubectl工具(或者API编程调用)执行增、删、改、查等操作并将其保存在etcd中持久化存储。从这个角度来看,Kubernetes其实是一个高度自动化的资源控制系统,它通过跟踪对比etcd库里保存的“资源期望状态”与当前环境中的“实际资源状态”的差异来实现自动控制和自动纠错的高级功能。
在介绍资源对象之前,我们先了解一下Kubernetes集群的两种管理角色:Master和Node。
1.4.1 Master
Kubernetes里的Master指的是集群控制节点,每个Kubernetes集群里需要有一个Master节点来负责整个集群的管理和控制,基本上Kubernetes的所有控制命令都发给它,它来负责具体的执行过程,我们后面执行的所有命令基本都是在Master节点上运行的。Master节点通常会占据一个独立的服务器(高可用部署建议用3台服务器),其主要原因是它太重要了,是整个集群的“首脑”,如果宕机或者不可用,那么对集群内容器应用的管理都将失效。
Master节点上运行着以下一组关键进程。
- Kubernetes API Server (kube-apiserver):提供了HTTP Rest接口的关键服务进程,是Kubernetes里所有资源的增、删、改、查等操作的唯一入口,也是集群控制的入口进程。
- Kubernetes Controller Manager (kube-controller-manager):Kubernetes里所有资源对象的自动化控制中心,可以理解为资源对象的“大总管”。
- Kubernetes Scheduler (kube-scheduler):负责资源调度(Pod调度)的进程,相当于公交公司的“调度室”。
另外,在Master节点上还需要启动一个etcd服务,因为Kubernetes里的所有资源对象的数据全部是保存在etcd中的。
Node
除了Master,Kubernetes集群中的其他机器被称为Node节点,在较早的版本中也被称为Minion。与Master一样,Node节点可以是一台物理主机,也可以是一台虚拟机。Node节点才是Kubernetes集群中的工作负载节点,每个Node都会被Master分配一些工作负载(Docker容器),当某个Node宕机时,其上的工作负载会被Master自动转移到其他节点上去。
每个Node节点上都运行着以下一组关键进程。
- kubelet:负责Pod对应的容器的创建、启停等任务,同时与Master节点密切协作,实现集群管理的基本功能。
- kube-proxy:实现Kubernetes Service的通信与负载均衡机制的重要组件。
- Docker Engine (docker):Docker引擎,负责本机的容器创建和管理工作。
Node节点可以在运行期间动态增加到Kubernetes集群中,前提是这个节点上已经正确安装、配置和启动了上述关键进程,在默认情况下kubelet会向Master注册自己,这也是Kubernetes推荐的Node管理方式。一旦Node被纳入集群管理范围,kubelet进程就会定时向Master节点汇报自身的情报,例如操作系统、Docker版本、机器的CPU和内存情况,以及当前有哪些Pod在运行等,这样Master可以获知每个Node的资源使用情况,并实现高效均衡等资源调度策略。而某个Node超过指定时间不上报信息时,会被Master判断为“失联”,Node的状态被标记为不可用(Not Ready),随后Master会触发“工作负载大转移”的自动流程。
我们可以执行下述命令查看集群中有多少个Node:
# kubectl get nodes
NAME STATUS AGE
127.0.0.1 Ready 1d
然后,通过kubectl describe node <node_name>来查看某个Node的详细信息:
# kubectl describe node 127.0.0.1
Name: 127.0.0.1
Role:
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/os=linux
kubernetes.io/hostname=127.0.0.1
Taints: <none>
CreationTimestamp: Mon, 02 Jul 2018 10:11:27 +0800
Phase:
Conditions:
Type Status LastHeartbeatTime LastTransitionTime Reason Message
---- ------ ----------------- ------------------ ------ -------
OutOfDisk False Tue, 03 Jul 2018 15:10:49 +0800 Mon, 02 Jul 2018 10:11:27 +0800 KubeletHasSufficientDisk kubelet has sufficient disk space available
MemoryPressure False Tue, 03 Jul 2018 15:10:49 +0800 Mon, 02 Jul 2018 10:11:27 +0800 KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure False Tue, 03 Jul 2018 15:10:49 +0800 Mon, 02 Jul 2018 10:11:27 +0800 KubeletHasNoDiskPressure kubelet has no disk pressure
Ready True Tue, 03 Jul 2018 15:10:49 +0800 Mon, 02 Jul 2018 10:11:38 +0800 KubeletReady kubelet is posting ready status
Addresses: 127.0.0.1,127.0.0.1,127.0.0.1
Capacity:
alpha.kubernetes.io/nvidia-gpu: 0
cpu: 1
memory: 1883844Ki
pods: 110
Allocatable:
alpha.kubernetes.io/nvidia-gpu: 0
cpu: 1
memory: 1883844Ki
pods: 110
System Info:
Machine ID: f9d400c5e1e8c3a8209e990d887d4ac1
System UUID: 13C940BE-9125-4594-9C8B-82E19C997FF3
Boot ID: 09a9b2bf-14cf-4e32-a724-8b279d44a387
Kernel Version: 3.10.0-514.26.2.el7.x86_64
OS Image: CentOS Linux 7 (Core)
Operating System: linux
Architecture: amd64
Container Runtime Version: docker://1.13.1
Kubelet Version: v1.5.2
Kube-Proxy Version: v1.5.2
ExternalID: 127.0.0.1
Non-terminated Pods: (3 in total)
Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits
--------- ---- ------------ ---------- --------------- -------------
default mysql-pgb63 0 (0%) 0 (0%) 0 (0%) 0 (0%)
default myweb-c994c 0 (0%) 0 (0%) 0 (0%) 0 (0%)
default myweb-jgcqn 0 (0%) 0 (0%) 0 (0%) 0 (0%)
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.
CPU Requests CPU Limits Memory Requests Memory Limits
------------ ---------- --------------- -------------
0 (0%) 0 (0%) 0 (0%) 0 (0%)
No events.
上述命令展示了Node的如下关键信息。
- Node基本信息:名称、标签、创建时间等。
- Node当前的运行状态,Node启动以后会做一系列的自检工作,比如磁盘是否满了,如果满了就标注OutOfDisk=True,否则继续检查内存是否不足(如果内存不足,就标注MemoryPressure=True),最后一切正常,就设置为Ready状态(Ready=True),该状态表示Node处于健康状态,Master将可以在其上调度新的任务了(如启动Pod)。
- Node的主机地址与主机名。
- Node上的资源总量:描述Node可用的系统资源,包括CPU、内存数量、最大可调度Pod数量等,注意到目前Kubernetes已经实验性地支持GPU资源分配了(alpha.kubernetes.io/nvidia-gpu=0)。
- Node可分配资源量:描述Node当前可用于分配等资源量。
- 主机系统信息:包括主机等唯一标识UUID、Linux kernel版本号、操作系统类型与版本、Kubernetes版本号、kubelet与kube-proxy的版本号等。
- 当前正在运行等Pod列表概要信息。
- 已分配的资源使用概要信息,例如资源申请的最低、最大允许使用量占系统总量等百分比。
- Node相关的Event信息。
1.4.3 Pod
Pod是Kubernetes的最重要也最基本的概念,如图1.4所示是Pod的组成示意图,我们看到每个Pod都有一个特殊的被成为“根容器”的Pause容器。Pause容器对应的镜像属于Kubernetes平台的一部分,除了Pause容器,每个Pod还包含一个或多个紧密相关的用户业务容器。 Pod的组成.png为什么Kubernetes会设计出一个全新的Pod概念并且Pod有这样特殊的组成结构?
原因之一:在一组容器作为一个单元的情况下,我们难以对“整体”简单地进行判断及有效地进行行动。比如,一个容器死亡了,此时算是整体死亡么?引入业务无关并且不易死亡的Pause容器作为Pod的根容器,以它的状态代表整体容器组的状态,就简单、巧妙地解决了这个难题。
原因之二:Pod里的多个业务容器共享Pause容器的IP,共享Pause容器挂接的Volume,这样既简化了密切关联的业务容器之间的通信问题,也很好地解决了它们之间的文件共享问题。
Kubernetes为每个Pod都分配了唯一的IP地址,称之为Pod IP,一个Pod里的多个容器共享Pod IP地址。Kubernetes要求底层网络支持集群内任意两个Pod之间的TCP/IP直接通信,这通常采用虚拟而层网络技术来实现,例如Flannel、Open vSwitch等,因此我们需要牢记一点:在Kubernetes里,一个Pod里的容器与另外主机上的Pod容器能够直接通信。
Pod其实有两种类型:普通的Pod及静态Pod(Static Pod),后者比较特殊,它并不存放在Kubernetes的etcd存储里,而是存放在某个具体的Node上的一个具体文件中,并且只在此Node上启动运行。而普通的Pod一旦被创建,就会被放入到etcd中存储,随后会被Kubernetes Master调度到某个具体的Node上并进行绑定(Binding),随后该Pod被对应的Node上的kubelet进程实例化成一组相关的Docker容器并且启动起来。在默认情况下,当Pod里的某个容器停止时,Kubernetes会自动检测到这个问题并且重新启动这个Pod(重启Pod里的所有容器),如果Pod所在的Node宕机,则会将这个Node上的所有Pod重新调度到其他节点上。Pod、容器与Node的关系图如图1.5所示。 Pod、容器与Node的关系.pngKubernetes里的所有资源对象都可以采用yaml或者JSON格式的文件来定义或描述,下面是我们在之前Hello World例子里用到的myweb这个Pod的资源定义文件:
apiVersion: v1
kind: Pod
metadata:
name: myweb
spec:
replicas: 2
selector:
app: myweb
template:
metadata:
labels:
app: myweb
spec:
containers:
- name: myweb
image: kubeguide/tomcat-app:v1
ports:
- containerPort: 8080
env:
- name: MYSQL_SERVICE_HOST
value: 'mysql'
- name: MYSQL_SERVICE_PORTT
value: '3306'
Kind为Pod表明这是一个Pod的定义,metadata里的name属性为Pod的名字,metadata里还能定义资源对象的标签(Label),这里表明myweb拥有一个name=myweb的标签(Label)。Pod里所包含的容器组的定义则在spec一节中声明,这里定义了一个名字为myweb、对应镜像为kubeguide/tomcat-app:v1的容器,该容器注入了名为MYSQL_SERVICE_HOST='mysql'和MYSQL_SERVICE_PORT='3306'的环境变量(env关键字),并且在8080端口(containerPort)上启动容器进程。Pod的IP加上这里的容器端口(containerPort),就组成了一个新的概念--Endpoint,它代表着此Pod里的一个服务进程的对外通信地址。一个Pod也存在着具有多个Endpoint的情况,比如当我们把Tomcat定义为一个Pod时,可以对外暴露管理端口与服务端口这两个Endpoint。
我们所熟悉的Docker Volume在Kubernetes里也有对应的概念--Pod Volume,后者有一些扩展,比如可以用分布式文件系统GlusterFS实现后端存储功能;Pod Volume是定义在Pod之上,然后被各个容器挂载到自己的文件系统忠的。
这里顺便提一下Kubernetes的Event概念,Event是一个事件的记录,记录了事件的最早产生时间、最后重现时间、重复次数、发起者、类型,以及导致此事件的原因等众多信息。Event通常会关联到某个具体的资源对象上,是排查故障的重要参考信息,之前我们看到Node的描述信息包括了Event,而Pod同样有Event记录,用来发现某个Pod迟迟无法创建时,可以用kubectl describe pod xxxx来查看它的描述信息,用来定位问题的原因,比如下面这个Event记录信息表明Pod里的一个容器被探针检测失败一次:
Events:
FirstSeen LastSeen Count From SubobjectPath Type Reason Message
--------- -------- ----- ---- ------------- ---- ------ -------
10h 12m 32 {kubelet k8s-node-1} spec.container{kube2sky} Warning Unhealthy Liveness probe failed: Get http://172.17.1.2:8080/healthz:net/http: request canceled {Client.Timeout exceeded while awaiting headers}
每个Pod都可以对其能使用的服务器上的计算资源设置限额,当前可以设置限额的计算资源有CPU与Memory两种,其中CPU的资源单位为CPU (Core)的数量,是一个绝对值而非相对值。
一个CPU的配额对于绝大多数容器来说是相当大的一个资源配额了,所以,在Kubernetes里,通常以千分之一的CPU配额为最小单位,用m来表示。通常一个容器的CPU配额被定义为100~300m,即占用0.1~0.3个CPU。由于CPU配额是一个绝对值,所以无论在拥有一个Core的机器上,还是在拥有48个Core的机器上,100m这个配额所代表的CPU的使用量都是一样的。与CPU配额类似,Memory配额也是一个绝对值,它的单位是内存字节数。
在Kubernetes里,一个计算资源进行配额限定需要设定以下两个参数。
- Request:该资源的最小申请量,系统必须满足要求。
- Limits:该资源最大允许使用量,不能被突破,当容器试图使用超过这个量的资源时,可能会被Kubernetes Kill并重启。
通常我们会把Request设置为一个比较小的数值,符合容器平时的工作负载情况下的资源需求,而把Limit设置为负载均衡情况下资源占用的最大量。比如下面这些定义,表明MySQL容器申请最少0.25个CPU及64MiB内存,在运行过程中MySQL容器所能使用的资源配额为0.5个CPU及128MiB内存:
spec:
containers:
- name: db
image: mysql
resources:
request:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
本节最后笔者给出Pod及Pod周边对象的示意图作为总结,如图1.6所示,后面部分涉及这张图里的对象和概念,以进一步加强理解。
Pod及周边对象.png
1.4.4 Laebl(标签)
Label是Kubernetes系统中另外一个核心概念。一个Label是一个key=value的键值对,其中key与vaue由用户自己指定。Label可以附加到各种资源对象上,例如Node、Pod、Service、RC等,一个资源对象可以定义任意数量的Label,同一个Label也可以被添加到任意数量的资源对象上去,Label通常在资源对象定义时确定,也可以在对象创建后动态添加或者删除。
我们可以通过指定的资源对象捆绑一个或多个不同的Label来实现多维度的资源分组管理功能,以便于灵活、方便地进行资源分配、调度、配置、部署等管理工作。例如:部署不同版本的应用到不同的环境中;或者监控和分析应用(日志记录、监控、告警)等。一些常用等label示例如下。
- 版本标签:"release" : "stable" , "release" : "canary"...
- 环境标签:"environment" : "dev" , "environment" : "production"
- 架构标签:"tier" : "frontend" , "tier" : "backend" , "tier" : "middleware"
- 分区标签:"partition" : "customerA" , "partition" : "customerB"...
- 质量管控标签:"track" : "daily" , "track" : "weekly"
Label相当于我们熟悉的“标签”,給某个资源对象定义一个Label,就相当于給它打了一个标签,随后可以通过Label Selector(标签选择器)查询和筛选拥有某些Label的资源对象,Kubernetes通过这种方式实现了类似SQL的简单又通用的对象查询机制。
Label Selector可以被类比为SQL语句中的where查询条件,例如,name=redis-slave这个label Selector作用于Pod时,可以被类比为select * from pod where pod's name = 'redis-slave'这样的语句。当前有两种Label Selector的表达式:基于等式的(Equality-based)和基于集合的(Set-based),前者采用“等式类”的表达式匹配标签,下面是一些具体的例子。
- name=redis-slave:匹配所有具有标签name=redis-slave的资源对象。
- env != production:匹配所有不具有标签env=production的资源对象,比如env=test就满足此条件的标签之一。
- name in (redis-master):匹配所有具有标签name=redis-master或者name=redis-slave的资源对象。
- name not in (php-frontend):匹配所有不具有标签name=php-frontend的资源对象。
可以通过多个Label Selector表达式的组合实现复杂的条件,多个表达式之间用“,”进行分隔即可,几个条件之间是“AND”的关系,即同时满足多个条件,比如下面的例子:
name=redis-slave,env!=production
name notin (php-fronted),env!=production
以myweb Pod为例,Label定义在其metadata中:
apiVersion: v1
kind: Pod
metadata:
name: myweb
labels:
app: myweb
管理对象RC和Service在spec中定义Selector与Pod进行关联:
apiVersion: v1
kind: ReplicationController
metadata:
name: myweb
spec:
replicas: 1
selector:
app: myweb
template:
...略...
apiVersion: v1
kind: Service
metadata:
name: myweb
spec:
selector:
app: myweb
ports:
- port: 8080
新出现的管理对象如Deploment、ReplicaSet、DaemonSet和Job则可以在Selector中使用基于集合的筛选条件定义,例如:
selector:
matchLabels:
app: myweb
matchExpressions:
- {key: tier, operator: In, values: [frontend]}
- {key: environment, operator: NorIn, values: [dev]}
matchLabels用于定义一组Label,与直接写在Selector中作用相同:matchExpression用于定义一组基于集合的筛选条件,可用的条件运算符包括:In、NotIn、Exists和DoesNotExist。
如果同时设置了matchLabels和matchExpression,则两组条件为“AND”关系,即所有条件需要满足才能完成Selector的筛选。
Label Selector在Kubernetes中多重要使用场景有以下几处。
- kube-controller进程通过资源对象RC上定义都Label Selector来筛选要监控的Pod副本的数量,从而实现Pod副本的数量始终符合预期设定的全自动控制流程。
- kube-proxy进程通过Service的Label Selector来选择对应的Pod,自动建立起每个Service到对应Pod的请求转发路由表,从而实现Service的智能负载均衡机制。
- 通过对某些Node定义特定的Label,并且在Pod定义文件中使用NodeSelector这种标签调度策略,kube-scheduler进程可以实现Pod“定向调度”的特性。
在前面的留言板例子中,我们只使用了一个name=XXX的Label Selector。让我们看一个更复杂的例子。假设为Pod定义了Label: release、env和role,不同的Pod定义了不同的Label值,如图1.7所示,如果我们设置了“role=frontend”的Label Selector,则会选取到Node 1和Node 2上到Pod。
Label Selector的作用范围 1.png
而设置“release=beta”的Label Selector,则会选取到Node 2和Node 3上的Pod,如图1.8所示。
总结:使用Label可以給对象创建多组标签,Label和Label Selector共同构成了Kubernetes系统中最核心的应用模型,使得被管理对象能够被精细地分组管理,同时实现了整个集群的高可用性。
Label Selector的作用范围 2.png
1.4.5 Replication Controller
上一节的例子中已经对Replication Controller(简称RC)的定义和作用做了一些说明,本节对RC的概念进行深入描述。
RC是Kubernetes系统中的核心概念之一,简单来说,它其实是定义了一个期望的场景,即声明某种Pod的副本数量在任意时刻都符合某个预期值,所以RC的定义包括如下几个部分。
- Pod期待的副本数(replicas)。
- 用于筛选目标Pod的Label Selector。
- 当Pod的副本数量小于预期数量时,用于创建新Pod的Pod模版(template)。
下面是一个完整的RC定义的例子,即确保拥有tier=frontend标签的这个Pod(运行Tomcat容器)在整个Kubernetes集群中始终只有一个副本:
apiVersion: v1
kind: ReplicationController
metadata:
name: frontend
spec:
replicas: 1
selector:
tier: frontend
template:
metadata:
labels:
app: app-demo
tier: frontend
spec:
containers:
- name: tomcat-demo
image: tomcat
ports:
imagePullPolicy: IfNotPresent
env:
- name: GET_HOSTS_FROM
value: dns
ports:
- containerPort: 80
当我们定义了一个RC并提交到Kubernetes集群中以后,Master节点上的Controller Manager组件就得到通知,定期巡检系统中当前存活的目标Pod,并确保目标Pod实例的数量刚好等于此RC的期望值,如果有过多的Pod副本在运行,系统就会停掉一些Pod,否则系统就会再自动创建一些Pod。可以说,通过RC,Kubernetes实现了用户应用集群的高可用性,并且大大减少了系统管理员在传统IT环境中需要完成的许多手工运维工作(如主机监控脚本、应用监控脚本、故障恢复脚本等)。
下面我们以3个Node节点的集群为例,说明Kubernetes如何通过RC来实现Pod副本数量自动控制的机制。假如我们的RC里定义redis-slave这个Pod需要保持3个副本,系统将可能在其中的两个Node上创建Pod。图1.9描述了在两个Node上创建redis-slave Pod的情形。
假设Node2上的Pod2意外终止,根据RC定义的replicas数量2,Kubernetes将会自动创建并启动一个新的Pod,以保住整个集群中始终有两个redis-slave Pod在运行。
如图1.10所示,系统可能选择Node3或者Node1来创建一个新的Pod。
根据RC定义创建新的Pod.png
此外,在运行时,我们可以通过修改RC的副本数量,来实现Pod的动态缩放(Scaling)功能,还可以通过执行kubectl scale命令来一键完成:
$kubectl scale rc redis-slave --replicas=3
scaled
Scaling的执行结果如图1.11所示。
需要注意的是,删除RC并不会影响通过该RC已创建好的Pod。为了删除所有Pod,可以设置replicas的值为0,然后更新该RC。另外,kubectl提供了stop和delete命令来一次性删除RC和RC控制的全部Pod。
当我们的应用升级时,通常会通过Build一个新的Docker镜像,并用新的镜像版本来替代旧的版本的方式达到目的,在系统升级的过程中,我们希望是平滑的方式,比如当前系统中10个对应的旧版本的Pod,最佳的方式是旧版本的Pod每次停止一个,同时创建一个新版本的Pod,在整个升级过程中,此消彼长,而运行中的Pod数量始终是10个,几分钟以后,当所有的Pod都已经是最新版本时,升级过程完成。通过RC的机制,Kubernetes很容易就实现了这种高级实用的特性,被称为“滚动升级”(Rolling Update),具体的操作方法详见第四章。
由于Replication Controller与Kubernetes代码中的模块Replication Controller同名,同时这个词也无法准确表达它的本意,所以在Kubernetes v1.2时,它就升级成了另外一个新的概念--Replica Set,官方解释为“下一代的RC”,它与RC当前存在的唯一区别是:Replica Sets支持基于集合的Label selector(Set-based selector),而RC只支持基于等式的Label Selector(equality-based selector),这使得Replica Set的功能更强,下面是等价于之前RC例子的Replica Set的定义(省去了Pod模版部分的内容):
apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
name: frontend
spec:
selector:
matchLabels:
tier: frontend
matchExpression:
- {key: tier, operator: In, values: [frontend]}
template:
.......
kubectl命令工具适用于RC的绝大部分命令都同样适用于Replica Set。此外,当前我们很少单独使用Replica Set,它主要被Deployment这个更高层的资源对象所使用,从而形成一整套Pod创建、删除、更新的编排机制。当我们使用Deployment时,无须关心它是如何创建和维护Replica Set的,这一切都是自动发生的。
Replica Set与Deployment这两个重要资源对象逐步替换了之前的RC的作用,是Kubernetes v1.3里Pod自动扩容(伸缩)这个告警功能实现的基础,也将继续在Kubernetes未来的版本中发挥重要的作用。
最后我们总结一下关于RC(Replica Set)的一些特性与作用。
- 在大多数情况下,我们通过定义一个RC实现Pod的创建过程及副本数量的自动控制。
- RC里包括完整的Pod定义模版。
- RC通过Label Selector机制实现对Pod副本的自动控制。
- 通过改变RC里的Pod副本数量,可以实现Pod的扩容或缩容功能。
- 通过改变RC里的Pod模版中的镜像版本,可以实现Pod的滚动升级功能。
1.4.6 Deployment
Deployment是Kubernetes v1.2引入的概念,引入的目的是为了更好地解决Pod的编排问题。为此,Deployment在内部使用了Replica Set来实现目的,无论从Deployment的作用与目的,它的YAML定义,还是从它的具体命令行操作来看,我们都可以把它看作RC的一次升级,两者相似度超过90%。
Deployment相对于RC的一个最大升级是我们随时知道当前Pod“部署”的进度。实际上由于一个Pod的创建、调度、绑定节点及在目标Node上启动对应的容器这一完整过程需要一定的时间,所以我们期待系统启动N个Pod副本的目标状态,实际上是一个连续变化的“部署过程”导致的最终状态。
Deployment的典型使用场景有以下几个。
- 创建一个Deployment对象来生成对应的Replica Set并完成Pod副本的创建过程。
- 检查Deployment的状态来看部署动作是否完成(Pod副本的数量是否达到预期的值)。
- 更新Deployment以创建新的Pod(比如镜像升级)。
- 如果当前Deployment不稳定,则回滚到一个早先的Deployment版本。
- 暂停Deployment以便于一次性修改多个PodTemplateSpec的配置项,之后再恢复Deployment,进行新的发布。
- 扩展Deployment以应对高负载。
- 查看Deployment的状态,以此作为发布是否成功的指标。
- 清理不再需要的旧版本ReplicaSets。
Deployment的定义与Replica Set的定义很类似,除了API声明与Kind类型等有所区别:
apiVersion: extensions/v1beta1 apiVersion: v1
kind: Deployment kind: ReplicaSet
metadata: metadata:
name: nginx-deployment name: nginx-repset
下面我们通过运行一些例子来一起直观地感受这个新概念。首先创建一个名为tomcat-deployment.yaml的Deployment描述文件,内容如下:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 1
selector:
matchLabels:
tier: frontend
matchExpressions:
- {key: tier, operator: In, values: [frontend]}
template:
metadata:
labels:
app: app-demo
tier: frontend
spec:
containers:
- name: tomcat-demo
image: tomcat
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
运行下述命令创建Deploymeny:
# kubectl create -f tomcat-deployment.yaml
deployment "tomcat-deploy" created
运行下述命令查看Deployment的信息:
# kubectl get deployments
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
tomcat-deploy 1 1 1 1 4m
对上述输出中涉及的数量解释如下:
- DESIRED:Pod副本数量的期望值,即Deployment里定义的Replica。
- CURRENT:当前Replica的值,实际上是Deployment所创建的Replica Set里的Replica值,这个值不断增加,直到达到DESIRED为止,表明整个部署过程完成。
- UP-TO-DATE:最新版本的Pod副本数量,用于指示在滚动升级的过程中,有多少个Pod副本已经成功升级。
- AVAILABLE:当前集群中可用的Pod副本数量,即集群中当前存活的Pod数量。
运行下述命令查看对应的Replica Set,我们看到它的命名与Deployment的名字有关系:
# kubectl get rs
NAME DESIRED CURRENT AGE
tomcat-deploy-1640611518 1 1 1m
运行下述命令查看创建的Pod,我们发现Pod的命名以Deployment对应的Replica Set的名字为前缀,这种命名很清晰地表明了一个Replica Set创建了哪些Pod,对于滚动升级这种复杂的过程来说,很容易排查错误:
# kubectl get pods
NAME READY STATUS RESTARTS AGE
tomcat-deploy-1640611518-zhrsc 1/1 Running 0 3m
运行kubectl describe deployments,可以清楚地看到Deployment控制的Pod的水平扩展过程,详见第2章的说明,这里不再赘述。
Pod的管理对象,除了RC和Deployment,还包括ReplicaSet、DaemonSet、StatefulSet、Job等,分别用于不同的应用场景中,将在第2章中进行详细介绍。
1.4.7 Horizotal(水平的) Pod Autoscaler(自动伸缩器)
在前两节提到过,通过手工执行kubectl scale命令,我们可以实现Pod扩容或缩容,如果仅仅到此为止,显然不符合谷歌对Kubernetes的定位目标----自动化、智能化。在谷歌看来,分布式系统要能够根据当前负载的变化情况自动触发水平扩展或缩容的行为,因为这一过程可能是频繁发生的、不可预料的,所以手动控制的方式是不现实的。
因此,Kubernetes的v1.0版本实现后,这帮大牛们就已经在默默研究Pod智能扩容的特性了,并在Kubernetes v1.1的版本中首先发布这一重量级新特性----Horizotal Pod Autoscaler(Pod横向自动扩容,简称HPA)。随后的v1.2版本中HPA被升级为稳定版本(apiVersion:autoscaling/v1),但同时仍然保留旧版本(apiVersion:extensions/v1beta1)。从v1.6版本开始,对根据应用自定义指标进行自动扩容和缩容的功能进行增强,API版本为autoscaling/v2alpha1,仍在不断演进过程中。
HPA与之前的RC、Deployment一样,也属于一种Kubernetes资源对象。通过追踪分析RC控制的所有目标Pod的负载变化情况,来确定是否需要针对性地调整目标Pod的副本数,这是HPA的实现原理。当前,HPA可以有以下两种方式作为Pod负载的度量指标。
- CPUUtilizationPercentage。
- 应用程序自定义的度量指标,比如服务在每秒内的相应的请求数(TPS或QPS)。
CPUUtilizationPercentage是一个算数平均值,即目标Pod所有副本自身的CPU利用率的平均值。一个Pod自身的CPU利用率是该Pod当前CPU的使用量除以它的Pod Request的值,比如我们定义一个Pod的Pod Request为0.4,而当前Pod的CPU使用量为0.2,则它的CPU使用率为50%。如此一来,我们就可以就算出来一个RC控制的所有Pod副本的CPU利用率的算术平均值了。如果某一时刻CPUUtilizationPercentage的值超过80%,则意味着当前的Pod的副本数很可能不足以支撑接下来更多的请求,需要进行动态扩容,而当请求高峰时刻过去后,Podd的CPU利用率又会降下来,此时对应的Pod副本数应该自动减少到一个合理的水平。
CPUUtilizationPercentage计算过程中使用到的Pod的CPU使用量通常1min内的平均值,目前通过查询Heapster扩展组件来得到这个值,所以需要安装部署Heapster,这样一来便增加了系统的复杂度和实施HPA特性的复杂度,因此,未来的计划是Kubernetes自身实现一个基础性能数据采集模块,从而更好地支持HPA和其他需要用到基础性能数据的功能模块。此外,我们也看到,如果目标Pod没有定义Pod Request的值,则无法使用CPUUtilizationPercentage来实现Pod横向自动扩容的能力。除了使用CPUUtilizationPercentage,Kubernetes从v1.2版本开始尝试支持应用程序自定义的度量指标,目前仍然为实验特性,不建议在生产环境中使用。
下面是HPA定义的一个具体例子:
apiVersion: autoscaling/v1
kind: HorizotalPodAutoscaler
metadata:
name: php-apache
namesapce: default
spec:
maxReplicas: 10
minReplicas: 1
scaleTargeRef:
kind: Deployment
name: php-apache
targetCPUUtilizationPercentage: 90
根据上面的定义,我们可以知道这个HPA控制的目标对象为一个名为php-apache的Deployment里d的Pod副本,当这些Pod副本的CPUUtilizationPercentage的值超过90%时会触发自动扩容行为,扩容或缩容时必须满足的一个约束条件是Pod的副本数要介于1与10之间。
除了可以通过直接定义yaml文件并且调用kubectl create的命令来创建一个HPA资源对象的方式,我们还能通过下面的简单命令行直接创建等价的HPA对象:
# kubectl autoscale deployment php-apache --cpu-percent=90 --min=1 --max=10
第二章将会给出一个完整的HPA例子来说明其用法和功能。
1.4.8 StatefulSet
在Kubernetes系统中,Pod的管理对象RC、Deployment、DaemonSet和Job都是面向无状态的服务。但现实中有很多服务是有状态的,特别是一些复杂的中间件集群,例如MySQL集群、MongoDB集群、Kafka集群、Zookeeper集群等,这些应用集群有以下一些共同点。
- 每个节点都有固定的身份ID,通过这个ID,集群中的成员可以相互发现并且通信。
- 集群的规模是比较固定的,集群规模不能随意变动。
- 集群里的每个节点都是有状态的,通常会持久化数据到永久存储中。
- 如果磁盘损坏,则集群里的某个节点无法正常运行,集群功能受损。
如果用RC/Deployment控制Pod副本数的方式来实现上述有状态的集群,则我们会发现第一点是无法满足的,因为Pod的名字是随机产生的,Pod的IP地址也是在运行期才确定且可能有变动的,我们事先无法为每个Pod确定唯一不变的ID,为了能够在其他节点上恢复某个失败的节点,这种集群中的Pod需要挂接某种共享存储,为了解决这个问题,Kubernetes从v1.4版本开始引入了PetSet这个新的资源对象,并且在v1.5版本时更名为StatefulSet,StatefulSet从本质上来说,可以看作Deployment/RC的一个特殊变种,它有如下一些特性。
- StatefulSet里的每个Pod都有稳定、唯一的网络标识,可以用来发现集群内的其他成员。假设StatefulSet的名字叫kafka,那么第一个Pod叫kafak-0,第二个Pod叫kafak-1,以此类推。
- StatefulSet控制的Pod副本的启停顺序是受控的,操作第n个Pod时,前n-1个Pod已经时运行且准备好的状态。
- StatefulSet里的Pod采用稳定的持久化存储卷,通过PV/PVC来实现,删除Pod时默认不会删除与StatefulSet相关的存储卷(为了保证数据的安全)。
StatefulSet除了要与PV卷捆绑使用以存储Pod的状态数据,还要与Headless Service配合使用,即在每个StatefulSet的定义中要声明它属于哪个Headless Service。Headless Service与普通Service的关键区别在于,它没有Cluster IP,如果解析Headless Service的DNS域名,则返回的是该Service对应的全部Pod的Endpoint列表。StatefulSet在Headless Service的基础上又为StatefulSet控制的每个Pod实例创建了一个DNS域名,这个域名的格式为:
$(podname).$(headless service name)
比如一个3节点的Kafka的StatefulSet集群,对应的Headless Service的名字为kafka,StatefulSet的名字为kafka,则StatefulSet里面的3个Pod的DNS名称分别为kafka-0.kafka、kafka-1.kafka、kafka-3.kafka,这些DNS名称可以直接在集群的配置文件中固定下来。
1.4.9 Service(服务)
1.概述
Service也是Kubernetes里的最核心的资源对象之一,Kubernetes里的每个Service其实就是我们经常提起的微服务架构中的一个“微服务”,之前我们所说的Pod、RC等资源对象其实都是为这节所说的“服务”------Kubernetes Service作“嫁衣”的。图1.12显示了Pod、RC与Service的逻辑关系。
从图1.12中我们看到,Kubernetes的Service定义了一个服务的访问入口地址,前端的应用(Pod)通过这个入口地址访问其背后的一组由Pod副本组成的集群实例,Service与其后端Pod副本集群之间则是通过Label Selector来实现“无缝对接”的。而RC的作用实际上是保证Service的服务能力和服务质量始终处于预期的标准。
通过分析、识别并建模系统中的所有服务为微服务-----Kubernetes Service,最终我们的系统由多个提供不同业务能力而又彼此独立的微服务单元所组成,服务之间通过TCP/IP进行通信,从而形成了我们强大而又灵活的弹性网格,拥有了强大的分布式能力、弹性扩展能力、容错能力,于此同时,我们的程序架构也变得简单和直观许多,如图1.13所示。
Kubernetes所提供的微服务网格架构.png
既然每个Pod都会被分配一个单独的IP地址,而且每个Pod都提供了一个独立的Endpoint(Pod IP+ContainerPort)以被客户端访问,现在多个Pod副本组成了一个集群来提供服务,那么客户端如何来访问它们呢?一般的做法是部署一个负载均衡器(软件或硬件),为这组Pod开启一个对外的服务端口如8000端口,并且将这些Pod的Endpoint列表加入8000端口的转发列表中,客户端就可以通过负载均衡器的对外IP地址+服务端口来访问服务,而客户端的请求最后会被zhaun fa转发到哪个Pod,则由负载均衡器的算法所决定。
Kubernetes也遵循了上述常规做法,运行在每个Node上的kube-proxy进程其实就是一个智能的软件负载均衡器,它负责把对Service的请求转发到后端的某个Pod实例上,并在内部shi xian实现服务的负载均衡与会话机制。但Kubernetes发明了一种很巧妙又影响深远的设计:Service不是共用一个负载均衡的IP地址,而是每个Service分配了全局唯一的虚拟IP地址,这个虚拟IP地址被称为Cluster IP。这样一来,每个服务就变成了具备唯一IP地址的“通信节点”,服务调用就变成了最基础的TCP网络通信问题。
我们知道,Pod的Endpoint地址会随着Pod的销毁和重新创建而发生改变,因为新Pod的IP地址与之前旧Pod的不同。而Service一旦被创建,Kubernetes就会自动为它分配一个可用的Cluster IP,而且在Service的整个生命周期内。它的Cluster IP不会发生改变。于是,服务发现这个棘手的问题在Kubernetes的架构里也得到轻松解决:只要用Service的Name与Service的Cluster IP地址做一个DNS域名映射即可完美解决问题。现在想想,这真是一个很棒的设计。
说了这么久,下面我们动手创建一个Service,来加深对它的理解。首先我们创建一个名为tomcat-service.yaml的定义文件,内容如下:
apiVersion: v1
kind: Service
metadata:
name: tomcat-service
spec:
ports:
- port: 8080
selector:
tier: frontend
上述内容定义了一个名为“tomcat-service”的Service,它的服务端口为8080,拥有“tier-frontend”这个Label的所有Pod实例都属于它,运行下面的命令进行创建:
# kubectl create -f tomcat-service.yaml
service "tomcat-service" created
注意到我们之前在tomcat-deployment.yaml里定义的Tomcat的Pod刚好拥有这个标签,所以我们刚才创建的tomcat-service已经对应到了一个Pod实例,运行下面的命令可以查看tomcat-service的Endpoint列表,其中172.17.1.3是Pod的IP地址,端口8080是Container暴露的端口:
# kubectl get endpoints
NAME ENDPOINTS AGE
kubernetes 192.168.18.131:6443 15d
tomcat-service 172.17.1.3:8080 1m
你可能有疑问:“说好的Service的Cluster IP呢?怎么没有看到?”我们运行下面的命令即可看到tomcat-service被分配的Cluster IP及更多的信息:
# kubectl get svc tomcat-service -o yaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: 2018-10-17T10:04:21Z
name: tomcat-service
namespace: default
resourceVersion: "10169415"
selfLink: /api/v1/namespaces/default/services/tomcat-service
uid: 04caf53f-d1f4-11e8-83a3-5254008f2a0b
spec:
clusterIP: 10.254.169.39
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
tier: frontend
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}
在spec.ports的定义中,targetPort属性用来确定提供该服务的容器所暴露(EXPOSE)的端口号,即具体业务进程在容器内的targetPort上提供TCP/IP接入;而port属性则定义了Service的虚拟端口。前面我们定义Tomcat服务时,没有指定targetPort,则默认targetPort与port相同。
接下来,我们来看看Service的多端口问题。
很多服务都存在多个端口的问题,通常一个端口提供业务服务,另外一个端口提供管理服务,比如Mycat、Codis等常见中间件。Kubernetes Service支持多个Endpoint,在存在多个Endpoint的情况下,要求每个Endpoint定义一个名字区分。下面是Tomcat多端口的Service定义样例:
apiVersion: v1
kind: Service
metadata:
name: tomcat-service
spec:
ports:
- port: 8080
name: service-port
- port: 8005
name: shutdown-port
selector:
tier: frontend
多端口为什么需要給每个端口命名呢?这就涉及Kubernetes的服务发现机制了,我们接下来进行讲解。
2.Kubernetes的服务发现机制
任何分布式系统都会涉及“服务发现”这个基础问题,大部分分布式系统通过提供特定的API接口来实现服务发现的功能,但这样做会导致平台的入侵性比较强,也增加了开发测试的困难。Kubernetes则采用了直观朴素的思路去解决这个棘手的问题。
首先,每个Kubernetes中的Service都有一个唯一的Cluster IP及唯一的名字,而名字是由开发者自己定义的,部署时也没有改变,所以完全可以固定在配置中。接下来的问题就是如何通过Service的名字找到对应的Cluster IP?
最早时Kubernetes采用了Linux环境变量的方式解决这个问题,即每个Service生成一些对应的Linux环境变量(ENV),并在每个Pod的容器在启动时,自动注入这些环境变量,以下是tomcat-service产生的环境变量条目:
TOMCAT_SERVICE_SERVICE_HOST=10.254.93.4
TOMCAT_SERVICE_SERVICE_PORT_SERVICE_PORT=8080
TOMCAT_SERVICE_SERVICE_PORT_SHUTDOWN_PORT=8005
TOMCAT_SERVICE_SERVICE_PORT=8080
TOMCAT_SERVICE_PORT=tcp://10.254.93.4:8080
TOMCAT_SERVICE_PORT_8080_TCP_ADDR=10.254.93.4
TOMCAT_SERVICE_PORT_8080_TCP=tcp://10.254.93.4:8080
TOMCAT_SERVICE_PORT_8080_TCP_PROTO=tcp
TOMCAT_SERVICE_PORT_8080_TCP_PORT=8080
TOMCAT_SERVICE_PORT_8005_TCP=tcp://10.254.93.4:8005
TOMCAT_SERVICE_PORT_8005_TCP_ADDR=10.254.93.4
TOMCAT_SERVICE_PORT_8005_TCP_PROTO=tcp
TOMCAT_SERVICE_PORT_8005_TCP_PORT=8005
上述环境变量中,比较重要的是前3条环境变量,我们可以看到,每个Service的IP地址及端口都是有标准的命名规范,就可以通过代码访问系统环境变量的方式得到所需的信息,实现服务调用。
考虑到环境变量的方式获取Service的IP与端口的方式仍然不太方便,不够直观,后来Kubernetes通过Add-On增值包的方式引入了DNS系统,把服务名作为dns域名,这样一来,程序就可以直接使用服务名来建立通信连接了。目前Kubernetes上的大部分应用都已经采用了DNS这些新型的服务发现机制,后面的章节中我们会讲述如何部署这套DNS系统。
3.外部系统访问Service的问题
为了更好深入地理解和掌握Kubernetes,我们需要弄明白Kubernetes里的“三种IP”这个关键问题,这三种分别如下。
- Node IP:Node节点的IP地址。
- Pod IP:Pod的IP地址。
- Cluster IP:Service的IP地址。
首先,Node IP是Kubernetes集群中每个节点的物理网卡的IP地址,这是一个真实存在的物理网络,所有属于这个网络的服务器之间都能通过这个网络直接通信,不管它们中是否有部分节点不属于这个Kubernetes集群。这也表明了Kubernetes集群之外的节点访问Kubernetes集群之内的某个节点或者TCP/IP服务时,必须要通过Node IP进行通信。
其次,Pod IP是每个Pod的IP地址,它是Docker Engine根据docker0网桥的IP地址段进行分配的,通常是一个虚拟的二层网络,前面我们说过,Kubernetes里一个Pod里的容器访问另外一个Pod里的容器,就是通过Pod IP所在的虚拟二层网络进行通信的,而真实的TCP/IP流量则是通过Node IP所在的物理网卡流出的。
最后,我们说说Service的Cluster IP,它也是一个虚拟的IP,但更像是一个“伪造”的IP网络,原因有以下几点。
- Cluster IP仅仅作用于Kubernetes Service这个对象,并由Kubernetes管理和分配IP地址(来源于Cluster IP地址池)。
- Cluster IP无法被Ping,因为没有一个“实体网络对象”来响应。
- Cluster IP只能结合Service Port组成一个具体的通信端口,单独的Cluster IP不具备TCP/IP通信的基础,并且它们属于Kubernetes集群这样一个封闭的空间,集群之外的节点如果要访问这个通信端口,则需要做一些额外的工作。
- 在Kubernetes集群之内,Node IP网、Pod IP网与Clsuter IP之间的通信,采用的是Kubernetes自己设计的一种编程方式的特殊的路由规则,与我们所熟知的IP路由有很大的不同。
根据上面的分析和总结,我们基本明白了:Service的Cluster IP属于Kubernetes集群内部的地址,无法在集群外部直接使用这个地址。那么矛盾来了:实际上我们开发的业务系统中肯定多少由一部分服务是要提供給Kubernetes集群外部的应用或者用户来使用的,典型的例子就是Web端的服务模块,比如上面的tomcat-service,那么用户怎么访问它?
采用NodePort是解决上述问题的最直接、最常用的做法。具体做法如下,以tomcat-service为例,我们在Service的定义里做如下扩展即可(黑体字部分):
apiVersion: v1
kind: Service
metadata:
name: tomcat-service
spec:
type: NodePort
ports:
- port: 8080
nodePort: 31002
selector:
tier: frontend
其中,nodePort:31002这个属性表明我们手动指定tomcat-service的NodePort为31002,否则Kubernetes会自动分配一个可用的端口。接下来,我们在浏览器里访问http://<nodePort IP>:31002,就可以看到Tomcat的欢迎界面了,如图1.14所示。
通过NodePort访问Service.png
NodePort的实现方式是在Kubernetes集群里的每个Node上为需要外部访问的Service开启一个对应的TCP监听端口,外部系统只要用任意一个Node的IP地址+具体的NodePort端口号即可访问此服务,在任意Node上运行netstat命令,我们就可以看到有NodePort端口被监听:
# netstat -tlp|grep 31002
tcp6 0 0 [::]:31002 [::]:* LISTEN 19043/kube-proxy
但NodePort还没有完全解决外部访问Service的所有问题,比如负载均衡问题,假如我们的集群中有10个Node,则此时最好有一个负载均衡器,外部的请求只需要访问此负载均衡器的IP地址,由负载均衡负责转发流量到后面某个Node的NodePort上。如图1.15所示。
NodePort与Load balancer.png
图1.15中的Load balancer组件独立于Kubernetes集群之外,通常是一个硬件的负载均衡器,或者是以软件方式实现的,例如HAProxy或者Nginx。对于每个Service,我们通常需要配置一个对应的Load balancer实例来转发流量到后端的Node上,这的确增加了工作量及出错的概率。于是Kubernetes提供了自动化的解决方案,如果我们的集群运行在谷歌的GCE公有云上,那么只要我们把Service的type=NodePort改为type=LoadBalancer,此时Kubernetes会自动创建一个对应的Load balancer实例并返回它的IP地址供外部客户端使用。此时Kubernetes会自动创建一个对应的Load balancer实例并返回它的IP地址供外部客户端使用。其他公有云提供商只要实现了支持此特性的驱动,则也可以达到上述目的。此外,裸机上的类似机制(Bare Metal Service Load Balancers)也正在被开发。
1.4.10 Volume(存储卷)
Volume是Pod中能够被多个容器访问的共享目录。Kubernetes的Volume概念、用途和目的与Docker的Volume比较类似,但两者不能等价。首先,Kubernetes中的Volume定义在Pod上,然后被一个Pod里的多个容器挂载到具体的文件目录下;其次,Kubernetes中的Volume中的数据也不会丢失。最后,Kubernetes支持多种类型的Volume,例如Gluster、Ceph等先进的分布式文件系统。
Volume的使用也比较简单,在大多数情况下,我们先在Pod上声明一个Volume,然后在容器里引用该Volume并Mount到容器里的某个目录上。举例来说,我们要給之前的Tomcat Pod增加一个名字为datavol的Volume,并且Mount到容器的/mydata-data目录上,则只要对Pod的定义文件做如下修正即可(注意黑体字部分):
template:
metadata:
labels:
app: app-demo
tier: frontend
spec:
volumes:
- name: datavol
emptyDir: {}
containers:
- name: tomcat-demo
image: tomcat
volumeMounts:
- mountPath: /mydata-data
name: datavol
imagePullPolicy: IfNotPersent
除了可以让一个Pod里的多个容器共享文件、让容器的数据写到宿主机的磁盘上或者写文件到网络存储中,Kubernetes的Volume还扩展出了一种非常有实用价值的功能,即容器配置文件集中化定义与管理,这是通过ConfigMap这个新的资源对象来实现的,后面我们会详细说明。
Kubernetes提供了非常丰富的Volume类型,下面逐一进行说明。
1.emptyDir
一个emptyDir Volume是在Pod分配到Node时创建的。从它的名称就可以看出,它的初始内容为空,并且无须指定宿主机上对应的目录文件,因为这是Kubernetes自动分配的一个目录,当Pod从Node上移除时,emptyDir中的数据也会被永久删除。empty的一些用途如下。
- 临时空间,例如用于某些应用程序运行时所需的临时目录,且无须永久保留。
- 长时间任务的中间过程CheckPoint的临时保存目录。
- 一个容器需要从另一个容器中获取数据的目录(多容器共享目录)。
目前,用户无法控制emptyDir使用的介质种类。如果kubelet的配置是使用硬盘,那么所有emptyDir都将创建在该硬盘上。Pod在将来可以设置emptyDir是位于硬盘、固态硬盘上还是基于内存的tmpfs上,上面的例子便采用了emptyDir类的Volume。
2.hostPath
hostPath为在Pod上挂载宿主机上的文件或目录,它通常可以用于以下几方面。
- 容器应用程序生成的日志文件需要永久保持时,可以使用宿主机的高速文件系统进行存储。
- 需要访问宿主机上Docker引擎内部数据结构的容器应用时,可以通过定义hostPath为宿主机/var/lib/docker目录,使容器内部应用可以直接访问Docker的文件系统。
在使用这种类型的Volume时,需要注意以下几点:
- 在不同的Node上具有相同配置的Pod可能会因为宿主机上的目录和文件不同而导致对Volume上目录和文件的访问结构不一致。
- 如果使用了资源配额管理,则Kubernetes无法将hostPath在宿主机上使用的资源纳入管理。
在下面对例子中使用宿主机的/data目录定义了一个hostPath类型的Volume:
volumes:
- name: "persistent-storage"
hostPath:
path: "/data"
3.gcePersistentDisk
使用这种类型的Volume表示使用谷歌公有云提供的永久磁盘(Persistent Disk,PD)存放Volume的数据,它与emotyDir不同,PD上的内容会被永久保存,当Pod被删除时,PD只是被卸载(Unmount),但不会被删除。需要注意的是,你需要先创建一个永久磁盘(PD),才能使用gcePersistentDisk。
使用gcePersistentDisk有以下一些限制条件。
- Node(运行kubeket的节点)需要是GCE虚拟机。
- 这些虚拟机需要与PD存在于相同的GCE项目和Zone中。
通过gcloud命令即可创建一个PD:
gcloud compute disks create --size=500GB --zone=us-centrall-a my-data-disk
定义gcePersisentDisk类型的Volume的示例如下:
volumes:
- name: test-volume
# This GCE PD must already exist.
gcePersistentDisk:
pdName: my-data-disk
fsType: ext4
4.awsElasticBlockStore
与GCE类似,该类型的Volume使用亚马逊公有云提供的EBS Volume存储数据,需要先创建一个EBS Volume才能使用awsElasticBlockStore。
使用awsElasticBlockStore的一些限制条件如下:
- Node(运行kubelet的节点)需要是AWS EC2实例。
- 这些AWS EC2实例需要与EBS volume存在于相同的region和availability-zone中。
- EBS只支持单个EC2实例mount一个volume。
通过aws ec2 create-volume命令可以创建一个EBS volume:
aws ec2 create-volume --availability-zone eu-west-1a --size 10 --volume-type gp2
定义awsElasticBlockStore类型的Volume的示例如下:
volumes:
- name: test-volume
# This AWS EBS volume must already exist.
awsElasticBlockStore:
volumeID: aws://<availability-zone>/<volume-id>
fsType: ext4
5.NFS
使用NFS网络文件系统提供的共享目录存储数据时,我们需要在系统中部署一个NFS Server。定义NFS类型的Volume的示例如下:
volumes:
- name: nfs
nfs:
# 改为你的NFS服务器地址
server: nfs-server.localhost
path: "/"
6.其他类型的Volume
- iscsi:使用iSCSI存储设备上的目录挂载到Pod中
- flocker:使用Flocker来管理存储卷
- glusterfs:使用开源GlusterFS网络文件系统的目录挂载到Pod中。
- rbd:使用Ceph块设备共享存储(Rados Block Device)挂载到Pod中。
- gitRepo:通过挂载一个空目录,并从GIT库clone一个git repository以供Pod使用。
- secret:一个secret volume用于为Pod提供加密的信息,你可以将定义在Kubernetes中的secret直接挂载的volume总是不会持久化的。
1.4.11 Persistent Volume
之前我们提到的Volume是定义在Pod上的,属于“计算资源”的一部分。而实际上,“网络存储是相对独立于“计算资源”而存在的一种实体资源。比如在使用虚拟机的情况下,我们通常会先定义一个网络存储,然后从中划出一个“网盘”并挂接到虚拟机上。Persistent Volume(简称PV)和与之关联的Persistent Volume Claim(简称PVC)也起到了类似的作用。
PV可以理解成Kubernetes集群中的某个网络存储中对应的一块存储,它与Volume很类似,但有以下区别。
- PV只能是网络存储,不属于任何Node,但可以在每个Node上访问。
- PV并不是定义在Pod上的,而是独立于Pod之外定义。
- PV目前支持的类型包括:gcePersistentDisk、AWSElasticBlockStore、AzureFile、AzureDisk、FC(Fibre Channel)、Flocker、NFS、iSCSI、RBD(Rados Block Device)、CephFS、Cinder、GlusterFS、VsphereVolume、Quobyte Volumes、VMware Photon、Portworx Volumes、ScaleIO Volumes和HostPath(仅供单机测试)。
下面给出了NFS类型PV的一个yaml定义文件,声明了需要5Gi的存储空间;
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv0003
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
nfs:
path: /somepath
server: 172.17.0.2
比较重要都是PV的accessModes属性,目前有以下类型。
- ReadWriteOnce:读写权限、并且只能被单个Node挂载。
- ReadOnlyMany:只读权限、允许被多个Node挂载。
- ReadWriteMany:读写权限、允许被多个Node挂载。
如果某个Pod想申请某种类型的PV,则首先需要定义一个PersistentVolumeClaim(PVC)对象:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: myclaim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 8Gi
然后在Pod的Volume定义中引用上述PVC即可:
volumes:
- name: mypd
persistentVolumeClaim:
claimName: myclaim
最后,我们说说PV的状态,PV是有状态的对象,它有以下几种状态。
- Available:空闲状态。
- Bound:已经绑定到某个PVC上。
- Released:对应的PVC已经删除,但资源还没有被集群收回。
- Failed:PV自动回收失败。
共享存储的原理解析和实践指南详见3.8节。
1.4.12
Namespace(命名空间)是Kubernetes系统中的另一个非常重要的概念,Namespace在很多情况下用于实现多租户的资源隔离。Nameaspace通过将集群内部的资源对象“分配”到不同的Namespce中,形成逻辑上分组的不同项目、小组或用户组,便于不同的分组在共享使用整个集群的资源的同时还能被分别管理。
Kubernetes集群在启动后,会创建一个名为“default”的Namespace,通过kubectl可以查看到:
$ kubectl get namespaces
NAME STATUS AGE
default Active 21h
docker Active 21h
kube-public Active 21h
kube-system Active 21h
接下来,如果不特别指明Namespace,则用户创建的Pod、RC、Service都被系统创建到这个默认的名为default的Namespace中。
Namespace的定义很简单。如下所示的yaml定义了名为development的Namespace。
apiVersion: v1
kind: Namespace
metadata:
name: development
一旦创建了Namespace,我们在创建资源对象时就可以指定这个资源对象属于哪个Namespace。比如在下面的例子中,我们定义了一个名为busybox的Pod,放人development这个Namespace里:
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: development
spec:
containers:
- image: busybox
command:
- sleep
- "3600"
name: busybox
此时,使用kubectl get命令查看将无法显示:
# kubectl get pods
NAME READY STATUS RESTARTS AGE
这是因为如果不加参数,则kubectl get 命令将仅显示属于“default”命名空间的资源对象。
可以在kubectl命令中加入--namespace参数来查看某个命名空间中的对象:
# kubectl get pods --namespace=development
NAME READY STATUS RESTARTS AGE
busybox 1/1 Running 0 2m
当我们給每个租户创建一个Namespace来实现多租户的资源隔离时,还能结合Kubernetes的资源配额管理,限定不同租户能占用的资源,例如CPU使用量、内存使用量等。关于资源配额管理等问题,在后面的章节中会详细介绍。
1.4.13 Annotation(注解)
Annotation与Label类似,也使用key/value键值对的形式进行定义。不同的是Label具有严格的命名规则,它定义的是Kubernetes对象的元数据(Metadata),并且用于Label Selector。而Annotation则是用户任意定义的“附加”信息,以便于外部工具进行查找,很多时候,Kubernetes的模块自身会通过Annotation的方式标记资源对象的特殊信息。
通常来说,用Annotation来记录的信息如下。
- build信息、release信息、Docker镜像信息等,例如时间戳、release id号、PR号、镜像hash值、docker registry地址等。
- 日志库、监控库、分析库等资源库的地址信息。
- 程序调试工具信息,例如工具、版本号等。
- 团队等联系信息,例如电话号码、负责人名称、网址等。
1.4.14 小结
上述组件是Kubernetes系统的核心组件,它们共同构成了Kubernetes系统的框架和计算模型。通过对它们进行灵活组合,用户就可以快速、方便地对容器集群进行配置、创建和管理。除了本章所介绍的核心组件,在Kubernetes系统中还有许多辅助配置的资源对象,例如LimitRange、Resurce。另外,一些系统内部使用的对象Binding、Event等请参考Kubernetes的API文档。
容器化代表着未来,随着越来越多的公司熟悉docker、kubernetes、Istio等开源技术,普通公司也能利用相关技术实现容器化、弹性伸缩、容灾容备等等,从而让普通公司也有与大厂比肩的技术能力。接下来的博文内容,将都和docker、kubernetes、Istio等技术相关。
如果需要給我修改意见的发送邮箱:erghjmncq6643981@163.com
资料参考:《Kubernetes-权威指南》
转发博客,请注明,谢谢。
网友评论