为什么我们需要Pod

docker底层:

一些容器间具有紧密协作的关系,它们需要被成组调度

如果只是解决容器的成组调度的问题,那k8s可以在调度器层面实现,就没有必要提出Pod的概念,Pod概念更重要的意义是容器设计模式。具体来说,Pod是一个逻辑概念,它的本质还是宿主机操作系统上的Linux容器的Namespace和Cgroups,Pod其实是一组共享了某些资源的容器,Pod里的所有容器都共享一个Network Namespace,并且可以声明共享同一个Volume,这可以通过docker实现:docker run --net=B --volumes-from=B --name=A image-A,但是这样的话一组容器就存在启动的先后关系问题,Pod里的容器就不是对等关系而是拓扑关系了

所以Pod的实现用到了一个中间容器Infra容器镜像为k8s.gcr.io/pause,是一个用汇编语言编写的、永远处于暂停状态的容器,解压后的大小也仅有100-200KB)。创建了Infra容器后,用户容器就可以加入到Infra容器的Network Namespace中了,他们的进出流量都可以认为是通过Infra容器完成的。

这样对于Pod内的容器来说:

Pod中所有的Init Container定义的容器,都会比spec.containers定义的用户容器先启动,并且会逐一启动

深入解析Pod对象

在Pod级别的配置一定是用来描述”机器“的,例如调度、网络、存储以及安全相关的属性

Pod中几个重要字段的含义和用法:

NodeSelector

用法:

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

这样这个pod就永远只会被调度到携带了disktype: ssd的node上运行

NodeName字段由一般由调度器设置,也可以通过自行设置骗过调度器

HostAliases

定义了Pod的host文件(比如/etc/hosts)里面的内容

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

注意如果要修改hosts一定吧要通过这种方式进行,否则在Pod被删除重新创建之后kubelet会覆盖自行文件修改的内容

shareProcessNamespace

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

这意味着这个Pod中的容器共享PID Namespace,如果使用ps ax查看运行的进程的话,可以看到这个Pod中运行的所有容器进程

host*

凡是Pod中的容器要共享主机的Namespace,一定是定义在Pod级别的

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

Containers级别的属性(InitContainers/Containers)

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
	image: nginx
	lifecycle:
      # 和ENTRYPOINT异步执行,不保证先后顺序
	  postStart:
	    exec:
	      command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
	  # 阻塞当前容器结束流程,直到这个钩子执行完毕
	  preStop:
	    exec:
	      command: ["/usr/sbin/nginx", "-s", "quit"]

Pod的生命周期

主要体现在Pod API对象的Status字段,pod.status.phase就是Pod的当前状态

Projected Volume(投射数据卷)

几种特殊的Volume,不是为了存放数据或者完成和宿主机之间的数据交换,而是为容器提供预先定义好的数据:

apiVersion: v1
kind: Pod
metadata:
  name: test-projected-volume
spec:
  containers:
  - name: test-secret-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交给k8s保管:

kubectl create secret generic user --from-file=./username.txt
kubectl create secret generic pass --from-file=./password.txt

也可以直接创建secret对象:

apiVersion: v1
kind: secret
metadata:
  name: mysecret
type:
  Opaque
# 数据必须经过base64编码后传递
data:
  user: YWRtaW4=
  pass: MWYyZDF1MmU2N2Rm
apiVersion: v1
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

这样这个容器就会不断的输出打印Pod中metadata指定的三个labels了

Downward API能获取的信息一定是Pod里容器进程启动之前就能够确定下来的信息,如果要获取Pod容器运行后才会出现的信息,例如容器进程的PID,则需要考虑在Pod里定义一个sidecar容器

容器健康检查和恢复机制

可以为Pod中的容器定义一个健康检查“探针”(Probe),kubelet就会根据Probe的返回值决定这个容器的状态,而不是直接以Docker的返回值决定容器状态

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: test-liveness-exec
spec:
  containers:
  - name: liveness
    image: busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep rm -rf /tmp/healthy; sleep 600
    livenessProbe:
      exec:
        command:
        - cat
        - /tmp/healthy
	  # 容器启动5s后开始执行,每5s执行一次
      initialDelaySecond: 5
      periodSeconds: 5

