为什么我们需要Pod
docker底层:
- Namespace做隔离
- Cgroups做资源限制
- rootfs(Union FS)做文件系统
容器的本质是进程,那么kubernetes的本质就是操作系统
容器本身是“单进程模型”,并不是说容器里只能运行一个进程,而是指容器无法管理多个进程,容器中PID=1的进程是应用本身,它不具有像正常操作系统中的init进程或者systemd进程那样拥有进程管理的功能。
一些容器间具有紧密协作的关系,它们需要被成组调度:
- 发生直接的文件交换
- 使用localhost或者Socket文件进行本地通信
- 发生非常频繁的远程调用
- 共享某些Linux Namespace(例如一个容器要加入另一个容器的Network Namespace)
如果只是解决容器的成组调度的问题,那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内的容器来说:
- 它们可以直接通过localhost进行通信
- 它们看到的网络设备和Infra容器看到的一致
- 一个Pod只有一个IP地址,也就是这个Pod的Network Namespace对应的IP地址
- 其他所有的网络资源都是一个Pod一份,且Pod中的所有容器共享
- Pod的生命周期与Infra容器一致
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)
ImagePullPolicy
:定义了image拉取的策略,默认为always(特别是image没有tag或者tag为latest的时候)Lifecycle
:在容器状态发生变化的时候触发一系列“钩子”
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的当前状态
- Pending:API对象已经被创建并保存到etcd当中了
- Running:已经调度成功,包含的容器都已经创建成功且至少有一个正在运行
- Ready:不仅Running了,而且已经准备好对外提供服务了
- Succeeded:一次性任务中最常见
- Failed
- Unknown:可能是Master和kubelet间的通信出了问题
Projected Volume(投射数据卷)
几种特殊的Volume,不是为了存放数据或者完成和宿主机之间的数据交换,而是为容器提供预先定义好的数据:
- Secret:把想要加密的数据放到etcd中
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
- ConfigMap
用法和Secret类似,以无须加密的方式提供配置信息 - Downward API:让Pod里的容器能够直接获取这个Pod API对象本身的信息
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容器
- ServiceAccountToken
Service Account对象的作用就是Kubernetes系统内置的一种“服务账户”,它是Kubernetes进行权限分配的对象,而这样的授权信息和文件,其实就是被保存在它所绑定的一种特殊的Secret对象里,这种对象就叫做ServiceAccountToken
有了这种授权,Pod中的应用程序就可以加载这些授权文件,访问并操作Kubernetes API了
容器健康检查和恢复机制
可以为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
指定的
- Always:在任何情况下,主要容器不在运行状态,就自动重启容器
- OnFailure:只在容器异常时才自动重启容器
- Never:从不启动容器,如果关心上下文环境,就需要把restartPolicy设置为Never,这样容器的内容才不会被GC
注意:Pod的恢复过程永远发生在当前节点上,一旦一个Pod与一个节点绑定,除非这个绑定发生了变化(pod.spec.node字段被修改),否则它永远不会离开这个节点,如果想让Pod出现在其他的可用节点上,就必须使用Deployment这样的控制器来管理Pod
关于Pod在什么情况下会显示Failed状态(只要Pod包含的容器有任意的可能性重启,Pod都不会是Failed):
- 只要Pod的restartPolicy指定的策略允许重启异常的容器,那么这个Pod会保持Running状态并重启容器,否则Pod会进入Failed状态
- 对于包含多个容器的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字段允许指定一个副本数,在滚动更新的时候该数量的副本不会更新到最新版本