Kubernetes 基础

容器技术系列分享(四)

Kubernetes 基础

目录

  • Kubernetes 是什么
  • 为什么要用 Kubernetes
  • 一个简单的例子
  • Kubernetes 基本概念和术语
    • Master
    • Node
    • Pod
    • Label
    • Replication Controller
    • Deployment
    • Horizontal Pod Autoscaler
    • StatefulSet
    • Service
    • Volume
    • Persisten Volume
    • Namespace
    • Annotation

Kubernetes 是什么

Kubernetes 是一个基于容器技术的分布式架构方案,它是谷歌内部系统 Borg 的一个开源版本。

Kubernetes是一个开放的开发平台,它不局限于任何一种语言,任何服务都可以映射为 Kubernetes 的 Service,并通过标准的 TCP 通信协议进行交互。

Kubernetes 具有完备的集群管理能力,包括多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和服务发现机制、内建智能负载均衡器、强大的故障发现和自我修复能力、服务滚动升级和在线扩容能力、可扩展的资源自动调度机制,以及多粒度的资源配额管理能力。同时,Kubernetes 提供了完善的管理工具,这些工具涵盖了包括开发、部署测试、运维监控在内的多个环节。

为什么要用 Kubernetes

Docker 容器化技术当前已经被很多公司所采用,其从单机走向集群已成为必然,而云计算的蓬勃发展正在加速这一过程。Kubernetes 作为当前唯一被业界广泛认可和看好的 Docker 分布式系统解决方案,可以遇见,在未来会有大量的新系统选择它。

使用 Kubernetes 可以收获哪些好处?

  • 首先,最直接的感受就是可以“轻装上阵”地开发复杂系统了。以前动不动就需要十几个人而且团队里需要不少技术达人一起分工协助才能设计实现的和运维的分布式系统,在采用 Kubernetes 解决方案后,只需要一个精悍的小团队就能轻松应付。
  • 其次,使用 Kubernetes 就是在全面拥抱微服务架构。微服务架构的核心是将一个巨大的单体应用分解为许多小的互相连接的微服务,一个微服务背后可能有多个实例副本在支撑,副本的数量可能会随着系统的负荷变化而进行调整,内嵌的负载均衡器在这里发挥了重要作用。微服务架构使得每个服务都可以由专门的开发团队来开发,开发者可以自由选择开发技术,这对于大规模团队来说很有价值,另外每个微服务独立开发、升级、扩展,因此系统具备很高的稳定性和快速迭代能力。谷歌将微服务架构的基础设施直接打包到 Kubernetes 解决方案中,让我们有机会直接应用微服务架构解决复杂业务系统的架构问题。
  • 然后,我们的系统可以随时随地整体“搬迁”到别的机房或公有云上。Kubernetes 最初的目标就是运行在谷歌自家的公有云 GCE 中,未来会支持更多的公有云及基于 OpenStack 的私有云。同时,在 Kubernetes 的架构方案中,底层网络的细节完全被屏蔽,基于服务的 ClusterIP 甚至都无需我们改变运行期的配置文件,就能将系统从物理环境中无缝迁移到公有云中,或者在服务高峰期将部分服务对应的 Pod 副本放入公有云中以提升系统的吞吐量。
  • 最后,Kubernetes 系统架构具备了超强的横向扩容能力。对于互联网公司来说,用户规模就等价于资产,谁拥有更多的用户,谁就能在竞争中胜出,因此超强的横向扩容能力是互联网业务系统的关键指标之一。不用修改代码,一个 Kubernetes 集群即可从只包含几个 Node 的小集群平滑扩展到拥有上百个 Node 的大规模集群,我们利用 Kubernetes 提供的工具,甚至可以在线完成集群扩容。只要我们的微服务设计的好,结合硬件或者公有云资源的线性增加,系统就能够承受大量用户并发访问所带来的巨大压力。

一个简单的例子

这是一个简单的 Java Web 应用,运行在 Tomcat 里。JSP 页面通过 JDBC 直接访问 MySQL 数据库并展示数据。

此应用需要启动两个容器: Web App 容器和 MySQL 容器,并且 Web App 容器需要访问 MySQL 容器。

