ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Kubernetes编排原理(一)

2022-02-27 19:02:50  阅读:215  来源: 互联网

标签:容器 Kubernetes Deployment nginx 编排 deployment 原理 Pod k8s


Kubernetes编排原理(一)

Pod

Pod是Kubernetes项目的原子调度单位,k8s项目单独调度器是统一按照Pod而非容器的资源需求进行计算的。

如果把k8s比作云时代的操作系统,容器就相当于进程,Pod就相当于虚拟机

容器设计模式

Pod其实是一组共享了某些资源的容器,Pod里的所有容器都共享一个Network Namespace,并且可以声明共享一个Volume。

Infra容器

在Kubernetes项目里,Pod的实现需要使用一个中间容器,这个容器叫做Infra容器。在这个Pod中,Infra容器永远是第一个被创建的容器,用户定义的其他容器则通过Join Network Namespace的方式与Infra容器关联在一起。

Infra容器使用一个特殊的镜像,叫做k8s.gcr.io/pause,是一个用汇编语言编写的,永远处于"暂停"状态单独容器,解压后也只有100~200KB。

在Infra容器"hold"Network Namespace后,用户容器就可以加入Infra容器的Network Namespace中了,所以如果你查看这些容器在宿主机上的Namespace文件,它们指向的值是完全一样的。

这也意味着,对Pod里的容器来说:

1.它们可以使用localhost进行通信;

2.它们"看到"的网络设备和Infra"看到"的完全一样

3.一个Pod只有一个IP地址,也就是Pod的Network Namespace对应的IP地址;

4.当然,其他所有网络资源都是一个Pod一份,并且被该Pod内的所有容器共享;

5.Pod的生命周期只跟Infra容器一致,与其他容器无关。

6.Volume定义在Pod层级,Pod里的容器都可以声明挂载此Volume

对于同一个Pod里的所有用户容器来说,它们的进出流量也可以认为都是通过Infra容器完成的,如果你要开发一个k8s网络插件,应该重点考虑如何配置这个Pod的Network Namespce,而不是每一个用户容器如何使用你的网络配置。

Pod的这种"超亲密关系"容器设计思想,实际上就是希望,当用户想在一个容器里运行多个功能无关的应用时,应该优先考虑它们是否更应该被描述成一个Pod里的多个容器。

例子:

1.WAR包与Web服务器

为了避免每升级一个WAR包版本就重新打包WAR与Web服务器的镜像,可以分别把WAR包和Tomcat分别做成镜像,然后把它们作为一个Pod里的两个容器组合在一起。Pod的配置文件如下所示:

apiVersion: v1
kind: Pod
metadata:
	name: javaweb-2
spec:
    initContainers:
    - image: geektime/sample:v2
      name: war
      command: ["cp", "/sample.war", "/app"]
      valumeMounts:
      - mountPath: /app
        name: app-volume
   containers: 
   - image: geektime/tomecat:7.0
     name: tomcat
     command: ["sh","-c","/root/apache-tomcat-7.0.42-v2/bin/start.sh"]
     volumeMounts:
     - mountPath: /root/apache-tomcat-7.0.42-v2/webapps
       name: app-volume
     ports: 
     - containerPort: 8080
       hostPort: 8001
   volumes:
   - name: app-volume
     emptyDir: {}

WAR包容器的 类型不再是普通容器,而是一个Init Container类型的 容器。在Pod中,所有Init Container定义的容器,都会比spec.containers定义的用户容器先启动,并且Init Container容器会按照顺序逐一启动,直到它们都启动并且退出了,用户容器才会启动。

按照这样的定义创建Pod,war容器会先启动并且把WAR包复制到/app目录中,因为/app目录是挂载的是app-volume,所以当tomcat容器启动后WAR包会出现在

其/root/apache-tomcat-7.0.42-v2/webapps目录下,因为该目录也是挂载了app-volume的。

sidcar

这种所谓的组合操作,正是容器设计模式里最常见的一种模式,称为sidecar,顾名思义,sidecar指的是我们可以在一个Pod中启动一个辅助容器,来完成一些独立于主进程(主容器)的工作。比如在上面的Pod中,Tomcat容器是我们的主容器,而WAR包容器的存在只是为了给主容器提供一个WAR包而已。所以,我们用Init Container的方式先运行WAR包服务器,扮演了一个sidecar的角色。