根据Probe的返回值,就可以判断容器的状态

livenessProbe还可以定义为发起HTTP或者TCP请求的方式:

...
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
    httpHeaders:
    - name: X-Custom-Header
      value: Awesome
  initialDelaySecond: 3
  periodSeconds: 3
...
livenessProbe:
  tcpSocket:
    port: 8080
  initialDelaySeconds: 15
  periodSeconds: 20

kubernetes会将Unhealthy的容器重启,Kubernetes没有Stop语义,所以重启实际上是重新创建容器

这个重新创建的策略,是由pod级别的pod.spec.restartPolicy指定的

  1. 只要Pod的restartPolicy指定的策略允许重启异常的容器,那么这个Pod会保持Running状态并重启容器,否则Pod会进入Failed状态
  2. 对于包含多个容器的Pod,只有其中所有容器都进入异常后,Pod才会进入Failed状态

Preset

可以预定义一些属性,Kubernetes可以自动给对应的Pod对象加上这些属性

例如下面的这个preset.yaml

apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
  name: allow-database
spec:
  # 仅会作用于selector选出来的Pod
  selector:
    matchLabels:
      role: frontend
  env:
    - name: DB-PORT
      value: "6379"
  volumeMounts:
    - mountPath: /cache
	  name: cache-volume
  volume:
    - name: cache-volume
      emptyDir: {}

"控制器"思想

Pod这个API对象看似复杂,实际上就是对容器的进一步抽象和封装,容器这个“沙盒“虽然好用,但是对于描述应用来说还是太过于简单了,所以需要Pod来对容器进行组合,添加更多的属性和字段,然后由Kubernetes来更好的操作它。

Kubernetes操作Pod的对象也是一个API对象(K8s原则:用一种对象管理另外一种对象的”艺术“),而这种对象被称作控制器(controller),Kubernetes中有一个叫做kube-controller-mananger的组件,这个组件就是一系列控制器的集合(在github repo: kubernetes/pkg/controller)

Kubernetes定义了一种通用的编排模式——控制循环(control loop),有些地方也把它称作调谐(reconcile)或者调谐循环(reconcile loop)

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

一个典型的Deployment可以用以下的YAML文件定义:

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

可以发现,template的定义和定义一个kind=Pod的资源完全一样,这表明Deployment控制的对象来自于一个模板,Pod模板有一个专属的名字PodTemplate,还有其他类型的模板,例如Volume的模板

在所有的API对象的Metadata里都有一个名为ownerReference的字段,用于保存当前这个API对象的拥有者

作业副本与水平扩展

Deployment实际上不是直接控制Pod对象,而是支持滚动更新(rolling update)的ReplicaSet,从而实现Pod的水平扩展/伸缩能力,一个典型的ReplicaSet可以按照下述方式定义:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-app
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    meatadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9

ReplicaSet的定义其实是Deployment的子集,Deployment、ReplicaSet和Pod之间的关系是一种“层层控制”的关系:Deployment控制ReplicaSet(版本),ReplicaSet控制Pod(副本数)

Deployment -> ReplicaSet(v1)
ReplicaSet(v1) -> Pod1
ReplicaSet(v1) -> Pod2
ReplicaSet(v1) -> Pod3
pod1: {shape: circle}
pod2: {shape: circle}
pod3: {shape: circle}

如果修改了已经部署的Deployment的Pod模板,则会自动触发“滚动更新”,滚动更新中,Deployment Controller会保证在任何时间窗口内, 只有指定比例的Pod处于离线状态,同时,只有指定比例新的Pod被创建,这两个参数默认是DESIRED值的25%,可以通过 RollyUpdateStrategy 这个字段来配置

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

spec.updateStrategy.rollingUpdate.partition字段允许指定一个副本数,在滚动更新的时候该数量的副本不会更新到最新版本