Kubernetes 常用功能

引言

前面我们已经简单地介绍了 Kubernetes 是什么以及如何使用,本文我们将更加全面的介绍 Kubernetes 常用的资源(或者说其中定义的一些概念),更多关于 Kubernetes 的介绍均收录于<Kubernetes系列文章>中。

常用功能介绍

上一章已经大致介绍了在 Kubernetes 中创建的基本组件,包括他们的基本功能概述。那么接下来我们将更加详细地介绍所有类型的 Kubernetes 功能(又称对象或资源),以便你理解在何时、如何及为何要使用每一个对象。

Pod

我们已经了解到,pod 是一组并置的容器,它是 Kubernetes 中的基本构建模块。在实际应用中我们并不会单独部署容器,更多的是针对一组 pod 的容器进行部署和操作。值得注意的是,当一个 pod 包含多个容器时,这些容器总是运行于同一个工作节点上一个 pod 绝不会跨越多个工作节点。
pod-node
关于为何需要 pod 这种容器?为何不直接使用容器?为何甚至需要同时运行多个容器?难道不能简单地把所有进程都放在一个单独的容器中吗?接下来我们将一一回答上述问题。

想象一个由多个进程组成的应用程序,无论是通过ipc (进程间通信)还是本地存储文件进行通信,都要求它们运行于同一台机器上。在 Kubernetes 中,我们经常在容器中运行进程,由于每一个容器都非常像一台独立的机器,此时你可能认为在单个容器中运行多个进程是合乎逻辑的,然而在实践中这种做法并不合理。

容器被设计为每个容器只运行一个进程(除非进程本身产生子进程)。如果在单个容器中运行多个不相关的进程,那么保持所有进程运行、管理它们的日志等工作就得我们自己来做。例如,我们需要包含一种在进程崩溃时能够自动重启的机制。同时这些进程都将记录到相同的标准输出中,而此时我们将很难确定每个进程分别记录了什么。

综上所述,我们需要让每个进程运行于自己的容器中,然后我们用 pod 这一层抽象来使多个容器之间的交互就像同一个主机内的多个进程之间的交互一样,而这就是 Docker 和 Kubernetes 期望使用的方式。在包含容器的 pod 下,我们可以同时运行一些密切相关的进程,并为它们提供(几乎) 相同的环境,此时这些进程就好像全部运行于单个容器中一样,同时又保持着一定的隔离。这样一来,我们便能全面地利用容器所提供的特性,同时对这些进程来说它们就像运行在一起一样,实现两全其美。

容器原理

Kubernetes 通过配置 Docker 来让一个 pod 内的 所有容器共享相同的 Linux 命名空间,而不是每个容器都有自己的一组命名空间。由于一个 pod 中的所有容器都在相同的 network 和 UTS 命名空间下运行,所以它们都共享相同的主机名和网络接口。同样地,这些容器也都在相同的 IPC 命名空间下运行,因此能够通过 IPC 进行通信。但当涉及文件系统时,情况就有所不同。由于大多数容器的文件系统来自容器镜像,因此默认情况下,每个容器的文件系统与其他容器完全隔离。但我们可以使用名为 Volume 的 Kubernetes 资源来共享文件目录,这一点我们后面会介绍。

这里需强调的一点是,由于一个 pod 中的容器运行于相同的 Network 命名空间中,因此它们共享相同的 IP 地址和端口空间。此外,一个 pod 中的所有容器也都具有相同的 loopback 网络接口,因此容器可以通过 localhost 与同一 pod 中的其他容器进行通信。

那么,它是怎么做到共享各类命名空间的呢?实际上,这都依赖于 Pause 容器,它是一个 pod 的 init 容器,当要启动一个 pod 时,kubelet 会先为该 pod 启动一个 pause 容器,它在 pod 中担任 Linux 命名空间共享的基础。随后,kubelet 才会去启动该 pod 内的真正的容器,在这个过程中只要通过 --net=container:pause--ipc=contianer:pause--pid=container:pause 这三个参数,就能让所有容器共享 pause 容器的网络空间,ipc 空间和 pid 空间,不过 pid 空间默认是不共享的。
pod-namespace
好了,我们已经知道了同一个 pod 内的容器是如何通讯的,那么不同的 pod 之间又是如何通讯的呢?Kubernetes 集群中的所有 pod 都在同一个共享网络地址空间中,还记不记得启动集群时最初配置的 PodSubnet,每个 pod 都可以通过其他 pod 的 IP 地址来实现相互访问。换句话说,它们之间没有 NAT(网络地址转换)网关。
pod-network
因此,pod 之间的通信在逻辑上其实是非常简单的。不论是将两个 pod 安排在单一的还是不同的工作节点上,同时不管实际节点间的网络拓扑结构如何,这些 pod 内的容器都能够像在无 NAT 的平坦网络中一样相互通信,就像局域网(LAN)上的计算机一样。此时,每个 pod 都有自己的 IP 地址,并且可以通过这个专门的网络实现 pod 之间互相访问。这个专门的网络通常是由额外的软件基于真实链路实现的,我们前面使用的 flannel 就是其中的一种。

规划 Pod 中的容器

虽然我们可以在单个 pod 中同时运行多个容器,但是你要知道 pod 是 Kubernetes 缩扩容的基本单位,为了让缩扩容更有效率,我们应该根据不同应用的扩缩容需求,来决定是否应该将他们塞进同一个 pod 中。为了让缩扩容达到最灵活的水平,你可以认为是尽可能地将不同应用容器分散到不同的 pod 中。

那么,什么情况下要将多个容器塞进单一 pod 呢?如果一个应用是由一个主进程和多个辅助进程组成,可以考虑将他们放入同一个 pod。主进程和辅助进程需要使用 ipc 通讯,或者它们需要相同的网络地址,或者它们需要共用相同的磁盘。
pod-with-multiple-container

通过 YAML 管理 Pod

在使用 Kubernetes 时,一般都是使用 YAML 来管理 Pod 的,因为这样更加正式,而且可以通过 git 来维护 pod 的配置文件。本文不会解释 YAML 中所有属性的意义,而是简单的教大家怎么用。如果您想知道某一个属性的含义,最好直接访问官方文档。同时您也可以通过 kubectl explain pod,或者 kubectl explain pod.spec来查询每个字段的含义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# kubectl explain pod
KIND: Pod
VERSION: v1

DESCRIPTION:
Pod is a collection of containers that can run on a host. This resource is
created by clients and scheduled onto hosts.

FIELDS:
apiVersion <string>
APIVersion defines the versioned schema of this representation of an
object. Servers should convert recognized schemas to the latest internal
value, and may reject unrecognized values. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources

kind <string>
Kind is a string value representing the REST resource this object
represents. Servers may infer this from the endpoint the client submits
requests to. Cannot be updated. In CamelCase. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds

metadata <Object>
Standard object's metadata. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata

spec <Object>
Specification of the desired behavior of the pod. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status

status <Object>
Most recently observed status of the pod. This data may not be up to date.
Populated by the system. Read-only. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status

这里我们简单写一个 YAML 来启动前面的 kubia 容器:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1 # 该描述文件遵循 v1 版本的 API
kind: Pod # 在描述一个 Pod 对象
metadata: # 元数据,包括名称、命名空间、标签和关于该容器 的其他信息
name: kubia-manual # pod 名字
spec: # 包含 pod 内容的实际说明,例如 pod 的容器、卷和其他数据
containers:
- image: beikejiedeliulangmao/kubia # 使用的容器
name: kubia
ports: # 应用监听的端口,在 pod 定义中指定端口纯粹是展示性的,忽略它们对于客户端是否可以通过端口连接到 pod 不会带来任何影响,但明确定义端口仍是有意义的,在端口定义下,每个使用集群的人都可以快速查看每个 pod 对外暴露的端口,还可以给每个端口指定一个名称
- containerPort: 8080
protocol: TCP