启动 MySQL 应用

首先为 MySQL 服务创建一个 RC 定义文件: mysql-rc.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: ReplicationController
metadata:
name: mysql
spec:
replicas: 1
selector:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: "123456"

创建好 mysql-rc.yaml 文件后,将它发布到 Kubernetes 集群中:

kubectl create -f mysql-rc.yaml

接下来,我们用 kubectl 命令查看刚刚创建的 RC:

kubectl get rc

查看 Pod 的创建情况:

k8s01_javaweb01

我们通过 docker ps 命令查看正在运行的容器,发现提供 MySQL 服务的 Pod 容器以及创建并正常运行了。

创建 MySQL 服务

为 MySQL 服务创建一个 Service 定义文件 mysql-svc.yaml:

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
selector:
app: mysql

运行 kubectl 命令,创建 service:

kubectl create -f mysql-svc.yaml

运行 kubectl 命令,查看刚刚创建的 service:

kubectl get svc

k8s01_javaweb02

注意到这里分配了一个 CLUSTER-IP,这是一个虚拟地址,Kubernetes 集群中其他新创建的 Pod 就可以通过 Service 的 Cluster IP + Port 来连接它了。

启动 Tomcat 应用

我们用和启动 MySQL 同样的步骤启动 Tomcat。首先,创建对应的 RC 文件 myweb-rc.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: ReplicationController
metadata:
name: myweb
spec:
replicas: 2
selector:
app: myweb
template:
metadata:
labels:
app: myweb
spec:
containers:
- name: myweb
image: java-web:v1
ports:
- containerPort: 8080

kubectl create -f myweb-rc.yaml

kubectl get rc

kubectl get pods

k8s01_javaweb03

创建 Tomcat 服务

为 Tomcat 服务创建一个 Service 定义文件 myweb-svc.yaml:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: myweb
spec:
type: NodePort
ports:
- port: 8080
nodePort: 30003
selector:
app: myweb

kubectl create -f myweb-svc.yaml

kubectl get service

k8s01_javaweb04

Kubernetes 基本概念和术语

Kubernetes 中的大部分概念如 Node、Pod、Replication Controller、Service 等都可以看作一种 “资源对象”,几乎所有的资源对象都可以通过 Kubernetes 提供的 kubectl 工具(或者 API 编程调用)执行增、删、改、查等操作并将其保存在 Etcd 中持久化存储。从这个角度看,Kubernetes 其实是一个高度自动化的资源控制系统,它通过跟踪对比 Etcd 库里保存的“资源期望状态”与当前环境中的“实际资源状态”的差异来实现自动控制和自动纠错的高级功能。

在介绍资源对象之前,我们先了解一下 Kubernetes 集群的两种管理角色:Master 和 Node。

Master

Kubernetes 里的 Master 指的是集群控制节点,每个 Kubernetes 集群里需要有一个 Master 节点来负责整个集群的管理和控制,基本上 Kubernetes 的所有控制命令都发给它,它来负责具体的执行过程。Master 节点通常会占据一个独立的服务器(高可用部署建议用3台服务器),其主要原因是它太重要了,是整个集群的“首脑”,如果宕机或不可用,那么对集群内容器应用的管理都将失效。

Master 节点上运行着以下一组关键进程:

  • Kubernetes API Server (kube-apiserver):提供了 HTTP Rest 接口的关键服务进程,是 Kubernetes 里所有资源的增、删、改、查等操作的唯一入口,也是集群控制的入口进程。
  • Kubernetes Controller Manager (kube-controller-manager):Kubernetes 里所有资源对象的自动化控制中心。
  • Kubernetes Scheduler (kube-scheduler):负责资源调度(Pod 调度)的进程。

另外,Kubernetes 集群还需要一组 Etcd 服务,因为 Kubernetes 里所有资源对象的数据都保存在 Etcd 中。

Node

除了 Master, Kubernetes 集群中的其他机器被称为 Node 节点。与 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 会触发工作负载转移的自动流程。

kubectl get nodes

kubectl describe node k8s-node-1