2.容器的日志收集

假设现在有一个应用,会不断把日志输出到容器的/var/log目录,这时我们就可以把一个Pod里的Volume挂载到应用容器的/var/log目录,然后在Pod里运行一个sidecar容器,它也声明挂载同一个Volume到自己的/var/log上。接下来sidecar容器就只需要做一件事,那就是不断从自己的/var/log目录里读取日志文件,转发到MongoDB或者Elasticsearch中存储起来。这样,一个最基本的日志收集工作就完成了。

Pod字段和含义

凡是调度,网络,存储,安全相关的属性,跟容器的Linux Namespace相关的属性,以及Pod中的容器要共享宿主机的Namespace的属性,一定是Pod级别的。

NodeSelector,一个供用户将Pod和Node绑定的字段。

apiVersion: v1
kind: Pod
spec:
nodeSelector:
disktype: ssd

这样的配置意味着这个Pod永远只能在携带了disktype: ssd标签的节点上运行,否则将调度失败。

NodeName,一旦一个Pod的这个字段被赋值,k8s就会认为这个Pod已调度,调度的结果就是赋值的节点名称。所以,这个字段一般由调度器负责设置,但用户也可以设置它来"骗过"调度器,当然这种做法一般在测试或者调试时才会用到。

HostAliases,定义了Pod的hosts文件(比如/etc/hosts)里的内容,用法如下:

apiVersion:
kind: pod
...
spec:
hostAliases:
- ip: "10.1.2.3"
hostnames:
- "foo.remote"
- "bar.remote"
...

这样,当这个Pod启动后,/etc/hosts里的文件的内容将如下所示:

cat /etc/hosts
# Kubernetes管理的hosts文件
127.0.0.1 localhost
...
10.244.135.10 hostaliases-pod
10.1.2.3  foo.remote
10.1.2.3  bar.remote

最下面两行记录就是通过HostAliases字段为Pod设置的。需要指出的是,在Kubernetes项目中,如果要设置hosts文件里的内容,一定要通过这种方法,如果直接修改了hosts文件,在Pod被删除重建之后,kubelet会自动覆盖被修改的内容。

shareProcessNamespace Pod里的容器共享PID Namespace。例如:

apiVersion: v1
kind: Pod
metadata:
	name: nginx
spec: 
	shareProcessNamespace : true
	containers: 
	- name: nginx
	  image: nginx
	- name: shell
	  image: busybox
	  stdin: true
	  tty: true

此Pod里创建后使用kubectl attach命令连接到shell容器的tty上:

kubectl attach -it nginx -c shell

在shell容器中使用ps命令查看所有正在运行的进程:

/# ps ax

可以看到,在这个容器里,不仅可以看到它本身的ps ax指令,还可以看到nginx容器的进程,以及Infra容器的/pause进程。此Pod里的每个容器的进程,对于所有容器来说都是可见的。因为它们共享了同一个PID Namespace。

共享宿主机Namespace:

hostNetwork=true共享宿主机Network Namespace

hostIPC=true共享宿主机IPC Namespace

hostPID=true共享宿主机PID Namespace

apiVersion: v1
kind: Pod
metadata: 
	name: nginx
spec:
	hostNetwork: true
	hostIPC: true
	hostPID: true
	containers:
	- name: nginx
	  image: nginx
	- name: shell
	  image: busybox
	  stdin: true
	  tty: true

上述Pod定义意味着,Pod中的所有容器会直接使用宿主机的网络,直接与宿主机进行IPC通信,"看到"宿主机所有正在运行的进程。

Containers字段和含义

Pod中最重要的字段当属Containers(Init Containers也是Pod对容器的定义),k8s项目中对Container的定义,和Docker相比没有太大区别,前面介绍容器时谈到的Image,Command,workingDir,Ports以及volumeMounts等都是构成k8s中Container的主要字段。不过,有一些属性值得额外关注:

ImagePullPolicy