然后我们通过 kubectl create -f kubia-manual.yaml 启动它,启动成功后可以查看完整的 pod 描述 kubectl get po kubia-manual -o yaml,可以查看所有运行中的 pod kubectl get pod,也可以查看 pod 的 logkubectl logs kubia-manual,容器化的应用程序通常会将日志记录到标准输出和标准错误流,这样当pod 删除时,日志也会被删除,如果希望 log 永久保存,我们需要使用集群范围的日志系统,这些我们后面会介绍。

每天或者每次日志文件达到 10MB 大小时,容器日志都会自动轮替。kubectl logs 命令仅显示最后一次轮替后的日志条目。

在调试阶段,我们可以通过 kubectl port-forward kubia-manual 8888:8080 创建代理,这样我们就能在本地连接到 pod。
pod-port-forward

对于微服务架构,部署的微服务数量可以轻松超过20个甚至更多。这些组件可能是副本(部署同一组件的多个副本)和多个不同的发布版本(stable、beta、canary等)同时运行。这样一来可能会导致我们在系统中拥有数百个 pod, 如果没有可以有效组织这些组件的机制,将会导致产生巨大的混乱。好在我们可以通过 label 来组织 pod 和所有其他 Kubernetes 对象。

如下图所示,我们给 pod 加上 app 和 rel 两个标签来表示应用名和环境。每个可以访问集群的开发或运维人员都可以通过查看pod标签轻松看到系统的结构,以及每个pod的角色。
pod-label
label 的内容写在 YAML 的 metadata 中:

1
2
3
4
5
6
7
apiVersion: v1 # 该描述文件遵循 v1 版本的 API
kind: Pod # 在描述一个 Pod 对象
metadata: # 元数据,包括名称、命名空间、标签和关于该容器 的其他信息
name: kubia-manual # pod 名字
labels: # 标签
app: kubia
rel: beta

我们可以通过 kubectl get po --show-labels 查看 pod 的 labels,也可以通过 kubectl get po -L app,rel 将 label 直接显示在列上。但是目前来看,label 并没有实际价值。接下来我们用 label 做些真正有意义的事。

我们可以通过 kubectl get po -l app=kubia 筛选 pod,匹配表达式可以是:

  • 等于:app=kubia
  • 不等于:app !=kubia
  • 范围匹配:app in (kubia)
  • 范围过滤:app notin (kubia)
  • 多匹配条件:app=kubia,rel=beta

目前为止,我们还只是在命令行中使用 label,实际上,我们还可以在 YAML 中使用,比如通过 label 来控制 pod 的部署需求,例如:我们可以给 node 打上标记 disk=ssd 或者 gpu=true,然后在 pod 的描述中选择特定的 node 部署。

1
2
3
4
5
6
7
8
apiVersion: v1 # 该描述文件遵循 v1 版本的 API
kind: Pod # 在描述一个 Pod 对象
metadata: # 元数据,包括名称、命名空间、标签和关于该容器 的其他信息
name: kubia-manual # pod 名字
spec:
nodeSelector:
gpu: true
disk: ssd

除标签外,pod 和其他对象还可以包含注解。注解也是键值对,所以它们本质上与标签非常相似。但与标签不同,注解并不是为了保存标识信息而存在的,注解主要用于工具使用,或者 Kubernetes 引入新特性时,也会用注解的方式引入。就像我们前面提到的云提供商的 LB 服务,就是通过注解接入该特性的,只不过它描述的对象是 Service 而不是 pod。

1
2
3
4
5
6
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"

再回到如何组织 Kubernetes 资源的问题,除了使用 label 之外,我们还可以使用命名空间,它可以做到资源的隔离,我们可以通过 kubectl get ns 查看所有的命名空间。而且我们的 kubectl 命令行默认是针对 default 命名空间操作的,如果要操作指定命名空间的资源可以通过 kubectl -n target-ns command。除了隔离资源,命名空间还可用于仅允许某些用户访问某些特定资源,甚至限制单个用户可用的计算资源数量,这些我们后面会介绍。

副本机制

正如你前面所学到的,pod 代表了 Kubernetes 中的基本部署单元,而且你已知道如何手动创建、监督和管理它们。但是在实际的用例里,你希望你的部署能自动保持运行,并且保持健康,无须任何手动干预。要做到这一点,你几乎不会直接创建 pod , 而是创建 ReplicationController 或 Deployment 这样的资源,接着由它们来创建并管理实际的 pod,并且在它们失败的时候自动重新启动它们。

Kubernetes 可以通过存活探针 (liveness probe) 检查容器是否还在运行。可以为 pod 中的每个容器单独指定存活探针。如果探测失败,Kubemetes 将定期执行探针并重新启动容器。探针的种类有 3 种:

  • HTTP GET:探针对容器的IP地址(你指定的端口和路径)执行 HTTP GET 请求。如果服务器返回错误响应状态码或者根本没有响应,那么探测就被认为是失败的,容器将被重新启动。
  • TCP套接字:尝试与容器指定端口建立TCP连接。如果连接成功建立,则探测成功。否则,容器重新启动。
  • Exec探针:在容器内执行任意命令,并检查命令的退出状态码。如果状态码是 0, 则探测成功。所有其他状态码都被认为失败。

我们可以在 YAML 中指定存活探针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1 # 该描述文件遵循 v1 版本的 API
kind: Pod # 在描述一个 Pod 对象
metadata: # 元数据,包括名称、命名空间、标签和关于该容器 的其他信息
name: kubia-manual # pod 名字
spec:
containers:
- image: beikejiedeliulangmao/kubia # 使用的容器
name: kubia
livenessProbe: # 使用 http 探针
httpGet:
path: /
port: 8080
initialDelaySeconds: 15 # 15秒初始延迟
# 如果没有设置初始延迟,探针将在启动时立即开始探测容器,这通常会导致探测失败,因为应用程序还没准备好开始接收请求。如果失败次数超过阈值,在应用程序能正确响应请求之前,容器就会重启。

如果上述 HTTP GET 探针连续 5 次失败就会认为探测失败,并重启容器。

当你想知道为什么前一个容器终止时,你想看到的是前一个容器的日志,而不是当前容器的。可以通过添加–previous选项来完成: kubectl logs mypod --previous, 通过 kubectl describe po mypod 可以查看 pod 为何重启。

对于在生产中运行的 pod, 一定要定义一个存活探针。没有探针的话,Kubernetes 无法知道你的应用是否还活着。只要进程还在运行,Kubernetes 会认为容器是健康的。探针应该尽可能的检查应用的可用性,同时也要尽可能轻量不能耗费太多资源。

Kubernetes 中有两种副本管理的资源 ReplicationController 和 ReplicaSet,它们本质上是一致的。它们都会持续监控正在运行的pod列表,并保证相应”类型”(通过 label 选择)的 pod 的数目与期望相符。如正在运行的 pod 太少,它会根据 pod 模板创建新的副本。如正在运行的 pod 太多,它将删除多余的副本。

如下就是一个 ReplicationController 的配置文件 YAML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: ReplicationController
metadata:
name: kubia
spec:
replicas: 3
selector: # 通过 app 标签匹配 pod
app: kubia
template: # 通过模板定义 pod
metadata:
labels: # 指定 pod 的标签
app: kubia
spec:
containers:
- image: beikejiedeliulangmao/kubia # 使用的容器
name: kubia

