Kubernetes部署之终极指南 - semaphoreci


我们学习和使用的第一个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    <none>        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对象确保您可以在不停机的情况下逐步部署,回滚和扩展应用程序。这种情况乍一看,我们想知道“有什么意义?“,但是一旦我们全面了解,我们就会理解每个组件的作用和目的。事实上,很多人最终都认为,如果我们负责设计系统,他们会想出一些非常相似的东西!持续集成使您对代码充满信心。为了将这种信心扩展到发布过程,您的部署操作需要配备安全带。

容器和POD
在Kubernetes,最小的部署单位不是容器,而是一个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就行了?为什么还要设置和部署副本?

声明与命令
Kubernetes是一个  声明系统(与命令系统相反)。这意味着我们不能给它排序指定。我们不能说,“运行这个容器。”我们所能做的就是描述我们想要的东西,并等待Kubernetes采取行动来协调我们拥有的东西,以及我们想要拥有的东西。
换句话说,我们可以说,“ 我想要一个40英尺长的黄色门的蓝色容器“, Kubernetes会为我们找到这样一个容器。如果它不存在,它将构建它; 如果已经有一个,但它是绿色的红色门,它将为我们绘制它成我们要求的颜色; 果已经有一个大小和颜色合适的容器,Kubernetes什么都不做,因为  我们  已经匹配  了我们想要的东西。
在软件容器术语中,我们可以说,“ 我想要一个名为pod命名为web,其中应该有一个容器,它将运行  nginx 镜像。
如果该pod尚未存在,Kubernetes将创建它。如果该pod已经存在且符合我们的规范,Kubernetes不需要做任何事情。
我们如何扩展我们的  web 应用程序,以便它在多个容器或pod中运行?

副本集使缩放pod变得容易
如果我们所拥有的只是一个pod,并且我们想要更多相同的pod,我们所能做的就是回到Kubernetes,然后问它,“ 我想要一个名为的pod  web2,具有以下规格:...... ”并重新使用它规范和以前一样。然后重复我们想要拥有pod的次数。
这是相当不方便的,因为现在我们的工作是跟踪所有这些pod,并确保它们全部同步并使用相同的规范。
为了简化操作,Kubernetes为我们提供了更高级别的构造,即  副本集。看起来非常像一个pod的规格,不同的是它带有一个数字,表明有多少副本:即pod与我们想要的特定的规范。
所以我们告诉Kubernetes,“ 我想要一个名为的副本集  web,它应该有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

Kubernetes将相应地确保有3个匹配的pod。如果我们从头开始,将创建3个pod。如果我们已经有3个pod,那么什么都没做,因为  我们  已经匹配  了我们想要的东西。

副本集与扩展和高可用性特别相关
对于缩放,因为我们可以更新现有的副本集以修改所需的副本数。因此,Kubernetes将创建或删除pod,以便最终变成你所需的数字。
为了实现高可用性,Kubernetes将持续监控集群上发生的情况,并确保无论发生什么情况,我们仍然拥有所需的数量。如果一个名为web的pod节点发生故障,  Kubernetes会创建另一个pod来替换它。如果是节点没有关闭,但暂时无法访问或无响应,当它恢复过来时,我们可能有了一个多余的pod,Kubernetes会终止一个pod,以确保我们仍然拥有所需的数量。

当我们更改pod的定义时会发生什么?
更改pod的定义并不罕见。例如,我们经常希望用更新的版本替换我们正在使用的容器镜像。
请记住:副本集的任务是“ 确保有符合此规范的N个pod。”如果我们更改该定义会发生什么?突然间,可能没有匹配你的新规范定义的pod了。
到目前为止,我们知道声明系统应该如何工作:Kubernetes应该立即创建符合我们新规范的N pod。旧的豆荚只会留下来,直到我们手动清理它们。
如果这些pod可以在CI / CD管道中干净地自动移除,那将很好,新pod的创造可能以更加渐进的方式发生。