该字段定义了镜像拉去策略,默认是Always即每次创建Pod都重新拉取一次镜像,Never表示从不主动拉去这个容器镜像,IfNotPresent表示只在宿主机上不存在这个镜像时才拉取。

Lifecycle

该字段定义的是Container Lifecycle Hooks,即在容器状态变化时触发的一系列钩子,例如:

apiVersion: v1
kind: Pod
metadata:
	name: lifecycle-demo
spec:
	containers:
	- name: lifecycle-demo-container
	  image: nginx
	  lifecycle:
	  	postStart:
	  		exec: 
	  		  command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
	  	preStop:
	  		exec:
	  		  command: ["/usr/sbin/nginx", "-s", "quit"]

postStart指的是在容器启动后立即执行一个指定动作。postStart定义的操作虽然是在Docker容器ENTRYPOINT执行之后,但它并不严格保证顺序。也就是说,在postStart启动时,ENTRYPOINT可能尚未结束。

preStop发生的时机是容器被结束之前,它会阻塞当前容器的结束流程,直到这个Hook定义的操作完成之后,才允许容器被结束。

Pod生命周期

Pod生命周期的变化主要体现在Pod API对象的Status部分,这是它除Metadata和Spec外的第三个重要字段。其中,pod.status.phase就是Pod的当前状态,它有如下几种可能:

1.Pending

这个状态意味着,Pod的YAML文件已经提交给了k8s,API对象已经被创建并保存到etcd当中。但是,这个Pod里有些容器因为某种原因不能被创建,比如调度不成功。

2.Running

这个状态下,Pod已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行。

3.Succeed

这个状态意味着,Pod里的所有容器都正常运行完毕,并且已经退出了。这种情况在运行一次性任务时最为常见。

4.Failed

这个状态下,Pod里至少有一个容器以不正常的状态(非0的返回码)退出。出现这个状态意味着需要想办法调试这个容器的应用,比如查看Pod的Events和日志。

5.Unknown

这是一个异常状态,意味着Pod的状态不能持续的被kubelet汇报给kube-apiserver,这可能是主从节点(master和kubelet)间的通信出现了问题。

Pod对象的Status字段还可以细分除一组Conditions。包括:PodScheduled,Ready,Initialized以及Unshedulable,它们主要用于描述造成当前Status的具体原因是什么。

比如Pod当前的Status是Pending,对应的Condition是Unshedulable,这意味着它的调度除了问题。

Ready这个细分状态非常值得关注,它意味着Pod不仅已经正常启动(Running状态),而且可以对外提供服务了。

投射数据卷

在k8s中有几种特殊的Volume,它们存在的意义不是为了存放容器里的数据,也不是用于容器和宿主机之间的数据交换,而是为容器提供预先定义好的数据。所以,从容器的角度来看,这些Volume里的信息就仿佛是被k8s"投射"进容器中的,这正是Projected Volume的含义。

到目前为止,k8s支持的常用Projected Volume共有以下4种:

1.Secret

2.ConfigMap

3.Downward Api

4.ServiceAccountToken

Secret

Secret的作用是帮你把Pod想要访问的加密数据存放到etcd中,之后就可以通过在Pod的容器里挂载Volume的方式访问这些Secret里保存的信息了。

Secret存放数据库的Credential信息:

apiVersion: v1
kind: Pod
metadata:
	name:  test-projected-volume
spec:
	containers:
	- name: test-secrret-volume
	  image: busybox
	  args:
	  - sleep
	  - "86400"
	  volumeMounts:
	  - name: mysql-cred
	  	mountPath: "/projected-volume"
	  	readOnly: true
	volumes:
	- name: mysql-cred
	  projected:
	  	sources:
	  	- secret:
	  		name: user
	  	- secret:
	  		name: pass

创建Secret:

方法一:命令行创建

cat ./username.txt
admin
cat ./password.txt
cloudc0w!
kubectl create secret generic user --from-file=./username.txt
kubectl create secret generic pass --from-file=./username.txt

方法二:使用kubectl create secret 命令通过YAML方式创建

apiVersion: v1
kind: Secret
metadata:
	name: mysecret
type: Opaque
data:
  user: YWtaW4=
  pass: MWYyZDFlMmU2N2Rm