模板中的 pod 标签显然必须和 ReplicationController 的标签选择器匹配,否则控制器将无休止地创建新的容器。如果定义 ReplicationController 时不要指定 pod 选择器,会让 Kubernetes 从 pod 模板中提取它。

您可能会遇到这样的情况,线上环境的某一个服务节点出错了,我们需要把它摘下来确认出错的原因,这时候我们可以取消该 pod 和 ReplicationController 的绑定关系,然后单独确认它的问题,确认完后再将其还回去,这个过程可以通过修改 pod 的标签来实现,只要让 pod 的标签和 ReplicationController 的标签匹配不上就行了,这时候 ReplicationController 会自动创建一个新的 pod 来维持原来的 pod 数量。

如果你想进行 pod 的缩扩容,可以直接修改 ReplicationController 的副本数量 replicas,ReplicationController 会自动帮你进行缩扩容工作。

前面说过 Kubernetes 中有两种副本管理的资源 ReplicationController 和 ReplicaSet,那么用哪个呢?可以说 ReplicaSet 新一代的 ReplicationController,我们应该直接使用 ReplicaSet,但是通常我们并不是直接使用 ReplicaSet 资源,而是在创建更高层级的 Deployment 资源时自动创建他们,这个我们后面会介绍。

ReplicaSet 相较于 ReplicationController,pod 的匹配能力更强,ReplicationController 智能匹配 label=target,而 ReplicaSet 除此之外还支持 in,notin,exists(是否存在一个 label,值不重要),DoesNotExists。

ReplicationController 和 ReplicaSet 都用于在 Kubernetes 集群上运行部署特定数量的 pod。但是,当你希望 pod 在集群中的每个节点上运行时(并且每个节点都需要正好一个运行的pod实例),就需要用到 DaemonSet。它一般用作执行日志收集器和资源监视器。

让我们假设有一个名为 ssd-monitor 的守护进程,它需要在包含固态驱动器(SSD)的所有节点上运行。你将创建一个 DaemonSet,它在标记为具有SSD的所有节点上运行这个守护进程。集群管理员已经向所有此类节点添加了 disk=ssd 的标签,因此你将使用节点选择器创建 DaemonSet,该选择器只选择具有该标签的节点,如图所示。
maemon-set
如下的 YAML 就是 ssd-monitor 的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: apps/v1beta2
kind: DaemonSet
metadata:
name: ssd-monitor
spec:
selector:
matchLabels:
app: ssd-monitor
template:
metadata:
labels:
app: ssd-monitor
spec:
nodeSelector:
disk: ssd
containers:
- name: main
image: luksa/ssd-monitor

到目前为止,我们只谈论了需要持续运行的 pod。你会遇到只想运行完成工作后就终止任务的情况。ReplicationController、ReplicaSet 和 DaemonSet 会持续运行任务,永远达不到完成态。这些 pod 中的进程在退出时会重新启动。但是在一个可完成的任务中,其进程终止后,不应该再重新启动。

Kubernetes 通过 Job 资源提供了对此的支持,这与我们在本章中讨论的其他资源类似,但它允许你运行一种 pod, 该 pod 在内部进程成功结束时(通过进程返回值判断),不重启容器。一旦任务完成,pod 就被认为处于完成状态。

在发生节点故障时,该节点上由 Job 管理的 pod 将按照 ReplicaSet 的 pod 的方式,重新安排到其他节点。如果进程本身异常退出(进程返回错误退出代码时),可以将 Job 配置为重新启动容器。

如下的例子就是一个简单的 Job:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: batch/v1
kind: Job
metadata:
name: batch-job
spec:
completions: 5 # 需要执行五次
paralleism: 2 # 最多 2 个 pod 并发
template: # pod 模板
metadata:
labels:
app: batch-job
spec:
restartPolicy: OnFailure # 出错时重启
containers: # 使用的容器
- name: main
image: luksa/batch-job

除了这种一次性的 Job 之外,我们还可以创建周期性的 Job,这种资源叫做 CornJob。在计划的时间内,CronJob 资源会创建 Job 资源,然后 Job 创建 pod。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: batch-job-corn
spec:
schedule: "0,15,30,45 * * * *" # 每天每小时的 0 ,15,30,45分钟运行
jobTemplate: # 创建 job 用到的模板
spec:
template: # pod 模板
metadata:
labels:
app: batch-job-corn
spec:
restartPolicy: OnFailure # 出错时重启
containers: # 使用的容器
- name: main
image: luksa/batch-job

如果你不熟悉 cron 时间表格式,你会在网上找到很棒的教程和解释,我这里简单地介绍一下,时间表从左到右包含以下五个条目: 分钟,小时,每月中第几天,月,星期几。

服务

现在已经学习过了 pod, 以及如何通过 ReplicaSet 和类似资源部署运行。尽管特定的 pod 可以独立地应对外部刺激,现在大多数应用都需要根据外部请求做出响应。pod 需要一 种寻找其他 pod 的方法来使用其他 pod 提供的服务,不像在没有 Kubernetes 的世界,系统管理员要在用户端配置文件中明确指出服务的精确的 IP 地址或者主机名来配置每个客户端应用,但是同样的方式在 Kubernetes 中并不适用,因为 pod 是短暂的,它们随时会启动或者关闭,而且只有在 pod 启动时才会分配 ip,所以并不能提前预估到 ip 地址,而且水平扩容意味着会有多个 pod 提供相同服务,而实际上客户端并不关心 pod 的数量,它只希望有一个不变的 ip 可以用来访问自己期望的服务。

Kubernetes 中可以使用服务来为一组功能相同的 pod 提供一个不变的接入口。当服务存在时,它的 IP 地址和端口不会改变。客户端通过 IP 地址和端口号建立连接,这些连接会被路由到提供该服务的任意一个 pod 上。通过这种方式,客户端不需要知道每个单独的提供服务的 pod 的地址,这样这些 pod 就可以在集群中随时被创建或移除。

和 ReplicationController 一样,Service 也通过标签的过滤来暴露一组 pod 的服务。

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports: # 可以暴露多个端口
- port: 80 # 服务端口
targetPort: 8080 # 服务将连接转发到容器端口
selector: # 匹配标签
app: kubia

创建 Service 之后,会被分配一个内部集群 IP,通过这个 IP 无论是在集群内的宿主机上还是集群内的 pod 中都能访问到该服务。下面的例子展示了通过 kubectl exec <pod-name> curl <service-ip>:port在一个 pod 中访问服务的例子。
curl-service
如果希望特定客户端产生的所有请求每次都指向同一个 pod, 可以设置服务的 sessionAffinity 属性为 ClientIP 这种方式将会使服务代理将来自同一个 client IP 的所有请求转发至同一个 pod 上。

1
2
3
4
apiVersion: v1
kind: Service
spec:
sessionAffinity: ClientIP

通过服务,我们已经有了一个不变的服务 IP,Pod 内怎么知道服务的 IP 呢?Kubernetes 为客户端提供了发现服务 IP 和端口的方式。一个是通过环境变量指定,它可以在 pod 的YAML 中配置。另一个方法是通过 DNS 发现服务。Kubernetes 内部也有一个 dns 服务它会根据服务的变化快速做出响应。这种方法相较于环境变量来说更加灵活。Kubernetes 内部 DNS 的规则是 <service-name>.<service-namespace>.svc.cluster.local,如果客户端和服务在同一个命名空间的话,可以省略命名空间和后缀kubectl exec -it kubia-kqtr6 curl kubia:8080。值得一提的是,DNS 只能帮我们得到 IP 信息,至于服务使用了哪个端口,仍然需要在环境变量中配置。