部署驱动副本集
这正是部署deployment的作用  。乍一看,部署的规范看起来非常像副本集的规范:它具有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

但是,部署不会直接创建或删除pod。他们将这项工作委托给一个或多个副本集。当我们创建部署时,它会使用我们提供的确切pod规范创建副本集。当我们更新部署并调整副本数时,它会将该更新传递到副本集。

配置更改时
当我们需要更新pod规范本身时,事情变得有趣。例如,我们可能想要更改要使用的镜像(因为我们发布了新版本)或应用程序的参数(通过命令行参数,环境变量或配置文件)。
当我们更新pod规范时,部署 将使用更新的pod规范创建  新的副本集。该副本集的初始大小为零。然后,该副本集的大小逐渐增加,同时减小另一副本集的大小。
我们可以想象我们前面有一个混音板,我们将在新的副本设备上淡入(调高音量),同时我们淡出(调低音量)旧的。在整个过程中,请求将发送到旧版和新版副本集的pod,而不会对用户造成任何停机。
这是大方向,有许多小细节使这个过程更加强大。

破碎的部署和就绪状态探测
如果我们推出一个破损的版本,它可能会导致整个应用程序崩溃(一次一个pod!),因为Kubernetes将逐步用新的(损坏的)版本替换旧的吊舱。
除非我们使用  就绪状态探测/准备探针。准备情况探测是我们添加到容器规范的测试。这是一个二进制测试,只能说“工作”或“不工作”,并将定期执行。(默认情况下,每10秒钟。)
Kubernetes支持三种实现就绪探针的方法:

  1. 在容器内运行命令;
  2. 针对容器发出HTTP(S)请求; 要么
  3. 打开容器的TCP套接字。

Kubernetes使用该测试的结果来了解容器和它所属的容器是否已准备好接收流量。当我们推出新版本时,Kubernetes将等待新的pod标记为“准备好”,然后再转到下一个版本。
如果一个pod永远不会达到就绪状态,因为准备就绪探测失败了,Kubernetes永远不会继续前进。部署停止,我们的应用程序继续使用旧版本运行,直到我们解决问题。
如果  没有  就绪探测器,则只要可以启动容器,就认为该容器已就绪。因此,如果要利用该功能,请确保定义准备情况探测!

回滚以便从错误的部署中快速恢复
在任何时候,在滚动更新期间甚至更晚,我们都可以告诉Kubernetes:“ 嘿,我改变了主意; 请返回到该部署的先前版本。“它将立即切换”旧“和”新“副本集的角色。从那时起,它将增加旧副本集的大小(直到部署的名义大小),同时减小另一个副本的大小。
一般而言,这不限于两个“旧”和“新”副本集。在引擎盖下,有一个被认为是“最新”的副本集,我们可以将其视为“目标”副本集。那就是我们要努力的目标; 这是Kubernetes将逐步扩大规模的那个。同时,可以有任意数量的其他副本集,对应于旧版本。
例如,我们可能会在10个副本上运行应用程序的第1版。然后我们开始推出版本2.在某些时候,我们可能有7个pod运行版本1,3个pod运行版本2。然后我们可能决定发布版本3而不等待版本2完全部署(因为它修复了我们之前没有注意到的问题)。虽然正在部署版本3,但我们可能会决定返回版本1。Kubernetes将仅相应地调整副本集的大小(对应于应用程序的版本1,2和3)。