需要注意Secret对象要求这些数据必须是经过Base64转码的,以免出现明文密码的安全隐患。

可以通过kubectl get secrets查看Secret对象是否已经存在。

创建好Secret之后就可以创建上面定义的Pod了:

kubectl create -f test-projected-volume.yaml

当Pod变成Running状态后,进入容器查看这些Secret对象是否已经在容器里了:

kubectl exec -it test-projected-volume -- /bin/bash
ls /projected-volume
user
pass
cat /projected-volume/user
root
cat /projected-volume/pass
1f2d1e2e67df #(MWYyZDFlMmU2N2Rm的明文)

可见,保存在etcd里的用户名和密码信息已经以文件的形式出现在容器的Volume目录里了,文件的名称就是kubectl create secret 指定的key或者Secret对象的data子字段的key,内容就是其明文的值。

更重要的是,像这样通过挂载方式进入容器里的Secret,一旦其对应的etcd里的数据更新,这些Volume里的文件也会更新。其实这是kubelet组件在定时维护这些Volume。但是,这个更新会有一定的延时,所以在编写应用程序时,在发起数据库连接的代码处要写好重试和超时的逻辑。

ConfigMap

ConfigMap与Secret类似,区别在于ConfigMap保存的是无需加密的,应用所需的配置信息。除此之外,ConfigMap的用法几乎与Secret完全想同你:你可以使用kubectl create configmap从为文件或者从目录创建ConfigMap,也可以直接编写ConfigMap对象的YAML文件。

ConfigMap保存Java应用配置文件:

# .properties文件的内容
$cat example/ui.properties
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice
# 从.properties文件创建ConfigMap
$ kubectl create configmap ui-config --from-file=example/ui.properties
# 查看这个ConfigMap里保存的信息
$ kubectl get configmap ui-config -o yaml
apiVersion: v1
data:
  ui.properties: |
  	color.good=purple
  	color.bad=yellow
  	allow.textmode=true
  	how.nice.to.look=fairlyNice
kind: ConfigMap
metadata:
	name: ui-config
...
Downward API

Downward API的作用是让Pod里的容器能够直接获取这个Pod API对象本身的信息。

举例:

apiVersion:
kind: Pod
metadata:
	name: test-downwardapi-volume
	labels:
	zone: us-est-coast
	cluster: test-cluster1
	rack: rack-22
spec:
  containers:
  	- name: client-container
  	  image: k8s.gcr.io/busybox
  	  command: ["sh", "-c"]
  	  args:
  	  - while true; do
  	  		if [[ -e /etc/podinfo/labels ]]; then
  	  			echo -en '\n\n'; cat /etc/podinfo/labels; fi;
  	  		sleep 5;
  	  	done;
  	  volumeMounts:
  	  	- name: podinfo
  	  	  mountPath: /etc/podinfo
  	  	  readOnly: false
  volumes:
  	- name: podinfo
  	  projected:
  	  	sources:
  	  	- downwardAPI:
  	  		items:
  	  		 - path: "labels"
  	  		   fieldRef:
  	  		   	 fieldPath: metadata.labels

这个Downward API Volume声明了要暴露Pod的metadata.labels信息给容器。通过这样的声明方式,当前Pod的Labels字段的值就会被k8s自动挂载到容器的/etc/podinfo/labels文件。

需要注意的是,Downward API能够获取的信息一定是Pod容器进程启动之前就确定下来的信息。

ServiceAccountToken

Service Account

Service Account对象都是作用就是k8s系统内置的一种"账户服务",它是k8s进行权限分配的对象。Service Account的授权信息和文件,实际上保存在它所绑定的一个特殊的Secret对象里,这个特殊的Secret对象叫做ServiceAccountToken。任何在k8s集群上运行的应用,都必须使用ServiceAccountToken里保存的授权信息(也就是Token),才能合法的访问API Server。

为了方便使用,k8s已经提供了一个默认的"服务账户(Service Account)"。并且,任何一个在k8s里运行的Pod都可以直接使用它,而无须显示声明挂载它。这一点可以通过查看任意一个在k8s及群里运行的Pod,会发现每一个Pod都已经自动声明了一个类型是Secret,名为default-token-xxx的Volume,然后自动挂载在每个容器的一个固定目录(/var/run/secrets/kubernetes.io/serviceaccount)。应用只需加载这些授权文件就可以访问并操作k8s API了,如果使用k8s官方的Client包(k8s.io/client.go)的话,它还可以自动加载该目录下的文件。

