图解Kubernetes的Pod核心资源-来白嫖啊
推荐手机阅读原文:https://mp.weixin.qq.com/s/nR6P6eidE1r5A2viLCFHWA
推荐手机阅读阅读:https://mp.weixin.qq.com/s/nR6P6eidE1r5A2viLCFHWA
推荐手机阅读阅读:https://mp.weixin.qq.com/s/nR6P6eidE1r5A2viLCFHWA
一、Pod定义
如下图,在K8S中资源调度的基本单位是Pod
Pod其实是一个抽象的概念,Pod里是我们的业务容器(docker/containerd)。像大家听过的Deployment、StatefulSet、CronJob等资源调度对象所调度的资源都是Pod。
为了更好的理解Pod的概念,大家可以将Pod理解成VM 虚拟机,将Pod中的容器理解成VM中的进程。既然这样理解,就意味着Pod中的容器进程可以直接通过localhost+端口号实现网络互通,也意味着Pod中的容器可以实现类似直接读取彼此产出到磁盘上的文件的效果。
如上图:容器1访问:127.0.0.1:8082
可以访问到容器2。
在实际用应用中,比如我们有两个服务:服务A和服务B,并且他俩之间只能通过本地回环网卡通信,那我们在就应该将它们分配进同一个pod中。
那什么是资源调度? 简单来说就是将:为Pod挑选一个合适的物理节点,然后将Pod中的容器启动好对外提供服务。
二、Pod入门yaml描述文件
简单的,只要将我们想启动的Docker容器填入Pod资源对象的containers字段中,再通过kubectl命令创建Pod,K8S会为Pod选择一个合适的Node,并在该Node上启动用户指定的容器。
如下是一个Pod的Yaml描述文件,Pod中定义了两个容器分别是:nginx、shell
apiVersion: v1 # 必选,API的版本号
kind: Pod # 必选,类型Pod
metadata: # 必选,元数据
name: daemon-pod # 必选,符合RFC 1035规范的Pod名称
spec:
containers:
- name: nginx
image: nginx:latest # 必选,容器所用的镜像的地址
- name: shell
image: busybox
stdin: true
tty: true
使用kubectl命名创建Pod,可以看到Ready数为2、Node字段说明该Pod运行在叫node02的宿主机上
验证,找到node02
登录node02,查看node02上是否有相应的docker容器
可以看到K8S不光启动了nginx、busybox容器,还多启动一个叫pause的容器,大家也叫它infra容器。
Infra容器的作用:Pod中的所有容器会共享一个NetworkNamespace,因为在创建pod中的业务容器前,会先创建pause容器占用NetworkNamespace,后续创建的业务容器都加入到pause的网络中,相当于在Docker run命令中添加参数:--net=container:pause
,这也是为什么Pod中的所有容器的ip其实都是pod的ip。
如下图,进入shell容器中,看到它的ip其实就是上图中的pod ip。
在nginx容器中,也能看到容器的ip就是pod的ip
三、共享NetworkNamespace
如下图是K8S创建Pod时,Pod的网络协议栈的初始化过程
简单解读,理解pause容器是K8S网络模型中的精髓~
- kubelet通过CRI协议向底层的容器运行时(docker/containerd)下发命令,让它创建一个叫pause 的初始化容器,这个pause容器里运行着一个极简的C程序,具体的逻辑就是将自己阻塞中,目的是让pause容器快速占用并一直持有一个networkname
- 创建pause容器时,会携带参数
--net=node
意为不初始化网络协议栈,说白了就是除了自带的lo回环网卡外,不添加其他的网卡。 - kubelet通过CNI协议为pause容器初始化网络协议,也就是给它添加网络并分配IP
- Pod中定义的业务容器都加入pause容器的network namespace,它们都使用统一分配给pause的IP
疑问:为什么pause容器的网络协议栈不由容器运行时创建它时立即分配好呢?
答:这是个好问题,这么做也是呼应了K8S网络的核心目标思想:
-
IP分配,换句话说K8S要保证在整个集群中每个POD要独立不重复的IP地址
-
IP路由,换句话说K8S要保证在整个集群中各个POD的IP是要互通的
这也是它为什么设计这个流程的原因
四、共享PID
默认情况下Pod中的各容器是不会共享同一个统一个PID Namespace的,需要手动添加参数shareProcessNamespace: true
apiVersion: v1
kind: Pod
metadata:
name: daemon-pod
spec:
# 共享pid namespace
shareProcessNamespace: true
containers:
- name: nginx
image: nginx:latest
- name: shell
image: busybox
stdin: true
tty: true
验证:如下图,在shell容器中可以看到nginx进程(通过这更好的将pod理解成虚拟机),同理登陆pod重点任意容器也能看到pid=1的进程是pause进程。
此时pause容器为Pod提供1号进程,在Uninx中,进程为1的进程被称作init进程。
这个init进程很特殊,因为它会维护一张进程表并不断的检测其他进程的状态,当出现:子进程因父进程的异常退出而变成“孤儿进程”或者是叫“僵尸进程”时,init进程(pause)会收养这个游离的进程,然后在它退出时,释放它占用的资源,否则会可能会出现大量的僵尸进程占用操作系统的进程表项。
在k8s1.8之前,默认是启用共享pid namespace
在k8s1.8之后,则需要像本小节一样,通过参数shareProcessNamespace
显示的开启
问:既然共享了pause的pid有这么多好处,为啥后续版本的k8s不再默认开启了呢?
答:一方面:k8s推荐的做法是,单个pod里面放尽量少的容器,如只放你的业务容器,这样僵尸进程带来的影响几乎可以忽略不计,共享与不共享,意义不大。
另一方面:像一些特殊的如systemd镜像,启动需要获取pid1,否则无法启动
五、容器生命周期
可以通过lifecycle
字段在容器创建完成后以及关闭前执行指定的动作,如创建用户/创建目录,启动脚本等
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:latest
lifecycle:
postStart: # 容器创建完成后执行的指令, 可以是exec httpGet TCPSocket
exec:
command:
- sh
- -c
- 'mkdir /data/ '
preStop: # 关闭前的操作
httpGet:
path: /
port: 80
# exec:
# command:
# - sh
# - -c
# - sleep 9
restartPolicy: Always
spec.containers.lifecycle.postStart
参数可以指定容器在创建完成后执行一段指令
回忆一下,容器还有个command参数即:spec.containers.command
也可以指定一段指令。注意点如下:
- 这俩command执行的先后并不能100%保证。
spec.containers.lifecycle.postStart
的执行依赖于容器创建后的环境
而spec.initContainers.command
的不会依赖业务容器的环境,执行时间也会先于如上两个command。
六、初始化容器
6.1、简介
业务容器的启动依赖很多环境配置,如:
- wget等可以从网络上下载文件的命令
- 或者是有些命令需要以root的权限运行初始化,如修改文件的权限、修改内核参数
如果有攻击性的程序获得了使用这些命令的权限,就会有很大的安全隐患,为了安全,我们是不希望业务容器中包含这些危险的命令。
这时可以使用initcontainer完成这种工作,因为initcontainer运行结束后会退出,没有后续的安全隐患。
6.2、与普通容器的区别
- 初始化容器会依次执行,上一个运行结束,下一个才会执行。
- 初始化容器不成功结束,不会启动业务容器,K8S会不断的重启该Pod。但是如果Pod的restartPolicy设置为Never,K8S就不再重启该Pod了。
- init容器不支持:lifecycle、livenessProbe、readinessProbe、startupProbe探针
6.3、实验
准备yaml,initContainer和业务容器共享挂载volume的方式,让业务容器共享initContainer的初始化文件
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: test-init-container
name: test-init-container
spec:
replicas: 1
selector:
matchLabels:
app: test-init-container
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: test-init-container
spec:
volumes:
- name: data
emptyDir: {}
initContainers:
- name: init01
image: busybox
volumeMounts:
- name: data
mountPath: /tmp
command:
- sh
- -c
- touch /tmp/test-init-container.txt
containers:
- image: nginx
name: nginx
volumeMounts:
- name: data
mountPath: /tmp
查看Pod执行的Event可以看到先执行了初始化容器的相关操作
[root@master01 initContainer]# kubectl describe pod test-init-container-79d689d7d8-fgz2s
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 6m47s default-scheduler Successfully assigned default/test-init-container-79d689d7d8-fgz2s to master01
Normal Pulling 6m47s kubelet Pulling image "busybox"
Normal Pulled 6m43s kubelet Successfully pulled image "busybox" in 3.781619189s
Normal Created 6m43s kubelet Created container init01
Normal Started 6m43s kubelet Started container init01
Normal Pulling 6m42s kubelet Pulling image "nginx"
Normal Pulled 6m11s kubelet Successfully pulled image "nginx" in 31.093619016s
Normal Created 6m11s kubelet Created container nginx
Normal Started 6m11s kubelet Started container nginx
验收
[root@master01 initContainer]# kubectl exec -ti test-init-container-79d689d7d8-fgz2s -- sh
Defaulted container "nginx" out of: nginx, init01 (init)
# ls /tmp
test-init-container.txt
七、Pod探针
名称 | 作用 | 引入版本 |
---|---|---|
startupProbe | 1. 用于判断容器内的应用进程是否成功启动。 2. 若配置了startupProbe。直到它检测通过前,会禁用其他探针 3. startupProbe检测未通过,会使用restartPolicy重启策略重启 4. startupProbe探测成功后将不再探测。 |
1.16 |
readinessProbe | 1. 用户探测容器内的程序是否健康,是否可以接收流量 2. 探测成功表示该容器已经完全启动,可接收流量。 3. 若未配置,默认返回success |
|
livenessProbe | 1. 用于判断容器是否运行 2. 若探测失败kubelet根据重启策略重启容器 3. 若未配置,默认返回success |
检测方式一:ExecAction
原理如下:执行一个脚本,返回0表示成功,返回非0表示异常
[root@master01 yamls]# touch 1
[root@master01 yamls]# cat 1
[root@master01 yamls]# echo $?
0
[root@master01 yamls]# cat 123123.txt
cat: 123123.txt: 没有那个文件或目录
[root@master01 yamls]# echo $?
1
#!/bin/bash
#K8S 的存活探针
function liveness()
{
result=`nmap 127.0.0.1 -p $Target_PORT | grep $Target_PORT/tcp | awk '{print $2}'`
if [ "$result" != "open" ];then
echo 'port not open'
return 1
fi
}
liveness
检测方式二:TcpSocketAction
通过Tcp连接检查容器内的端口是否是连通的,如果连通,认为容器健康。原理如下:
# 连通的情况
[root@master01 yamls]# telnet 10.10.10.101 2380
Trying 10.10.10.101...
Connected to 10.10.10.101.
Escape character is '^]'.
^CConnection closed by foreign host.
# 非连通的情况
[root@master01 yamls]# telnet 10.10.10.101 2381
Trying 10.10.10.101...
telnet: connect to address 10.10.10.101: Connection refused
检测方式三:HttpGetAction
通过应用程序暴露的Http接口,来检查应用程序是否健康,若返回的状态码在[200,400)之间,认为程序健康。
7.1、livenessProbe
目的:判断容器是否启动了,检测失败后会重启容器
参考
livenessProbe:
failureThreshold: 5
httpGet:
path: /health
port: 8080
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
7.2、readinessProbe
目的:探测容器中的应用是否是正常的
检测通过:表示应用可以接收流量,READY状态变成1/1
[root@master01 yamls]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 54s
检测失败,READY状态变成0/1,且RESTARTS且0,表示不会重启容器
[root@master01 yamls]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx 0/1 Running 0 54s
参考
readinessProbe:
failureThreshold: 3
httpGet:
path: /ready
port: 8181
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
7.3、startupProbe
在有了livenessProbe和readinessProbe之后,为啥还整一个startupProbe出来呢?
可用来应对极端情况:应用启动时各种加载配置,导致启动的特别慢。 最终导致livenessProbe检查失败,当livenessProbe检查失败时,k8s会重启容器。 重启之后应用启动还是慢,livenessProbe还是失败,就进入了死循环
startupProbe其实就是将等待探测应用正常启动的步骤从livenessProbe中提取出来,放在livenessProbe步骤前。若配置了startupProbe,livenessProbe和readinessProbe会先被禁用,等startupProbe通过后,livenessProbe和readinessProbe才会生效。
实验:
apiVersion: v1 # 必选,API的版本号
kind: Pod # 必选,类型Pod
metadata: # 必选,元数据
name: nginx # 必选,符合RFC 1035规范的Pod名称
spec: # 必选,用于定义容器的详细信息
containers: # 必选,容器列表
- name: nginx # 必选,符合RFC 1035规范的容器名称
image: nginx:latest # 必选,容器所用的镜像的地址
ports: # 可选,容器需要暴露的端口号列表
- name: http # 端口名称,如果需要暴露多个端口,需要保证每个port的name不能重复
containerPort: 80 # 端口号
protocol: TCP # 端口协议,默认TCP
startupProbe:
httpGet: # httpGet检测方式,生产环境建议使用httpGet实现接口级健康检查,健康检查由应用程序提供。
path: /api/successStart # 检查路径
port: 80
restartPolicy: Always
因为没有/api/successStart
接口,所以startupProbe检测不通过
如下:pod的status为running,但是Ready为0
[root@master01 yamls]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx 0/1 Running 0 19s
查看详情:
startupProbe会按照继续按策略探测,当失败的次数达到预期后,会重启,如下重启了4次了
[root@master01 yamls]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx 0/1 CrashLoopBackOff 4 (27s ago) 2m57s
可以将HttpGet检测方式换成TcpSocket去检测80端口,startupProbe校验即可通过
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:latest
startupProbe:
tcpSocket:
port: 80
restartPolicy: Always
[root@master01 yamls]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 14s
八、Pod退出流程
当我们关闭或者删除pod时,Pod的状态会变成:Terninating
[root@master01 yamls]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx 0/1 Terninating 0 2m16s
另外你会发现,执行delete命令时,会等待一段时间
[root@master01 yamls]# kubectl delete pod nginx
pod "nginx" deleted
这个等待的时间是k8s给pod留出来的一段宽限期,可以通过kubectl edit pod xxx
查看pod的配置,默认情况下有一个叫terminationGracePeriodSeconds: 30
的参数,值为30秒,表示在pod被delete之后,有30秒的宽限期。
在这个宽限期中会做收尾的工作如:
- 将pod所属的service的endpoint对应记录删除
- 执行lifecycle 的 preStop 定义的命令
上文说过lifecycle,重新贴出来,如下:
lifecycle:
postStart: # 容器创建完成后执行的指令, 可以是exec httpGet TCPSocket
exec:
command:
- sh
- -c
- 'mkdir /data/ '
preStop: # 容器关闭前的操作
httpGet:
path: /
port: 80
exec:
command:
- sh
- -c
- sleep 9
九、HPA
9.1、简介
全称:Horizontal Pod Autoscaler
可以根据CPU、内存使用率或者是自定义的指标完成对Pod的自动扩缩容
查看K8S集群的HPA相关API
- v1版本是稳定版,只支持CPU指标
- v2beta1支持CPU、内存、自定义指标
- v2beta2支持CPU、内存、自定义指标、额外指标ExternelMetrics(公有云厂商提供)
9.2、使用
使用限制:
- 不能对如DaemonSet类型的资源进行扩所容。
- 必须安装了metrics-server
- 必须配置requests参数
准备测试环境
# 创建模版yaml
kubectl create deployment nginx-dp --image=nginx --dry-run=client -oyaml > nginx-dp.yaml
# 更新resources
containers:
- image: nginx
name: nginx
resources:
requests:
cpu: 10m
# 创建deployment
kubectl apply -f nginx-dp.yaml
# 查看pod的CPU指标
[root@master01 yamls]# kubectl top pod
NAME CPU(cores) MEMORY(bytes)
busybox 0m 0Mi
nginx-dp-84c4fd8fc6-s7mnx 0m 6Mi
# 为dp暴露一个service
kubectl expose deployment nginx-dp --port=80
可以通过如下命令使用HPA
# 当CPU使用率超过10%就扩容,扩容最大数Pod数为10,最小数为1
kubectl autoscale deployment nginx-dp --cpu-percent=10 --min=1 --max=3
压测,可以观察到pod会被自动扩容
while true; do wget -q -O- http://192.168.217.66 > /dev/null;done
注意点:如果CPU或者是Memory的飙升的源头是数据库压力,那么我们对pod进行扩容不仅没有好转,返回适得其反。
十、静态Pod
10.1、简介
静态Pod是由kubelet直接管理的,且只能在该kubelet所在的Node上运行。
静态Pod不受ApiServer管理,无法与ReplicationController、Deployment、DaemonSet进行关联。
kubelet也无法对静态Pod进行健康检查。
有两种方式创建静态Pod,分别是使用:静态文件/Http,若使用静态配置文件创建pod,需要在kubelet的启动参数statucPodPath
中指定静态Pod的yaml描述文件的位置。
10.2、实验
只要将pod的yaml文件放入指定的目录中,过一会便能通过kubectl查看到pod。
尝试通过kubectl删除Pod,会一直处于pending状态,这是因为kubectl会通过apiserver下发删除的命令,而apiserver无法管理静态pod。故,若想删除静态Pod,需要将对应的pod的yaml文件移出statucPodPath
。
十一、更多Pod属性
十二、对比DockerCompose、DockerSwarm
像Docker公司推出的集群调度工具:Docker Compose或是Docker Swarm它们调度的基本单位都是Docker容器。
点击查看白日梦的笔记:玩转Docker容器调度-DockerCompose、DockerSwarm
而在K8S中集群调度的基本单位是上文中长篇介绍的Pod,他们两者维度是不同的。Pod显然是站在更高的维度上。
因为在容器编排领域中,难点不是为容器选择一个合适的节点然后将容器启动好。难点是解决应用间复杂的相互依赖关系。
比如下:
- 不同应用之间通过本地文件相互通信
- 不同应用之间通过Http协议/RPC协议相互通信
- 不同应用之间通过localhost+端口号互通
- 在所有宿主机上均启动一个Pod副本
前文说了,大家可以把Pod理解成传统意义上的虚拟机,Pod中的容器相当于虚拟机中的不同应用,Pod中有哪些容器由开发人员说了算。
这样其实就是变相的将容器编排最为复杂环节的皮球重新踢给了开发人员,由他们自己描述好,也就实现了天然的解决了应用的复杂依赖关系编排这一难点。K8S调度时也要以Pod为基本单位去选择一个相对合适宿主机,批量的启动好Pod中的容器就行。
十三、参考
Kubernetes官网
《Kubernetes权威指南》
《Kubernetes网络原理与实践》