Kubernetes编排原理(一)

Kubernetes编排原理(一)

Pod

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

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

容器设计模式

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

Infra容器

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

Kubernetes编排原理(一)

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。

Kubernetes编排原理(一)

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的关系图:

Kubernetes编排原理(一)

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,就再也不能回滚。

上一篇:CentOS Stream kubernetes 前面增加一个nginx转发


下一篇:Matalb定制LAMMPS data文件