这种把k8s客户端以容器的方式运行在集群里,然后使用Service Account自动授权的方式称为"InClusterConfig"。

容器健康检查和恢复机制

在k8s中,可以为Pod里的容器定义一个健康检查"探针"(Probe),这样,kubelet就会根据Probe的返回值决定这个容器的状态,而不是直接以容器是否运行(来自Docker返回的信息)作为依据。

举例:

apiVersion: v1
kind: Pod
metadata:
	labels:
	   test: liveness
	name: test-liveness-exec
spec:
  conntainers:
  - name: liveness
    image: busybox
    args: 
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
    livenessProbe:
    	exec:
    	  command:
    	  - cat
    	  - /tmp/healthy
    initialDelaySeconds: 5
    periodSeconds: 5

此Pod定义了一个容器,此容器在启动时会创建 /tmp/healthy文件,然后30秒之后会把把此文件删除。livenessProbe在容器启动5秒后(initialDelaySeconds)会在容器中执行 cat /tmp/healthy命令,每5秒(periodSeconds)执行一次,如果文件存在此命令返回0,Pod就会认为这个容器不仅已经启动了,而且是健康的。

创建上面的Pod,使用kubectl describe 查看Pod的Events,会发现30秒之后Pod的Events报告了一个异常但是使用kubectl get pod查看Pod状态任然是Running的,但是RESTARTS变成了1,说明这个异常的容器已经被k8s重启了,在此过程中Pod的Running状态保持不变。

需要注意,k8s没有Docker的Stop语义,所以虽是Restart(重启),实际上是重新创建了容器。

Pod的恢复机制

pod.spec.restartPolicy定义Pod的恢复机制,默认值是Always即无论这个Pod里的容器何时发生异常,它一定会被重建。

需要注意,Pod的恢复过程永远发生在当前节点上,而不会跑到别的节点上。事实上一旦一个Pod与一个节点绑定,除非这个绑定发生了变化(pod.spec.node字段被修改),否则它永远也不会离开这个节点。即使这个宿主机宕机了,这个Pod也不会主动迁移到其他节点。如果想让Pod出现在其他可用的节点上,就必须使用Deployment这样的"控制器"来管理Pod,哪怕你只需要一个Pod副本。

pod.spec.restartPolicy取值:

Always: 在任何情况下,只要容器不在运行状态,就自动重启容器。

OnFailure:只在容器异常时才自动重启容器。

Never:从不重启容器。

restartPolicy与Pod状态对应关系:

1.对于单容器Pod,只要Pod的restartPolicy指定的策略允许重启异常容器,那么这个Pod就会保持Running状态并重启容器,否则Pod进入Failed状态

2.对于包含多个容器的Pod,只有其中所有容器都进入异常状态后,Pod才会进入Failed状态。在此之前Pod都是Running状态。此时Ready字段显示正常容器的个数。

PodPreset

举例:

apiVersion: setting.k8s.io/vlalphal
kind: PodPreset
metadata:
	name: allow-database
spec:
  selector:
  	matchLabels:
  		role: fronted
  env:
  - name: DB_ROOT
    value: "6379"
  volumeMounts:
  	- mountPath: /cache
  	  name: cache-volume
  volumes:
  	- name: cache-volume
  	  emptyDir: {}

这样这个PodPreset就会作用于带有role: fronted标签的Pod对象了,后面提交的Pod定义都会被追加上此处定义的字段,Pod还会被加上一个annotation,表示该Pod对象被那个PodReset改过。如果定义了多个PodReset作用于同一个Pod,k8s会自动合并修改,有冲突的部分不会修改。

PodPreset只会在Pod API对象被创建之前追加在这个对象身上,而不会影响任何Pod的控制器定义。比如,现在提交的是一个Deployment对象,那么Deployment对象不会被PodPreset改变,被修改的只是这个Deployment创建出来的Pod。