到目前为止,我们只是讨论了 Service 如何暴露集群内部的服务,那么能不能让 service 绑定到集群外的服务呢?这就牵扯到 Service 的 EndPoint 的资源,在创建服务时,它会根据标签选择器找到所有集群内部的 pod,然后生成对应的 EndPoint 资源,当访问服务端口时,实际上会通过 EndPoint 进行转发。

我们可以通过 kubectl describe svc kubia 查看 service 的 EndPoint。理解了 EndPoint 的概念后,我们回到刚才的问题,因为服务与 EndPoint 的解耦,我们实际上可以为 service 手动加入自定义的 EndPoint。

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Endpoints
metadata:
name: external-service # endpoint 的名字必须和 service 名字相同,它们通过这一层纽带绑定在一起
subsets:
- addresses: # endpoint 的 ip
- ip: 11.11.11.11
- ip: 22.22.22.22
ports: # endpoint 的端口
- port: 80

service-external-ip
在前面,我们已经或多或少的提到了集群外的客户端如何访问集群内的 Service。总结一下,总共有三种方案:NodePort,LoadBalance,Ingress。

将一组 pod 公开给外部客户端的第一种方法是创建一个服务并将其类型设置为 NodePort。通过创建 NodePort 服务,可以让 Kubernetes 在其所有节点上保留一个端口(所有节点上都使用相同的端口号),并将传入的连接转发给作为服务部分的 pod。

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: kubia-nodeport
spec:
type: NodePort # node port 类型的服务
ports:
- port: 80 # service ip 对应的端口
targetPort: 8080 # pod 的目标端口
nodePort: 30123 # 通过任意节点的 30123 就可以访问服务,如果不设置该项会随机选用一个端口
selector:
app: kubia

创建成功后,通过 kubectl get svc,会看到刚才创建的服务,您会发现其中 PORT 列有两个端口80:30123/TCP,前面的是集群内 Service IP 用到的端口,后面的是 NodePort。在使用 NodePort 类型的服务时,一定要注意尽可能让客户端能够访问到尽可能多的节点 NodePort,这样当任意一个节点宕机后,仍然不影响使用。
nodeport-service
第二种方案是通过 LoadBalance,在云提供商上运行的 Kubernetes 集群通常支持从云基础架构自动提供负载平衡器。如果 Kubernetes 在不支持 LoadBalancer 服务的环境中运行,则不会调配负载平衡器,但该服务仍将表现得像一个 NodePort 服务。这是因为 LoadBalancer 服务是 NodePort 服务的扩展。

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Service
metadata:
name: kubia-nodeport
spec:
type: LoadBalancer # LoadBalancer 类型的服务
ports:
- port: 80 # service ip
targetPort: 8080 # pod 的目标端口

创建成功后,可以通过kubectl get svc,会看到刚才创建的服务,然后通过 EXTERNAL-IP + service ip 访问服务。外部客户端(可以使用 curl)连接到负载均衡器的80端口,并路由到其中一个节点上的隐式分配节点端口(实际上就是 NodePort)。之后该连接被转发到一个pod实例。
lb-service
当外部客户端通过节点端口连接到服务时(这也包括先通过负载均衡器时的情况),随机选择的 pod 并不一定在接收连接的同一节点上运行。可能需要额外的网络跳转才能到达 pod, 但这种行为并不符合期望。可以通过将服务配置为仅将外部通信重定向到接收连接的节点上运行的 pod 来阻止此额外跳数。

1
2
spec:
externalTrafficPolicy: Local

这个参数有一个缺点,如果本地没有该服务的 Pod,它不会自动转发到其他节点的 Pod,而是挂起连接。所以,使用的时候需要确保使用的节点上至少有一个服务 Pod。而且可能会出现流量不平均分配的问题。
local-service-disadvantage
通常,当集群内的客户端连接到服务时,支持服务的 pod 可以获取客户端的 IP 地址。但是,当通过节点端口接收到连接时,由于对数据包执行了源网络地址转换(SNAT), 因此数据包的源 IP 将发生更改。后端的 pod 无法看到实际的客户端 IP, 这对于某些需要了解客户端 IP 的应用程序来说可能是个问题。不过前面说的 Local LB 方式,因为不涉及额外的跳跃(不执行 SNAT),所以可以保留到客户端 IP。

最后一个暴露服务的方式是 Ingress,和 LB 不同的是,Ingress 只需要一个公网 IP 就能为多个服务提供外部访问能力。当客户端向 Ingress 发送 HTTP 请求时,Ingress 会根据请求的主机名和路径决定请求转发到的服务。
ingress
因为 Ingress 工作在应用层(HTTP),所以可以提供 Service 无法实现的功能,比如基于 cookie 的会话亲和性。

我们可以通过如下配置文件创建一个 Ingress:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: kubia
spec:
rules: # 可以匹配多个域名
- host: kubia.example.com # 映射服务的域名
http:
paths: # 可以匹配多个地址
- path: /
backend:
serviceName: kubia # 将请求映射到服务的端口
servicePort: 80

和 LoadBalancer 类型的 service 一样,Ingress 也需要云服务提供商的支持,如果创建成功,Ingress 的 ip 地址就能在kubectl get ingress 中看到,随后我们只要将该 ip 和域名绑定到 DNS 中就可以访问了。
ingress2
可能已经从 Ingress 的配置文件中看出它的写法非常像 nginx 的配置文件,实际上 Ingress 的其中一种实现方式就是基于 nginx。你可以像在 nginx 中配置多个域名多个 path 的匹配规则,将他们映射到不同的 service pod 中。同样你也可以像 nginx 一样支持 TLS 认证,一般来说我们会将证书和秘钥存储在 Kubernetes 的secret 资源中,然后在 Ingress 配置文件中引用它们。

1
2
3
4
5
6
7
8
9
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: kubia
spec:
tls: # 所有 tls 设置
- hosts:
- kubia.example.com # 需要使用 tls 的域名
secretName: tls-secret # secret 资源的名字

我们已经知道,如果 pod 的标签与服务的 pod 选择器相匹配,那么 pod 就将作为服务的后端。只要创建了具有适当标签的新 pod,它就成为服务的一部分,并且请求开始被重定向到 pod。那么如果新启动的 pod 还没有准备好接受请求呢,该 pod 可能需要时间来加载配置或数据,或者可能需要执行预热过程以防止第一个用户请求时间太长影响了用户体验。在这种情况下,不希望该 pod 立即开始接收请求,尤其是在运行的实例可以正确快速地处理请求的情况下。不要将请求转发到正在启动的 pod 中,直到完全准备就绪。

在前面,我们介绍了存活探针,通过它可以感知到 Pod 是否运转正常,和它类似,Kubernetes 中还有一个就绪探针,通过它来确认 Pod 是否准备就绪,配置文件的写法也和存活探针一样都在 ReplicationController 的 pod template 中。当 pod 准备就绪时,才会将请求路由到该 pod 中。像存活探针一样,就绪探针也有三种类型的:Exec 探针,HTTP GET 探针,TCP Socket 探针。它们的使用和前面介绍的存活探针一样。