MaxSurge和MaxUnavailable
Kubernetes并没有一次完全更新我们的部署。早些时候,我们说部署有“一些额外的参数”:这些参数包括  MaxSurge  和  MaxUnavailable,它们表示更新应该进行的速度。
在推出新版本时,我们可以想象两种策略。我们可以非常保守我们的应用程序可用性,并决定在  关闭旧的pod 之前启动新的pod  。只有在新的pod启动,运行和准备好后,我们才能终止旧的。
这意味着我们的群集上有一些备用容量。但是,可能会出现这样的情况:我们无法承担任何额外的pod,因为我们的群集已满,并且我们更愿意在开始新的pod之前关闭旧的pod。
MaxSurge  表示我们愿意在滚动更新期间运行多少个额外的pod,而  MaxUnavailable  表示在滚动更新期间我们可以丢失多少个pod。这两个参数都特定于部署(换句话说,每个部署可以具有不同的值)。两个参数都可以表示为pod的绝对数量,或者表示为部署大小的百分比; 两个参数都可以为零(但不能同时)。
让我们看一下MaxSurge和MaxUnavailable的一些典型值,以及它们的含义。
将MaxUnavailable设置为0意味着“ 在新服务器启动并准备好为流量提供服务之前,不要关闭任何旧服务器 ”。
将MaxSurge设置为100%意味着“ 立即启动所有新pod ”,这意味着我们的群集上有足够的备用容量,并且我们希望尽可能快地启动。
两个参数的默认值均为25%,这意味着在更新大小为100的部署时,会立即创建25个新pod,而关闭25个旧pod。每次新的pod出现(并标记为准备好)时,可以关闭另一个旧pod。每次旧pod完成其关闭(并且其资源已被释放)时,可以创建另一个新pod。

演示
很容易看到这些参数在起作用。我们不需要编写自定义YAML,定义准备探针或类似的东西。
我们所要做的就是告诉部署使用无效图像; 例如,不存在的图像。容器永远不会出现,Kubernetes永远不会将它们标记为“准备就绪”。
如果你有一个Kubernetes集群(像minikube这样的单节点集群或者Docker Desktop很好),你可以在不同的终端运行以下命令来观察将要发生的事情:

  • kubectl get pods -w
  • kubectl get replicasets -w
  • kubectl get deployments -w
  • kubectl get events -w

然后,使用以下命令创建,扩展和更新部署:
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镜像)。

了解选择器和标签
事实证明,当我们之前说过时,“ 副本集的工作是确保确切存在与正确规范匹配的N个pod ”,这并不是正在发生的事情。实际上,副本集不会查看pod的规格,而只会查看其标签。
换句话说,如果pod 正在运行不要紧 ,比如nginx 或  redis 或什么; 重要的是他们有正确的标签。(在上面的示例中,标签看起来像  run=web 和  pod-template-hash=xxxyyyzzz。)
副本集包含一个  选择器,它是一个逻辑表达式,用于“选择”(就像SELECT SQL中的查询一样  )许多pod。副本集确保存在正确数量的pod,必要时创建或删除pod; 但它不会改变现有的pod。
万一你想知道:是否绝对有可能手动创建带有这些标签的pod,但运行不同的图像(或使用不同的设置),并欺骗我们的副本集。起初,这听起来像是一个很大的潜在问题。但实际上,我们不太可能不小心选择“正确”(或“错误”,取决于perspective)标签,因为它们涉及pod规范的哈希函数,它几乎是随机的。

作为负载平衡器的服务
选择器也被  服务使用,它们充当Kubernetes流量,内部和外部的负载平衡器。我们可以 使用以下命令为  部署创建服务web:

kubectl expose deployment web --port=80

该服务将拥有自己的内部IP地址(由名称表示  ClusterIP),并且端口80上与此IP地址的连接将在此部署的所有pod中进行负载平衡。
事实上,这些连接将在与服务选择器匹配的所有pod中进行负载平衡。在那种情况下,该选择器将是  run=web。
编辑部署并触发滚动更新时,会创建新的副本集。此副本集将创建pod,其标签将包括(以及其他)  run=web。因此,这些pod将自动接收连接。
这意味着在部署期间,部署不会重新配置或通知负载均衡器已启动和停止容器。它通过 与负载均衡器关联的服务的选择器自动发生  。
(如果您想知道探针和健康检查如何发挥作用: 只有当一个容器的所有容器都通过了准备检查时,它才会被添加为服务的有效  端点。换句话说,只有当一个容器实际准备就绪后,它才会开始接收流量它。)