控制器思想

Pod其实就是容器的升级版,它对容器进行了组合,添加了更多属性和字段,如果说容器好比光秃秃的集装箱,那么Pod就是在这些"集装箱"上安装了吊环,k8s这台吊车才可以更轻松的控制它。而k8s操作这些"集装箱"的逻辑都是由控制器完成的。

Deployment是k8s中一个最基本的控制器

举例:

apiVersion: apps/v1
kind: Deployment
metadata:
	name: nginx-deployment
spec:
  selector:
    matchLabels:
    	app: nginx
  replicas: 2
  template:
    metadata:
      labels:
         app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

这个Deployment定义的编排动作很简单:请确保携带了app: nginx标签的Pod的个数永远等于spec.replicas指定的个数--2。这就意味着,如果在这个集群中,携带app: nginx标签的Pod的个数大于2,就会有旧的Pod被删除;反之,就会有新的Pod被创建。

kube-controller-manager实际上是一系列控制器的集合,k8s项目的pkg/controller目录下的每一个控制器都以独有的方式负责某种编排功能,Deployment正是这些控制器中的一种。这些pkg/controller目录下的控制器都遵循k8s项目中的一个通用编排模式--控制循环。

用Go语言风格的伪代码来描述控制循环:(X为待编排对象)

for  {
    实际状态 := 获取集群中对象X的实际状态(Actual State)
    期望状态 := 获取集群中对象X的期望状态(Desired State)
    if 实际状态 == 期望状态{
        什么都不做
    }else{
        执行编排动作,将实际状态调整为期望状态
    }
}

在具体实现中,实际状态往往来自k8s集群本身,比如kubelet通过心跳汇报的容器状态和节点信息,或者监控系统中保存的应用监控数据,又或者控制器主动收集的它自己感兴趣的信息。期望状态一般来自用户提交得到YAML文件,比如Deployment对象中Replicas字段的值,这些信息往往保存在etcd当中。

Deployment控制器模型实现

1.Deployment控制器从etcd中获取所有携带了app: nginx标签的Pod,然后统计它们的数量,这就是实际状态。

2.Deployment对象的Replicas字段值就是期望状态。

3.Deployment控制器比较两个状态,然后根据结果确定是创建Pod,还是删除已有的Pod。

可以看到,一个k8s对象的主要编排逻辑,实际上是在第3步的"对比"阶段完成的。这个操作通常称作调谐。调谐的过程则称作调谐循环或者同步循环,它们指的都是一个概念--控制循环。调谐的最终结果往往是对被控制对象的某种写操作。比如,增加Pod,删除已有的Pod,或者更新Pod的某个字段。

类似于Deployment这样的控制器都是由控制器定义(包括期望状态)和被控制对象的模板组成的。

作业副本与水平扩展

Deployment看似简单,但实际上它实现了k8s项目中一个非常重要的功能:Pod的"水平扩展/收缩"。如果你更新了Deployment的Pod模板,那么Deployment就需要遵循一种叫做滚动更新的方式,来升级现有容器。而这个能力的实现依赖k8s项目中一个非常重要的概念:ReplicaSet。

举例:

apiVersion: apps/v1
kind: ReplicaSett
metadata:
	name: nginx-set
	labels:
		app: nginx
spec:
   replicas: 3
   selector:
   		matchLabels:
   			app: nginx
 	template:
 		metadata:
 			labels:
 				app: nignx
 		spec:
 		  containers:
 		  - name: nginx
 		  	image: ningx:1.7.9

ReplicaSet是Deployment的一个子集,Deployment控制器实际操作的是ReplicaSet对象,而不是Pod对象。对于一个Deployment所管理的Pod,它的ownerReference就是ReplicaSet。

Deployment与它的ReplicaSet以及Pod之间实际上是一种"层层控制"的关系。ReplicaSet负责通过控制器模式保证系统中的 Pod个数永远等于指定个数,Deployment同样通过控制器模式来操作ReplicaSet的个数和属性,进而实现水平扩展/收缩和滚动更新这两个编排动作。

水平扩展/收缩

水平扩展/收缩非常容易实现,Deployment Controller 只需要修改它所控制的ReplicaSet的Pod副本个数就可以了。