与存活探针不同,如果容器未通过准备检查,不会被终止或重新启动。这是存活探针与就绪探针之间的重要区别。存活探针通过杀死异常的容器并用新的正常容器替代它们来保持 pod 正常工作,而就绪探针确保只有准备好处理请求的 pod 才可以接收它们(请求)。这在容器启动时最为必要,当然容器运行一段时间后也是有用的这个就绪探针依旧会发挥作用,如果任何时刻就绪探针报错,Kubernetes 就会把该 pod 从 server endpoint 中摘除。
readness-probe
上面的介绍中,都是涉及和单个 service pod 通讯,那如果我们想要和所有 service pod同时建立连接时,怎么做呢?一个可行的办法是将 service 的 clusterIp 属性设为 None,那样这个 service 就会变为一个 headless service,当我们访问 service 的 dns 时,dns 会将所有的 pod ip 返回。

服务是 Kubernetes 的一个重要概念,也是让许多开发人员感到困扰的根源。出于这个原因,了解一下如何排除服务故障是很有必要的,如果无法通过服务访问 pod, 应该根据下面的列表进行排查:

  • 首先,确保从集群内连接到服务的集群 IP, 而不是从外部。
  • 不要通过 ping 服务 IP 来判断服务是否可访问(请记住,服务的集群 IP 是虚拟IP, 是无法 ping 通的)。
  • 如果已经定义了就绪探针,请确保它返回成功;否则该pod不会成为服务的一部分。
  • 要确认某个容器是服务的一部分,请使用kubectl get endpoints来检查相应的端点对象。
  • 如果尝试通过 FQDN 或其中一部分来访问服务(例如,myservice.mynamespace.svc.cluster.local 或 myservice.mynamespace), 但并不起作用,请查看是否可以使用其集群 IP 而不是 FQDN 来访问服务。
  • 检查是否连接到服务公开的端口,而不是目标 pod 端口。
  • 尝试直接连接到 pod IP 以确认 pod 正在接收正确端口上的连接。
  • 如果无法通过 pod 的 IP 访问应用,请确保应用不是仅绑定到本地主机127.0.0.1。

我们之前说过,pod 类似逻辑主机,在逻辑主机中运行的进程共享诸如 CPU、RAM、网络接口等资源。但是磁盘并不会共享,需要谨记一点,pod 中的每个容器都有自己独立的文件系统,因为文件系统来自容器镜像。在某些场景下,我们可能希望新的容器可以在之前容器结束的位置继续运行,比如在物理机上重启进程。可能不需要(或者不想要)整个文件系统被持久化,但又希望能保存实际数据的目录。

Kubernetes 通过定义存储卷来满足这个需求,它们不像 pod 这样的顶级资源,而是被定义为 pod 的一部分,并和 pod 共享相同的生命周期。这意味着在 pod 启动时创建卷,并在删除 pod 时销毁卷。因此,在容器重新启动期间,卷的内容将保持不变,在重新启动容器之后,新容器可以识别前一个容器写入卷的所有文件。另外,如果一个 pod 包含多个容器,那这个卷可以同时被所有的容器使用。

假设有一个带有三个容器的 pod,一个容器运行了一个 web 服务器,该 web 服务器的 HTML 页面目录位于 /var/htdocs, 并将站点访问日志存储到 /var/logs 目录中。第二个容器运行了一个代理来创建 HTML 文件,并将它们存放在 /var/html 中,第三个容器处理在 /var/logs 目录中找到的日志(转换、压缩、分析它们或者做其他处理)。这里我们使用名为 emptyDir 的卷,卷被绑定到 pod 的 lifecycle(生命周期)中,只有在 pod 存在时才会存在,但是也有一些类型的卷支持 pod 和卷消失之后,卷的文件也可能保持原样,并可以挂载到新的卷中。
volume
除了前面提到的 emptyDir 之外,卷的类型有很多种,不同卷类型有各种用途。这里我们只介绍一些常用的卷。最简单的卷类型是 emptyDir 卷,它从一个空目录开始,运行在 pod 内的应用程序可以写入它需要的任何文件。因为卷的生存周期与 pod 的生存周期相关联,所以当删除 pod 时,卷的内容就会丢失。下面就是一个 emptyDir 卷的简单例子:

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: Pod
metadata:
name: fortune
spec:
containers: # 定义两个容器,挂在同一个卷到不同的目录
- image: luksa/fortune
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs # 读写模式
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true # 只读模式
ports:
- containerPort: 80
protocol: TCP
volumes: # 创建名为 html 的 emptyDir 卷,并挂在上述 2 个容器中
- name: html
emptyDir: {}

我们可以使用端口转发 kubectl port-forward fortune 8080:80 来确认该 pod 的运转情况,通过 curl http://localhost:8080 访问 pod 中的服务。

我们也可以通过 git 仓库来初始化前面提到的 emptyDir 卷,实际上这是另一种名为 gitRepo 的卷,它通过克隆 Git 仓库并在 pod 启动时(但在创建容器之前)检出特定版本来填充数据。但是要注意 gitRepo 在 pod 启动后并不会自动同步 git 仓库的最新内容。要想在 pod 运行期间同步 git 仓库的内容,我们需要给 pod 加一个负责同步工作的容器。

大多数 pod 应该忽略它们的主机节点,因此它们不应该访问节点文件系统上的任何文件。但是某些系统级别的 pod(切记,这些通常由 DaemonSet 管理)确实需要读取节点的文件或使用节点文件系统来访问节点设备。Kubernetes 通过 hostPath 卷实现了这一点。

hostPath 卷指向节点文件系统上的特定文件或目录。在同一个节点上运行并在其 hostPath 卷中使用相同路径的 pod 可以看到相同的文件。
hostpath
hostPath 卷是我们介绍的第一种类型的持久性存储,因为 gitRepo 和 emptyDir 卷的内容都会在 pod 被删除时被删除,而 hostPath 卷的内容则不会被删除。如果删除了一个 pod, 并且下一个 pod 使用了指向主机上相同路径的 hostPath 卷,则新 pod 将会发现上一个 pod 留下的数据,但前提是必须将其调度到与第一个 pod 相同的节点上,如果调度到了另一个节点,则会找不到数据。

当运行在一个 pod 中的应用程序需要将数据保存到磁盘上,并且即使该 pod 重新调度到另一个节点时也要求具有相同的数据可用。这就不能使用到目前为止我们提到的任何卷类型,由于这些数据需要可以从任何集群节点访问,因此必须将其存储在某种类型的网络存储(NAS) 中,例如 nfs。

1
2
3
4
5
volumes:
- name: data
nfs: # 使用 nfs 卷
server: 1.2.3.4 # nfs 服务 ip
path: /some/path # nfs 服务提供的路径

到目前为止,我们探索过的所有持久卷类型都要求 pod 的开发人员了解集群中可用的真实网络存储的基础结构。例如,要创建支持 NFS 协议的卷,开发人员必须知道 NFS 节点所在的实际服务器。这违背了 Kubernetes 的基本理念:”向应用程序及其开发人员隐藏真实的基础设施,使他们不必担心基础设施的具体状态,并使应用程序可在大量云服务商和数据企业之间进行功能迁移”。

理想的情况是,在 Kubernetes 上部署应用程序的开发人员不需要知道底层使用的是哪种存储技术,同理他们也不需要了解应该使用哪些类型的物理服务器来运行 pod, 与基础设施相关的交互是集群管理员独有的控制领域。

当开发人员需要一定数量的持久化存储来进行应用时,可以向 Kubernetes 请求,就像在创建 pod 时可以请求CPU、内存和其他资源一样。系统管理员可以对集群进行配置让其可以为应用程序提供所需的服务。