上述命令会展示 Node 的如下关键信息:

  • Node 基本信息:名称、标签、创建时间等。
  • Node 当前的运行状态,Node 启动后会做一系列的自检工作,比如磁盘是否满了,如果满了就标注 OutOfDisk=True,否则继续检查内存是否不足(如果内存不足,就标注 MemoryPressure=True),最后一切正常,就设置为 Ready 状态(Ready=True),该状态表示 Node 处于健康状态, Master 将可以在其上调度新的任务了(如启动 Pod)。
  • Node 的主机地址与主机名
  • Node 上的资源总量:描述 Node 可用的系统资源,包括 CPU、内存、最大可调度 Pod 数量等。
  • Node 可分配资源量:描述 Node 当前可用于分配的资源量。
  • 主机系统信息:包括主机的唯一标识 UUID、Linux Kernel 版本号、操作系统类型与版本、Kubernetes 版本号、kubelet 与 kube-proxy 版本号等。
  • 当前正在运行的 Pod 列表概要信息。
  • 已分配的资源使用概要信息,例如资源申请的最低、最大允许使用量占系统总量的百分比。
  • Node 相关的 Event 信息。
Pod

Pod 是 Kubernetes 的最重要也最基本的概念。每个 Pod 都有一个特殊的被称为“根容器” 的 Pause 容器。Pause 容器对应的镜像属于 Kubernetes 平台的一部分,除了 Pause 容器,每个 Pod 还包含一个或多个紧密相关的用户业务容器。

k8s01_pod01

为什么 Kubernetes 会设计出一个全新的 Pod 概念并且 Pod 有这样特殊的组成结构?

  • 原因之一:在一组容器作为一个单元的情况下,我们难以对“整体”简单地进行判断及有效地进行行动。比如,一个容器死亡了,此时算是整体死亡么?是 N/M 的死亡率么?引入业务无关并且不易死亡的 Pause 容器作为 Pod 的根容器,以它的状态代表整个容器组的状态,就简单、巧妙地解决了这个难题。
  • 原因之二:Pod 里的多个业务容器共享 Pause 容器的 IP,共享 Pause 容器挂载的 Volume,这样既简化了密切关联的业务容器之间的通信问题,也很好地解决了它们之间的文件共享问题。

Kubernetes 为每个 Pod 都分配了唯一的 IP 地址,称之为 PodIP,一个 Pod 里的多个容器共享 PodIP 地址。Pod 的 IP 加上 Pod 里的容器端口(containerPort),就组成了一个概念————Endpoint,它代表此 Pod 里的一个服务进程的对外通信地址。一个 Pod 也存在着具有多个 Endpoint 的情况。

k8s01_pod02

Label

Label 是 Kubernetes 系统中另外一个核心概念。一个 Label 是一个 key=value 的键值对,其中 Key 与 value 由用户自己指定。Label 可以附加到各种资源上,例如 Node、Pod、Service、RC 等,一个资源对象可以定义任意数量的 Label,同一个 Label 也可以被添加到任意数量的资源对象上去,Label 通常在资源对象定义时确定,也可以在创建对象后动态添加或者删除。

我们可以通过给指定的资源对象捆绑一个或多个不同的 Label 来实现多维度的资源分组管理功能,以便于灵活、方便地进行资源分配、调度、配置、部署等管理工作。例如:部署不同版本的应用到不同的环境中;或者监控和分析应用(日志记录、监控、告警)等。一些常用的 Label 示例如下:

  • 版本标签:”release”:”stable”,”release”:”canary”…
  • 环境标签:”environment”:”dev”,”environment”:”production”…
  • 架构标签:”tier”:”frontend”,”tier”:”backend”…
  • 分区标签:”partition”:”customerA”,”partition”:”customerB”…
  • 质量管控标签:”track”:”daily”,”track”:”weekly”…

给某个资源对象定义一个 Label,就相当于给它打了一个标签,随后可以通过 Label Selector(标签选择器)查询和筛选拥有某些 Label 的资源对象,Kubernetes 通过这种方式实现了类似 SQL 的简单又通用的查询机制。

