image.png
上图展示了一个Pod的完整的生命周期,其中包括Init ContainerPod Hook健康检查三个主要部分。
在介绍Pod生命周期前,先了解下Pod的状态,因为Pod状态可以反映出当前Pod的具体状态信息,也是分析排错的一个必备的方式。

Pod状态

首先先了解下Pod的状态值,可以通过kubectl explain pod.status命令来了解关于Pod状态的一些信息,Pod的状态定义在PodStatus对象中,其中有一个phase字段,下面是phase的可能取值:

  • 挂起(Pending):Pod信息已经提交给了集群,但还没有被调度器调度到合适的节点或Pod里的镜像正在下载
  • 运行中(Running):该容器已经绑定到了一个节点上,Pod中所有的容器容器都已被创建。至少有一个容器正在运行,或正处于启动或者重启状态
  • 成功(Succeeded):Pod中的所有容器都被成功终止,并且不会再重启
  • 失败(Failed):Pod中所有容器都已终止了,并且至少有一个容器是因为失败终止。也就是说,容器以非0状态退出或者被系统终止
  • 未知(Unknown):因为某些原因无法取得Pod的状态,通常是因为与Pod所在主机通信失败导致的

除此之外,podStatus对象还包含一个PodCondition的数组,里面包含的属性有:

  • lastProbeTime:最后一次探测Pod Condition的时间戳
  • lastTransitionTime:上次Condition从一种状态转换到另一种状态的时间。
  • message:上次Condition状态转换的详细描述。
  • reason:Condition最后一次转换的原因。
  • status:Condition状态类型,可以为”True”、”False”、”Unknown”。
  • type:Condition类型,包括如下:
    • PodSchedule(Pod已经被调度到其他的node里)
    • Ready(Pod能够提供服务请求,可以被添加到所有可匹配服务的负载均衡池中)
    • Initialized(所有的init containers已经启动成功)
    • Unschedulabe(调度程序现在无法调度Pod,例如由于缺乏资源或其他限制)
    • ContainersReady(Pod里的所有容器都是ready状态)

重启策略

可以通过配置restartPolicy字段来设置Pod中所有容器的重启策略,其值可能为AlwaysOnFailureNever,默认值为Always。restartPolicy仅指通过kubelet在同一节点上重新启动容器。通过kubelet重新启动的容器将以指数增加延迟(10s,20s,40s)重启启动,上限为5min,并在成功执行10min后重置。不同类型的控制器可以控制Pod的重启策略:

  • Job:适用于一次性任务如批量计算,任务结束后Pod会被此类控制器清楚。Job的重启策略只能是”OnFailure”或者”Never”。
  • Replication Controller,ReplicaSet,Deployment,此类控制器希望Pod一直运行下去,他们的重启策略只能是”Always”。
  • DaemonSet:每个节点上启动一个Pod,此类控制器的重启策略也应该是”Always”。

初始化容器

首先来了解下Pod中最先启动的Init Container,也就是常说的初始化容器。Init Container就是用来做初始化工作的容器,可以是一个或者多个,如果有多个的话,这些容器会按定义的顺序依次执行。一个Pod里面的所有容器是共享数据和Netware Namespace的,所以Init Container里面产生的数据可以被主容器使用到。从上面的Pod生命周期的图中可以看出初始化容器是独立于主容器之外的,只有所有的初始化容器执行完成之后,主容器才会被启动。初始化容器有的应用场景:

  • 等待其他模块Ready:这个可以解决服务之间的依赖问题,比如一个web服务,该服务依赖另外一个数据库服务,但是在启动这个web服务的时候并不能保证依赖的这个数据库服务已经正常启动,所以可能会出现一段时间内web服务连接数据库异常。要解决这个问题的话就可以在web服务的Pod中使用一个Init Container,在这个初始化容器中去检查数据库是否已经准备好了,准备好了过后初始化容器就结束退出,然后我们主容器的web服务才会被启动起来。
  • 做初始化配置:比如集群里检测所有已经存在的成员节点,为主容器准备好集群的配置信息,这样主容器起来后就能用这个配置信息加入集群。
  • 其他场景:如将Pod注册到一个中央数据库、配置中心等。