在 Kubernetes 集群中为了使应用能够正常请求存储资源,同时避免处理基础设施细节,引入了两个新的资源,分别是持久卷和持久卷声明。

研发人员无须向他们的 pod 中添加特定技术的卷,而是由集群管理员设置底层存储,然后通过 Kubernetes API 服务器创建PersistentVolume(持久卷,简称 PV)并注册。在创建持久卷时,管理员可以指定其大小和所支持的访问模式。

当集群用户需要在其 pod 中使用持久化存储时,他们首先创建持久卷声明(PersistentVolumeClaim, 简称 PVC)清单,指定所需要的最低容量要求和访问模式,然后用户将持久卷声明清单提交给 Kubernetes API 服务器,Kubernetes 将找到可匹配的持久卷并将其绑定到持久卷声明。

持久卷声明可以当作 pod 中的一个卷来使用,其他用户不能使用相同的持久卷,除非先通过删除持久卷声明绑定来释放。
pv-pvc
首先,我们假设自己是 Kubernetes 管理员,我们要创建一些持久化卷:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv
spec:
capacity: # 定义 PV 大小
storage: 1 Gi
accessModes: # 可以被单个客户端挂在为读写模式,或者被多个客户端挂载为只读
- ReadWriteOnce
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain # 当 PVC 被删除,PV 将会被保留(不清理和删除),需要管理员手动清理 PV 才能被下一个 PVC 绑定
nfs: # PV 指定 nfs 设备
server: 1.2.3.4 # nfs 服务 ip
path: /some/path # nfs 服务提供的路径

持久卷不属于任何命名空间, 它跟节点一样是集群层面的资源。
pv-pvc-resource
假设现在需要部署一个需要持久化存储的 pod,将要用到之前创建的持久卷,所以我们需要现在开始创建一个声明:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc # 将来会在 pod 中使用该名字来查找 PVC
spec:
resources:
requests:
storage: 1Gi # 申请 1GiB 空间
accessModes:
- ReadWriteOnce # 允许单个客户端访问(读写)

当创建好声明,Kubernetes 就会找到适当的持久卷并将其绑定到声明,持久卷的容量必须足够大以满足声明的需求,并且卷的访问模式必须包含声明中指定的访问模式。可以通过 kubectl get pv 确认持久卷与 PVC 的绑定情况。

访问模式:

  • RWO—ReadWriteOnce—仅允许单个节点挂载读写。
  • ROX—ReadOnlyMany—允许多个节点挂载只读。
  • RWX—ReadWriteMany—允许多个节点挂载读写这个卷。

创建好 PVC,之后我们就可以在 Pod 中直接使用:

1
2
3
4
volumes:
- name: data
persistentVolumeClaim:
claimName: pvc # 通过名字引用 PVC

如你所见,使用持久卷和持久卷声明可以轻松获得持久化存储资源,无须研发人员处理下面实际使用的存储技术,但这仍然需要一个集群管理员来支持实际的存储。幸运的是,Kubernetes 还可以通过动态配置持久卷来自动执行此任务,这个资源叫做 StorageClass,管理员可以创建多种 StorageClass,每种 StorageClass 对应了一种卷类型比如 nfs,在用户创建 PVC 时指定使用的 StorageClass,Kubernetes 会根据该 StorageClass 中配置的 PV 申请方式,自动创建新的 PV 并绑定到 PVC 上。
storage-class

配置信息和敏感数据

几乎所有的应用都需要配置信息(不同部署示例间的区分设置、访问外部系统的证书等),并且这些配置数据不应该被嵌入应用本身。在 Kubernetes 我们可以通过 ConfigMap 和 Secret 传递配置选项给运行在 Kubernetes 上的应用程序。一般来说 ConfigMap 用来传输普通的配置信息,而 Secret 用来传输敏感信息。

首先,我们要知道在 Pod 的配置文件中,我们可以修改容器的 ENTRYPOINT command 和 arguments。绝大多数情况下,只需要设置自定义参数。命令一般很少被覆盖,除非针对一些未定义 ENTRYPOINT 的通用镜像,例如 busybox。值得注意的是,容器的命令和参数设置在 pod 启动后无法修改。

1
2
3
4
5
6
kind: Pod
spec:
containers:
- image: some/image
command: ["/bin/command"]
args: ["argl", "arg2", "arg3"]

Kubernetes 允许为 pod 中的每一个容器都指定自定义的环境变量集合,与容器的命令和参数设置相同,环境变量列表无法在 pod 创建后被修改。

1
2
3
4
5
6
7
8
9
10
kind: Pod
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL # 定义环境变量
value: "30"
- name: USE_INTERVAL # 定义环境变量的同时,引用另一个环境变量
value: "$(INTERVAL)123"
name: fortune

pod 定义硬编码意味着需要有效区分生产环境与开发过程中的 pod 定义。为了能在多个环境下复用 pod 的定义,需要将配置从 pod 定义描述中解耦出来。幸运的是,你可以通过一种叫作 ConfigMap 的资源对象完成解耦,用 valueFrom 字段替代 value 字段使 ConfigMap 成为环境变量值的来源。

Kubernetes 允许将配置选项分离到单独的资源对象 ConfigMap 中,本质上就是一个键/值对映射,值可以是短字面量,也可以是完整的配置文件。应用无须直接读取 ConfigMap,甚至根本不需要知道其是否存在。映射的内容通过环境变量或者卷文件的形式传递给容器,而并非直接传递给容器。对于不同的环境(开发,测试,生产),我们可以创建多个配置清单,通过命名空间的隔离来保证配置清单可以使用相同的名字,这样我们就能完全复用同一份 Pod 配置文件。
configmap
我们可以通过命令或者配置文件来创建 ConfigMap:kubectl create configmap fortune-config --from-literal=sleep-interval=25kubectl create -f fortune-config.yaml

1
2
3
4
apiVersion: v1
kind: configMap
data:
sleeo-interval: 25

创建好 ConfigMap 后,我们可以在 Pod 中引用 ConfigMap 中的内容

1
2
3
4
5
6
7
8
9
10
11
12
kind: Pod
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL # 定义环境变量
valueFrom:
configMapKeyRef: # 绑定 configmap 和 key
name: fortune-config
key: sleeo-interval
args: ["$(INTERVAL)"] # 通过环境变量传递 ConfigMap 的内容到容器参数中
name: fortune

环境变量或者命令行方式一般作为配置较少信息时方案,如果要配置的内容很多时,我们还可以使用 ConfigMap 卷将配置信息暴露为文件,通过这种类型的卷,ConfigMap 中的每个条目都将暴露为一个文件,在容器中可以通过查看文件的内容来获取配置信息。

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
data:
my-nginx-config.conf: | # 所有条目第一行最后的管道符号表示后续的条目值是多行字面量。
server {
listen 80;
server_name www.example.com;
...
}
sleep-interval: 25
kind: ConfigMap

创建好上述 ConfigMap 之后,我们可以在 Pod 中挂在 configMap 类型的卷:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
name: fortune-configmap-volume
spec:
containers:
- image: nginx:alpine
name: web-server
volumeMounts:
- name: config
mountPath: /etc/nginx/conf.d
readOnly: true
volumes:
- name: config
configMap:
name: fortune-config

volume-config-map
当进行了 configMap 的挂载后,原容器内的 /etc/nginx/conf.d 会被隐藏,只会看到 configMap 的内容,当然我们也可以只挂在 configMap 中的单独几个配置项,这样就只会覆盖同名文件。