当前有两种 Label Selector 的表达式:基于等式的(Equality-based)和基于集合的(Set-based),前者采用“等式类”的表达式匹配标签,下面是一些具体的例子:

  • name = redis-slave:匹配所有具有标签 name=redis-slave 的资源对象。
  • env != production:匹配所有不具有标签 env=production 的资源对象,比如 env=test 就满足。

使用集合操作的表达式匹配标签例子:

  • name in (redis-master, redis-slave):匹配所有具有标签 name=redis-master 或者 name=redis-slave 的资源对象。
  • name not in (php-frontend):匹配所有不具有标签 name=php-frontend 的资源对象。

可以通过多个 Label Selector 表达式的组合实现复杂的条件选择,多个表达式之间用“,”进行分割即可,几个条件之间是 “AND” 的关系,即同时满足多个条件。

新出现的管理对象如 Deployment、ReplicaSet、DaemonSet 和 Job 则可以在 Selector 中使用基于集合的筛选条件定义,例如:

1
2
3
4
5
6
selector:
matchLabels:
app: myweb
matchExpressions:
- {key: tier, operator: In, values: [frontend]}
- {key: environment, operator: NotIn, values: [dev]}

matchLabels 用于定义一组 Label,与直接写在 Selector 中作用相同;matchExpressions 用于定义一组基于集合的筛选条件,可用的条件运算符包括:In、NotIn、Exists 和 DoesNotExist。

如果同时设置了 matchLabels 和 matchExpressions,则两组条件为 “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 “定向调度” 的特性。
Replication Controller

RC 是 Kubernetes 系统中的核心概念之一,简单来说,它其实是定义了一个期望的场景,即声明某种 Pod 的副本数量在任意时刻都符合某个预期值,所以 RC 的定义包括如下部分:

  • Pod 期待的副本数(replicas)
  • 用于筛选目标 Pod 的 Label Selector
  • 当 Pod 的副本数量小于预期数量时,用于创建新 Pod 的 Pod 模版(template)

下面是一个完整的 RC 定义的例子,即确保拥有 tier=frontend 标签的这个 Pod(运行 Tomcat 容器)在整个 Kubernetes 集群中始终只有一个副本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: ReplicationController
metadata:
name: frontend
spec:
replicas: 1
selector:
tier: frontend
template:
metadata:
labels:
app: app-demo
tier: frontend
spec:
containers:
- name: tomcact-demo
image: tomcat
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 环境中需要完成的许多手工运维工作(如主机监控脚本、应用监控脚本、故障恢复脚本等)。

在 RC 运行时,我们可以通过修改 RC 的副本数量,来实现 Pod 的动态缩放(Scaling)功能:

kubect scale rc frontend --replicas=3

在 Kubernetes v1.2时,Replication Controller升级成了 Replica Set,新功能支持集合的 Label selector(Set-based selector)。不过一般很少单独使用Replica Set,它主要被 Deployment 这个更高层的资源对象所使用,从而形成一整套 Pod 创建、删除、更新的编排机制。下面是等价于前面 RC 例子的 Replica Set 的定义(省去了 Pod 模版部分的内容):

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
name: frontend
spec:
selector:
matchLabels:
tier: frontend
matchExpressions:
- {key: tier, operator: In, values: [frontend]}
template:
......

最后我们总结一下关于 RC(Replica Set)的一些特性与作用:

  • 在大多数情况下,我们通过定义一个 RC 实现 Pod 的创建过程及副本数量的自动控制。
  • RC 里包括完整的 Pod 定义模版。
  • RC 通过 Label Selector 机制实现对 Pod 副本的自动控制。
  • 通过改变 RC 里的 Pod 副本数量,可以实现 Pod 的扩容或缩容功能。
  • 通过改变 RC 里 Pod 模版中的镜像版本,可以实现 Pod 的滚动升级功能。
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。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

创建 Deployment:

kubectl create -f tomcat-deployment.yaml

查看 Deployment 信息:

kubectl get deployments

k8s01_deployment

