戳蓝字“CSDN云计算”关注我们哦!
来源 / 译者 | 伪架构师
原文作者 | Jérôme Petazzoni
要把容器化的应用部署起来?在 Kubernetes 中部署容器化应用,总要涉及到
Deployment,这里有这个对象的所有内容。
我们最早学会的 Kubernetes 命令之一就是 kubectl run。具备 Docker 经验的用户,不免会用 docker run 命令和这个命令进行对比,结论可能是:运行容器就是这么简单。
我们来看看,在运行一个基本的 kubectl run 命令的时候,都发生了些什么:
$ kubectl run web --image=nginx
deployment.apps/web created
集群中创建了什么?
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/web-65899c769f-dhtdx 1/1 Running 0 11s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 443/TCP 46s
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
deployment.apps/web 1 1 1 1 11s
NAME DESIRED CURRENT READY AGE
replicaset.apps/web-65899c769f 1 1 1 11s
我们并没有看到容器,而是一组未知对象:
Deployment:web
ReplicaSet:web-65899c769f
Pod:web-65899c769f-dhtdx
此处的 kubernetes 服务可以忽略,它在我们运行命令之前就已经存在了。
我只想要个容器!为什么看到了三个不同的对象?
简单说来,这些 Kubernetes 对象能在不停服务的情况下,为应用提供渐进式部署、回滚以及伸缩的支持。
初次见面难免会好奇:究竟是怎么回事?在了解这些问题之后,就会理解每个对象的角色和存在价值了。
持续集成提升了对代码的信心。要把这种信心扩展到发布流程之中,部署操作就需要更多保障。
在 Kubernetes 中,一个 Deployment 的最小单元不是容器,而是 Pod。Pod 是一组容器(当然这一组也可以只有一个),它们运行在同一台服务器中,并共享一些资源。
例如 Pod 中的容器能够通过 localhost 互相通信。在网络视角中,这些容器中的所有进程都是本地的。
但是我们永远无法创建独立的容器:最相近的操作也只能是创建一个仅包含单一容器的一个 Pod。
我们想让 Kubernetes 创建 NGINX,完整的台词是:“我要一个 Pod,其中只包含一个容器,这个容器运行的是 nginx 镜像”。
# pod-nginx.yml
# Create it with:
# kubectl apply -f pod-nginx.yml
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
name: http
这就只有一个 Pod,那 ReplicaSet 和 Deployment 是怎么回事?
指令和声明
Kubernetes 是一个声明式系统(和指令式系统相对),这就意味着我们无法给它发出命令。我们不能说:“运行这个容器”。我们能做的只能是——描述我们需要的东西,然后等 Kubernetes 根据现有内容,同步为预期内容。
打个比方,我们可以说:“我要一个 40 英尺高的有黄色门的蓝色容器”,Kubernetes 会为我们查找这种容器,如果找不到,就会创建一个;如果已经有了,但它是绿色红门的,Kubernetes 就会帮我们上色;如果已经有了完全符合要求的容器,因为现有内容和预期内容一致,所以 Kubernetes 什么都不会做。
回到软件容器的话题,我们可以说:“我想要一个名字叫 web 的 Pod,其中应该有单独的容器,运行的是 nginx 镜像”。
如果这个 Pod 不存在,Kubernetes 会创建出来。如果符合我们要求的 Pod 已经存在,Kubernetes 无需进行任何动作。
基于这种思路,怎样对 web 应用进行伸缩,来满足多容器或 Pod 的运行需要呢?
ReplicaSet 简化了 Pod 的伸缩过程
如果我们只有一个 Pod,我们想要更多的同样的 Pod,我们可能会给 Kubernetes 提出这样的要求:“我们需要一个叫做 web2 的 Pod,具体要求是:…”,然后重复之前的 Pod 规范。想要多少 Pod,就重复执行多少次。
这明显很不方便,我们要自己跟踪所有的 Pod,确保它们都同步了正确的状态,并符合特定的规范。
Kubernetes 提供了高级一些的抽象来简化这个过程:ReplicaSet。ReplicaSet 的对象结构和 Pod 很相似,只不过它还有个副本数量的字段,用于描述我们所需要的符合规范的 Pod 数量。
有了 ReplicaSet,我们就可以告诉 Kubernetes:“我需要一个叫做 web 的 ReplicaSet,其中包含 3 个 Pod,这些 Pod 符合如下规范:……”,Kubernetes 会根据这个指令来确认,是不是刚好有三个符合规范的 Pod。如果我们从头开始,就会创建这 3 个 Pod。如果已经有了 3 个 Pod,什么事都不会发生——我们的要求和现状一致。
# pod-replicas.yml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: web-replicas
labels:
app: web
tier: frontend
spec:
replicas: 3
selector:
matchLabels:
tier: frontend
template:
metadata:
labels:
app: web
tier: frontend
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
我们可以修改现存 ReplicaSet 的副本数量,以此来完成伸缩。Kubernetes 会根据伸缩指令来创建或删除 Pod,让 Pod 数量符合要求。
高可用方面,因为 Kubernetes 会持续的对集群进行监控,确保无论什么情况下都保有指定数量的运行实例。
如果节点当机,恰好其中有一个 web 所属的 Pod,Kubernetes 会另外创建一个 Pod 来替换它。如果节点没有当机,不过是有一段时间无法联系或者没有响应,那么它再次恢复可用之后,就会多出一个 Pod,Kubernetes 会中止一个 Pod 来保证数量符合要求。
修改 Pod 定义并不罕见。比如我们经常会希望把容器镜像替换为新版本。
记住:ReplicaSet 的使命是,“确保有 N 个符合规范的 Pod。”如果我们修改了定义,会发生什么呢——突然就没有符合新规范的 Pod 了。
写到这里,我们已经知道了声明式系统的工作方式:Kubernetes 会立刻创建 N 个符合新规范的 Pod。旧的 Pod 会一致存在,直到我们手工清理。
如果能用 CI/CD 对这些过期 Pod 做一个自动清理可能不错;如果新 Pod 的创建能用更优雅的方式也会更好。
前面说的需要就是 Deployment 的职责。粗看上去,Deployment 的规范和 ReplicaSet 很像:其中包含了 Pod 规范,以及副本数量。(还有一些后面会讨论的参数)
# deployment-nginx.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
selector:
matchLabels:
app: nginx
replicas: 3
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
Deployment 并不会直接负责 Pod 的创建和删除。它会把这些工作委托给一个或多个 ReplicaSet。
在我们创建 Deployment 的时候,它会用自己的 Pod 规范创建一个 ReplicaSet。
当更新一个 Deployment 并修改副本数量时,它会把更新内容传递给下游的 ReplicaSet。
当配置发生了变化
需要更新 Pod 规范的时候,事情就有意思了。例如我们可能需要使用新版本的镜像(因为我们发布了新的版本),或者修改应用的参数(通过命令行参数、环境变量或者配置文件)。
在我们更新 Pod 规范时,Deployment 会用新的 Pod 规范创建新的 ReplicaSet。新的 ReplicaSet 的初始实例数量是 0。接下来 ReplicaSet 的实例数量会逐步提升,同时逐渐减少另一个 ReplicaSet 的尺寸。
可以想象一下,面前有个混音台,我们要让新的 ReplicaSet 淡入,同时把旧的那个淡出。
整个过程之中,请求被发送给新旧两个 ReplicaSet,用户不会感觉服务中断。
全景大致如此,其中还有很多小细节,让整个过程更加健壮。
如果我们推出了一个故障版本,因为 Kubernetes 会持续把旧 Pod 替换成新的(故障)版本,它可能会让整个应用坏掉(逐个 Pod)。
除非我们用上了就绪检测。
就绪检测是在容器规范中加入的一个测试过程。他是一个二进制测试,结果只有两个“能行”或者“不行”,这个测试会以指定的间隔被执行(缺省情况下是每 10 秒)。
Kubernetes 支持三种方式的就绪检测:
在容器内运行一个命令;
向容器发出一个 HTTP(S) 请求;
向容器发起一个 TCP 连接。
Kubernetes 会通过测试结果来了解容器及其所处 Pod 是否准备就绪可以接受流量。在我们推出新版本时,Kubernetes 会等到新 Pod 测试得到“就绪”结果之后,才会进入下一步。
如果一个 Pod 因为就绪检测持续失败,永远无法进入就绪状态,Kubernetes 也不会进入下一步。部署过程会停止,应用会继续使用老版本运行,直到我们解决了问题。
如果没有就绪检测,那么这个容器成功启动后就会被当成是就绪状态。所以最好能使用就绪检测来保障业务。
在滚动更新过程中或之后的任何时间,我们都可以告诉 Kubernetes:“我改主意了,请回到这个 Deployment 的前一个版本。”,这个操作会切换新旧 ReplicaSet 的地位。在这个点开始,会提高旧版 ReplicaSet 的实例数量到指定数值,同时降低新版的的实例数量。
一般来说,并不限于新旧两个 ReplicaSet。归根结底,有一个 ReplicaSet 被视为“最新”版本,我们可以将这个版本作为目标 ReplicaSet,所谓目标,就是我们希望运行的,也是 Kubernetes 会逐步拉起的一个版本。同时也可以有任意多个其它版本的 ReplicaSet,对应旧版本。
例如我们在运行 10 个副本的版本 1 应用,然后开始推出版本 2。在某个时间点,我们可能有了 7 个版本 1、3 个版本 2 的 Pod 正在运行。如果我们不想等版本 2 完全推出,决定推出版本 3。在版本 3 部署的时候,我们又想回到版本 1。整个过程,Kubernetes 都会根据需要对各个版本的 ReplicaSet 中的副本数量进行调整。
Kubernetes 不一定是一次更新一个 Pod 的。之前我们提到 Deployment 还有一些额外的参数,这些参数中包括了 MaxSurge 和 MaxUnavailable,这两个参数决定了更新过程的速度。
试想一下,推出新版本过程中的两个策略:
我们可能对应用的可用性非常谨慎,因此决定在关闭旧版本 Pod 之前,首先要启动新 Pod。只有新 Pod 启动、运行并就绪之后,才终结旧 Pod。
上这个假设中有个隐含条件就是我们的集群中是有剩余资源的。然而如果我们的集群已经满载,无法负担多余 Pod 的消耗,那么我们自然是希望首先关掉旧的,然后才启动新的。
MaxSurge 指出了我们在滚动更新时,可以有多少个额外的 Pod;而 MaxUnavailable 则代表在滚动更新时,我们可以忍受多少个 Pod 无法提供服务。这两个参数可以是 Pod 数量,也可以是 Deployment 的实例数量百分比;两个参数都可以设置为 0(但是不能同时为 0)。
接下来看看这两个参数的常见取值,以及背后的意图。
MaxUnavailable 设置为 0 意味着:“在新 Pod 启动并就绪之前,不要关闭任何旧 Pod”。
MaxSurge 设置为 100% 的意思是:“立即启动所有新 Pod”,也就是说我们有足够的资源,我们希望尽快完成更新。
这两个参数的却升值都是 25%,如果我们更新一个 100 Pod 的 Deployment,会立刻创建 25 个新 Old,同时会关闭 25 个旧 Pod。每次有 Pod 启动就绪,就可以关闭旧 Pod。每次有旧 Pod 完成关闭过程(释放资源),就可以创建另一个新 Pod 了。
演示时间
可以很方便的观察这些参数的作用。我们不需要编写自己的 YAML、定义就绪检测等东西。
我们需要做的事情只是,使用一个无效的镜像,例如一个不存在的镜像。这个容器永远无法启动,Kubernetes 也永远无法把它标记为就绪。
如果你有个 Kubernewtes 集群(Minikube 或者 Docker 桌面版的单结点集群都可以),可以在不同终端运行下面的命令,来看看发生了什么:
kubectl get pods -w
kubectl get replicasets -w
kubectl get deployments -w
kubectl get events -w
然后用下面的命令来创建、伸缩以及更新一个 Deployment:
kubectl run deployment web --image=nginx
kubectl scale deployment web --replicas=10
kubectl set image deployment web nginx=that-image-does-not-exist
会看到部署过程停顿了,但是还有 80% 的应用容量是可用的。
如果我们运行 kubectl rollout undo deployment web,Kubernetes 就会回滚到使用 nginx 镜像的旧版本。
前面我们说过,ReplicaSet 的任务是确保有 N 个符合规范的 Pod。这其实并不完全。实际上 ReplicaSet 并不关心 Pod 的规范,它关心的只是标签。
换句话说,不论 Pod 运行的是 nginx 还是 redis 还是什么别的什么东西;所有的关注点都是,它们要有正确的标签。前面的例子中,标签大概是 run=web 以及 pod-template-hash=xxxyyyzzz 的形式。
ReplicaSet 包含了一个 selector 成员,内容是一个逻辑表达式,功能和 SQL 中的 SELECT 类似,用来选择符合要求的 Pod。ReplicaSet 保证 Pod 的数量正确,如有必要,就会新建或者删除 Pod,但是不会修改已经存在的 Pod。
这样会有个设想:可能可以手工创建带有这些标签的 Pod ,但是却用的不同镜像(或者不同配置),就能骗过 ReplicaSet 了。
粗看上去,这可能是个很大的潜在问题。但实际上,我们很难恰巧选择了正确的标签,这是因为标签中包含了根据 Pod 规范运算得出的哈希值。
Service 负载均衡
选择器还用在 Service 上,这个对象负责 Kubernetes 的内外部的负载均衡。我们可以给 web 创建一个 Service:
kubectl expose deployment web --port=80
这个服务会有它自己的内部 IP 地址(ClusterIP),连接到这个地址的 80 端口会被负载均衡到这个 Deployment 所有 Pod 之中。
事实上这个连接的负载均衡范围是所有符合 Service 标签选择器的 Pod 中,例如这里对应的是 run=web。
在我们编辑 Deployment 并触发滚动时,就会创建新的 ReplicaSet。这个 ReplicaSet 会创建 Pod,新 Pod 标签会包含 run=web,所以这些 Pod 就会自动的接到流量。
这表明在滚动更新时,Deployment 不需要因为 Pod 的的启动停止,而去重新配置或者通知负载均衡器。负载均衡器通过 selector 自动的完成任务。
如果你好奇就绪检测的内幕:Pod 只有在所有成员容器都通过就绪检测之后才会作为有效的 Endpoint被加入服务。换句话说,Pod 只有准备就绪之后才会开始接收流量。
Kubernetes 部署的高级策略
有些事后我们希望在推出新版本时候还有更多的控制。
两个知名流行技术是蓝绿部署以及金丝雀部署。
在蓝绿部署中,我们希望立即把所有流量从旧版本切换到新版本,而不是象之前说的渐进切换。提出这种要求可能有几个原因:
我们不想混合新旧请求,希望能够尽可能清晰的从旧版本切换到新版本;
我们正在更新多个组件(例如 Web 前端和 API 后端),不想新版本前端和旧版后端发生联系;
如果出现问题,我们希望有能力尽快回滚,无需等旧版本容器重启。
在 Kubernetes 中,可以用创建多个 Deployment 的方式来完成蓝绿部署,通过对 Service 的 Selector 字段的控制来进行切换。
下面的命令会创建两个 Deployment:blue 和 green,分别使用 nginx 和 httpd 镜像:
kubectl create deployment blue --image=nginx
kubectl create deployment green --image=httpd
接下来我们创建一个 Service,起初不会发送任何流量:
kubectl create service clusterip web --tcp=80
然后我们更新 web 服务的选择器:kubectl edit service web。这个命令会从 Kunernetes API 中抓取服务对象的定义,在文本编辑器中打开。在其中查找:
selector:
app: web
把其中的 web 替换成 blue 或者 green 或者别的什么。保存并退出。kubectl 会把更新的定义推送给 Kubernetes API,然后 web 服务现在就会向特定的 Deployment 发送流量了。
可以用 kubectl get svc web 命令获取服务的地址,并使用 curl 进行访问。
我们用文本编辑器作出的变更,也可以完全使用命令行来完成,例如 kubectl patch 命令:
kubectl patch service web -p '{"spec": {"selector": {"app": "green"}}}'
蓝绿部署的好处是,流量切换几乎是立刻完成的,推出和回滚都可以很方便的通过更新 Serevice 定义来完成。
有时我们不想让测试版本影响所有用户,即使是短时间也不行。所以我们可以部分推出新版本。例如我们部署新旧两组实例,1% 的流量发送给新版本。
接下来我们在新旧版本的监控数据中进行观察。如果情况允许,就可以向前推进;如果延迟、错误率或者其它什么东西看起来有问题,就回滚到旧版本。
由于 Kubernetes 的标签和选择器的机制,可以很简单的实现这种策略。
前面的例子中,我们修改了服务的选择器,接下来我们修改一下 Pod 标签。
例如设置服务的选择器,让它选择带有 status=enabled 的 Pod,然后给特定的 Pod 打上标签:
kubectl label pod fronted-aabbccdd-xyz status=enabled
也可以一次打上多个标签:
kubectl label pods -l app=blue,version=v1.5 status=enabled
删除标签同样简单:
kubectl label pods -l app=blue,version=v1.4 status-
我们看到了一些用于安全部署的技术,其中的一些能够很方便的降低因部署造成的停机时间,这让我们可以在不担心影响用户的情况下提高部署频度。
有些技术给我们系上安全带,阻止问题版本影响服务。还有些别的服务让我们感觉安心。有点像主机游戏中的保存按钮——在尝试困难操作之前,我们知道如果出了问题,我们还可以回到从前。
Kubernetes 让开发和运维团队能够使用这些技术来提高部署的安全性。如果部署的危险系数降低,那么就可以更频繁地、渐进地进行部署,并可以更方便的观察变更的后果。
这一切都会让我们的新特性和修复特性能够更快面世,让我们的应用有更好的可用性。这也是实现容器化和持续交付的重要基础。
推荐阅读
我国自主开发的编程语言“木兰”是又一个披着“洋”皮的红芯浏览器吗?
大年初一,武汉加油!