前面已经提到过,使用环境变量传递配置时,当 Pod 运行起来后就无法更改,但是如果使用的是 configMap 卷的话,则不会出现这种问题,一旦配置内容发生变化,容器内的文件也会跟着变化,同时,Kubernetes 有机制能让配置文件的变化事件通知给容器。但是要注意的是,不同容器的相同 ConfigMap 的更新过程并不是同步的,换句话说在进行 ConfigMap 的更新时,会出现多个 Pod 之间配置内容不同步的问题。

到目前为止传递给容器的所有信息都是比较常规的非敏感数据。然而正如开头提到的,配置通常会包含一些敏感数据,如证书和私钥,需要确保其安全性。为了存储与分发此类信息,Kubernetes 提供了一种称为 Secret 的单独资源对象。Secret 结构与 ConfigMap 类似,均是键/值对的映射。Secret 的使用方法也与 ConfigMap 相同,可以

  • 将 Secret 条目作为环境变量传递给容器
  • 将 Secret 条目暴露为卷中的文件

前面介绍 Ingress 时提到 TLS 一般都是通过 Secret 来传递证书。我们可以通过 kubectl create secret generic fortune-https --from-file=https.key --from-file=https.cert 来创建 Secret 资源。当我们通过 kubect1 get secret fortune-https -o yaml 查看 Secret 资源时,你会发现实际上在显示的时候,它会先通过 BASE64 编码 value 之后显示,这样做的原因是 Secret 不像 ConfigMap 那样只能存储文本,它还能存储二进制文件,所以它会先用 BASE64 编码后,再显示出来。当然并不是说 Secret 只能显示 BASE64 加密后的内容,我们也可以通过 stringData 来设置非二进制数据,这样就不会涉及 BASE64 编码。

再回到我们前面的 nginx 例子,我们可以再为其挂载一个 secret 卷来植入证书文件,同时修改 nginx 的配置文件,让其使用 secret 中的证书,最后达到的效果如下图所示。
secret
我们已经通过挂载 secret 卷至文件夹/etc/nginx/certs 将证书与私钥成功传递给容器。但是有一点要提的是,secret 卷采用的是内存文件挂载,存储在 Secret 中的数据不会写入磁盘,这样就无法被窃取。

Pod 访问 Kubernetes API

前面我们已经说过了 ConfigMap 和 Secret 这些资源可以为 Pod 传递预先设定好的资源,但是对于那些不能预先知道的数据,比如 pod 的 IP、 主机名或者是 pod 自身的名称,则需要通过 Kubernetes API 获取。

在 Kubernetes 中有两类 API,其中一个是 DownloadAPI,它不像传统 REST 服务那样访问,而是像 ConfigMap 那样在 pod 配置文件中引用:

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: Pod
metadata:
name: downward
spec:
containers:
- name: main
image: busybox
command: ["sleep", "9999999"]
resources:
requests:
cpu: 15m # 指定 pod 的 cpu 需求,如果宿主机的剩余 cpu 资源小于pod需要的量,那么该 pod 就不会被调度到这个节点
memory: 1OOKi
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name # 引用 pod manifest 中的元数据字段
- name: CONTAINER
valueFrom:
resourceFieldRef: # 通过 resourceFieldRef 获取容器请求的 CPU 和 内存使用量
resource: requests.cpu
divisor: 1m

除了环境变量之外,我们也可以通过卷的形式使用 downloadAPI。

1
2
3
4
5
6
7
volumes:
- name: downward
downwardAPI:
items:
- path: "podName"
fieldRef:
fieldPath: metadata.name

正如我们看到的,Downward API 方式并不复杂,它使得应用独立于 Kubernetes。不过通过 Downward API 的方式获取的元数据是相当有限的,如果需要获取更多的元数据,需要使用直接访问 Kubernetes API 服务器的方式。

我们已经了解到了很多 Kubernetes 的资源类型。但如果打算开发一个可以与 Kubernetes API 交互的应用,要首先了解各种 API 的 REST 接口。我们可以通过 kubectl cluster-info 来获取 Kubernetes API 的 EndPoint,并通过 kubectl proxy 来代理这些 API,这样我们就能在本地查看各个 API curl http://localhost:8001

1
2
3
4
5
6
7
8
9
10
11
12
13
curl 127.0.0.1:8001
#{
# "paths": [
# "/api",
# "/api/v1",
# "/apis",
# "/apis/",
# "/apis/admissionregistration.k8s.io",
# "/apis/admissionregistration.k8s.io/v1",
# "/apis/admissionregistration.k8s.io/v1beta1",
# ...
# ]
#}

您可以通过这些 API 来操作整个 Kubernetes 集群,你能做到所有 Kubernetes 能做到,比如查看 Pod,修改 Pod,创建 Pod 等等,你可以操作任意资源。但是如果你要在 Pod 中访问这些 API 可不像我们这里使用的 kubectl proxy 这么简单,你需要找到 API 服务器的地址,确保是和 API服务器交互,而不是一个中间人,同时你还要通过服务器的认证,Kubernetes 通过账号和账号权限来管理 Pod 中能使用的资源范围。

最简单的访问 Kubernetes API 的方法是在容器中访问 curl https://kubernetes,而在每个 pod 中都存有一个自己的 Kubernetes API 账号信息,它们存储在容器的 /var/run/secrets/kubernetes.io/serviceaccount/ 中,该文件夹中有三个文件:

  • ca.crt:包含了 CA 的证书,用来对 Kubernetes API 服务器证书进行签名
  • token:获得授权,认证的凭证
  • namespace: 获取 pod 所在的命名空间

简要说明 pod 如何与 Kubernetes 交互:

  • 应用应该验证 API 服务器的证书是否是证书机构所签发,这个证书是在 ca.crt 文件中。
  • 应用应该将它在 token 文件中持有的凭证通过 Authorization 标头来获得 API服务器的授权。
  • 当对 pod 所在命名空间的 API 对象进行 CRUD 操作时,应该使用 namespace 文件来传递命名空间信息到 API 服务器。

Kubernetes-api
实际上我们不仅能通过命令行访问 Kubernetes API,在 Go,python,java 等语言中都有相关的库可以用来和 Kubernetes 交互。

还有一点是,除了每个 pod 中默认的用户之外,我们还可以在 Kubernetes 中创建特定的账号然后将一些资源的权限绑定到该账号上,然后将该账号绑定到特定的 pod 上。

Deployment

现在你己经知道如何将应用程序组件打包进容器,将它们分组到 pod 中,使用 ReplicaSet 维持 Pod 的可用性,并为它们提供临时存储或持续化存储,将密钥或配置文件注入,并可以使用 service 来使 pod 之间相互通信。但是,如果我们要升级自己的应用程序时,我们要怎么办,把原来的 Pod 都删了然后手动创建新的么?Kubernetes 当然为你准备的对应的资源,它就是 Deployment,它可以帮助你实现真正的零停机升级过程。

在 Kubernetes 中,一般使用的应用升级方式是滚动升级,下图就描述了滚动升级的过程,每当启动一个新版的 Pod 后,才会删除一个久的 Pod,最终将所有 Pod 都滚动升级到最新版本。
roll-update
我们可以通过 Deployment 来进行应用的部署和升级。当创建好一个 Deployment 资源后,它会创建一个 ReplicaSet,而 ReplicaSet 再进行 Pod 的维护。当要更新应用时,我们只需要修改 Deployment 资源的目标状态,Kubernetes 会帮我们处理整个中间过程。

我们可以通过如下配置文件创建一个 Deployment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: kubia
spec:
replicas: 3
template:
metadata:
name: kubia
labels:
app: kubia
spec:
containers:
- image: luksa/kubia:v1
name: nodejs