对上述输出中涉及的数量解释如下:

  • NAME: Deployment 的名字。
  • READY: Replica 状态值,包含已成功运行的 Pod 数量,和要求的值。
  • UP-TO-DATE: 最新版本的 Pod 的副本数量,用于指示在滚动升级的过程中,有多少个副本已经成功升级。
  • AVAILABLE: 当前集群中可用的 Pod 副本数量,即集群中当前存活的 Pod 数量。

查看 Replica Set,可以看到它的命名与 Deployment 的名字有关系:

kubectl get rs

查看创建的 Pod,我嗯发现 Pod 的命名以 Deployment 对应的 Replica Set 的名字为前缀,这种命名很清晰地表明了一个 Replica Set 创建了哪些 Pod,对于 Pod 滚动升级这种复杂的过程来说,很容易排查错误:

kubectl get pods

k8s01_rs

Pod 的管理对象,除了 RC 和 Deployment,还包括 ReplicaSet、DaemonSet、StatefulSet、Job 等,分别用于不同的应用场景中。

Horizontal Pod Autoscaler

前面我们提到过,通过手动执行 kubectl scale 命令,可以实现 Pod 扩容或缩容。而 Kubernetes v1.1 版本中发布了一个新特性————Horizontal Pod Autoscaling(Pod 横向自动扩容,简称 HPA)。

HPA 与之前的 RC、Deployment 一样,也属于一种 Kubernetes 资源对象。通过追踪分析 RC 控制的所有目标 Pod 的负载变化情况,来确定是否需要针对性地调整目标 Pod 的副本数。当前,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 副本数很可能不足以支撑接下来更多的请求,需要进行动态扩容,而当请求高峰时段过去后, Pod 的 CPU 利用率又会降下来,此时对应的 Pod 副本数应该自动减少到一个合理的水平。

下面是 HPA 定义的一个具体例子:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: php-apache
namespace: default
spec:
maxReplicas: 10
minReplicas: 1
scaleTargerRes:
kind: Deployment
name: php-apache
targerCPUUtilizationPercentage: 90
StatefulSet

在 Kubernetes 系统中,Pod 的管理对象 RC、Deployment、DaemonSet 和 Job 都是面向无状态的服务。但现实中有很多服务是有状态的,特别是一些复杂的中间件集群,例如 MySQL 集群、MongoDB 集群、ZooKeeper 集群等,这些应用集群有以下一些共同点:

  • 每个节点都有固定的身份 ID,通过这个 ID,集群中的成员可以互相发现并且通信。
  • 集群的规模是比较固定的,集群规模不能随意变动。
  • 集群里的每个节点都是有状态的,通常会持久化数据到永久存储中。
  • 如果磁盘损坏,则集群里的某个节点无法正常运行,集群功能受损。

如果用 RC/Deployment 控制 Pod 副本数的方式来实现上述有状态的集群,我们会发现第 1 点是无法满足的,因为 Pod 的名字是随机产生的,Pod 的 IP 地址也是在运行期才确定且可能有变动的,我们实现无法为每个 Pod 确定唯一不变的 ID。另外,为了能够在其他节点上恢复某个失败的节点,这种集群中的 Pod 需要挂接某种共享存储,为了解决这个问题,Kubernetes 从 v1.4 版本开始引入了 PetSet 这个新的资源对象,并且在 v1.5 版本是更名为 StatefulSet,StatefulSet 从本质上来说,可以看作 Deployment/RC 的一个特殊变种,它有如下一些特性:

  • StatefulSet 里的每个 Pod 都有稳定、唯一的网络标识,可以用来发现集群内的其他成员。假设 StatefulSet 的名字叫 kafka,那么第一个 Pod 叫 kafka-0,第二个叫 kafka-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 里面的 3 个 Pod 的 DNS 名称分别为 kafka-0.kafka、kafka-1.kafka、kafka-2.kafka,这些 DNS 名称可以直接在集群的配置文件中固定下来。

Service

Service 也是 Kubernetes 里的最核心的资源对象之一,Kubernetes 里的每个 Service 其实就是我们经常提起的微服务架构中的一个“微服务”,之前提到的 Pod、RC 等资源对象都是为它服务的。

k8s01_service