举例:

有下面nginx-deployment.yaml文件:

apiVersion: apps/v1
kind: Deployment
metadata:
        name: nginx-deployment
        labels:
             app: nginx
spec:
  selector:
    matchLabels:
        app: nginx
  replicas: 3
  template:
    metadata:
      labels:
         app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

创建api对象:

kubectl create -f nginx-deployment.yaml --record

--record的作用是记录下你每次操作所执行的命令,以便之后查看。

检查nginx-deployment创建后的状态信息:

kubectl get deployments
NAME 				DESIRED 	CURRENT 	UP-TO-DATE 		AVAILABEL 	AGE
nginx-deployment	3			0			0				0			1s

返回结果含义:

1.DESIRED:用户期望的Pod副本个数(spec.replicas的值)

2.CURRENT:当前处于Running状态的Pod的个数。

3.UP-TO-DATE:当前处于最新版本的Pod的个数。最新版本指的是Pod的spec部分与Deployment里Pod模板里定义的完全一致。

4.AVAILABEL:当前已经可用的Pod个数,即既是Running状态又是最新版本并且已经处于Ready(健康检查显示正常)状态的Pod的个数。

查看Deployment对象变化:

root@k8s-master:/opt/k8s# kubectl rollout status deployment/nginx-deployment
Wating for rollout to finish: 2 out of 3 new replicas have been updated...
deployment "nginx-deployment" successfully rolled out

此时查看这个Deployment所控制的ReplicaSet:

root@k8s-master:/opt/k8s# kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-5bf87f5f59   3         3         3       6m31s

这个ReplicaSet的名字由Deployment名字和一个随机字符串共同组成。这个随机字符串叫做pod-template-hash,ReplicaSet会把这个随机字符串加在它所控制的所有Pod的标签里,从而避免这些Pod与及群里的其他Pod混淆。

滚动更新

修改Pod模板:

root@k8s-master:/opt/k8s# kubectl edit deployment/nginx-deployment
// vim方式修改nginx版本为1.9.1
deployment.apps/nginx-deployment edited

查看Deploymentd Events可以看到"滚动更新"过程:

Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
  Normal  ScalingReplicaSet  20m    deployment-controller  Scaled up replica set nginx-deployment-5bf87f5f59 to 3
  Normal  ScalingReplicaSet  8m28s  deployment-controller  Scaled up replica set nginx-deployment-678645bf77 to 1
  Normal  ScalingReplicaSet  90s    deployment-controller  Scaled down replica set nginx-deployment-5bf87f5f59 to 2
  Normal  ScalingReplicaSet  90s    deployment-controller  Scaled up replica set nginx-deployment-678645bf77 to 2
  Normal  ScalingReplicaSet  89s    deployment-controller  Scaled down replica set nginx-deployment-5bf87f5f59 to 1
  Normal  ScalingReplicaSet  89s    deployment-controller  Scaled up replica set nginx-deployment-678645bf77 to 3
  Normal  ScalingReplicaSet  88s    deployment-controller  Scaled down replica set nginx-deployment-5bf87f5f59 to 0

新ReplicaSet管理的Pod副本从0变成1,再变成2,最后变成3。而旧的ReplicaSet管理的Pod副本数从3变成2,再变成1,最后变成0。这样就完成了一组Pod的版本升级过程。像这样,将一个集群中正在运行的多个Pod版本交替地逐一升级的过程,就是滚动更新。

需要注意的是,如果想在更新的过程中不影响服务,一定要使用Pod的健康检查机制检查应用的Running状态,而不能简单的依赖容器的Running状态。不然,虽然容器已经Running了,但是服务尚未启动,滚动更新的效果也就达不到了。

Deployment Controller还会确保在任何时间窗口内,只有指定比例的Pod处于离线状态。同时,它也会确保在任何时间窗口内,只有指定比例的新Pod被创建出来。这两个比例的值都是可配置的,默认都是DESIRED的25%。

在上面这个例子中,它有3个副本,那么任何最多只有一个新Pod被创建,最多只能有一个Pod处于离线状态。也就是最多只有4个Pod同时存在集群中,至少有2个Pod处于可用状态。通过RollingUpdateStrategy可以配置这个策略:

