Pending状态意味着Pod的YAML文件已经提交给Kubernetes,API对象已经被创建并保存在Etcd当中。但是,这个Pod里有些容器因为某种原因而不能被顺利创建。比如,调度不成功(可以通过kubectl describe pod命令查看到当前Pod的事件,进而判断为什么没有调度)。
可能原因:资源不足(集群内所有的Node都不满足该Pod请求的CPU、内存、GPU等资源); HostPort 已被占用(通常推荐使用Service对外开放服务端口)。
首先还是通过 kubectl describe pod命令查看当前Pod的事件。可能的原因有:
1)镜像拉取失败,比如镜像地址配置错误、拉取不了国外镜像源(gcr.io)、私有镜像密钥配置错误、镜像太大导致拉取超时 (可以适当调整kubelet的-image-pull-progress-deadline和-runtime-request-timeout选项)等。
2)CNI网络错误,一般需要检查CNI网络插件的配置,比如:无法配置Pod 网络、无法分配IP地址。
3)容器无法启动,需要检查是否打包了正确的镜像或者是否配置了正确的容器参数
4)Failed create pod sandbox,查看kubelet日志,原因可能是磁盘坏道(input/output error)。
通常是镜像名称配置错误或者私有镜像的密钥配置错误导致。
此状态说明容器曾经启动了,但又异常退出。这时可以先查看一下容器的日志。
通过命令kubectl logs 和kubectl logs --previous 可以发下一些容器退出的原因,比如:容器进程退出、健康检查失败退出;此时如果还未发现线索,还而已到容器内执行命令(kubectl exec cassandra - cat /var.log/cassandra/system.loq)来进一步查看退出原因;如果还是没有线索,那就需要SSH登录该Pod所在的Node上,查看Kubelet或者Docker的日志进一步排查。
通常处于Error状态说明Pod启动过程中发生了错误。
常见的原因:依赖的ConfigMap、Secret或PV等不存在;请求的资源超过了管理员设置的限制,比如超过了LimitRange等;违反集群的安全策略,比如违反了PodSecurityPolicy.等;容器无法操作集群内的资源,比如开启RDAC后,需要为ServiceAccount配置角色绑定。
从v1.5开始,Kubernetes不会因为Node失联而删除其上正在运行的Pod,而是将其标记为Terminating 或 Unknown 状态。想要删除这些状态的Pod有三种方法:
1)从集群中删除Node。使用公有云时,kube-controller-manager会在VM删除后自动删除对应的Node。而在物理机部署的集群中,需要管理员手动删除Node(kubectl delete node)。
2)Node恢复正常。kubelet会重新跟kube-apiserver通信确认这些Pod的期待状态,进而再决定删除或者继续运行这些Pod。用户强制删除,用户可以执行(kubectl delete pods pod-name --grace-period=0 --force)强制删除Pod。除非明确知道Pod的确处于停止状态(比如Node所在VM或物理机已经关机),否则不建议使用该方法。特别是StatefulSet 管理的Pod,强制删除容易导致脑裂或数据丢失等问题。
3)Pod行为异常,这里所说的行为异常是指Pod没有按预期的行为执行,比如没有运行podSpec 里面设置的命令行参数。这一般是podSpec yaml文件内容有误,可以尝试使用 --validate 参数重建容器,比如(kubectl delete pod mypod 和 kubectl create --validate -f mypod.yaml);也可以查看创建后的podSpec是否是对的,比如(kubectl get pod mypod -o yaml);修改静态Pod的Manifest后未自动重建,kubelet 使用inotify 机制检测 /etc/kubernetes/manifests 目录(可通过 kubelet 的 -pod-manifest-path 选项指定)中静态Pod的变化,并在文件发生变化后重新创建相应的 Pod。但有时也会发现修改静态Pod的 Manifest后未自动创建新 Pod的情景,此时已过简单的修复方法是重启 Kubelet。
Unknown 这个异常状态意味着Pod的状态不能持续地被 kubelet汇报给 kube-apiserver,这很有可能是主从节点(Master 和 Kubelet)间的通信出现了问题。
PodScheduled
pod正处于调度中,刚开始调度的时候,hostip还没绑定上,持续调度之后,有合适的节点就会绑定hostip,然后更新etcd数据
Initialized
pod中的所有初始化容器已经初启动完毕
Ready
pod中的容器可以提供服务了
Unschedulable
不能调度,没有合适的节点
CrashLoopBackOff: 容器退出,kubelet正在将它重启
InvalidImageName: 无法解析镜像名称
ImageInspectError: 无法校验镜像
ErrImageNeverPull: 策略禁止拉取镜像
ImagePullBackOff: 正在重试拉取
RegistryUnavailable: 连接不到镜像中心
ErrImagePull: 通用的拉取镜像出错
CreateContainerConfigError: 不能创建kubelet使用的容器配置
CreateContainerError: 创建容器失败
m.internalLifecycle.PreStartContainer 执行hook报错
RunContainerError: 启动容器失败
PostStartHookError: 执行hook报错
ContainersNotInitialized: 容器没有初始化完毕
ContainersNotReady: 容器没有准备完毕
ContainerCreating: 容器创建中
PodInitializing:pod 初始化中
DockerDaemonNotReady: docker还没有完全启动
NetworkPluginNotReady: 网络插件还没有完全启动
Evicte: pod被驱赶
在我前面Docker的Cgroup文章中,就提到过,为什么我们对容器进行资源限制。同理:首先K8s中pod使用宿主机的资源默认情况下是无节制的,但是当一个集群搭建成功后并投入生产环境中。如果其中的某一个pod因为不明原因出现了bug,疯狂占用宿主机资源,抢占其他pod的资源。势必会导致整个集群的瘫痪,所以pod资源的限制是非常有必要的
在资源控制器中我们也可以准确的找到相应的资源控制字段:
- kubectl explain deployment.spec.template.spec.containers.resources
- kubectl explain statefulset.spec.template.spec.containers.resources
在官方文档中(资源配额 | Kubernetes) 我们可以得知:pod控制的资源总共为三大类:
cpu,memory,hugepages(巨页)。其中我们运用最多的限制还是cpu和memory。
pod对于cpu限制的参数有两种表达形式:
第一种是指定个数的表达形式,例如:1 ,2, 0.5 ,0.2 ,0.3 指定cpu的个数(该个数可以为整数,也可以为小数点后一位的小数)
第二种是以毫核为单位的表达形式:100m 500m 1000m 2000m (这里1000m等价于一个cpu,该方式来自于cpu时间分片原理得来)
pod对内存参数的要求十分严谨。对硬件有过研究的朋友,应该会了解到,我们常常提到的GB,MB,TB其实是于实际字节总数是有误差的(原因是GB是以10为底数计量,而真正的换算是以2为底数计量。导致了总量的误差。)而真正没有此误差的单位是Ki Mi Gi Ti
所以k8s中pod资源限制为了对pod的内存限制更为精准,采用的单位是 Ki Mi Gi Ti
需求:创建一个deployment资源,镜像使用nginx:latest,创建3个pod副本,镜像拉取策略使用IfNotPresent,重启策略使用Always,容器资源预留cpu 0.25个,内存128MiB,上限最多1个cpu,1g内存
- kubectl create deployement nginx-test --images=nginx:latest --dry-run=client -o yaml >test.yaml
- vim test.yaml
- apiVersion: apps/v1
- kind: Deployment
- metadata:
- creationTimestamp: null
- labels:
- app: nginx-test
- name: nginx-test
- spec:
- replicas: 3
- selector:
- matchLabels:
- app: nginx-test
- template:
- metadata:
- labels:
- app: nginx-test
- spec:
- containers:
- - image: nginx:latest
- name: nginx
- resources:
- requests:
- memory: "128Mi"
- cpu: "250m"
- limits:
- memory: "1Gi"
- cpu: "1"
- imagePullPolicy: IfNotPresent
- restartPolicy: Always
-
-
- kubectl apply -f test.yaml
- #查看pod的资源使用情况
- kubectl describe pod nginx-test
- #查看node节点pod资源使用详情
- kubectl describe node node02
探针是由 kubelet 对容器执行的定期诊断(pod中探针又分为三类):
存活探针(livenessProbe)探测容器是否运行正常。如果探测失败则kubelet杀掉容器(不是Pod),容器会根据重启策略决定是否重启
就绪探针(readinessProbe)探测Pod是否能够进入READY状态,并做好接收请求的准备。如果探测失败Pod则会进入NOTREADY状态(READY为0/1)并且从所关联的service资源的端点(endpoints)中踢出,service将不会再把访问请求转发给这个Pod
启动探针(startupProbe)探测容器内的应用是否启动成功,在启动探针探测成功之前,其它类型的探针都会暂时处于禁用状态
注意:启动探针只是在容器启动后按照配置满足一次后就不再进行后续的探测了。存活探针和就绪探针会一直探测到Pod生命周期结束为止
kubectl explain pod.spec.containers
exec : 通过command字段设置在容器内执行的Linux命令来进行探测,如果命令返回码为0,则认为探测成功,返回码非0则探测失败
httpGet : 通过向容器的指定端口和uri路径发起HTTP GET请求,如果HTTP返回状态码为 >=200且<400的(2XX,3XX),则认为探测成功,返回状态码为4XX,5XX则探测失败
tcpSocket1 : 通过向容器的指定端口发送tcp三次握手连接,如果端口正确却tcp连接成功,则认为探测成功,tcp连接失败则探测失败
探针探测结果有以下值:
Success:表示通过检测。
Failure:表示未通过检测。
Unknown:表示检测没有正常进行。
探针(Probe)有许多可选字段,可以用来更加精确的控制Liveness和Readiness两种探针的行为(Probe):
- apiVersion: v1
- kind: Pod
- metadata:
- name: demo-live
- labels:
- app: demo-live
- spec:
- containers:
- - name: demo-live
- image: centos:7
- args: #创建测试探针探测的文件
- - /bin/sh
- - -c
- - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
- livenessProbe:
- initialDelaySeconds: 10 #延迟检测时间
- periodSeconds: 5 #检测时间间隔
- exec: #使用命令检查
- command: #指令,类似于运行命令sh
- - cat #sh 后的第一个内容,直到需要输入空格,变成下一行
- - /tmp/healthy #由于不能输入空格,需要另外声明,结果为sh cat"空格"/tmp/healthy
-
-
-
- 容器在初始化后,执行(/bin/sh -c "touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600")首先创建一个 /tmp/healthy 文件,然后执行睡眠命令,睡眠 30 秒,到时间后执行删除 /tmp/healthy 文件命令。而设置的存活探针检检测方式为执行 shell 命令,用 cat 命令输出 healthy 文件的内容,如果能成功执行这条命令一次(默认successThreshold:1),存活探针就认为探测成功,由于没有配置(failureThreshold、timeoutSeconds),所以执行(cat /tmp/healthy)并只等待1s,如果1s内执行后返回失败,探测失败。在前 30 秒内,由于文件存在,所以存活探针探测时执行 cat /tmp/healthy 命令成功执行。30 秒后 healthy 文件被删除,所以执行命令失败,Kubernetes 会根据 Pod 设置的重启策略来判断,是否重启 Pod。
- vim httpget.yaml
- apiVersion: v1
- kind: Pod
- metadata:
- name: liveness-httpget
- namespace: default
- spec:
- containers:
- - name: liveness-httpget-container
- image: soscscs/myapp:v1
- imagePullPolicy: IfNotPresent
- ports:
- - name: http
- containerPort: 80
- livenessProbe:
- httpGet:
- port: http
- path: /index.html
- initialDelaySeconds: 1
- periodSeconds: 3
- timeoutSeconds: 10
-
- kubectl create -f httpget.yaml
-
- kubectl exec -it liveness-httpget -- rm -rf /usr/share/nginx/html/index.html
-
- kubectl get pods
-
- 在这个配置文件中,可以看到 Pod 也只有一个容器。initialDelaySeconds 字段告诉 kubelet 在执行第一次探测前应该等待 3 秒。periodSeconds 字段指定了 kubelet 每隔 3 秒执行一次存活探测。kubelet 会向容器内运行的服务(服务会监听 8080 端口)发送一个 HTTP GET 请求来执行探测。如果服务器上 /healthz 路径下的处理程序返回成功代码,则 kubelet 认为容器是健康存活的。如果处理程序返回失败代码,则 kubelet 会杀死这个容器并且重新启动它。
-
- 任何大于或等于 200 并且小于 400 的返回代码标示成功,其它返回代码都标示失败。
- vim tcpsocket.yaml
- apiVersion: v1
- kind: Pod
- metadata:
- name: probe-tcp
- spec:
- containers:
- - name: nginx
- image: soscscs/myapp:v1
- livenessProbe:
- initialDelaySeconds: 5
- timeoutSeconds: 1
- tcpSocket:
- port: 8080
- periodSeconds: 10
- failureThreshold: 2
-
- kubectl create -f tcpsocket.yaml
-
- kubectl exec -it probe-tcp -- netstat -natp
- Active Internet connections (servers and established)
- Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
- tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1/nginx: master pro
-
- kubectl get pods -w
- NAME READY STATUS RESTARTS AGE
- probe-tcp 1/1 Running 0 1s
- probe-tcp 1/1 Running 1 25s #第一次是 init(5秒) + period(10秒) * 2
- probe-tcp 1/1 Running 2 45s #第二次是 period(10秒) + period(10秒) 重试了两次
- probe-tcp 1/1 Running
-
-
-
- 这个例子同时使用 readinessProbe 和 livenessProbe 探测。kubelet 会在容器启动 5 秒后发送第一个 readinessProbe 探测。这会尝试连接 goproxy 容器的 8080 端口。如果探测成功,kubelet 将继续每隔 10 秒运行一次检测。除了 readinessProbe 探测,这个配置包括了一个 livenessProbe 探测。kubelet 会在容器启动 15 秒后进行第一次 livenessProbe 探测。就像 readinessProbe 探测一样,会尝试连接 goproxy 容器的 8080 端口。如果 livenessProbe 探测失败,这个容器会被重新启动。
- vim readiness-httpget.yaml
- apiVersion: v1
- kind: Pod
- metadata:
- name: readiness-httpget
- namespace: default
- spec:
- containers:
- - name: readiness-httpget-container
- image: soscscs/myapp:v1
- imagePullPolicy: IfNotPresent
- ports:
- - name: http
- containerPort: 80
- readinessProbe:
- httpGet:
- port: 80
- path: /index1.html
- initialDelaySeconds: 1
- periodSeconds: 3
- livenessProbe:
- httpGet:
- port: http
- path: /index.html
- initialDelaySeconds: 1
- periodSeconds: 3
- timeoutSeconds: 10
-
- kubectl create -f readiness-httpget.yaml
-
- //readiness探测失败,无法进入READY状态
- kubectl get pods
-
- kubectl exec -it readiness-httpget sh
-
- # cd /usr/share/nginx/html/
- # ls
- 50x.html index.html
- # echo 123 > index1.html
- # exit
-
- kubectl get pods
-
- kubectl exec -it readiness-httpget -- rm -rf /usr/share/nginx/html/index.html
-
- kubectl get pods -w
- vim readiness-myapp.yaml
- apiVersion: v1
- kind: Pod
- metadata:
- name: myapp1
- labels:
- app: myapp
- spec:
- containers:
- - name: myapp
- image: soscscs/myapp:v1
- ports:
- - name: http
- containerPort: 80
- readinessProbe:
- httpGet:
- port: 80
- path: /index.html
- initialDelaySeconds: 5
- periodSeconds: 5
- timeoutSeconds: 10
- ---
- apiVersion: v1
- kind: Pod
- metadata:
- name: myapp2
- labels:
- app: myapp
- spec:
- containers:
- - name: myapp
- image: soscscs/myapp:v1
- ports:
- - name: http
- containerPort: 80
- readinessProbe:
- httpGet:
- port: 80
- path: /index.html
- initialDelaySeconds: 5
- periodSeconds: 5
- timeoutSeconds: 10
- ---
- apiVersion: v1
- kind: Pod
- metadata:
- name: myapp3
- labels:
- app: myapp
- spec:
- containers:
- - name: myapp
- image: soscscs/myapp:v1
- ports:
- - name: http
- containerPort: 80
- readinessProbe:
- httpGet:
- port: 80
- path: /index.html
- initialDelaySeconds: 5
- periodSeconds: 5
- timeoutSeconds: 10
- ---
- apiVersion: v1
- kind: Service
- metadata:
- name: myapp
- spec:
- selector:
- app: myapp
- type: ClusterIP
- ports:
- - name: http
- port: 80
- targetPort: 80
-
- kubectl create -f readiness-myapp.yaml
-
- kubectl get pods,svc,endpoints -o wide
- NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
- pod/myapp1 1/1 Running 0 3m42s 10.244.2.13 node02 <none> <none>
- pod/myapp2 1/1 Running 0 3m42s 10.244.1.15 node01 <none> <none>
- pod/myapp3 1/1 Running 0 3m42s 10.244.2.14 node02 <none> <none>
-
- NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
- ......
- service/myapp ClusterIP 10.96.138.13 <none> 80/TCP 3m42s app=myapp
-
- NAME ENDPOINTS AGE
- ......
- endpoints/myapp 10.244.1.15:80,10.244.2.13:80,10.244.2.14:80 3m42s
-
-
- kubectl exec -it pod/myapp1 -- rm -rf /usr/share/nginx/html/index.html
-
- //readiness探测失败,Pod 无法进入READY状态,且端点控制器将从 endpoints 中剔除删除该 Pod 的 IP 地址
- kubectl get pods,svc,endpoints -o wide
- NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
- pod/myapp1 0/1 Running 0 5m17s 10.244.2.13 node02 <none> <none>
- pod/myapp2 1/1 Running 0 5m17s 10.244.1.15 node01 <none> <none>
- pod/myapp3 1/1 Running 0 5m17s 10.244.2.14 node02 <none> <none>
-
- NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
- ......
- service/myapp ClusterIP 10.96.138.13 <none> 80/TCP 5m17s app=myapp
-
- NAME ENDPOINTS AGE
- ......
- endpoints/myapp 10.244.1.15:80,10.244.2.14:80 5m17s
启动探针存在的必要:
有时候,会有一些现有的应用在启动时需要较长的初始化时间。 要这种情况下,若要不影响对死锁作出快速响应的探测,设置存活探测参数是要技巧的。 技巧就是使用相同的命令来设置启动探测,针对 HTTP 或 TCP 检测,可以通过将 failureThreshold * periodSeconds
参数设置为足够长的时间来应对糟糕情况下的启动时间。(因为启动探针在启动后,其他的两种探针就会进入短暂的禁用装态,于是给容器启动预留了充足的时间,保证容器中业务启动的顺利进行。而且启动探针只会启动一次,后续工作就完全交给其他两个探针,直到容器的生命周期结束)。
- ports:
- - name: liveness-port
- containerPort: 8080
- hostPort: 8080
-
- livenessProbe:
- httpGet:
- path: /healthz
- port: liveness-port
- failureThreshold: 1
- periodSeconds: 10
-
- startupProbe:
- httpGet:
- path: /healthz
- port: liveness-port
- failureThreshold: 30
- periodSeconds: 10
在这里,幸亏有了启动探测,应用程序将会有最多 5 分钟(30 * 10 = 300s)的时间来完成其启动过程。 一旦启动探测成功一次,存活探测任务就会接管对容器的探测,对容器死锁作出快速响应。 如果启动探测一直没有成功,容器会在 300 秒后被杀死,并且根据
restartPolicy
来执行进一步处置。
postStart 配置 exec.command 字段设置 Linux 命令,实现当应用容器启动时,会执行的额外操作
preStop 配置 exec.command 字段设置 Linux 命令,实现当应用容器退出时,会执行的最后一个操作
- apiVersion: v1
- kind: Pod
- metadata:
- name: lifecycle-demo
- spec:
- containers:
- - name: lifecycle-demo-container
- image: soscscs/myapp:v1
- lifecycle: #此为关键字段
- postStart:
- exec:
- command: ["/bin/sh", "-c", "echo Hello from the postStart handler >> /var/log/nginx/message"]
- preStop:
- exec:
- command: ["/bin/sh", "-c", "echo Hello from the prestop handler >> /var/log/nginx/message"]
- volumeMounts:
- - name: message-log
- mountPath: /var/log/nginx/
- readOnly: false
- initContainers:
- - name: init-myservice
- image: soscscs/myapp:v1
- command: ["/bin/sh", "-c", "echo 'Hello initContainers' >> /var/log/nginx/message"]
- volumeMounts:
- - name: message-log
- mountPath: /var/log/nginx/
- readOnly: false
- volumes:
- - name: message-log
- hostPath:
- path: /data/volumes/nginx/log/
- type: DirectoryOrCreate
- kubectl create -f post.yaml
-
- kubectl get pods -o wide
- NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
- lifecycle-demo 1/1 Running 0 2m8s 10.244.2.28 node02 <none> <none>
-
- kubectl exec -it lifecycle-demo -- cat /var/log/nginx/message
- Hello initContainers
- Hello from the postStart handler
-
- //在 node02 节点上查看
- [root@node02 ~]# cd /data/volumes/nginx/log/
- [root@node02 log]# ls
- access.log error.log message
- [root@node02 log]# cat message
- Hello initContainers
- Hello from the postStart handler
- #由上可知,init Container先执行,然后当一个主容器启动后,Kubernetes 将立即发送 postStart 事件。
-
- //删除 pod 后,再在 node02 节点上查看
- kubectl delete pod lifecycle-demo
-
- [root@node02 log]# cat message
- Hello initContainers
- Hello from the postStart handler
- Hello from the poststop handler
- #由上可知,当在容器被终结之前, Kubernetes 将发送一个 preStop 事件。