pending
用户发起创建 Pod 的命令到达 API Server, API Server 会将节点信息写入 etcd, 此时 Pod 还未被 scheduler 调度处于 pending 状态。
ContainerCreating:
scheduler 此时会根据对应的亲和性,容忍度等调度策略,开始将Pod 绑定至对应节点并且创建容器,此时 Pod 处于 ContainerCreating 状态。
Running:
当 Pod 与节点绑定后,kubelet 开始调用CRI, CNI, CSI 来吊起Pod对应的容器,如何吊起成功,此时 Pod 将处于 Succeeded状态,如果吊起失败将处于 Failed 状态。
Unknown:
在kubelet 吊起Pod 的过程中,有可能由于网络问题原因等,无法获取 Pod 状态,此时 Pod 将处于Unknown 状态。
Evicted:
由于节点资源问题,可能导致 Pod 处于被驱逐状态。
具体的状态机如下:
从 Pod 的完整生命周期中可以发现 Pod 在启停的过程中有很多的status, 但是在状态机流转中却主要有五种状态,这几种状态被称为 **Pod Phase, **他们代表着 Pod 主生命周期的状态:
Pod 的实际状态是由Pod Phase 主生命周期状态与该周期内的具体时间点的状态组成的,查看一看运行中 Pod 的yaml 如下:
apiVersion: v1
kind: Pod
metadata:
...
spec:
...
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2022-11-22T01:50:26Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2022-11-22T01:50:27Z"
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: "2022-11-22T01:50:27Z"
status: "True"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2022-11-22T01:50:26Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: containerd://715cd04ba10ebd75a296e29d61f16ff6636935057dda116794c9c94100c21bc5
image: docker.io/library/nginx:alpine
imageID: docker.io/library/nginx@sha256:455c39afebd4d98ef26dd70284aa86e6810b0485af5f4f222b19b89758cabf1e
lastState: {}
name: nginx
ready: true
restartCount: 0
started: true
state:
running:
startedAt: "2022-11-22T01:50:27Z"
hostIP: 192.168.146.190
phase: Running
podIP: 10.10.1.27
podIPs:
- ip: 10.10.1.27
qosClass: BestEffort
startTime: "2022-11-22T01:50:26Z"
可以发现此时Pod 的主生命周期 Phase 为 Running 状态,具体细节状态 conditions 包括 Initialized, Ready, ContainersReady, PodScheduled
在我们执行 get pod 命令查看 pod 运行状态的结果就是根据 Pod Phase 与 Conditions 状态计算出此时时间点 Pod 的状态:
当Pod 处于异常状态的时候,可以通过上表大致推算出 Pod 异常的位置,然后再排查。
资源限制
对于k8s 来说确保 Pod 的高可用其实就是确保 Pod 在节点上的资源是够用的,但是一个节点不可能只运行一个 Pod, 此时高可用策略就是 Pod 在节点上的 Qos 策略,根据资源限制的 limits 和 requests 的不同比例,Pod 的 Qos 主要分为三大类:
每种类型的 Qos 都有不同的适用环境,只有根据实际情况制定不同的资源限制,才能保证既不会造成资源浪费又保证 Pod 正常运行:
基于 Taint 的 Evictions
当节点不可达或节点重启时,需要驱逐该节点上的 Pod 至其他节点,此时 k8s 会为 Pod 自动增加 Toleration:
但是有的 Pod 依赖本地存储,不希望被驱逐,就可以增加tolerationSeconds 以避免被驱逐,等待节点重启完毕。
健康检查探针主要分为三种:
探测方法包括:
Post-start 和 Pre-Stop Hook
有时候需要在容器启动前作一些动作或者优雅停止容器,k8s 提供了对应的 hook:
postStart 结束之前,容器不会被标记为 running 状态,但是无法保证 postStart 脚本和容器的Entrypoint 哪个先执行。
只有当 Pod 被终止时, k8s 才会执行 prestop 脚本,意味着当 Pod 完成或容器退出时,preStop 脚本不会被执行。
terminationGracePeriodSecons 的分解
删除 Pod 有一个默认时间为30s, 该时间段分为两段,duration1 用于执行优雅停止脚本,当优雅停止脚本执行完后,duration2 就开始执行删除杀死命令。
优雅启动
执行 exec 命令进行 readiness 探活
apiVersion: v1
kind: Pod
metadata:
name: initial-delay
spec:
containers:
- name: initial-delay
image: centos
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 300;
readinessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 30
periodSeconds: 5
执行 httpGet 请求进行 readiness 探活
apiVersion: v1
kind: Pod
metadata:
name: http-probe
spec:
containers:
- name: http-probe
image: nginx
readinessProbe:
httpGet:
### this probe will fail with 404 error code
### only httpcode between 200-400 is retreated as success
path: /healthz
port: 80
initialDelaySeconds: 30
periodSeconds: 5
successThreshold: 2
liveness 探活
apiVersion: v1
kind: Pod
metadata:
name: liveness1
spec:
containers:
- name: liveness
image: centos
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 10
periodSeconds: 5
启动前执行脚本命令
apiVersion: v1
kind: Pod
metadata:
name: poststart
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
readiness-gate 等待外部条件和容器自身conditions 均处于就绪状态, Pod 才处于 running 状态
apiVersion: v1
kind: Pod
metadata:
labels:
app: readiness-gate
name: readiness-gate
spec:
readinessGates:
- conditionType: "www.example.com/feature-1"
containers:
- name: readiness-gate
image: nginx
---
apiVersion: v1
kind: Service
metadata:
name: readiness-gate
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: readiness-gate
优雅停止
停止前执行脚本命令
apiVersion: v1
kind: Pod
metadata:
name: prestop
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
preStop:
exec:
command: [ "/bin/sh","-c","nginx -s quit; while killall -0 nginx; do sleep 1; done" ]
kubelet 发送终止信号至容器内进程,然后容器进程自己处理终止信号量,并等待对应时间,默认等待60s
apiVersion: v1
kind: Pod
metadata:
name: no-sigterm
spec:
terminationGracePeriodSeconds: 60
containers:
- name: no-sigterm
image: centos
command: ["/bin/sh"]
args: ["-c", "while true; do echo hello; sleep 10;done"]
apiVersion: v1
kind: Service
metadata:
name: nginx-basic
spec:
type: ClusterIP
ports:
- port: 80
protocol: TCP
name: http
selector:
app: nginx
Service 对象如果不定义 Selector,Endpoint Controller 不会为该 Service 自动创建 Endpoint。但是用户可以手动创建 Endpoint 对象,并设置任意 IP 地址到 Address 属性,访问该服务的请求会被转发至目标地址。
apiVersion: v1
kind: Service
metadata:
name: service-without-selector
spec:
ports:
- port: 80
protocol: TCP
name: http
通过这种方式可以为集群外的一组 Endpoint 创建服务,比如说有一批不在k8s 集群内的机器需要部署服务,但是需要通过 k8s 的服务发现对外提供访问,这个时候可以创建不带 Selector 的 Service, 然后我们自己在手动创建对应的 endpoint 将该 Service 与对应集群外的机器IP地址绑定,就实现了集群外创建服务。
Service 类型
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: tencent.com
Service Topology
Service 引入了 topologyKeys 属性,可以通过如下设置来控制流量:
apiVersion: v1
kind: Service
metadata:
name: prefer-nodelocal
spec:
ports:
- port: 80
protocol: TCP
name: http
selector:
app: nginx
topologyKeys:
- "kubernetes.io/hostname"
- "topology.kubernetes.io/zone"
- "topology.kubernetes.io/region"
- "*"
Pod 处于ready 状态的 addresses 列表:
[admin@ali-jkt-dc-bnc-airflow-test02 k8s]$ kubectl get endpoints ngx-hpa-svc -o yaml
apiVersion: v1
kind: Endpoints
metadata:
creationTimestamp: "2022-11-21T15:18:56Z"
name: ngx-hpa-svc
namespace: default
resourceVersion: "724452"
uid: 35780a4b-4e21-4e4c-aee1-b8b1d63f2d40
subsets:
- addresses:
- ip: 10.10.1.14
nodeName: ali-jkt-dc-bnc-airflow-test03
targetRef:
kind: Pod
name: ngx-hpa-dep-75b9d99c9b-nvbwk
namespace: default
uid: 6af07c2f-5c21-4bb8-b71b-e14b56ad473e
- ip: 10.10.1.15
nodeName: ali-jkt-dc-bnc-airflow-test03
targetRef:
kind: Pod
name: ngx-hpa-dep-75b9d99c9b-djcwh
namespace: default
uid: 5128ae39-5719-4855-a88e-2b0989d5eb22
ports:
- port: 80
protocol: TCP
创建一个处于 unready 状态的 Pod 和 对应的 Service:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
readinessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
################
apiVersion: v1
kind: Service
metadata:
name: nginx-basic
spec:
type: ClusterIP
ports:
- port: 80
protocol: TCP
name: http
selector:
app: nginx
查看 endpoints 处于 notReadyAddresses 状态:
[admin@ali-jkt-dc-bnc-airflow-test02 k8s]$ kubectl get endpoints nginx-basic -o yaml
apiVersion: v1
kind: Endpoints
metadata:
annotations:
endpoints.kubernetes.io/last-change-trigger-time: "2023-03-08T00:50:03Z"
creationTimestamp: "2023-03-08T00:50:03Z"
name: nginx-basic
namespace: default
resourceVersion: "13629154"
uid: 7f315e16-3092-4686-a46a-0d6ed938cd9b
subsets:
- notReadyAddresses:
- ip: 10.10.2.23
nodeName: ali-jkt-dc-bnc-airflow-test01
targetRef:
kind: Pod
name: nginx-deployment-6bdf5fd58d-vzhcf
namespace: default
uid: e61ec9b9-0b5d-4477-8d40-9396a7e55bc4
ports:
- name: http
port: 80
protocol: TCP
设置 PublishNotReadyAddresses 为 True :
apiVersion: v1
kind: Service
metadata:
name: nginx-publish-notready
spec:
publishNotReadyAddresses: true
type: ClusterIP
ports:
- port: 80
protocol: TCP
name: http
selector:
app: nginx
再次查看对应endpoint 中的 addresses 都处于 ready 状态:
[admin@ali-jkt-dc-bnc-airflow-test02 k8s]$ kubectl get endpoints nginx-publish-notready -o yaml
apiVersion: v1
kind: Endpoints
metadata:
annotations:
endpoints.kubernetes.io/last-change-trigger-time: "2023-03-08T00:54:42Z"
creationTimestamp: "2023-03-08T00:54:42Z"
name: nginx-publish-notready
namespace: default
resourceVersion: "13629581"
uid: c87192d8-7cf2-4704-a5bb-e6d5b7f2d9f1
subsets:
- addresses:
- ip: 10.10.2.23
nodeName: ali-jkt-dc-bnc-airflow-test01
targetRef:
kind: Pod
name: nginx-deployment-6bdf5fd58d-vzhcf
namespace: default
uid: e61ec9b9-0b5d-4477-8d40-9396a7e55bc4
ports:
- name: http
port: 80
protocol: TCP
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: example-abc
labels:
kubernetes.io/service-name: example
addressType: IPv4
ports:
- name: http
protocol: TCP
port: 80
endpoints:
- addresses:
- "10.1.2.3"
conditions:
ready: true
hostname: pod-1
nodeName: node-1
zone: us-west2-a
每台机器上都运行一个 kube-proxy 服务,它监听 API Server 中 service 和 endpoint 的变化情况,并通过 iptables 等来为服务配置负载均衡(仅支持TCP和UDP)。
kube-proxy 可以直接运行在物理机上,也可以以static pod 或者 DaemonSet 的方式运行。
kube-proxy 当前支持以下几种实现:
在了解 kube-proxy 工作原理前,需要对 Netfilter 和 iptables 的有个初步的了解。
Linux 内核处理数据包:Netfilter 框架
上图展现的是一个数据包,被一个Linux 系统接收以后,内核在处理数据包的时候会经过哪些流转,数据包流转的过程称为 Netfilter.
关于Netfilter 更多介绍的可以看这篇文章:
走进Linux内核之Netfilter框架
iptables 与 Netfilter 交互流程
iptables 就是 Netfilter 框架中运行在用户态空间的一个网络地址转发组件。
用户可以在 iptbales 中定义 NAT(网络地址转发)规则,iptables 配合 Netfilter 就实现了负载均衡。
一个网络数据包被网卡接收到以后,就会告诉CPU需要处理,此时CPU将会被硬件中断,但是硬件中断的过程中CPU 是没办法处理别的事情了,所以硬件中断需要快速恢复,此时 CPU 会调用软中断 softIRQ Handler。
网卡在接收到数据包时会快速复制一份数据到 Linux kernel 中,此时 softIRQ 就会读取这份数据,构造 SKB(SK Buffer)数据包,包含数据的 Header 和 Data, Header 中包含了地址信息。
softIRQ 在构造完 SKB 数据包后,会告诉Netfilter,Netfilter 就会从用户态的 iptables 中读取用户定义的地址转发规则,然后根据 NAT 规则修改数据包头,后续数据包将被转发到对应的地址。
iptables
网络数据包进入会先经过 PREROUTING 然后根据路由判断是不是进入本机进程,如果是进程本地,就走LOCAL_IN 否则就走 FORWARD 转发至 POSTROUTING 进入网络。
本地数据包处理完后,如果需要转出,就需要走 LOCAL_OUT 然后进入 POSTROUTING 进入网络。
数据包在每一个流转环节都有对应的 hook,通过 hook 就可以修改数据包流转地址。
kube-proxy 想要实现网络地址转换就需要通过 nat 相关的 hook 去修改数据包的流转,对应上图就是修改 PREROUTING 的 raw/mangle/dnat 和 LOCAL_OUT 的 raw/mangle/dnat/filter/snat。
kube-proxy 会watch apiserver ,创建对应的 IPtables 规则,将请求转发到对应的 Pod 地址。
kube-proxy 会在 iptbales 中注册一个 kube-services 的规则,所有 Pod 的网络请求或者外部请求都会进过 kube-services,kube-services 接收到请求后会根据集群中 Pod 具体的 IP 地址修改 iptables 规则,并且根据规则进行转换(上图中虚线框内的)。
如果说一个 Deployment 启了三个不同 ip 的Pod, 然后通过Service 访问 Deployment, 那么 kube-proxy 会将请求转发至哪个 Pod 呢?
这个就和 iptables 的路由规则有关了, kube-proxy 会根据 --mode random 生成三条规则对应三个 IP,每条规则被转发的几率分别为33%, 50%,100%,在进行转发的时候会自上而下转发。
IPVS
kube-proxy引入了IPVS模式,IPVS模式与iptables同样基于Netfilter,但是ipvs采用的hash表,iptables采用一条条的规则列表。iptables又是为了防火墙设计的,集群数量越多iptables规则就越多,而iptables规则是从上到下匹配,所以效率就越是低下。因此当service数量达到一定规模时,hash查表的速度优势就会显现出来,从而提高service的服务性能
每个节点的kube-proxy负责监听API server中service和endpoint的变化情况。将变化信息写入本地userspace、iptables、ipvs来实现service负载均衡,使用NAT将vip流量转至endpoint中。由于userspace模式因为可靠性和性能(频繁切换内核/用户空间)早已经淘汰,所有的客户端请求svc,先经过iptables,然后再经过kube-proxy到pod,所以性能很差。
ipvs和iptables都是基于netfilter的,两者差别如下:
k8s Service 通过虚 IP 地址或者节点端口为用户应用提供访问入口,然而这些 IP 地址和端口是动态分配的,如果用户重建一个服务,其分配的 clusterIP 和 nodePort,以及 LoadBalancerIP 都是会变化的,无法把一个可变的入口发布出去供他人访问。
k8s 提供了内置的域名服务,用户定义的服务会自动获得域名,而无论服务重建多少次,只要服务名不改变,其对应的域名就不会改变。
CoreDNS
k8s 内部域名解析时由CoreDNS 服务实现的,CoreDNS 包含一个内存态 DNS,以及与其他 controller 类似的控制器。
CoreDNS 的实现原理是,控制器监听Service 和 Endpoint 的变化并配置 DNS,客户端 Pod 在进行域名解析时,从 CoreDNS 中查询服务对应的地址记录。
不同类型服务的DNS 记录解析方式也不同:
k8s Pod 有一个与 DNS 策略相关的属性 DNSPolicy, 默认值是 ClusterFirst。
Pod 启动后的 /etc/resolv.conf 会被改写,所有的地址解析优先发送至 CoreDNS。
这里的 ndots:5 意思是在通过域名访问Pod的时候,5个点以下分割的短域名查询将按照 search 中的查询顺序自动补全,并且按照补全后的长域名进行查询。
当 Pod 启动时,同一 Namespace 的所有 Service 都会以环境变量的形式设置到容器内。
k8s 中的负载均衡主要有两种,一种是基于iptables/ipvs 的分布式四层负载均衡技术,kube-proxy 基于 iptables rules 为 k8s 形成全局统一的分布式负载均衡。
一种是基于七层应用层的 ingress, 该负载均衡方式将使业务开发人员与运维动作分离,可以使开发人员更加关注业务开发。
ingress 是一层代理,负责根据 hostname 和 path 将流量转发到不同的服务上,使得一个负载均衡器用于多个后台应用,k8s ingress Spec 是转发规则的集合。
Service 根据 Label 筛选出提供服务的 Pod,Ingress 在根据访问路径,将请求转发至对应的 Service,同时 Ingress 层可以根据需要配置对应的访问证书,在该层统一解密管理证书,解耦业务与运维视角。
Ingress Contoller
和其他的资源对象一样,Ingress 也有对应的 Controller ,它会监听 Ingress 对象,生成对应的负载均衡配置文件,保证实际状态与期望状态一致。
Ingress Controller 的定位是非常简单灵活的,就是只负责请求的接入和转发,定位简单的同时也成为了单一 Controller 不足的表现,没办法提供更多的功能,比如说:证书版本选择,加密算法选择,流量统计等。
为了弥补不足与适用各种状况,社区就产生了很多不同种类的 Ingress Controller,目前比较流行的就是 Nginx Ingress,这里就不展开讲解,感兴趣的可以看下这篇文章:
浅谈Kubernetes Ingress控制器的技术选型