从图中可以看到,Kubernetes 的 Service 定义了一个服务的访问入口地址,前端的应用(Pod)通过这个入口地址访问其背后的一组由 Pod 副本组成的集群实例,Service 与其后端 Pod 副本集群之间则是通过 Label Selector 来实现“无缝对接”的。而 RC 的作用实际上是保证 Service 的服务能力和服务质量始终处于预期的标准。

通过分析、识别并建模系统中的所有服务为微服务————Kubernetes Service,最终我们的系统由多个提供不同业务能力而又彼此独立的微服务单元所组成,服务之间通过 TCP/IP 进行通信,从而形成了强大而又灵活的弹性网格,拥有了强大的分布式能力、弹性扩展能力、容错能力,与此同时,我们的程序架构也变得简单和直观许多。

k8s01_microService

Kubernetes集群中,运行在每个 Node 上的 kube-proxy 进程其实就是一个智能的软件负载均衡器,它负责把对 Service 的请求转发到后端的某个 Pod 实例上,并在内部实现服务的负载均衡与会话保持机制。但 Service 不是共用一个负载均衡器的 IP 地址,而是每个 Service 分配了一个全局唯一的虚拟 IP 地址,这个虚拟 IP 被称为 ClusterIP。这样一来,每个服务就变成了具备唯一 IP 地址的“通信节点”,服务调用就变成了最基础的 TCP 网络通信问题。

我们知道,Pod 的 Endpoint 地址会随着 Pod 的销毁和重新创建而发生改变,因为新 Pod 的 IP 地址与之前旧 Pod 的不同。而 Service 一旦被创建,Kubernetes 就会自动为它分配一个可用的 Cluster IP,而且在 Service 的整个生命周期内,它的 Cluster IP 不会发生改变。于是,服务发现这个棘手的问题在 Kubernetes的架构里也得以轻松解决:只要用 Service 的 Name 与 Service 的 Cluster IP 地址做一个 DNS 域名映射即可完美解决问题。

Volume

Volume 是 Pod 中能够被多个容器访问的共享目录。Kubernetes 的 Volume 概念、用途和目的与 Docker 的 Volume 比较类似,但两者不能等价。首先,Kubernetes 中的 Volume 定义在 Pod 上,然后被一个 Pod 里的多个容器挂载到具体的文件目录下;其次,Kubernetes中的 Volume 与 Pod 的生命周期相同,但与容器的生命周期不相关,当容器终止或者重启时,Volume 中的数据也不会丢失。最后,Kubernetes 支持多种类型的 Volume,例如 GlusterFS、Ceph 等先进的分布式文件系统。

Volume 的使用也比较简单,在大多数情况下,我们先在 Pod 上声明一个 Volume,然后在容器里引用该 Volume 并 Mount 到容器里的某个目录上。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
......
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: IfNotPresent
......

除了可以让一个 Pod 里的多个容器共享文件、让容器的数据写到宿主机的磁盘上或者写文件到网络存储中,Kubernetes 的 Volume 还扩展出了一种非常有实用价值的功能,即容器配置文件集中化定义与管理,这是通过 ConfigMap 这个资源对象来实现的。

Kubernetes 提供了非常丰富的Volume 类型,这里说明两个最基础的:

emptyDir

一个 emptyDir Volume 是在 Pod 分配到 Node 时创建的。从它的名字就可以看出,它的初始内容为空,并且无须指定宿主机上对应的目录文件,因为这是 Kubernetes 自动分配的一个目录,当 Pod 从 Node 上移除时,emptyDir 中的数据也会被永久删除。emptyDir 的一些用途如下:

  • 临时空间,例如用于某些应用程序运行时所需的临时目录,且无须永久保留。
  • 长时间任务的中间过程 CheckPoint 的临时保存目录。
  • 一个容器需要从另一个容器中获取数据的目录(多容器共享目录)

hostPath

hostPath 为在 Pod 上挂载宿主机上的文件或目录,它通常可以用于以下几方面:

  • 容器应用程序生成的日志文件需要永久保存时,可以使用宿主机的高速文件系统进行存储。
  • 需要访问宿主机上 Docker 引擎内部数据结构的容器应用时,可以通过定义 hostPath 为宿主机 /var/lib/docker 目录,使容器内部应用可以直接访问 Docker 的文件系统。

