目录:
(1)k8s指南-概述
(2)k8s指南-架构
(3)k8s指南-工作负载(1)
(4)k8s指南-工作负载(2)
(5)k8s指南-工作负载(3)
(6)k8s指南-工作负载(4)
(7)k8s指南-Service
(8)k8s指南-Ingress
(9)k8s指南-DNS与服务发现
(10)K8S指南-平滑升级与自动扩缩容
工作负载是在kubernetes上运行的应用程序。
在kubernetes中,pod代表的是集群上处于运行状态的一组容器。
Pod有确定的生命周期,例如,当某个pod在你的集群中运行时,pod运行所在的节点出现致命错误时,所有该节点上的pods都会失败。kubernetes将这类失败视为最终状态,也就是说,即使该节点后来恢复正常,你也需要创建新的pod来恢复应用。
用户无需直接管理每个pod,而是通过负载资源来进行管理一组pods。这些资源配置控制器来确保合适类型、处于运行状态的pod个数是正确的,并与你所指定的状态一致。
Kubernetes提供若干种内置的工作负载资源:
- Deployment和ReplicaSet。Deplyment很适合用来管理集群上的无状态应用,Deployment中的所有pod都是等价的。
- Job和CronJob。定义一些一直运行到结束并停止的任务,Job用来表达一次性任务,而CronJob则是周期性任务。
概述
Pod是可以在Kubernetes中创建、管理的最小的可部署计算单元。
Pod是一组容器,这些容器共享存储、网络以及容器运行规则声明。Pod所建模的是特定于应用的逻辑主机,其中包含一个或多个应用容器,这些容器相对紧密的耦合在一起。
除了应用容器外,Pod还可以包含在Pod启动期间运行的Init容器。
使用Pod
就Docker概念的术语而言,Pod类似于共享命名空间和文件系统卷的一组Docker容器。
Kubernetes支持很多容器运行时,但Docker是其中最有名的,因此这里使用docker术语来描述pod相关的概念。
下面是一个pod示例,它由一个运行镜像nginx:1.14.2
的容器组成。
apiVersion: v2
kind: Pod
meatadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
执行命令来创建该pod:
kubectl apply -f https://k8s.io/examples/pods/simple-pod.yaml
Pod通常不是直接创建的,而是通过工作负载资源创建的。
创建了上述pod后,查看pod(kubectl get pods)发现其状态是pending的,describe后结果如下:
可以看出,这里显示pod调度失败。查看节点状态发现节点设置了不可调度:Ready,SchedulingDisabled
,想到在前文通过cordon命令将节点设置为不可调度了,因此需要重新设置节点为可调度:kubectl uncordon docker-desktop
,执行该命令后可以看到pod创建成功了。
用于管理pod的工作负载资源
通常你不需要直接创建pod,而是使用Deployment或Job这类工作负载来创建。如果需要跟踪Pod状态,可以考虑使用StatefulSet。
Kubernetes集群中的pod主要有两种用法:
- 运行单个容器的pod。每个pod一个容器是最常见的,在这种情况下,可以将pod看作单个容器的包装器。
- 运行多个协同工作的容器pod。pod可能封装由多个紧密耦合且需要共享资源的容器组成的应用程序。
每个pod都旨在运行给定应用程序的单个实例。如果希望横向扩展应用程序,则应该使用多个pod。在Kubernetes中,这被称为Replication副本。通常使用一种工作负载资源及其控制器来创建和管理一组pod副本。
pod多容器
Pod是支持多个容器的,容器之间共享资源和依赖、彼此通信、协调何时以及何种方式终止自身。
例如,一个pod中有两个容器,一个容器负责为共享卷中的文件提供web服务器支持,一个负责从远端更新这些文件。这两个容器均是应用程序容器。
还有些pod具有init容器和应用容器,Init容器会在启动应用容器之间运行并完成。
Pod天生为其成员容器提供了两种共享资源:网络和存储。
pod模板
上文说过,一般情况下pod是由工作负载资源及其控制器来创建和管理的。而如何创建和管理pod是有模板的,就是PodTemplate
。下面是一个例子,其中的template
启动一个容器并打印消息:
apiVersion: batch/v1
kind: Job
metadata:
name: hello
spec:
template:
# 这里是 Pod 模板
spec:
containers:
- name: hello
image: busybox:1.28
command: ['sh', '-c', 'echo "Hello, Kubernetes!" && sleep 3600']
restartPolicy: OnFailure
# 以上为 Pod 模板
修改pod模板或者切换到新的pod模板都不会对已经存在的pod起作用,pod不会直接收到模板的更新。要应用新的pod模板,就必须重新创建新的pod。
pod更新与替换
当某工作负载的pod模板被改变时,其控制器会基于更新的模板创建新的pod对象,而不是对现有的pod执行更新或者修补操作。
当然,Kubenetes并不禁止你直接管理pod。对运行中的pod的某些字段执行热更新是可能的。只不过有一些限制:
- pod的绝大多数元数据是不可变更的。如
namespace
、name
、uid
或者creationTimestamp
字段;generation
值只能增加不能减少。 - 如果
metadata.deletionTimstamp
被设置,则不可向metadata.finalizers
列表添加新的条目。 - pod更新不能改变除了
spec.container[*].image
,spec.initContainers[*].image
、spec.activeDeadlineSeconds
或spec.tolerations
之外的字段,且spec.tolerations
只能增加新条目。 - 在更新
spec.activeDeadlineSeconds
字段时,以下两种更新操作是被允许的:如果未配置,可以设置为一个正数;如果已经设置为一个正数,可以将其设置为更新的整数。
资源共享和通信
pod的成员容器间可以进行数据共享和通信。
一个pod可以设置一组共享的存储卷。Pod的所有容器都可以访问该共享卷,从而允许这些容器共享数据。卷还允许pod中的持久数据保留下来,即使其中的容器需要重新启动。
每个pod都在每个地址族中获得一个唯一的IP地址。Pod中的容器共享网络命名空间,包括IP地址和网络端口。Pod中的容器可以使用localhost互相通信,而当pod中的容器与其他pod中的容器通信时,需要通过IP联网的方式实现。
Pod中的容器所看到的系统主机名与pod配置的name属性值相同。
容器的特权模式
在Linux中,Pod中的任何容器都可以使用容器规约中的安全性上下文中的privileged
参数启用特权模式。这对于想要使用操作系统管理权限(如操作网络堆栈和访问设备)的容器很有用。
如果你的集群启用了WindowsHostProcessContainers
特性,你可以使用pod规约中的安全上下文的windosOptions.hostProcess
参数来创建Windows HostProcess Pod,这些pod中的所有容器都必须以Windows HostProcess容器方式运行。HostProcess Pod 可以直接运行在主机上,也能像Linux特权容器一样,用于执行管理任务。
Pod的生命周期
Pod遵循一个预定义的生命周期,起始于Pending
阶段,如果至少其中有一个主要容器正常启动,则进入Running
阶段,之后取决于Pod中是否有容器以失败状态结束而进入Succeeded
或者Failed
在Pod运行期间,kubelet能够重启容器以处理一些失败场景。在Pod内部,kubernetes跟踪不同容器的状态并确定使Pod重新变得健康所需要采取的动作。
Pod在其生命周期中只会被调度一次,一旦pod被调度到某个节点,pod就会一直在该节点运行,直到pod停止或者被终止。
Pod生命期和阶段
和一个个独立的应用容器一样,Pod也被认为是相对临时性的实体。Pod会被创建、赋予一个唯一的ID,并被调度到节点,并在终止之前一直运行在该节点。
任何给定的pod(UID标识)从不会被重新调度到不同的节点,但可以被一个新的,几乎完全相同的pod替换掉。如果需要,新pod的名字可以不变,但是其UID会不同。
Pod的status
字段是一个PodStatus对象,其中包含一个phase
字段。
Pod的阶段是pod在其生命周期中所处位置的简单宏观概述。该阶段并不是对容器或pod状态的综合汇总,也不是为了成为完整的状态机。
Pod阶段的数量和含义是严格定义的,如下所示:
取值 | 描述 |
---|---|
Pending | Pod已被Kubenetes系统接受,但有一个或多个容器尚未创建和运行。此阶段包括等待pod被调度的时间和通过网络下载镜像的时间 |
Running | Pod已经被绑定到了某个节点,pod中所有的容器都已被创建。至少有一个容器仍在运行或者正处于启动或重启状态 |
Succeeded | Pod中的所有容器均已成功终止,并且不会再重启 |
Failed | Pod中的所有容器均已终止,并且至少有一个容器是因为失败而终止。也就是说容器以非0状态退出或者被系统终止 |
Unknown | 无法获取pod的状态,通常出现在与pod所在主机通信失败的情况下 |
如果某节点挂掉了或者与集群中的其他节点失联,则该节点上所有的pod的phase
将被设置为Failed
。
容器状态
一旦调度器将pod分派给某个节点,kubelet就通过容器运行时为pod创建容器。容器的状态有三种:Waiting
, Running
和Terminated
。
要检查pod中容器的状态,可以使用kubelet describe pod <pod名称>
,其输出中包含pod中每个容器的状态。每种状态都有特定的含义:
Waiting
处于waiting状态的容器正在进行启动之前所必须的操作:拉取容器镜像或者应用Secret
数据。当你使用kubectl来查询包含waiting状态容器的pod时,你也会看到一个Reason字段,其中给出了容器处于等待状态的原因。
Running
Running
状态表明容器正在正常执行。如果配置了postStart
回调,那么该回调已经执行完成。
Terminated
处于该状态的容器已经执行完成或者因为某些原因导致的失败而终止。当使用kubectl来查询包含terminated状态的容器的pod时,你可以看到终止原因,错误码等信息。如果配置了preStop
回调,则在容器终止之前会执行此回调。
容器重启策略
Pod的spec
中包含一个restartPolicy
字段,其可能取值包括Always、OnFailure和Never,默认是Always。
重启策略适用于pod中的所有容器。当pod中的容器退出时,kubelet
会按照指数回退方式计算重启的延迟时间(10s、20s、40s、…),其最长延迟时间为5分钟。一旦某容器执行了10分钟且没有出现问题,则针对该容器的重启回退延迟计时器会重置。
Pod状况
上文说过容器状态是一个PodStatus
对象,其中包含容器的阶段字段,除此之外还包含一个PodConditions
数组,表明了pod的基本状况:
PodScheduled
: Pod已经被调度到某节点ContainersReady
:Pod中所有容器均已就绪Initialized
:所有的Init容器均已成功完成Ready
: Pod可以为请求提供服务,可以被添加到对应服务的负载均衡池中
容器探针
probe是由kubelet对容器执行的定期诊断。要执行诊断,kubelet既可以在容器内执行代码,也可以发起网络请求。
检查机制
使用探针检查容器有四种不同的方法,每个探针都必须准确定义为这四种机制中的一种:
exec
在容器内执行指定指令,如果命令退出时返回码为0则认为诊断成功。
grpc
使用gRPC执行一个远程过程调用。目标应该实现gRPC健康检查。如果响应状态是"SERVING",则认为是成功的。gRPC探针是一个alpha特性,只有在你启用了"GRPCContainerProbe"特性开关时才能使用。
httpGet
对容器的IP地址上指定端口和路径执行HTTP GET请求。如果响应的状态码大于等于200且小于400,则认为是成功的。
tcpSocket
对容器的IP地址上指定端口执行TCP检查,如果端口打开,则认为是成功的。如果远程系统(即容器)在打开连接后立即将其关闭,也算是健康的。
每次探测结果都将获得三种结果之一:成功,失败和未知。
探测类型
kubelet一共有三种类型的探针。
livenessProbe
存活探针,表明容器是否正在运行。如果存活探针探测失败,则kubelet会杀死容器,并且根据容器的重启策略来重启容器。如果容器不提供存活探针,则默认状态为Success。
readinessProbe
就绪探针,表明容器是否准备好为请求提供服务。如果探测失败,端点控制器将从与pod匹配的所有服务的端点列表中删除该pod的ip地址。如果容器不提供就绪探针,则默认状态为Success。初始化延迟之前的就绪状态默认为Failure。
startupProbe
启动探针,表明容器中的应用是否已经启动。如果提供了启动探针,则在此探针成功之前,其他探针都会被禁用。如果启动探测失败,kubelet将杀死容器,系统会根据容器重启策略来重启容器。如果容器没有提供启动探针,则默认状态为Success。
如果你的容器需要在启动期间加载大型数据、配置文件或执行迁移,你可以使用启动探针。
在删除pod时,pod会自动将自身置于未就绪状态,无论就绪探针是否存在。
Pod的终止
在前文介绍节点时提到过节点的优雅关闭和强制关闭,pod同样也有这样的情况。当你请求删除某个pod时,集群会记录并跟踪pod的优雅关闭周期,而不是直接强制杀死pod。默认情况下,所有的删除操作都有30s的宽限期,允许pod去做一些终止前的清理工作。当然,你也可以通过--grace-period=<seconds>
参数设置宽限期。
如果确实要立即强制删除pod,则必须设置--grace-period=0
且指定--force
参数才可以。
执行强制删除操作时,API服务器不再等待来自kubelet的确认消息,而是直接删除pod对象。同时在节点侧,被设置为立即终止的pod仍然会在被强行杀死之前获得一点点的宽限时间。
Init容器
Init容器是一种特殊容器,在应用容器启动之前运行。Init容器可以包括一些应用镜像中不存在的实用工具和安装脚本。
Init容器与应用容器非常像,但有两点不同:Init容器总是运行到完成(容器状态为terminated);一个初始化容器完全执行完成后才会启动另一个初始化容器。
如果pod的初始化容器启动失败,kubelet将会一直尝试重启该容器。但如果pod指定容器重启策略为Never,则Kubernetes会将整个pod状态设置为失败。
Init容器需要以initContainers
指定,以Container类型的对象数组形式来组织,并和应用容器的containers
数组相邻。如下所示:
spec:
initContainers:
- name: sys-init
... // 容器镜像相关参数
- name: install-agent
... // 容器镜像相关参数
containers:
- name: busi-service
... // 容器镜像相关参数
- name: busi-service2
... // 容器镜像相关参数
...
与普通容器的差别
Init容器支持应用容器的全部字段和特性,包括资源限制、数据卷和安全设置。但Init容器对资源请求和限制的处理稍有不同,具体下文将介绍。
除此之外,Init容器不支持lifecycle
、livenessProbe
、readinessProbe
和startupProbe
,因为它们必须在pod就绪之前就运行完成。
如果为一个pod指定了多个Init容器,这些容器会按顺序逐个运行。每个Init容器必须运行成功下一个才能运行。当所有的Init容器运行完成时,Kubernetes才会为pod初始化应用容器。
使用Init容器
下面是一个使用Init容器的例子,在这个pod中定义了两个Init容器,第一个等待myservice
启动,第二个等待mydb
启动。一旦这两个Init容器都启动完成,pod将启动应用容器。
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app.kubernetes.io/name: MyApp
spec:
containers:
- name: myapp-container
image: busybox:1.28
command: ['sh', '-c', 'echo The app is running! && sleep 3600']
initContainers:
- name: init-myservice
image: busybox:1.28
command: ['sh', '-c', "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]
- name: init-mydb
image: busybox:1.28
command: ['sh', '-c', "until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done"]
通过以下命令启动pod:
kubectl apply -f myapp.yaml
使用以下命令检查其状态:
kubectl get -f myapp.yaml
// 或者kubectl get pods
结果如下:
NAME READY STATUS RESTARTS AGE
myapp-pod 0/1 Init:0/2 0 6m
查看pod信息使用kubectl describe -f myapp.yaml
,输出结果会显示具体pod信息,可以看到第一个初始化容器Ready状态为False。
继续查看pod内Init容器的日志:
kubectl logs myapp-pod -c init-myservice # 查看第一个Init容器
kubectl logs myapp-pod -c init-mydb. # 查看第二个Init容器
可以看到失败原因是无法解析myservice。
创建一下service:
---
apiVersion: v1
kind: Service
metadata:
name: myservice
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376
---
apiVersion: v1
kind: Service
metadata:
name: mydb
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9377
创建服务的命令:
kubectl create -f services.yaml
服务创建完成后,再次查看pod状态:
kubectl get -f myapp.yaml
此时会发现pod正常启动。
如果pod还是无法启动,可以执行
kubectl get pods -n kube-system
来查看coredns
是否正常,查看其容器日志来分析问题。博主在使用macOS版的docker-desktop启动kubernetes时出现
coredns
无法启动的问题,此问题与kubernetes初始化时分配的子网有关,在kubernetes菜单中reset cluster后重启解决。
Init容器的行为
上文说过,init容器是按顺序启动的,前一个成功退出后下一个才会启动。如果某容器无法启动或者以错误状态退出,kubelet会根据Pod的restartPolicy
策略进行重试。
在Init容器全部创建成功之前,此时pod处于Pending
状态(表明pod中还有容器没有创建成功),且其状况Initializing
将是false(表明还有Init容器未创建成功)。
如果pod重启,所有Init容器必须重新执行。
Init容器拥有应用容器的除探针之外的所有字段。对于Init容器,只能修改image
字段,且修改后直接重启整个pod。
因为Init容器可能会被重启、重试或重新执行,所以Init容器的执行命令应该是幂等的。比如说Init容器启动时向一个目录写文件,应该对该文件已经存在做好预期准备。
资源
在给定的Init容器执行顺序下,资源使用适用于以下规则:
- 所有Init容器上定义的资源的limit/request的最大值将作为pod的初始有效request/limit值。
- pod对于资源的有效limit/request是如下两者中的较大者:(1)所有应用容器对某资源的limit/request之和。(2)对某个资源的初始有效limit/request。
- 基于有效limit/request完成调度,意味着Init容器能够为初始化过程预留资源
pod重启原因
Pod如果重启了,可能是以下原因导致:
- Init container的镜像被更新时,init容器将会重新运行,导致pod重启(应用容器的镜像更新只会使得应用容器被重启)。
- Pod的基础设施容器(如
pause
容器)更新时,pod将会重启。这种情况不多见,必须由具备root权限访问节点的人员来实施完成。 - 若Pod中的所有应用容器都终止了,并且
RestartPolicy
为Always
,则Pod会重启。
事实上,pod重启发生的情况比较少,更多的是应用容器的重启。比如当存活探针探测失败时,会根据重启策略来重启应用容器。
干扰
Pod不会消失,除非被用户或者控制器主动将其销毁或者出现了不可避免的硬件或软件系统错误。
用户或控制器主动销毁pod的情况称为自愿干扰,例如:
- 删除Deployment或者其他管理pod的控制器
- 更新Deployment的pod模板导致pod重启
- 直接删除pod
- 排空节点进行修复或升级
- 排空节点以缩小集群
- 从节点中移除一个pod,以允许其他pod使用该节点
其他由硬件或软件导致的不可避免的销毁pod的情况被称为应用的非自愿干扰,例如:
- 节点下层物理机硬件故障
- 集群管理员错误地删除虚拟机实例
- 云提供商或虚拟机管理程序故障导致虚拟机消失
- 内核错误
- 节点由于集群网络隔离从集群中消失
- 由于节点资源不足导致pod被驱逐
当干扰出现时,系统的可用性会受到影响。比如说,在一个节点上创建了一个pod,如果该节点出现故障导致pod不可用了,就会导致pod中的服务不可用。
以下是减轻非自愿干扰的一些方法:
- 确保pod在请求中给出所需资源。
- 如果需要更高的可用性,请创建多个pod并使用多区域集群。
创建多个应用副本和跨区域部署pod,当某个节点出现问题时,其他节点还可以正常提供服务。当某个区域出现问题时,这些pod还可以调度到其他可用区域。
而对于自愿干扰来说,Kubernetes也提供了一些特性来满足在出现自愿干扰情况下的系统可用性。这些特性也被称为干扰预算。
干扰预算
应用程序所有者可以为每个应用程序创建PodDistruptionBudget
对象(PDB)。PDB将限制在同一时间因自愿干扰导致的宕机的pod数量。管理员可以使用遵循PodDisruptionBudgets的接口来删除pod或Deployment。
PDB无法防止非自愿干扰。由于应用程序的滚动升级而被删除的pod会计入干扰预算,但是控制器(如Delpoyment和StatefulSet)在进行滚动升级时不受PDB的限制。
PDB示例
假设集群有3个节点,从node-1
到node-3
。集群上某个应用有3个副本,分别是pod-a
,pod-b
和pod-c
,另外还有一个不带PDB的无关pod,名为pod-x
。
三个pod分布在三个节点上,pod-x在第一个节点上。
三个pod都是depoyment的一部分,并且共同拥有一个PDB,该PDB要求3个pod中至少有2个始终处于可用状态。
现在假设管理员要重启系统,升级内核版本来修复内核中的缺陷。首先使用kubectl drain
命令尝试排空node-1
节点,此时系统会尝试驱逐pod-a
和pod-x
,这个操作是成功的,两个pod进入terminating
状态。 此时集群处于下面的状态:
node-1 draining | node-2 | node-3 |
---|---|---|
pod-a terminating, pod-x terminating | pod-b available | pod-c available |
然后Deployment控制器观察到其中一个pod正在终止,于是创建了一个替代pod:pod-d
。由于node1被封锁(cordon),pod-d
只能调度到另一个节点上。同样,pod-x
也会有一个新的替代品。
注意,对于StatefulSet来说,
pod-a
需要在替换pod创建之前完成终止,替代pod名称也是pod-a
,但UID是不同的。
在某一时刻,pod-a
被终止,集群状态如下:
node-1 draining | node-2 | node-3 |
---|---|---|
pod-b available,pod-d starting | pod-c available, pod-y |
此时管理员试图继续排空node-2
和node-3
,会发现drain命令被阻塞。因为对于Deployment来说,只剩下2个可用的pod,而PDB要求至少有2个可用pod,因此pod-b
和pod-c
是不能被驱逐的。
经过一段时间后pod-d
变得可用了,node-2
的drain命令将尝试按照某种顺序来驱逐两个pod,假设先是pod-b
,然后是pod-d
。前者驱逐成功,而后者还是会被拒绝,因为要满足PDB的要求。
Deployment控制器会继续创建pod-e
来代替被驱逐的pod-b
,然而新创建的pod是无法调度的,因此3个节点均被封锁(cordon)。也就是说,drain命令再次阻塞,集群将是以下状态:
node-1 draining | node-2 | node-3 | no node |
---|---|---|---|
pod-d available | pod-c available, pod-y | pod-e pending |
此时管理员需要增加一个节点到集群中才能完成升级操作。
参考资料
[1]. https://kubernetes.io/zh/docs/concepts/overview/
[2]. https://blog.csdn.net/weichuangxxb/article/details/103754021