创建好配置文件后,可以通过 kubectl create -f kubia-deployment-v1.yaml --record 创建该 Deployment 资源,确保在创建时使用了 –record 选项。这个选项会记录历史版本号,在之后的操作中非常有用。创建好后,您可以通过 kubectl rollout status deployment kubia查看 Deployment 的状态。

然后,假设我们现在要更新应用,我们只需要执行 kubectl set image deployment kubia nodejs=luksa/kubia:v2 就相当于修改了刚才创建的 Deployment 资源。这时候它会自动创建一个新的 ReplicaSet 资源,并滚动的升级 pod,每当新 ReplicaSet 中创建完成一个 pod 的创建就会删除旧的 ReplicaSet 中的一个 Pod,最终将所有 Pod 都更新。
deployment-roll-update
你可能会发现 Deployment 并没有在升级成功后直接删除旧的 ReplicaSet,而是保留了它,这是为什么呢?其实,这是为了回滚做准备,假设我们在升级的过程中,发现新版本的应用有 Bug,可以通过 kubectl rollout undo deployment kubia 进行回滚,这时候 Deployment 会停止升级,并且慢慢地将原来的 ReplicaSet 的 pod 都恢复回来。

为了让我们能够回滚到任意一个版本,Kubernetes 会一直保存之前的 ReplicaSet,我们可以通过 kubectl rollout history deployment kubia 查看所有的升级过程,还记得创建 deployment 时的 –record 参数吗?如果不给定这个参数,版本历史中的 CHANGE-CAUSE 这一栏会为空。这也会使用户很难辨别每次的版本做了哪些修改。

1
2
3
4
5
kubectl rollout history deployment kubia
#deployments ”kubia”:
#REVISION CHANGE-CAUSE
#2 kubectl set image deployment kubia nodejs=luksa/kubia:v2
#3 kubectl set image deployment kubia nodejs=luksa/kubia:v3

因为 Kubernetes 会为我们记录所有的更新历史,所以我们才可以通过 undo 指令回滚到任意一个特定版本kubectl rollout undo deployment kubia --to-revision=1,但是如果版本历史保存的过多会让 ReplicaSet 资源很混乱,所以可以通过 revisionHistoryLimit 来控制保存的历史版本数量(默认值是 10)。

在 Kubernetes 中,我们可以通过 minReadySeconds,maxSurge 和 maxUnavailable 来控制滚动升级的速率,minReadySeconds 的效果是让 Kubernetes 在 pod 就绪之后继续等待10秒,然后继续执行滚动升级,来减缓滚动升级的过程。而其他两个属性的效果如下:
roll-update-speed
除此之外,我们还可以通过 pauseresume 命令来暂停并恢复滚动升级的过程。

有状态的 Pod

我们已经知道了每个 Pod 中看到的内容,都是独立的镜像,而且是一次性的,当 Pod 销毁时就会消失。为了解决这个问题,我们可以使用 Kubernetes 的卷资源,但是通过它我们只能保证一个 PVC 与 ReplicaSet 绑定,其中的每个 Pod 都绑定到同一个 PVC 上,但是想象一下如果我们要运行的是数据库 Pod(一个 master,多个 slave),它们每个 pod 都需要独立的持久存储空间,而之前的 ReplicaSet 只能保证所有的 Pod 共用相同的 PVC,而且如果 Pod 重启了,它还需要绑定到原来的 PVC 上这样才会不丢失数据。对于这类应用,Kubernetes 提供了 StatefulSet 来管理。接下来,我们先看看 StatefulSet 的特性。

一个 StatefulSet 创建的每个 pod 都有一个从零开始的顺序索引,这个会体现在 pod 的名称和主机名上,同样还会体现在 pod 对应的固定存储上。这些 pod 的名称则是可预知的,因为它是由 StatefulSet 的名称加该实例的顺序索引值组成的。
replicaset-statefulset
此外,有状态的 pod 有时候需要通过其主机名来定位,而无状态的 pod 则不需要,因为每个无状态的 pod 都是一样的,在需要的时候随便选择一个即可。但对于有状态的 pod 来说,因为它们都是彼此不同的(比如拥有不同的状态),通常希望操作的是其中特定的一个,比如只想通过数据库的 master 进行写操作,slave 进行读操作。基于这个原因,每个 pod 都会有自己的域名,比如a-0.foo.default.svc.cluster.local,此外也可以通过 DNS 服务查找域名 foo.default.svc.cluster.local,它将返回所有 pod 的 ip。

最后,就像我们例子所说的那样,当 StatefulSet 的每个 Pod 都应该有一个和自己绑定的 PVC,而且当 pod 被删除后再次启动时,它必须挂载上之前的 PVC。

清楚了 StatefulSet 的特性后,就让我们来创一个 StatefulSet 资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: kubia
spec:
serviceName: kubia
replicas: 2
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia-pet
ports:
- name: http
containerPort: 8080
volumeMounts: # pod 中 PVC 绑定路径
- name: data
mountPath: /var/data
volumeClaimTemplates: # 创建 PVC 的模板,用于为每个 pod 创建 PVC
- metadata:
name: data
spec:
resources:
requests:
storage: 1Mi
accessModes:
- ReadWriteOnce

创建好上述 StatefulSet 资源后,Kubernetes 会一个接一个的创建相应的 pod,第二个 pod 会在第一 个 pod 运行并且处于就绪状态后创建。StatefulSet 这样的行为是因为:状态明确的集群应用对同时有两个集群成员启动引起的竞争情况是非常敏感的。所以依次启动每个成员是比较安全可靠的。特定的有状态应用集群在两个或多个集群成员同时启动时引起的竞态条件是非常敏感的,所以在每个成员完全启动后再启动剩下的会更加安全。

当启动成功后,您会看到两个 pod,和两个 pvc,每个 pod 都有自己绑定的 PVC。kubia-0 <-> data-kubia-0,kubia-1 <-> data-kubia-1。这时候,如果我们删除其中一个 pod,新的 pod 可能会被调度到其他节点上去,但是旧 pod 的全部标记(名称,主机名,存储)实际上都会转移到新的 pod 上。
StatefulSet-pod
缩容一个 StatefulSet, 然后在完成后再扩容它,与删除一个 pod 后让 StatefulSet 立马重新创建它的表现是没有区别的,删除 pod 会复用原 pod 的 PVC,而缩容后再扩容的话则会创建新的 PVC。需要记住的是,缩容一个 StatefulSet 只会删除对应的 pod, 留下卸载后的持久卷声明。

当进行 StatefulSet 的扩容后,新 pod 可能第一时间并没有数据,它可以通过 DNS 来获取现存 pod 的 ip,然后从它们那里同步数据,然后再开始对外暴露服务。

参考内容

[1] kubernetes GitHub 仓库
[2] Kubernetes 官方主页
[3] Kubernetes 官方 Demo
[4] 《Kubernetes in Action》
[5] 理解Kubernetes网络之Flannel网络
[6] Kubernetes Handbook
[7] iptables概念介绍及相关操作
[8] iptables超全详解
[9] 理解Docker容器网络之Linux Network Namespace
[10] A Guide to the Kubernetes Networking Model
[11] Kubernetes with Flannel — Understanding the Networking
[12] 四层、七层负载均衡的区别

贝克街的流浪猫 wechat
您的打赏将鼓励我继续分享!
  • 本文作者: 贝克街的流浪猫
  • 本文链接: https://www.beikejiedeliulangmao.top/container/kubernetes/concept/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 创作声明: 本文基于上述所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。