1:Pod
回顾
前面我们学习了Pod的生命周期,那么接下来我们来学习的是关于Pod的更多进阶的用法
2:Pod
资源配置
实际上我们在学习Pod生命周期的时候就是影响整个Pod的最大部分,但是还有一部分细节也会在Pod的启动过程中进行设置,比如容器启动之前还会为当前容器设置分配的CPU,内存等资源,我们知道我们可以通过Cgroup来对容器资源进行限制,同样在Pod中我们也可以直接配置某个容器的使用的CPU或者内存的上限,那么Pod是如何来使用和控制这些资源的分配的呢?
首先对于CPU,我们知道计算机里CPU的资源是按时间片的方式来进行分配的,系统里的每一个操作都需要CPU的处理,所以,哪儿个任务要是申请CPU时间片越多,那么它得到的CPU资源就越多,这个是比较容易理解的
CPU资源是以CPU单位度量的,Kubernetes中的而一个CPU等同于:
1:1个AWS CPU
2:1个GCP 核心
3:1个Azure vCore
4:裸机上具有超线程能力的英特尔处理器上的1个超线程
小数值也是可以使用的,一个请求0.5CPU的容器会获得1个CPU的容器的CPU的一半,我们也可以使用后缀为m表示毫,例如:100m,100milliCPU和0.1CPU都相同,需要注意的是精度不能超过1m
CPU请求只能使用绝对数量,而不是相对数量,0.1在单核,双核或者48核的计算机上CPU数值是一样的
Kubernetes集群中的每一个节点都可以通过操作系统的额命令来确认本节点的CPU内核数量,然后将这个数量乘以1000,得到的就是节点CPU的总毫数,比如一个节点有4核,那么该节点的CPU毫量就为4000m,如果你需要使用一半的CPU,则你的要求是 4000 * 0.5 = 2000m。在Pod里面我们可以通过下面两个参数来限制和请求CPU资源
1:spec.containers[].resources.limits.cpu:CPU的上限值,可以短暂超过,容器也不会停止
2:spec.containers[].resources.requests.cpu:CPU的请求值,Kubernetes调度算法里的依据值,可以超过
这里需要明白的是,如果resources.requests.cpu设置的值大于集群里每个节点的最大可用的CPU核心数,那么这个Pod将无法被调度,因为没有节点能够满足它
到这里应该明白,requests是用于集群的调度使用的资源,而limits才是真正的用于资源限制的配置,如果你需要保证你的应用优先级很高,也就是资源吃紧的情况下最后杀掉你的Pod,那么可以将requests和limits的值设置成一致的,这个可能后面会出一个QoS的文章来讲
比如我们定义一个Pod,给容器配置如下的资源
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- name: http
containerPort: 80
resources:
requests:
memory: 50Mi
cpu: 50m
limits:
memory: 100Mi
cpu: 100m
这里CPU我们给的是50m,也就是0.05core,这0.05core也就占了1个CPU里面的5%的资源时间,而限制资源是给的100m,但是需要注意的是CPU资源是可压缩资源,也就是容器达到了这个设定上限后,容器性能会下降,但是不会终止或退出,比如我们创建上面的资源
[root@k-m-1 pod]# kubectl apply -f pod-resources.yaml
pod/nginx created
[root@k-m-1 pod]# kubectl get pod -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx 1/1 Running 0 4m29s 100.114.94.151 k-m-1 <none> <none>
# 查找节点上的Pod
[root@k-m-1 pod]# crictl ps | grep nginx
d7a3838dcb237 eb4a571591807 7 minutes ago Running nginx 0 281a71757c779 nginx
# 我们可以拿着第一个容器ID去查看以下容器的详细信息,但是内容太多了,我们直接看重点
[root@k-m-1 pod]# crictl inspect d7a3838dcb237
[root@k-m-1 pod]# crictl inspect d7a3838dcb237 | grep cgroupsPath
"cgroupsPath": "kubepods-burstable-pod3898f841_5156_4caa_96dd_6ea1069558e4.slice:cri-containerd:d7a3838dcb2370a12a03d378153a1e074b1e9cf9fb996d7a48b01153bfe4851d",
# 我们直接去这个路径去找这个Cgroup的配置,这里我们需要说明两个目录
kubepods-besteffort.slice:当Request和Limit的配置为一致的时候,配置在这里
kubepods-burstable.slice:反之配置不一致的在这里,当然没有配置的在另外的目录
# 这里的目录名称就是Pod的uid和固定的kubepods-burstable-pod拼接而成
[root@k-m-1 pod]# cat /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod3898f841_5156_4caa_96dd_6ea1069558e4.slice/
# 因为我们是针对容器做的,所以我们可以针对容器的ID来找容器的目录然后去查看限制的CPU的大小
[root@k-m-1 cri-containerd-d7a3838dcb2370a12a03d378153a1e074b1e9cf9fb996d7a48b01153bfe4851d.scope]# cat cpu.max
10000 100000
[root@k-m-1 cri-containerd-d7a3838dcb2370a12a03d378153a1e074b1e9cf9fb996d7a48b01153bfe4851d.scope]# cat memory.max
104857600
# 不过这里需要提一下CentOS7和8,9的Cgroup版本不同它的目录存储的文件名也有所不一,这个可以自己去探索
然后我们再来了解一下内存的这块的资源控制,内存的换算单位比较简单:
1MiB = 1024KiB,内存这块在Kubernetes里一般用的是Mi的单位,当然你也可以使用Ki,Gi,甚至Pi,看具体的业务需求和资源容量
# 这里需要注意的是MiB != MB,MB是十进制单位,MiB是二进制单位,平时我们以为的MB等于1024KB,其实1MB=1000KB,1MiB才等于1024KiB,中间带字幕i的是国际电工协会(IEC)定的,走1024乘积;KB,MB,GB是国际单位制,走1000乘积
这里需要注意的是,内存是不可压缩性资源,如果容器使用内存资源达到了上限,那么就会OOM,造成内存溢出问题,容器就会终止和退出,我们也可以通过上面的方式去查看Cgroup文件的值来验证资源限制
超过容器限制的内存
当节点拥有足够的内存时,容器可以使用其请求的内存,但是,容器不允许使用超过其限制的内存,如果容器分配的内存超过其限制,该容器会成为被终止的候选容器,如果容器继续消耗超出其限制的内存,则终止容器,如果终止的容器可以被重启,则kubelet会重启它,就像其他任何类型的运行时失败一样
如下我们创建一个Pod,尝试分配超出其限制的内存,该容器的内存请求为50MiB,内存限制为100MiB
apiVersion: v1
kind: Pod
metadata:
name: mem-limit
spec:
containers:
- name: memory-limit
image: polinux/stress
resources:
requests:
memory: "50Mi"
limits:
memory: "100Mi"
command: ["stress"]
args:
- "--vm"
- "1"
- "--vm-bytes"
- "250M"
- "--vm-hang"
- "1"
在上面清单中的args部分中的配置,表示该容器会尝试使用250Mi的内存,这远高于我们的声明的100Mi的限制,我们直接应用上面的资源清单,来查看一下会出现什么状态、
[root@k-m-1 pod]# kubectl apply -f memory-limit.yaml
pod/mem-limit created
[root@k-m-1 pod]# kubectl get pod
NAME READY STATUS RESTARTS AGE
mem-limit 0/1 OOMKilled 0 13s
[root@k-m-1 pod]# kubectl get pod
NAME READY STATUS RESTARTS AGE
mem-limit 0/1 CrashLoopBackOff 6 (2m10s ago) 8m11s
[root@k-m-1 pod]# kubectl logs -f mem-limit
stress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [1] (415) <-- worker 7 got signal 9
stress: WARN: [1] (417) now reaping child worker processes
stress: FAIL: [1] (421) kill error: No such process
stress: FAIL: [1] (451) failed run completed in 0s
# 这个就是我们想要的结果,当内存超过限制的时候就会OOM,然后根据重启策略再次重启,然后循环往复
超过整个节点容量的内存
内存请求和限制是与容器关联的,Pod的内存请求是Pod中所有容器的内存请求之和,同理,Pod的内存限制是Pod中所有容器的内存限制之和
Pod的调度是基于requests值的,只有当节点拥有足够的满足Pod内存请求的内存时,才会将Pod调度至节点上运行
下面我们创建一个Pod,其内存超过了集群中任意一个节点有用的内存
apiVersion: v1
kind: Pod
metadata:
name: memory
spec:
containers:
- name: memory
image: polinux/stress
resources:
requests:
memory: "1000Gi"
limits:
memory: "1000Gi"
command: ["stress"]
args:
- "--vm"
- "1"
- "--vm-bytes"
- "150M"
- "--vm-hang"
- "1"
[root@k-m-1 pod]# kubectl apply -f memory.yaml
pod/memory created
[root@k-m-1 pod]# kubectl get pod
NAME READY STATUS RESTARTS AGE
memory 0/1 Pending 0 2s
[root@k-m-1 pod]# kubectl describe pod memory
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 7s default-scheduler 0/1 nodes are available: 1 Insufficient memory. preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.
给Pod分配扩展资源
除了我们经常使用的CPU和内存之外,其实我们还可以自己定义扩展资源,要求扩展资源,需要在你的容器清单中包括resources.requests字段,扩展资源可以使用任何完全限定名称,只是不能使用*.kubernetes.io/,比如example.com/foo就是有效的格式,其中example.com可以被替换为你的组织名称,而foo则是指描述性的资源名称
扩展资源类似于内存和CPU资源,一个节点拥有一定数量的内存和CPU资源,它们被节点上运行的所有组件共享,该节点也可以拥有一定数量的foo资源,这些资源同样被节点上运行的所有组件共享,此外我们可以创建请求一定数量的foo资源的Pod
假设一个节点拥有一种特殊类型的磁盘存储,容量为800GiB,那么我们就可以为该特殊存储创建一个名称,如example.com/special-storage,然后你就可以按照一定规格的块(如100GiB)对其进行发布,在这种情况下你的节点会通知其他拥有八个example.com/special-storage类型的资源
Capacity:
...
example.com/special-storage: 8
如果你想要允许针对特殊存储任意(数量)的请求,你可以按照1字节大小的块来发布特殊存储,在这种情况下,你将会发布800Gi数量的example.com/special-storage类型的资源
Capacity:
...
example.com/special-storage: 800Gi
然后容器就能请求任意数量(多达800Gi)字节的特殊存储
扩展资源对Kubernetes是不透明的,Kubernetes不知道扩展资源含义相关的任何信息,Kubernetes只了解一个节点拥有一定数量的扩展资源,扩展资源必须以整形数量进行发布,例如,一个节点可以发布4个dongle资源,但不能发布4.5个
在Pod中分配扩展资源之前,我们还需要将该扩展资源发布到节点上去,我们可以直接发送一个HTTP PATCH请求到Kubernetes API server来完成该操作,假设你的节点上带有4个course资源,下面是一个PATCH请求的示例,为你的节点发布4个course资源
PATCH /api/v1/noeds/<your-node-name>/status HTTP/1.1
Accept: application/json
Content-Type: application/json-patch+json
Host: k-m-1:8080
[
{
"op": "add",
"path": "/status/capacity/kudevops.io~1course",
"value": "4"
}
]
# 新开一个终端在本地映射一个端口
[root@k-m-1 pod]# kubectl proxy --port 8888
Starting to serve on 127.0.0.1:8888
[root@k-m-1 pod]# curl --header "Content-Type: application/json-patch+json" --request PATCH --data '[{"op": "add", "patch": "/status/capacity/kudevops.io~1course", "value": "4"}]' http://localhost:8888/api/v1/nodes/k-m-1/status
# 查看资源
[root@k-m-1 pod]# kubectl describe nodes k-m-1
......
Capacity:
cpu: 4
ephemeral-storage: 17394Mi
hugepages-1Gi: 0
hugepages-2Mi: 0
kudevops.cio/course: 4
memory: 5773204Ki
pods: 110
......
# 使用自定义资源
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- name: http
containerPort: 80
resources:
requests:
kudevops.cio/course: 3
limits:
kudevops.cio/course: 3
[root@k-m-1 pod]# kubectl apply -f extended.yaml
pod/nginx created
[root@k-m-1 pod]# kubectl describe nodes k-m-1
......
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 950m (23%) 0 (0%)
memory 330Mi (5%) 340Mi (6%)
ephemeral-storage 0 (0%) 0 (0%)
hugepages-1Gi 0 (0%) 0 (0%)
hugepages-2Mi 0 (0%) 0 (0%)
kudevops.cio/course 3 3
......
可以看到资源已经分配出去了,这个时候Pod就会正常启动了
[root@k-m-1 pod]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 114s
3:Pod
静态Pod
在Kubernetes集群中除了我们经常用到的普通的Pod外,还有一种特殊的Pod,叫做Static Pod,也就是我们所说的静态Pod,静态Pod有什么特殊的地方呢?
静态Pod直接由节点上的Kubelet进程来管理,不通过Master节点的apiserver,无法与我们常用的控制器Deployment或者StatefulSet进行关联,它由kubelet进程自己来监控,当Pod崩溃时会重启该pod,kubelet也无法对它们进行健康检查,静态pod始终绑定在某一个kubelet上,并且始终运行在同一个节点上,kubelet会自动为每一个静态Pod在kubernetes的apiserver上创建一个镜像Pod,因此我们可以在apiserver中查询到该Pod,但是不能通过apiserver进行控制(比如删除)
创建静态Pod有两种方式:配置文件和HTTP两种方式
1:配置文件就是放在特定目录下的JSON或者YAML格式的Pod定义清单,用kubelet --pod-manifest-path=<the directory>来启动kubelet进程,kubelet定期去扫描这个沐浴露,根据这个目录下出现或消失的YAML/JSON文件来创建或删除静态Pod
比如我们在节点上用静态pod方式来启动一个nginx的服务,配置文件路径为
[root@k-m-1 pod]# cat /var/lib/kubelet/config.yaml | grep staticPodPath
staticPodPath: /etc/kubernetes/manifests
# 从这里我们可以看出,它的静态Pod的资源清单放到/etc/kubernetes/manifests目录下就可以了
[root@k-m-1 manifests]# ls
etcd.yaml kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- name: http
containerPort: 80
[root@k-m-1 manifests]# ls
etcd.yaml kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yaml nginx.yaml
[root@k-m-1 manifests]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-k-m-1 1/1 Running 0 5s
这里可以看到我们并没有使用apply或者create进行部署,但是它已经有这个Pod了,也就意味着,其实这个Pod已经被kubelet拉起了,而且它的特点也是比较奇特的,名称上面
那么我们前面说了,apiserver是无法管理他们的,我们来尝试删除它试一下
[root@k-m-1 manifests]# kubectl delete pod nginx-k-m-1
pod "nginx-k-m-1" deleted
[root@k-m-1 manifests]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-k-m-1 0/1 Pending 0 3s
[root@k-m-1 manifests]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-k-m-1 1/1 Running 0 9s
这里我们要说明一下,这里删除的其实并非是真正的pod,而是删除的kubelet帮我们镜像的一份信息,
[root@k-m-1 ~]# kubectl get pod -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-k-m-1 1/1 Running 1 (23s ago) 19s 100.114.94.161 k-m-1 <none> <none>
[root@k-m-1 ~]# kubectl get pod -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-k-m-1 0/1 Pending 0 3s <none> k-m-1 <none> <none>
[root@k-m-1 ~]# kubectl get pod -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-k-m-1 1/1 Running 1 (34s ago) 4s 100.114.94.161 k-m-1 <none> <none>
# 可以看到信息还是没变化的,我们如果要删除它,那么我们直接将它的YAML/JSON文件移走就可以了
[root@k-m-1 manifests]# mv nginx.yaml ../
[root@k-m-1 manifests]# kubectl get pod
No resources found in default namespace.
2:通过HTTP创建静态Pods
kubelet周期地从-manifest-url=参数地指定地址下载文件,并且把它翻译成JSON/YAML格式地Pod定义,此后的操作方式与-pod-manifest-path=相同,kubelet会不时地重新下载该文件,当文件变化时对应地终止或重启静态Pod
Kubelet启动时,由--pod-manifst-path或--manifest-url=参数指定地目录下定义地所有Pod都会自动创建,例如,我们示例中地nginx
3:静态Pod中的动态增加和删除
运行中的kubelet周期扫描配置的目录(我们目前时/etc/kubernetes/manifests)下文件的变化,当这个目录中有文件出现或者消失时创建或删除Pods
4:Downward API
作为Kubernetes中最核心的资源对象,最基本的调度单元,我们可以发现Pod中属性还是非常繁多的,前面我们使用过一个volumes的属性,表示声明一个数据卷,我们可以通过命令kubectl explain pod.spec.volumes去查看该对象下面支持的属性非常多,其中它有一种模式叫做downward API,这个模式和其他模式都不一样的地方在于它不是为了存放容器的数据也不是用来进行容器和宿主机的数据交换的,而是让Pod里面的容器能够直接获取到这个pod对象本身的一些信息
目前Downward API提供了两种方式用于将Pod的信息注入到容器内部
1:环境变量:用于单个变量,可以将Pod信息和容器信息直接注入到容器内部
2:Volume挂载:将Pod信息生成为文件,直接挂载到容器内部中去
环境变量
我们通过Downward API来将Pod的IP,名称注入到容器的环境变量中去,然后到容器中打印全部变量来查看是否成功
apiVersion: v1
kind: Pod
metadata:
name: env-pod
namespace: default
spec:
containers:
- name: env-pod
image: busybox:latest
command: ["/bin/sh", "-c", "env"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
# 我们跑一下这个资源的Pod
[root@k-m-1 ~]# kubectl apply -f env-pod.yaml
pod/env-pod created
[root@k-m-1 ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
env-pod 0/1 Completed 0 11s
[root@k-m-1 ~]# kubectl logs -f env-pod
POD_IP=100.114.94.163
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_SERVICE_PORT=443
HOSTNAME=env-pod
SHLVL=1
HOME=/root
POD_NAME=default
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_SERVICE_HOST=10.96.0.1
PWD=/
我们可以看到Pod的IP,NAME都通过环境变量打印了出来,它其实可以默认注入很多变量到容器内
Volume挂载
Downward API除了提供环境变量的方式外,还提供了通过Volume挂载的方式去获取Pod的基本信息,接下来我们通过Downward API将Pod的Label,Annotation等信息通过Volume挂载到容器的某个文件中去,然后在容器内打印出来
apiVersion: v1
kind: Pod
metadata:
name: volume-pod
namespace: default
labels:
app: volume-pod
annotations:
own: gitlayzer
spec:
volumes:
- name: podinfo
downwardAPI:
items:
- path: labels
fieldRef:
fieldPath: metadata.labels
- path: annotations
fieldRef:
fieldPath: metadata.annotations
containers:
- name: volume-pod
image: busybox:latest
args:
- sleep
- "3600"
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
[root@k-m-1 pod]# kubectl apply -f volume-pod.yaml
pod/volume-pod created
[root@k-m-1 pod]# kubectl exec -it volume-pod /bin/ls /etc/podinfo
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
annotations labels
[root@k-m-1 pod]# kubectl exec -it volume-pod /bin/cat /etc/podinfo/labels
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
app="volume-pod"
[root@k-m-1 pod]# kubectl exec -it volume-pod /bin/cat /etc/podinfo/annotations
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
cni.projectcalico.org/containerID="12606ff02b66ef449e926831575611d9d42f5a5e4756a9dcca7552f2de1c69e6"
cni.projectcalico.org/podIP="100.114.94.155/32"
cni.projectcalico.org/podIPs="100.114.94.155/32"
kubectl.kubernetes.io/last-applied-configuration="{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{\"own\":\"gitlayzer\"},\"labels\":{\"app\":\"volume-pod\"},\"name\":\"volume-pod\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"args\":[\"sleep\",\"3600\"],\"image\":\"busybox:latest\",\"name\":\"volume-pod\",\"volumeMounts\":[{\"mountPath\":\"/etc/podinfo\",\"name\":\"podinfo\"}]}],\"volumes\":[{\"downwardAPI\":{\"items\":[{\"fieldRef\":{\"fieldPath\":\"metadata.labels\"},\"path\":\"labels\"},{\"fieldRef\":{\"fieldPath\":\"metadata.annotations\"},\"path\":\"annotations\"}]},\"name\":\"podinfo\"}]}}\n"
kubernetes.io/config.seen="2023-06-29T06:36:30.512869897+08:00"
kubernetes.io/config.source="api"
可以看到这个时候我们指定的两个字段已经被存储到Pod中的podinfo下去了,这里我们需要知道,它会基于mountPath创建一个目录,如果有则不创建,否则创建一个目录然后根据path名称创建文件,然后将内容写入到文件中去,我们需要知道的是Downawrd API能够获取到的信息一定是Pod里的容器进程启动之前能够确定下来的信息,而如果你想要获取到Pod容器运行后才会出现的信息,比如容器进程的PID,那就不能使用Downward API了,而应该考虑在Pod里面定义一个sidecar容器来获取了
在实际应用中,如果你的应用有获取Pod的基本信息的需求,一般我们就可以利用Downward API来获取基本信息,然后编写一个启动脚本或者利用initContainer将Pod的信息注入到容器中去,然后在我们自己的应用中就可以正常地处理相关地逻辑了