高级Kubernetes部署策略
有时,我们在推出新版本时需要更多控制权。您可能听说过的两种流行技术是  蓝/绿部署  和  金丝雀部署。
1.使用Kubernetes进行蓝/绿部署

在蓝/绿部署中,我们希望立即切换从旧版本到新版本的所有流量,而不是像之前解释的那样逐步进行。我们可能有几个原因,包括:

  • 我们不希望混合使用新旧请求,我们希望从一个版本到另一个版本的中断尽可能干净;
  • 我们一起更新多个组件(例如,Web前端和API后端),我们不希望新版本的Web前端与旧版本的API后端通信,反之亦然;
  • 如果出现问题,我们希望能够尽快恢复,甚至无需等待旧的容器重启。

我们可以通过创建多个部署(在Kubernetes意义上)实现蓝/绿部署,然后通过更改 我们服务的选择器从一个部署切换到另一个部署  。
这听起来比听起来容易!
以下命令将创建两个部署,  blue 并  green分别使用  nginx 和  httpd容器镜像:

kubectl create deployment blue --image=nginx
kubectl create deployment green --image=httpd

然后,我们创建一个名为的服务web,该服务  最初不会在任何地方发送流量:

kubectl create service clusterip web --tcp=80

现在我们可以运行:
kubectl edit service web

运行web来更新服务的选择器  ,web将从 Kubernetes API 检索服务定义  ,在文本编辑器中打开定义文件,寻找到:

selector:
  app: web

用  blue 或者  green替换web,保存并退出。 kubectl 将我们更新的定义推回到Kubernetes API,并且瞧!服务  web 现在将流量发送到相应的部署。
(您可以通过检索该服务的IP地址kubectl get svc web 并使用该连接到该IP地址  来验证自己  curl。)

我们通过文本编辑器进行的修改也可以完全从命令行完成,使用(例如)kubectl patch 如下:

[b]kubectl patch service web -p '{"spec": {"selector": {"app": "green"}}}'[/b]

蓝/绿部署的优势在于流量切换几乎是即时的,我们可以通过再次更新服务定义来快速回滚到以前的版本。

2. 使用Kubernetes进行金丝雀部署
金丝雀部署  暗示了用于煤矿的金丝雀,以检测危险浓度的有毒气体,如一氧化碳。矿工们会在笼子里放一只金丝雀。金丝雀对有毒气体的敏感性高于人类。如果金丝雀昏倒,那意味着矿工已经到达危险区域时,应该在他们昏倒之前回头退出来   。
这如何映射到软件部署?
有时,我们不能或不会影响所有有缺陷版本的用户,即使是短暂的时间。相反,我们会对新版本进行部分推广。例如,我们部署了几个运行新版本的副本; 或者我们将1%的用户发送到该新版本。
然后,我们比较当前版本和刚刚部署的金丝雀之间的指标。如果指标相似,我们可以继续。如果延迟,错误率或其他任何错误,我们会回滚。
由于Kubernetes的标签和选择器的本机机制,这种技术可以相当简单地进行设置。
值得注意的是,在前面的示例中,我们更改了服务的选择器,但也可以更改pod的标签。
例如,服务的选择器是否设置为查找带有标签的  status=enabledpod,我们可以将这样的标签应用于特定的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使开发人员和运营团队可以利用这些技术,从而实现更安全的部署。如果与部署相关的风险较低,则意味着我们可以更频繁地,逐步地部署,并在我们实施时更容易地看到我们的更改结果; 例如,而不是每周或每月部署一次。
最终结果是更高的开发速度,更低的修复和新功能的上市时间,以及更好的应用程序可用性。这首先是实施集装箱和持续交付的重点。