比如实现在 nginx pod启动之前去重新初始化首页内容,如下资源清单(init-pod.yaml):

  1. apiVersion: v1
  2. kind: Pod
  3. metadata:
  4. name: init-demo
  5. spec:
  6. volumes:
  7. - name: workdir
  8. emptyDir: {}
  9. initContainers:
  10. - name: install
  11. image: busybox
  12. command:
  13. - wget
  14. - "-O"
  15. - "/work-dir/index.html"
  16. - http://www.baidu.com
  17. volumeMounts:
  18. - name: workdir
  19. mountPath: "/work-dir"
  20. containers:
  21. - name: web
  22. image: nginx
  23. ports:
  24. - containerPort: 80
  25. volumeMounts:
  26. - name: work-dir
  27. mountPath: "/usr/share/nginx/html"

上面的资源清单首先在Pod顶层声明了一个名为workdir的volume,前面用了hostPath模式,这里使用了emptyDir{},这是一个临时的目录,数据会保存在kubelet的工作目录下面,生命周期等同于Pod的生命周期。

然后定义一个初始化容器,该容器下会下载一个html文件到/work-dir目录下,但是由于将该目录声明挂载到全局的Volume,同样的主容器的nginx也将目录/usr/share/nginx/html声明挂载到了全局的Volume,所在主容器的该目录下面也会同步初始化容器中创建的index.html文件。

直接创建上面的Pod:

$ kubectl apply -f init-pod.yaml

创建完成后可以查看该Pod的状态:

$ kubectl get pods
NAME                            READY   STATUS     RESTARTS   AGE
init-demo                       0/1     Init:0/1   0          4s

可以发现Pod现在的状态是Init:0/1,意思就是现在第一个初始化容器还在执行过程中,此时可以看到Pod的详细信息:

$ kubectl describe pod init-demo
Name:         init-demo
Namespace:    default
Priority:     0
Node:         node2/10.151.30.23
Start Time:   Thu, 22 Oct 2020 21:17:32 +0800
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"init-demo","namespace":"default"},"spec":{"containers":[{"image":"ngi...
Status:       Running
IP:           10.244.2.9
IPs:
  IP:  10.244.2.9
Init Containers:
  install:
    Container ID:  docker://0d53780635c2cbf7865648dcb9a4c981e23c2ce95edcaa3f474029a9a1db972a
    Image:         busybox
    Image ID:      docker-pullable://busybox@sha256:a9286defaba7b3a519d585ba0e37d0b2cbee74ebfe590960b0b1d6a5e97d1e1d
    Port:          <none>
    Host Port:     <none>
    Command:
      wget
      -O
      /work-dir/index.html
      http://www.baidu.com
    State:          Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Thu, 22 Oct 2020 21:17:47 +0800
      Finished:     Thu, 22 Oct 2020 21:17:47 +0800
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-hpd7s (ro)
      /work-dir from workdir (rw)
Containers:
  web:
    Container ID:   docker://c31aed9916ed5277d0b9be96787d336186f8d312b20d59c1fcd59548f3942312
    Image:          nginx
    Image ID:       docker-pullable://nginx@sha256:ed7f815851b5299f616220a63edac69a4cc200e7f536a56e421988da82e44ed8
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Thu, 22 Oct 2020 21:18:02 +0800
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /usr/share/nginx/html from workdir (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-hpd7s (ro)
Conditions:
  Type              Status
  Initialized       True 
  Ready             True 
  ContainersReady   True 
  PodScheduled      True 
Volumes:
  workdir:
    Type:       EmptyDir (a temporary directory that shares a pod's lifetime)
    Medium:     
    SizeLimit:  <unset>
  default-token-hpd7s:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-hpd7s
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  27m   default-scheduler  Successfully assigned default/init-demo to node2
  Normal  Pulling    27m   kubelet, node2     Pulling image "busybox"
  Normal  Pulled     27m   kubelet, node2     Successfully pulled image "busybox" in 12.597844015s
  Normal  Created    27m   kubelet, node2     Created container install
  Normal  Started    27m   kubelet, node2     Started container install
  Normal  Pulling    27m   kubelet, node2     Pulling image "nginx"
  Normal  Pulled     27m   kubelet, node2     Successfully pulled image "nginx" in 13.743250204s
  Normal  Created    27m   kubelet, node2     Created container web
  Normal  Started    27m   kubelet, node2     Started container web

从上面的描述信息可以看到初始化容器已经启动了,现在处于Running状态,所以还需要稍等,到初始化容器执行完成退出初始化容器会变成Completed状态,然后才会启动主容器,待到主容器也启动完成后,Pod的状态就会变成Running。

$ kubectl get pods -o wide
NAME                     READY   STATUS    RESTARTS   AGE    IP            NODE    NOMINATED NODE   READINESS GATES
init-demo                1/1     Running   0          28m    10.244.2.9    node2   <none>           <none>
$ curl 10.244.2.9
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=http://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>

Pod Hook

Pod是k8s集群中的最小单元,而Pod是由容器组成的,所以在讨论Pod的生命周期的时候可以先讨论下容器的生命周期。实际上k8s为容器提供了生命周期的钩子,就是Pod Hook,Pod Hook是由kubelet发起的,当容器中的进程启动前或者容器的进程终止之前运行,这是包括在容器的生命周期中。可以同时为Pod中所有容器配置hook。

k8s提供了两种钩子函数:

  • PostStart:这个钩子在容器创建后立即执行。但是,并不能保证钩子将在ENTRYPOINT之前运行,因为没有参数传递给处理程序。主要用于资源部署、环境准备等。不过需要注意的是如果钩子花费太长时间以至于不能运行或挂起,容器将不能达到running状态。
  • PreStop:这个钩子在容器终止之前立即被调用。她是阻塞的,意味着它是同步的,所以它必须在删除容器的调用发出之前完成。主要用于优雅关闭应用程序、通知其他系统等。如果钩子在执行期间挂起,Pod阶段将停留在running状态并且永不会达到fialed状态。

如果PostStart和PreStop钩子失败,它会杀死容器。所以应该让钩子函数尽可能的轻量。当然有些情况下,长时间运行命令是合理的,比如在停止容器之前预先保存状态。

另外有两种方式实现钩子函数:

  • Exec:用于执行一段特定的命令,但是该命令消耗的资源会被计入容器。
  • HTTP:对容器上的特定端口执行HTTP请求。

以下示例中,定义了一个Nginx Pod,其中设置了PreStop钩子函数,即在容器退出前,优雅的关闭Nginx(pod-prestop.yaml):

apiVersion: v1
kind: Pod
metadata:
  name: hook-demo2
spec:
  containers:
  - name: hook-demo2
    image: nginx
    lifecycle:
      preStop:
        exec:
          command: ["/usr/sbin/nginx","-s","quit"]

---
apiVersion: v1
kind: Pod
metadata:
  name: hook-demo3
spec:
  volumes:
  - name: message
    hostPath:
      path: /tmp
  containers:
  - name: hook-demo3
    image: nginx
    ports:
    - containerPort: 80
    volumeMounts:
    - name: message
      mountpath: /usr/share/
    lifecycle:
      preStop:
        exec:
          command: ['/bin/sh','-c','echo Hello from the preStop Handler > /usr/share/message']

上面定义的两个Pod,一个是利用preStop来进行优雅删除,另外一个是利用preStop来做一些记录的事情,直接创建Pod:

$ kubectl apply -f pod-prestop.yaml
$ kubectl get pods
NAME                            READY   STATUS    RESTARTS   AGE
hook-demo2                      1/1     Running   0          67s
hook-demo3                      1/1     Running   0          67s

创建完成后,我们可以直接删除hook-demo2这个Pod,在容器删除之前会执行preStop里面的优雅关闭命令,这个用法在后面的滚动更新的时候用来保证应用零宕机非常有用。第二个Pod我们声明了一个hotsPath类型的Volume,在容器里面声明挂载到了这个volume,所以当我们删除pod,退出容器之前,在容器里面输出的信息也会同样保存在宿主机(一定要是Pod被调度到的目标节点)的/tmp目录下面。

Hook调用的日志没有暴露给Pod,所以只能通过describe命令来获取,如果有错误将可以看到FailedPostStartHookFailedPreStopHook这样的event。

Pod健康检查

现在在Pod的整个生命周期中,能影响到Pod的就只剩健康检查这一部分了。在k8s集群当中,可以通过配置liveness probe(存活探针)readiness probe(可读性探针)来影响容器的生命周期:

  • kubelet通过使用liveness probe来确定应用程序是否正在运行,就是是否存活。一般来说,如果程序一旦崩溃了,k8s就会立刻知道这个程序已经终止了,然后就会重启这个程序。而liveness probe的目的就是来捕获当前应用程序还没有终止,还没有崩溃,如果出现了这些情况,那么就重启处于该状态下的容器,使应用程序在存在bug的情况下依然能够继续运行下去。
  • kubelet通过使用readiness probe来确定容器是否已经就绪可以接收流量了,就是是否准备好。只有当Pod中的容器都处于就绪状态的时候kubelet才会认定该Pod处于就绪状态,因为一个Pod下面可能会有多个容器。当然Pod如果处于非就绪状态,那么就会将他从Service的Endpoints列表中移出,这样流量就不会被路由到这个Pod中。

和前面的钩子函数一样,探针支持下面几种配置方式:

  • exec:执行一段命令
  • http:检测某个http请求
  • tcpSocket:使用此配置,kubelet将尝试在指定端口上打开容器的套接字。如果可以建立连接,容器被认为是健康的,如果不能就认为是失败的。实际上就是检查端口。

下面是使用存活探针的exec方式(liveness-exec.yaml):

apiVersion: v1
kind: Pod
metadata:
  name: liveness-exec
spec:
  containers:
  - name: liveness
    image: busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
    livenessProbe:
      exec:
        command:
          - cat
          - /tmp/healthy
      initialDelaySeconds: 5
      periodSeconds: 5

这里需要用到一个新的属性:livenessProbe,下面通过exec执行一段命令:

  • periodSeconds:表示让kubelet每隔5秒执行一次探针,也就是每五秒执行一次上面的cat /tmp/healthy命令,如果执行成功了,将返回0,那么kubelet就会认为当前这个容器是存活的,如果返回非0,那么kubelet就会把该容器杀掉然后重启它。默认是10秒,最小1秒。
  • initialDelaySeconds:表示在第一次执行探针的时候要等待5秒,这样能够确保我们的容器能够有足够的时间启动起来。

启动容器的时候,执行了如下命令:

$ /bin/sh -c "touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600"

意思是说在容器最开始的30秒内创建了一个/tmp/healthy文件,在这30秒内执行cat /tmp/healthy命令都会返回一个成功的返回码。30 秒后,删除这个文件后,现在执行cat /tmp/healthy就会失败了(默认检测失败3次才认为失败),所以这个时候就会重启容器了。

创建该Pod,30秒内查看Pod的Event:

$ kubectl apply -f liveness-exec.yaml

$ kubectl describe pod liveness-exec
......
  Normal  Started    20s        kubelet, node2  Started container liveness
......
 Normal   Started    46s               kubelet, node2  Started container liveness
  Warning  Unhealthy  3s (x3 over 13s)  kubelet, node2  Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
  Normal   Killing    3s                kubelet, node2  Container liveness failed liveness probe, will be restarted

可以观察到容器是正常启动的,在隔一会儿,比如 40s 后,再查看下 Pod 的 Event,在最下面有一条信息显示 liveness probe 失败了,容器将要重启。然后可以查看到 Pod 的 RESTARTS 值加 1 了:

$ kubectl get pods
NAME            READY   STATUS    RESTARTS   AGE
liveness-exec   1/1     Running   1          1m

使用HTTP GET请求来配置存活探针,使用一个 liveness 镜像来验证演示下:(liveness-http.yaml)

apiVersion: v1
kind: Pod
metadata:
  name: liveness-http
spec:
  containers:
  - name: liveness
    image: cnych/liveness
    args:
    - /server
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
        httpHeaders:
        - name: X-Custom-Header
          value: Awesome
      initialDelaySeconds: 3
      periodSeconds: 3

根据 periodSeconds 属性我们可以知道 kubelet 需要每隔3秒执行一次 liveness Probe,该探针将向容器中的 server 的 8080 端口发送一个 HTTP GET 请求。如果 server 的 /healthz 路径的 handler 返回一个成功的返回码,kubelet 就会认定该容器是活着的并且很健康,如果返回失败的返回码,kubelet 将杀掉该容器并重启它。initialDelaySeconds 指定kubelet 在该执行第一次探测之前需要等待3秒钟。

通常来说,任何大于200小于400的状态码都会认定是成功的返回码。其他返回码都会被认为是失败的返回码。

同样的,创建下该 Pod 测试下效果,10 秒后,查看 Pod 的 event,确认 liveness probe 失败并重启了容器:

$ kubectl apply -f liveness-http.yaml

$ kubectl describe pod liveness-http
......
 Normal   Pulled     12s (x3 over 76s)  kubelet, node1  Successfully pulled image "cnych/liveness"
  Normal   Created    12s (x3 over 76s)  kubelet, node1  Created container liveness
  Normal   Started    11s (x3 over 76s)  kubelet, node1  Started container liveness
......
 Normal   Pulling    15s (x4 over 102s)  kubelet, node1  Pulling image "cnych/liveness"
  Warning  Unhealthy  15s (x9 over 87s)   kubelet, node1  Liveness probe failed: HTTP probe failed with statuscode: 500
  Normal   Killing    15s (x3 over 81s)   kubelet, node1  Container liveness failed liveness probe, will be restarted
......
$ kubectl get pods
NAME            READY   STATUS             RESTARTS   AGE
liveness-http   1/1     Running            3          2m2s

另外前面提到了探针里面有一个initialDelaySeconds的属性,可以来配置第一次执行探针的等待时间,对于启动非常慢的应用这个参数非常有用,比如 Jenkins、Gitlab 这类应用,但是如何设置一个合适的初始延迟时间呢?这个就和应用具体的环境有关系了,所以这个值往往不是通用的,这样的话可能就会导致一个问题,我们的资源清单在别的环境下可能就会健康检查失败了,为解决这个问题,在 Kubernetes v1.16 版本官方特地新增了一个 startupProbe(启动探针),该探针将推迟所有其他探针,直到 Pod 完成启动为止,使用方法和存活探针一样:

startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30  # 尽量设置大点
  periodSeconds: 10

比如上面这里的配置表示慢速容器最多可以有5分钟(30个检查 * 10秒= 300s)来完成启动。

有时候应用程序可能暂时无法对外提供服务,例如,应用程序可能需要在启动期间加载大量数据或配置文件。在这种情况下,你不想杀死应用程序,也不想对外提供服务。那这个时候我们可以使用readiness probe来检测和减轻这些情况。Pod中的容器可以报告自己还没有准备好,不能处理集群服务发过来的流量。readiness probe的配置跟liveness probe基本上一致的。唯一的不同是使用readinessProbe而不是livenessProbe。两者同时使用的话就可以确保流量不会到达还为准备好的容器,准备好过后,如果应用程序出现了问题,则会重新启动容器。

另外除了periodSecondsinitialDelaySeconds属性外,探针还可以配置如下几个参数:

  • timeoutSeconds:探测超时时间,默认1秒,最小1秒。
  • successThreshold:探测失败后,最少连续探测成功多少次才认定为成功。默认是1,但是如果是liveness则必须是1。最小值是1。
  • failureThreshold:探测成功后,最少连续探测失败多少次才认定为失败。默认是3,最小值是1。

Pod资源配置

实际上上面几个步骤就是影响一个 Pod 生命周期的大的部分,但是还有一些细节也会在 Pod 的启动过程进行设置,比如在容器启动之前还会为当前的容器设置分配的 CPU、内存等资源,可以通过 CGroup 来对容器的资源进行限制,同样的,在 Pod 中也可以直接配置某个容器的使用的 CPU 或者内存的上限。

首先对于 CPU,我们知道计算机里 CPU 的资源是按“时间片”的方式来进行分配的,系统里的每一个操作都需要 CPU 的处理,所以,哪个任务要是申请的 CPU 时间片越多,那么它得到的 CPU 资源就越多,这个很容易理解。
然后还需要了解下 CGroup 里面对于 CPU 资源的单位换算:

1 CPU =  1000 millicpu(1 Core = 1000m)

0.5 CPU = 500 millicpu (0.5 Core = 500m)

这里的 m 就是毫、毫核的意思,Kubernetes 集群中的每一个节点可以通过操作系统的命令来确认本节点的 CPU 内核数量,然后将这个数量乘以1000,得到的就是节点总 CPU 总毫数。比如一个节点有四核,那么该节点的 CPU 总毫量为 4000m,如果你要使用0.5 core,则你要求的是 4000*0.5 = 2000m。在 Pod 里面我们可以通过下面的两个参数来限制和请求 CPU 资源:

  • spec.containers[].resources.limits.cpu:CPU 上限值,可以短暂超过,容器也不会被停止。
  • spec.containers[].resources.requests.cpu:CPU请求值,Kubernetes 调度算法里的依据值,可以超过。

这里需要明白的是,如果resources.requests.cpu设置的值大于集群里每个节点的最大 CPU 核心数,那么这个 Pod 将无法调度,因为没有节点能满足它。

到这里应该明白了,requests 是用于集群调度使用的资源,而 limits 才是真正的用于资源限制的配置,如果需要保证应用优先级很高,也就是资源吃紧的情况下最后再杀掉Pod,那么就把 requests 和 limits 的值设置成一致。

比如,定义一个 Pod,给容器的配置如下的资源:(pod-resource-demo1.yaml):

apiVersion: v1
kind: Pod
metadata:
  name: resource-demo1
spec:
  containers:
  - name: resource-demo1
    image: nginx
    ports:
    - containerPort: 80
    resources:
      requests:
        memory: 50Mi
        cpu: 50m
      limits:
        memory: 100Mi
        cpu: 100m

这里,CPU 给的是 50m,也就是 0.05core,这 0.05 core 也就是占了 1 CPU 里的 5% 的资源时间。而限制资源是给的是 100m,但是需要注意的是 CPU 资源是可压缩资源,也就是容器达到了这个设定的上限后,容器性能会下降,但是不会终止或退出。直接创建上面这个 Pod:

$ kubectl apply -f pod-resource-demo1.yaml

创建完成后,可以看到 Pod 被调度到 node2 这个节点上:

$ kubectl get pods -o wide
NAME             READY   STATUS    RESTARTS   AGE   IP             NODE         NOMINATED NODE   READINESS GATES
resource-demo1   1/1     Running   0          12m   10.244.3.58    node2         <none>           <none>

然后我们到 node2 节点上去查看 Pod 里面启动的 resource-demo1 这个容器:

$ docker ps |grep resource-demo1
c9e654d573fd        nginx                                      "nginx -g 'daemon of…"   14 minutes ago      Up 14 minutes                           k8s_resource-demo1_resource-demo1_default_020bc461-170a-4936-9c22-7c8516972d39_0
37ab54a2490a        gcr.azk8s.cn/google_containers/pause:3.1   "/pause"                 14 minutes ago      Up 14 minutes                           k8s_POD_resource-demo1_default_020bc461-170a-4936-9c22-7c8516972d39_0

其中第一个容器就是主容器,第二容器是 Infra 容器,可以去查看下主容器的信息:

$ docker inspect c9e654d573fd
......
"CpuShares": 51,
"Memory": 104857600,
"NanoCpus": 0,
"CgroupParent": "kubepods-burstable-pod020bc461_170a_4936_9c22_7c8516972d39.slice",
"BlkioWeight": 0,
"BlkioWeightDevice": null,
"BlkioDeviceReadBps": null,
"BlkioDeviceWriteBps": null,
"BlkioDeviceReadIOps": null,
"BlkioDeviceWriteIOps": null,
"CpuPeriod": 100000,
"CpuQuota": 10000,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": [],
"DeviceCgroupRules": null,
"DiskQuota": 0,
"KernelMemory": 0,
"MemoryReservation": 0,
"MemorySwap": 104857600,
"MemorySwappiness": null,
"OomKillDisable": false,
......

可以看到这个容器的一些资源情况,Pod 上的资源配置最终也还是通过底层的容器运行时去控制 CGroup 来实现的,进入如下目录查看 CGroup 的配置,该目录就是 CGroup 父级目录,而 CGroup 是通过文件系统来进行资源限制的,所以上面限制容器的资源就可以在该目录下面反映出来:

$ cd /sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod020bc461_170a_4936_9c22_7c8516972d39.slice
$ ls
cgroup.clone_children  cpu.cfs_period_us  docker-37ab54a2490a48c88918f53572e053784238bf3720cb1ae5bf01052040aba9a4.scope
cgroup.event_control   cpu.cfs_quota_us   docker-c9e654d573fd3e3e9a8bb78f3f904e60a59aa3384b344117fd401529e982ae37.scope
cgroup.procs           cpu.rt_period_us   notify_on_release
cpuacct.stat           cpu.rt_runtime_us  tasks
cpuacct.usage          cpu.shares
cpuacct.usage_percpu   cpu.stat
$ cat cpu.cfs_quota_us
10000

其中 cpu.cfs_quota_us 就是 CPU 的限制值,如果要查看具体的容器的资源,我们也可以进入到容器目录下面去查看即可。

最后我们了解下内存这块的资源控制,内存的单位换算比较简单:
1 MiB = 1024 KiB,内存这块在 Kubernetes 里一般用的是Mi单位,当然你也可以使用Ki、Gi甚至Pi,看具体的业务需求和资源容量。

这里注意的是MiB ≠ MB,MB 是十进制单位,MiB 是二进制,平时我们以为 MB 等于 1024KB,其实1MB=1000KB,1MiB才等于1024KiB。中间带字母 i 的是国际电工协会(IEC)定的,走1024乘积;KB、MB、GB是国际单位制,走1000乘积。

这里要注意的是,内存是不可压缩性资源,如果容器使用内存资源到达了上限,那么会OOM,造成内存溢出,容器就会终止和退出。我们也可以通过上面的方式去通过查看 CGroup 文件的值来验证资源限制。