@dujun
2015-04-03T20:45:21.000000Z
字数 16985
阅读 5454
Kubernetes写书
Kubernetes使用apiserver+etcd的模式建立了Kubernetes资源模型和一个为之服务的REST存储解决方案。那么什么是了Kubernetes的资源模型呢?举个例子,在进行pod的调度时,Kubernetes需要知道待调度的pod有"多大",以及pod将要运行的工作节点有"多大",上面提到的"多大"的定义就是Kubernetes的资源模型给出的。在Kubernetes中,绝大部分资源对象都是RESTful的,apiserver提供了一套完整的增、删、改、查(CRUD)和监控(watch)接口。Kubernetes REST资源对象有很多种,涵盖了Kubernetes的大部分重要概念,包括:pod,replication controller,service,minion,event,namespace等。可以说,Kubernetes的系统架构主要是围绕着REST资源模型建立起来的,这也是Kubernetes选用etcd作为后台存储的原因之一。Kubernetes所有资源对象的数据结构定义均在/pkg/api/{apiversion}/types.go
中。需要说明的是,Kubernetes目前提供三种API模式,即:v1beta1
、v1beta2
和v1beta3
,相应地,不同的API版本中Kubernetes对象的定义和操作略有不同,但也大同小异。本文对Kubernetes资源对象的分析主要基于v1beta1
版本。下文中除非特别说明,将不加区分Kubernetes资源对象、Kubernetes对象和REST对象这三种描述。
在Kubernetes中,pod而非单个单个容器是能够被创建、调度和管理的最小部署单元。那么什么是pod呢?简单地说,一个pod对应于一个由相关Docker容器构成的容器组(就像一个pod(豆荚)里面容纳了多个豆子一样),该pod内的所有容器共享存储卷(volume),一个存储卷就是挂载在一个Docker容器的镜像(文件系统)上的目录,该目录下的数据可以被同个pod中的容器共享。volume来源有多种类型,主要分两大类:来自宿主机的文件系统和GCEPersistentDisk,下文将详细解析pod的volume。因此,pod就是一个container+volume的结构。Pod建立了一个面向应用的“逻辑主机模型”,该“逻辑主机”内部运行着一组耦合度相对较高的Docker容器,在pod这个概念被提出之前,这些容器一般会被放在同一台宿主机或者虚拟机中运行。与运行的容器一样,pod的生命周期被认为是临时的而非持久的。
当系统运行着数量庞大的pod时,如何有效地分类与组织这pod就成了一个重要的问题。Kubernetes的解决方案是labels。每个pod都有一个标签(labels)——一组键值对,用于对pod进行选择和分类,形如:
"labels": {
"key1" : "value1",
"key2" : "value2"
}
通过labels选择Kubernetes对象只需向Kubernetes集群管理工具——kubectl传入-l key=value
参数即可。例如列举所有匹配标签{"name": "nginx"}
的pod可以这么操作。
$ kubectl get pods -l name=nginx
replication controller和service(在后面的章节会详细介绍)根据这些标签对pod进行选择与调度。一旦某个pod被分配到指定的工作节点(minion)上,该pod就会在这个工作节点上一直运行直到生命周期结束(运行中止或者被删除)而不会中途被调度到其它工作节点再次运行。当一个工作节点下线时,该工作节点上的pod也随之被调度删除。事实上,在Kubernetes中,labels和label selector是一种重要的且被广泛应用的用于组织、分类和选择Kubernetes对象的机制,故下文将详细介绍下Kubernetes labels和label selectors的工作原理。
labels是一组绑定到Kubernetes对象(譬如pod)上的键/值对,同一个对象labels属性的key值必须独一无二。labels的数据结构非常简单,就是一个key和alue均为string类型的map结构,如下所示。
Labels map[string]string `json:"labels,omitempty"`
labels的绑定操作发生在Kubernetes对象的创建时而且labels能够随意增删和修改,这些对象根据各自的labels属性被划分子集。Kubernetes设计者引入lables的主要目的是面向用户,使之成为用户级的Kubernetes对象标识属性,因为包含Kubernetes对象功能性和特征性描述的标签(labels)比对象名或UUID更加用户友好和有意义,而且使得用户能够以一种松耦合的方式实现用户自己的组织结构到系统对象之间的映射,无须客户端存储这些映射关系。但是,labels属性一般不直接作为系统内部唯一标识Kubernetes对象的依据,因为不同于对象名和UUID,labels并不保证独一无二性。一般情况下,我们允许甚至希望不同的Kubernetes对象携带相同的一个或一组labels。接下来我们举例说明labels的实际用途。我们知道,一个复杂的分布式服务系统包括若干各层级,简单分可以分成前端、后端、中间层等。从系统环境角度分,又可以分为开发环境、测试环境和生产环境等。部署的系统有时候为了升级的需要可能同时存在稳定版和升级测试版。在一个多用户的系统中,系统空间又将根据不同用户进行划分。不同系统组件的更新周期也存在差异,有些更新频率是周级别,有些则是天级别。因此,根据上述讨论的不同维度,我们可以为基于容器的服务系统的系统实体——pod贴上不同的标签(labels),方便进行管理和维护,labels示例如下所示。
"release" : "stable"
, "release" : "canary"
, ..."environment" : "dev"
, "environment" : "qa"
, "environment" : "production"
, ..."tier" : "frontend"
, "tier" : "backend"
, "tier" : "middleware"
, ..."partition" : "customerA"
, "partition" : "customerB"
, ..."track" : "daily"
, "track" : "weekly"
, ...以上的只是一些示例,Kubernetes用户完全可以根据自身实际情况自由地创建labels。
我们已经知道,labels由一组键/值对组成,一个合法的key值由两个部分组成——前缀(prefix)和名字(name),中间由一个"/"分隔,前缀和"/"是非必须的但名字是必须的。name字段最多由63个字符组成,接受的字符包括a-z,0-9和-,全部小写,开头和结尾只能是字母和数字([a-z,0-9]),中间用"-"连接,譬如"2n1-ame0"作为key值就合法,但"An1-ame0"或"2n1_ame0"作为key值就非法。如果指定了前缀,则前缀必须是一个DNS子域名(即一系列用"."分隔的DNS标签,总长度不超过253个字符)。如果前缀是缺省的,则认为该label是用户私有的,系统组件如果要使用labels必须指定一个前缀,譬如前缀kubernetes.io
就是Kubernetes核心组件的保留前缀。一个合法的value值最多由63个字符组成,接受的字符包括A-Z,a-z,0-9,-,_和.,但第一个字符必须是[A-Za-z0-9]中的一个。
label selector是Kubernetes核心的成组分类机制,通过label selector,客户端/用户能够识别是选择一组有共同特征或属性的Kubernetes对象。一个label selector可以由多个查询条件组成,这些查询条件用逗号分隔。当一个label selector存在多个查询条件时,这些查询条件需要同时满足,这时候逗号就充当了逻辑与(AND)的作用。在实际应用中,label selectors经常作为发送给apiserver的RESTful查询请求的条件参数,用于检索一个与label selector匹配的Kubernetes对象列表。Kubernetes API目前支持两种类型的label selector查询条件:基于值相等和基于子集的,下面将逐个分析之。
基于值相等的查询条件通过等值匹配labels的key和value来过滤Kubernetes对象。匹配的Kubernetes对象必须要包含所有指定的labels(包括key和alue),当然这些对象可能还包含其他的labels,这并不影响被label selector选中。这种类型的label selector支持三种操作符:=
,==
和!=
,其中前两个操作符从在语法上是等价的且代表"相等"的语意,而最后一个操作符代表"不相等"的语意。请看下面这两个例子。
environment = production
tier != frontend
前者选择所有的key值等于"environment"而且value值等于"production"的资源对象,后者选择所有的key值等于"tier"而且value值不等于"frontend"的资源对象。如果想过滤出位于"production"环境但非前端的资源对象可以使用","操作符,如下所示。
environment=production,tier!=frontend
基于子集的查询条件通过匹配labels的key和其所对应的value集合来过滤Kubernetes对象。匹配的Kubernetes对象必须要包含所有指定的labels(比如,所有的key值和每个key对应的至少一个value值)。这种类型的label selector支持三种操作符:in
,notin
和exists
(exists
操作符只适用于对key值的比较)。请看下面这三个例子。
environment in (production, qa)
tier notin (frontend, backend)
partition
第一个例子选择所有的key值等于"environment"而且value值等于"production"或"qa"的资源对象。第二个例子选择所有的key值等于"tier"而且value值不等于"frontend"且"backend"的资源对象。第三个例子选择所有的labels属性中包含key值等于"partition"的资源对象,不需要检查value值。类似地,","操作符充当了逻辑与(AND)的作用,譬如过滤出包含key值"partition"(不论value值等于什么)且不处于"qa"环境的资源对象可以使用下面的label selector语句。
partition,environment notin (qa)
可以看出,基于值相等的查询条件是基于子集的查询条件的一个特例,因为key=value
等价于key in value
,类似地,key!=value
等价于key notin (value)
。在一个label selector中,基于子集的查询条件可以和基于值相等的查询条件混合使用,例如:partition in (customerA, customerB),environment!=qa
。
Kubernetes API的LIST(返回一个特定的资源对象列表)和WATCH(检测一个特定的资源对象的数据变化情况)操作可能会用到label selectors来过滤出返回的资源对象的某个子集。label selector通过查询参数的方式传入RESTful API请求,以上提到的两种查询条件均支持,如下所示。
基于值相等的查询条件:?labels=key1%3Dvalue1,key2%3Dvalue2
基于子集的查询条件:?labels=key+in+%28value1%2Cvalue2%29%2Ckey2+notin+%28value3
目前,Kubernetes支持两个重要的资源对象使用label selectors来监控和管理它们的pod成员——services和replication controllers,这两个Kubernetes对象将在后面的篇幅详细讨论。
最后要说明的一点是,根据labels的特点,同一个pod(或其他资源对象)可能同时属于多个对象集合(回忆一下venn图集合相交的情况)。这一特性促进了扁平化、多维度的服务组织和部署架构,这对集群的管理(譬如配置和部署等)和应用的自我检查和分析(譬如:日志、监控、预警和分析等)非常有用。如果没有labels的这种将资源对象划分集合的能力,就需要创建很多隐含联系而且属性重叠的资源集合,而我们知道,单纯的分层嵌套的组织结构不能很好地支持从多个维度对系统资源对象集合进行切割。
任何一个新概念的引入绝不是偶然的,在回答“为什么要引入pod?”这个问题之前,我们先探讨一下为什么不直接在单个Docker容器中运行多个应用?不这样做的原因归纳起来有以下几点:
引入pod这一个概念的主要目的归纳起来就两点,即:
pod还可以为pod内的容器指定了一组共享的存储卷(volume),这些存储卷的作用是方便pod内容器之间共享数据以及在容器重启过程中避免数据丢失。更多关于volume的解析请参见下文。
未来,pod之间将能够共享IPC namespace,CPU和内存,见LPC2013。
与Docker提供的原始的,底层的容器接口不同,通过对底层API进行封装和抽象,pod简化了应用部署和管理流程。Pod可以看成是管理和横向扩展的单元,这样,docker容器的协同定位、命运共担、协同拷贝、主机托管、资源共享、协调复制和依赖管理都可以自动处理。
pod能够用来构建垂直集成应用栈,不过它的主要用途是支持构建协同定位、协同管理的应用程序,例如:
一言以蔽之,尽量不要在单个pod中运行同一个应用的多个实例,因为pod设计的目的就是用于不同应用程序之间的协同。
我们使用Kubernetes的客户端工具kubectl
来创建pod,该命令行工具支持对Kubernetes对象(pod,replication controller,service)的增、删、改、查(CRUD)操作以及其他对集群的管理操作。创建Kubernetes对象的一般方法是:
kubectl create -f obj.json
其中obj.json
可以是定义pod,replication controller,service等Kubernetes对象的JSON
格式的资源配置文件。
先来看一个简单的例子。
{
"id": "podtest",
"kind": "Pod",
"apiVersion": "v1beta1",
"desiredState": {
"manifest": {
"version": "v1beta2",
"id": "redis-sshd",
"containers": [{
"name": "master1",
"image": "10.10.103.215:5000/redis",
"ports": [{
"containerPort": 6379,
"hostPort": 6388
}]},
{"name": "master2",
"image": "10.10.103.215:5000/sshd",
"ports": [{
"containerPort": 22,
"hostPort": 8888
}]}
]
}
},
"labels": {
"name": "redis-master"
}
}
上面面配置信息描述了一个id
为podtest
的资源对象,以后可以用这个id
来唯一标识这个资源对象。而该配置信息的kind
字段表明该资源对象正是Kubernetes pod。apiVersion
字段表明客户端使用的服务端api版本是v1beta1
。desiredState:manifest:containers
字段描述了pod内的容器的属性,包括:容器名(name),镜像(image),端口映射(ports)等。这里需要详细说明下Kubernetes资源配置文件的desiredState
字段含义。一个Kubernetes资源配置文件就定义了一个资源对象(譬如pod)的期望状态(desired state)。"期望状态"是Kubernetes资源模型中一个非常重要的概念,许多资源对象告诉系统他的期望状态,而Kubernetes负责该资源对象的当前状态(current state)与期望状态匹配。当然,这是一个迭代式的渐进过程。例如,当你创建一个pod时,你声明该pod内容器正常运行所需的计算资源配置信息以及暗含地告诉Kubernetes不论发生什么情况都要保证pod内容器正常运行。如果pod内容器没有运行(譬如发生程序错误),Kubernetes将持续地为你重新创建他们(Kubernetes支持丰富的容器重启策略,这将在下文一一解析),然后将驱使他们进入你想要的期望状态,这个过程将一直持续直到你删除该pod。通过以上讨论,我们知道desiredState
还可以包含更多有用的信息,这在下文中会有更详细的解析。labels
字段即该pod的标签,该pod只有一个标签:"redis-master"。
将以上配置内容写入testpod.json
文件,并根据该配置文件创建一个包含两个容器的pod。这里要注意端口冲突问题,即同一个pod的不同容器或不同pod的容器的端口可能会映射到主机上的同一个端口(hostPort
),这样就引起了端口冲突。当然,永远不用担心容器端口(containerPort
)的冲突问题。
kubectl create -f testpod.json
podtest
注:kubectl并不会在屏幕打印创建pod过程中与docker容器相关的log信息,因此如果需要知道中间过程中发生了哪些与docker容器相关的事情,可以查看docker的log,即:
$ tail -f /var/log/upstart/docker.log
查看创建的pod信息:
$ kubectl get pod
POD CONTAINER(S) IMAGE(S) HOST LABELS STATUS
podtest master1 10.10.103.215:5000/redis 127.0.0.1/ name=redis-master Running
master2 10.10.103.215:5000/sshd
可以看到,pod已经处于运行状态,该pod包含两个用户容器:master1和master2,master1容器运行redis,其中master2容器运行sshd。如果要查看pod中容器输出的log信息,可以使用kubectl log pod container
获取,以master1容器为例。
$ kubectl log podtest master1
...
2015-01-08T11:52:15.583972037Z [1] 08 Jan 11:52:15.583 # Server started, Redis version 2.8.17
2015-01-08T11:52:15.584391860Z [1] 08 Jan 11:52:15.584 * The server is now ready to accept connections on port 6379
pod数据结构与pod资源文件
我们已经见识了pod资源文件的魔力,那如何根据自己的需要编写一个完整、正确的pod资源文件(manifest)呢?我们无法从互联网上甚至官方文档中找到一份权威的、详细的指导材料(即使有也只是一些零星的、不完整的样例),故需要自己从Kubernetes定义pod对象的源代码中提炼。我们先来看一下pod的数据结构。
type Pod struct {
TypeMeta `json:",inline"`
Labels map[string]string `json:"labels,omitempty"`
DesiredState PodState `json:"desiredState,omitempty"`
CurrentState PodState `json:"currentState,omitempty"`
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
}
TypeMeta
属性存储了所有REST资源对象(包括pod)的共有属性,他存储了资源对象的元数据,包括:资源类型(对应于资源文件的kind
字段),对象名(对应于资源文件的id
字段)、namespace、UUID、资源版本、创建时间、和资源对象的API版本(对应于资源文件的apiVersion
字段)等信息。labels属性即对应于资源文件的labels
字段。一个pod的状态包括两种:期望状态(DesiredState
)和当前状态(CurrentState
)。DesiredState.ContainerManifest
属性即代表了组成一个pod的volumes和containers的配置信息,该属性的内容与pod资源文件的desiredState.manifest
字段内容等价。CurrentState
属性则代表该pod的当前状态,该属性的内容不需要用户填入而是在系统运行过程中被系统填充进去的。NodeSelector
即一个label selector,用于为pod选取minion,如果需要可以在pod资源文件中设置,该属性在kube-scheduler调度pod时会用到,更多详细介绍请参考下文。这里我们将着重讨论DesiredState.ContainerManifest
属性各字段的含义以及与pod资源文件的联系,因为只要将DesiredState.ContainerManifest
属性各字段的含义解析清楚了,pod资源文件的结构就迎刃而解了。
字段 | 是否必需 | 值类型 | 含义 | 是否来自资源文件 | 备注 |
---|---|---|---|---|---|
version | 是 | string | manifest的版本号 | 是 | 目前必须是v1beta2,即使在资源文件中不是写v1beta2,Kubernetes也会硬编码成v1beta2 |
ID | 是 | string | manifest名字 | 是 | / |
UUID | 是 | string | 系统唯一标识pod实例的uuid | 否 | 只读属性,由系统注入 |
containers[] | 是 | list | 要在pod内启动的所有container | 是 | / |
containers[].name | 是 | string | 容器名 | 是 | 唯一标识容器,在同一个pod内必须独一无二 |
containers[].image | 是 | string | 容器使用的Docker镜像名 | 是 | / |
containers[].command[] | 否 | string list | ? | 是 | ? |
containers[].workingDir | 否 | string | 命令在docker容器内执行的初始工作目录 | 是 | 一旦设置便无法更新,默认是Docker default? |
containers[].resources | 否 | map | 容器所需的计算资源上限 | 是 | |
containers[].CPU | 否 | int | 容器所需的CPU资源 | 是 | |
containers[].Memory | 否 | int64 | 容器所需的内存资源上限 | 是 | |
containers[].volumeMounts[] | 否 | list | 暴露给容器且能够挂载到docker容器文件系统上的所有volume | 是 | / |
containers[].volumeMounts[].name | 否 | string | volume名 | 是 | 待挂载volume的名字,该字段值必须与在volume[]中定义的name值匹配 |
containers[].volumeMounts[].mountPath | 否 | string | volume在容器内的挂载点路径 | 是 | 该路径必须是绝对路径且长度不能超过512个字符 |
containers[].volumeMounts[].readOnly | 否 | boolean | 标识该volume是否是只读的 | 是 | 默认值是false,即可读可写 |
containers[].ports[] | 否 | list | 容器打开的所有端口 | 是 | 一旦设置便无法更新 |
containers[].ports[].name | 否 | string | 端口名 | 是 | 在pod内必须独一无二 |
containers[].ports[].containerPort | 是 | int | 容器监听的端口号 | 是 | 1-65535 |
containers[].ports[].hostPort | 否 | int | 容器端口在宿主机上的端口映射 | 是 | 1-65535,大部分容器不需要这个 |
containers[].ports[].protocol | 否 | string | 端口类型 | 是 | UDP或TCP,默认是TCP |
containers[].env[] | 否 | list | 在容器运行前设置的环境变量 | 是 | 是一组键值对 |
containers[].env[].name | 否 | string | 环境变量名 | 是 | / |
containers[].env[].value | 否 | string | 环境变量值 | 是 | / |
RestartPolicy | 否 | string | pod内容器重启策略 | 是 | 包含三种策略:Always 、OnFailure 和Never 。Always :一旦容器退出,则不论退出码是什么,Kubelet均重启该容器;OnFailure :如果容器的退出码非0,则Kubelet重启该容器,反之(代表容器正常退出),Kubelet不重启该容器;Never :如果一个退出,Kubelet不重启该容器而只是向master报告退出原因。如果不指定则默认是Always |
volumes[] | 否 | list | pod内由容器间共享的所有volume | 是 | / |
volumes[].name | 否 | string | volume名 | 是 | / |
volumes[].VolumeSource | 否 | object | 待挂载volume的种类 | 是 | 包括:HostPath、EmptyDir、GCEPersistentDisk等多种类型 |
volumes[].source.emptyDir | 否 | object | emptyDir类型volume | 是 | 默认的volume类型,代表挂载的volume是一个分享pod生命周期的临时目录。emptyDir的值是一个空对象,即:emptyDir: {} |
volumes[].source.hostDir | 否 | object | hostDir类型volume | 是 | 代表挂载的volume是一个已经存在宿主机上的目录。emptyDir的值是一个空对象,即:emptyDir: {} 。需要指定volumes[].source.hostDir.path |
volumes[].source.hostDir.path | 否 | string | 宿主机上一个暴露给容器的现存目录的路径 | 是 | / |
volumes[].source.GCEPersistentDisk | 否 | obj | GCEPersistentDisk类型volume | 是 | / |
DNSPolicy | 否 | list | 定义pod使用DNS服务的策略 | 是 | 有两种选择:ClusterFirst 和Default ,前者代表pod首先使用集群DNS,后者代表pod使用kubelet设置的DNS。默认值是Default |
表x DesiredState.ContainerManifest各字段说明
综上所述,一份完整的pod资源文件(pod manifest)内容包括:TypeMeta
、Labels
、NodeSelector
属性中与pod manifest对应的各字段和上表列举的container manifest各字段。最后,展示一份比较完整的container manifest示例结束本小节的讨论。
version: v1beta2 // Required.
containers: // Required.
- name: string // Required.
image: string // Required.
command: [string]
workingDir: string
volumeMounts:
- name: string
mountPath: string
readOnly: boolean
ports:
- name: string
containerPort: int
hostPort: int
protocol: string
env:
- name: string
value: string
restartPolicy:
- string: {}
volumes:
- name: string
source: emptyDir | HostDir
emptyDir: {}
hostDir:
path: string
通过以上讨论,我们已经能够创建pod,也能在pod内运行web server容器,到目前为止一切都还顺利,但是我们不要忘记还有很重要的一点——那就是持久化存储。我们知道容器的文件系统与容器的生命周期一致,当容器退出后其文件系统也随之被销毁,因此我们需要更多的持久化存储介质。为了实现持久化存储,我们需要声明一个磁盘卷(volume)作为pod的一部分并将其挂载到容器中,如下所示。
apiVersion: v1beta1
kind: Pod
id: storage
desiredState:
manifest:
version: v1beta2
id: storage
containers:
- name: redis
image: dockerfile/redis
volumeMounts:
# name字段必须与下面的volume名匹配
- name: redis-persistent-storage
# mountPath即volume在容器内的挂载点路径
mountPath: /data/redis
volumes:
- name: redis-persistent-storage
source:
emptyDir: {}
让我们看看我们都做了些什么?首先,我们在我们的pod内增加了一个volume,如下所示。
volumes:
- name: redis-persistent-storage
source:
emptyDir: {}
然后,我们在我们的容器中增加了一条对该volume的引用,如下所示。
volumeMounts:
# name字段必须与下面的volume名匹配
- name: redis-persistent-storage
# mountPath即volume在容器内的挂载点路径
mountPath: /data/redis
在Kubernetes中,emptyDir
类型的volume的生命周期与pod的生命周期一致,即比pod内任何容器的生命周期都要长,因此即使有一个容器退出并重新启动,我们的持久化存储也能继续存在而不受影响。接下来我们将展开对volume的详细讨论。
一个volume就是一个文件目录,该目录下会存放一些数据,Docker容器能够访问这些数据。Kubernetes Volume与Docker Volume类似但不完全一样。一个pod会在他的资源文件中的ContainerManifest
字段下指定其内部容器需要的所有volume。一个容器内的进程能够看到的文件系统由两个部分组成:一个Docker镜像和0各或多个volume。Docker镜像位于文件系统层次的最底层,所有volume都挂载在Docker镜像上,volume之间不相互挂载也不存在跨volume的硬链接。pod内容器都独立指定每个volume在其镜像上的挂载点,即pod资源文件的VolumeMounts
属性。在我们撰写这本书的时候,Kubernetes支持四种类型的volume:EmptyDir
,HostDir
,GCEPersistentDisk
和NFS
,下面将逐一进行讨论。
EmptyDir
类型的volume创建于pod绑定到工作节点上的时候,最初该volume是空的,当pod内的容器开始运行并不断向volume里写数据时,volume中的数据才不断膨胀。同一个pod内的容器都能读写EmptyDir
中的同一个文件。而当pod从工作节点解绑时,EmptyDir
中的数据就会被永久删除。目前EmptyDir
类型的volume主要用作临时空间,例如:基于磁盘的归并排序可以将EmptyDir
作为存放临时数据的中转站。在我们撰写这本书的时候,用户不能自己选择EmptyDir
类型的volume的存储介质类型。如果Kubelet被配置使用磁盘驱动,那么所有EmptyDir
类型的volume都将使用磁盘介质。
一个拥有HostDir
属性的volume被允许访问当前工作节点上的文件系统。HostDir
类型的volume的典型应用场景有:运行一个需要访问Docker内部构件的容器,那么就使用/var/lib/docker
目录作为一个HostDir
类型的volume;在一个容器内部运行cAdvisor,那么就使用/dev/cgroups
目录作为一个HostDir
类型的volume。但是请谨慎使用这种类型的volume,因为配置完全相同的pod(譬如都是从同一个pod模板创建的,更多关于pod模板的解释见下文)可能会在不同的工作节点上表现出不同的行为(因为不同的工作节点上的文件系统结构和内容各不相同)。
在使用GCEPersistentDisk之前,用户必须先使用gcloud
客户端工具或GCE API创建一个PD(Persistent Disk),如下所示。
gcloud compute disks create --size=500GB --zone=us-central1-a my-data-disk
注:更多关于GCEPersistentDisk的使用,请自行参考GCE官方文档:https://cloud.google.com/compute/docs/disks/,限于篇幅,这里不再赘述。
一个拥有GCEPersistentDisk
属性的volume被允许访问Google Compute Engine (GCE) 的Persistent Disk上的文件系统。使用GCEPersistentDisk
类型的volume存在一些限制:
1)工作节点(kubelet运行的地方)必须是GCE虚拟机。
2)用作工作节点的GCE虚拟机需要和PD处于同一个GCE project和zone。
3)避免创建使用同一个volume且至少一个有读写需求的多个pod。因为GCEPersistentDisk
对并发读/写有特殊的限制,具体变现为:如果pod P
已经挂载了一个可读/写volume,那么当第二个pod Q
尝试使用该volume时就会发生错误,不论Q
是想只读还是读/写该volume;如果pod P
已经挂载了一个只读volume,那么第二个pod Q
将只能只读而不能读/写该volume。
4)副本数大于1的replication controllers只会创建使用只读volume的的pod副本。
最后,展示一个使用GCEPersistentDisk
的pod资源文件来结束对GCEPersistentDisk
类型volume的讨论。
apiVersion: v1beta1
desiredState:
manifest:
containers:
- image: kubernetes/pause
name: testpd
volumeMounts:
- mountPath: "/testpd"
name: "testpd"
id: testpd
version: v1beta2
volumes:
- name: testpd
source:
persistentDisk:
# 该GCE PD必须已经存在
pdName: test
fsType: ext4
id: testpd
kind: Pod
在我们撰写这本书的时候,volume的存储介质(磁盘、SSD或tmpfs)是由kubelet根目录(一般位于:/var/lib/kubelet
)所在的文件系统的存储介质决定的。Kubernetes对EmptyDir
和HostDir
类型的volume的存储空间大小不做任何限制,而且不同容器和pod之间也没有任何隔离措施。未来,Kubernetes可能会为volume专门引入一个资源对象类型用于在一个支持多种类型存储介质的集群中能够选择volume的存储介质类型和限定volume的存储空间大小。
Kubernetes NFS类型的volume允许一块现有的网络硬盘(NFS)在同一个pod内的容器间共享。先来看下面这个在一个pod中使用NFS volume的例子。
...
desiredState:
manifest:
containers:
...
name: testpd
volumeMounts:
- mountPath: "/var/www/html/mount-test"
name: "myshare"
...
volumes:
- name: myshare
source:
nfsMount:
server: "172.17.0.2"
path: "/tmp"
readOnly: false
在上面这个例子中,我们可以看到一个名为myshare
的volume挂载到容器testpd
文件系统的/var/www/html/mount-test
路径上。该volume被定义为nfs类型,不是只读类型,来自于IP为172.17.0.2
的NFS服务器,该NFS服务器对外暴露/tmp
作为共享目录。
使用docker ps
命令查看新建的pod内的所有容器。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6fb701418241 10.10.103.215:5000/redis:latest "redis-server /etc/r 2 days ago Up 2 days k8s_master1.d2c39dbf_podtest.default.etcd_a041e598-972c-11e4-98cb-005056b43972_f8301b22
d1cbd23b5e74 10.10.103.215:5000/sshd:latest "/usr/sbin/sshd -D" 2 days ago Up 2 days k8s_master2.6f7a9ced_podtest.default.etcd_a041e598-972c-11e4-98cb-005056b43972_191738a2
b18cac9b1188 kubernetes/pause:go "/pause" 2 days ago Up 2 days 0.0.0.0:6388->6379/tcp, 0.0.0.0:8888->22/tcp k8s_net.d01ea6ed_podtest.default.etcd_a041e598-972c-11e4-98cb-005056b43972_0f7f5733
通过命令输出发现该pod内一共有3个容器。其中两个是用户容器,另一个是kubernetes的网络容器(k8s_net.*
),每个pod中都会自动运行一个网络容器,该网络容器负责将pod内的容器的端口绑定到宿主机所有的网络接口(包括localhost)上。网络容器负责端口映射,将master1容器的6379端口映射到宿主机上的6388端口,将master2容器的22端口映射到宿主机上的8888端口。下面我们来实验一下,我们先通过ssh的方式进入master2容器,然后再在master2容器内通过localhost访问master1容器。
$ ssh root@127.0.0.1 -p 8888
root@127.0.0.1's password:
Welcome to Ubuntu 12.04 LTS (GNU/Linux 3.13.0-32-generic x86_64)
这样就通过ssh的方式登录了master2容器,接着在master2容器内访问master1容器的6379端口。
$ telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
这样,就通过localhost:6379,连接上了master1容器。