apiVersion: apps/v1
kind: Deployment
metadata:
        name: nginx-deployment
        labels:
             app: nginx
spec:
...
  strategy:
     type: RollingUpdate
     rollingUpdate:
       maxSurge: 1
       maxUnavailable: 1

maxSurge指定的是除DESIRED数量外,在一次滚动更新中Deployment控制器还可以创建多少新Pod;而maxUnavailable指的是在一次滚动更新中Deployment可以删除多少旧Pod。也可以用百分比的形式来表示。比如maxSurge=50%表示一次可以创建50%*DESIRED个新Pod。

综上所述,下面有了两个应用版本的Deployment,ReplicaSet和Pod的关系图:

Deployment 的控制器实际上控制的是ReplicaSet的数目,以及每个ReplicaSet的属性。而一个应用的版本对应的正是一个ReplicaSet,这个版本应用的Pod数量由ReplicaSet通过它自己的控制器(ReplicaSet Controller)来保证。通过这样的多个ReplicaSet对象,k8s项目就实现了对多个应用版本的描述。

版本回滚

举例:

设置了一个错误的容器镜像版本

root@k8s-master:/opt/k8s# kubectl set image deployment/nginx-deployment nginx=nginx:1.91
deployment.apps/nginx-deployment image updated

查看以下ReplicaSet状态:

root@k8s-master:/opt/k8s# kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-5bf87f5f59   0         0         0       57m
nginx-deployment-678645bf77   3         3         3       45m
nginx-deployment-7789688b8f   1         1         0       105s

可以看到由于容器镜像不存在导致新版本一直没有READY,此时为了应用正常服务,需要回滚到上一个版本:

root@k8s-master:/opt/k8s# kubectl rollout undo deployment/nginx-deployment
deployment.apps/nginx-deployment rolled back
root@k8s-master:/opt/k8s# kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-5bf87f5f59   0         0         0       60m
nginx-deployment-678645bf77   3         3         3       48m
nginx-deployment-7789688b8f   0         0         0       5m18s

如果要想回滚到更早的版本需要使用 kubectl rollout history 命令查看每次Deployment变更对应的版本:

root@k8s-master:/opt/k8s# kubectl rollout history deployment/nginx-deployment
deployment.apps/nginx-deployment 
REVISION  CHANGE-CAUSE
1         kubectl create --filename=nginx-deployment.yaml --record=true
3         kubectl create --filename=nginx-deployment.yaml --record=true
4         kubectl create --filename=nginx-deployment.yaml --record=true

查看版本细节:

root@k8s-master:/opt/k8s# kubectl rollout history deployment/nginx-deployment --revision=1
deployment.apps/nginx-deployment with revision #1
Pod Template:
  Labels:	app=nginx
	pod-template-hash=5bf87f5f59
  Annotations:	kubernetes.io/change-cause: kubectl create --filename=nginx-deployment.yaml --record=true
  Containers:
   nginx:
    Image:	nginx:1.7.9
    Port:	80/TCP
    Host Port:	0/TCP
    Environment:	<none>
    Mounts:	<none>
  Volumes:	<none>

回滚到指定版本:

root@k8s-master:/opt/k8s# kubectl  rollout undo deployment/nginx-deployment --to-revision=1
deployment.apps/nginx-deployment rolled back

这样,Deployment Controller还会按照滚动更新的方式完成对Deployment 的降级操作。

限制ReplicaSet对象数量

每次对Pod定义的更新都会生成一个ReplicaSet对象有些多余,所以k8s提供了一个指令,能让我们对Deployment的多次更新只生成一个ReplicaSet。具体做法是在更新Deployment前先执行一条kubectl rollout pause指令,修改完之后再执行一条kubectl rollout resume指令。这样中间所作的更改只会触发一次滚动更新。

更简单的方法是设置Deployment的spec.revisionHistoryLimit字段,就是限制Deployment保留的历史版本个数。如果设为0,就再也不能回滚。

标签:容器,Kubernetes,Deployment,nginx,编排,deployment,原理,Pod,k8s
来源: https://www.cnblogs.com/yanshaoshuai/p/15943060.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有