在使用这种类型的 Volume 时,需要注意以下几点:

  • 在不同的 Node 上具有相同配置的 Pod 可能会因为宿主机上的目录和文件不同而导致对 Volume 上目录和文件的访问结果不一致。
  • 如果使用了资源配额管理,则 Kubernetes 无法将 hostPath 在宿主机上使用的资源纳入管理。
Persistent Volume

前面说到的 Volume 是定义在 Pod 上的,属于“计算资源”的一部分。而实际上,“网络存储”是相对独立于“计算资源”而存在的一种实体资源。比如在使用虚拟机的情况下,我们通常会先定义一个网络存储,然后从中划出一个“网盘”并挂载到虚拟机上。Persistent Volume(简称 PV)和与之相关联的 Persistent Volume Claim(简称 PVC)也起到了类似的作用。

PV 可以理解成 Kubernetes 集群中的某个网络存储中对应的一块存储,它与 Volume 很类似,但有以下区别:

  • PV 只能是网络存储,不属于任何 Node,但可以在每个 Node 上访问。
  • PV 并不是定义在 Pod 上的,而是独立于 Pod 之外定义。
  • PV 目前支持的类型包括:NFS、iSCSI、RBD、CephFS、Cinder、GlusterFS 等,还有各家的公有云存储。

下面给出了 NFS 类型 PV 的一个定义文件,声明了需要 5Gi 的存储空间:

1
2
3
4
5
6
7
8
9
10
11
12
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)对象:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: PersistentVolumeClain
metadata:
name: myclaim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 8Gi

然后,在 Pod 的 Volume 定义中引用上述 PVC 即可:

1
2
3
4
volumes:
- name: mypd
persistentVolumeClaim:
claimName: myclaim

最后,我们说说 PV 的状态,PV 是有状态的对象,它有以下几种状态:

  • Available: 空闲状态。
  • Bound: 已经绑定到某个 PVC 上。
  • Released:对应的 PVC 已经删除,但资源还没有被集群收回。
  • Failed:PV 自动回收失败。
Namespace

Namespace(命名空间)是 Kubernetes 系统中的另一个非常重要的概念,Namespace 在很多情况下用于实现多租户的资源隔离。Namespace 通过将集群内部的资源对象“分配”到不同的 Namespace 中,形成逻辑上分组的不同项目、小组、或用户组,便于不同的分组在共享使用整个集群的资源的同时还能被分别管理。

Namespace 的定义很简单,如下所示的 yaml 定义了名为 development 的 Namespace:

1
2
3
4
apiVersion: v1
kind: Namespace
metadata:
name: development

一旦创建了 Namespace,我们在创建资源对象时就可以指定这个资源对象属于哪个 Namespace。比如在下面的例子中,我们定义了一个名为 busybox 的 Pod,放入 development 这个 Namespace里:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: development
spec:
containers:
- image: busybox
command:
- sleep
- "3600"
name: busybox

当我们给每个租户创建一个 Namespace 来实现多租户的资源隔离时,还能结合 Kubernetes 的资源配额管理,限定不同租户能占用的资源,例如 CPU 使用量、内存使用量等。

Annotation

Annotation(注解)与 Label 类似,也使用 key/value 键值对的形式进行定义。不同的是 Label 具有严格的命名规范,它定义的是 Kubernetes 对象的元数据(Metadata),并且用于 Label Selector。而 Annotation 则是用户任意定义的“附加”信息,以便于外部工具进行查找,很多时候,Kubernetes 的模块自身会通过 Annotation 的方式标记资源对象的一些特殊信息。

通常来说,用 Annotation来记录的信息如下:

  • build 信息、release 信息、Docker 镜像信息等,例如时间戳、release id 号、PR 号、镜像 hash 值、docker registry 地址等。
  • 日志库、监控库、分析库等资源库的地址信息。
  • 程序吊饰工具信息,例如工具名称、版本号等。
  • 团队的联系信息,例如电话号码、负责人名称、网址等。