Pod资源限制

  1. # pod_resource_limit.yaml
  2. apiVersion: v1
  3. kind: Pod
  4. metadata:
  5. labels:
  6. app: sb_resource_limit
  7. name: sb_resource_limit
  8. namespace: dev
  9. spec:
  10. containers:
  11. - image: sb_ep:v1
  12. name: sb_ep
  13. # Always Never IfNotPresent可选
  14. imagePullPolicy: Never
  15. resources:
  16. request:
  17. memory: "256Mi"
  18. cpu: "250m"
  19. limit:
  20. memory: "512Mi"
  21. cpu: "500m"

在 Kubernetes 中,像 CPU 这样的资源被称作“可压缩资源”(compressible resources)。它的典型特点是,当可压缩资源不足时,Pod 只会“饥饿”,但不会退出。而像内存这样的资源,则被称作“不可压缩资源(incompressible resources)。当不可压缩资源不足时,Pod 就会因为 OOM(Out-Of-Memory)被内核杀掉。
由于 Pod 可以由多个 Container 组成,所以 CPU 和内存资源的限额,是要配置在每个 Container 的定义上的。这样,Pod 整体的资源配置,就由这些Container 的配置值累加得到。
Kubernetes 里为 CPU 设置的单位是“CPU 的个数”。比如,cpu=1 指的就是, 这个 Pod 的 CPU 限额是 1 个 CPU。当然,具体“1 个 CPU”在宿主机上如何解释,是 1 个 CPU 核心,还是 1 个 vCPU,还是 1 个 CPU 的超线程(Hyperthread),完全取决于宿主机的 CPU 实现方式。Kubernetes 只负责保证 Pod 能够使用到“1 个 CPU”的计算能力。 Kubernetes 允许你将 CPU 限额设置为分数,比如在我们的例子里,CPU limits 的值就是 500m。所谓 500m,指的就是 500 millicpu,也就是 0.5 个 CPU 的意思。这样, 这个 Pod 就会被分配到 1 个 CPU 一半的计算能力。 当然,你也可以直接把这个配置写成 cpu=0.5。但在实际使用时,我还是推荐你使用 500m 的写法,毕竟这才是 Kubernetes 内部通用的 CPU 表示方式。 而对于内存资源来说,它的单位自然就是 bytes。Kubernetes 支持你使用 Ei、Pi、Ti、 Gi、Mi、Ki(或者 E、P、T、G、M、K)的方式来作为 bytes 的值。比如,在我们的例子 里,Memory requests 的值就是 256Mi (2 的 26 次方 bytes) 。这里要注意区分 MiB(mebibyte)和 MB(megabyte)的区别。

备注:1Mi=10241024;1M=10001000

此外,不难看到,Kubernetes 里 Pod 的 CPU 和内存资源,实际上还要分为 limits 和 requests 两种情况,如下所示:

spec.containers[].resources.limits.cpu
spec.containers[].resources.limits.memory
spec.containers[].resources.requests.cpu
spec.containers[].resources.requests.memory

这两者的区别其实非常简单:在调度的时候,kube-scheduler 只会按照 requests 的值进行计算,进而确定分配到哪些节点。而在真正设置 Cgroups 限制的时候,kubelet 则会按照 limits 的值来进行设置。这样一来,进程在宿主机的操作系统上就被限制死了,不可能获得超过limit的资源。

QoS

  • Guaranteed:当 Pod 里的每一个 Container 都同时设置了 requests 和 limits,并且 requests 和 limits 值相等的时候,这个 Pod 就属于 Guaranteed 类别,当这个 Pod 创建之后,它的 qosClass 字段就会被 Kubernetes 自动设置为 Guaranteed。需要注意的是,当 Pod 仅设置了 limits 没有设置 requests 的时候, Kubernetes 会自动为它设置与 limits 相同的 requests 值,所以,这也属于 Guaranteed 情况。

    kubectl describe pod ... 查看 qosClass 字段
    
  • Burstable:而当 Pod 不满足 Guaranteed 的条件,但至少有一个 Container 设置了 requests。那 么这个 Pod 就会被划分到 Burstable 类别。

  • BestEffort :而如果一个 Pod 既没有设置 requests,也没有设置 limits,那么它的 QoS 类别就是 BestEffort。

QoS 划分的主要应用场景,是当宿主机资源紧张的时候,kubelet 对 Pod 进行 Eviction(即资源回收)时需要用到的。 具体地说,当 Kubernetes 所管理的宿主机上不可压缩资源短缺时,就有可能触发 Eviction。比如,可用内存(memory.available)、可用的宿主机磁盘空间 (nodefs.available),以及容器运行时镜像存储空间(imagefs.available)等等。 目前,Kubernetes 为你设置的 Eviction 的默认阈值如下所示:

memory.available<100Mi  # 内存
imagefs.available<15%   # docker安装目录所在的分区
imagefs.inodesFree      # docker安装目录所在的分区
nodefs.inodesFree<5%    # kubelet的启动参数--root-dir所指定的目录(默认/var/lib/kubelet)所在的分区
nodefs.available<10%    # kubelet的启动参数--root-dir所指定的目录(默认/var/lib/kubelet)所在的分区
kubelet --eviction-hard=imagefs.available<10%,memory.available<500Mi,nodefs.available<5%,nodefs.inodesFree<5%  \ 
--eviction-soft=imagefs.available<30%,nodefs.available<10% --eviction-soft-grace-period=imagefs.available=2m,nodefs.available=2m --eviction-max-pod-grace-period=600

Eviction 在 Kubernetes 里其实分为 Soft 和 Hard 两种模 式。 其中,Soft Eviction 允许你为 Eviction 过程设置一段“优雅时间”,比如上面例子里的 imagefs.available=2m,就意味着当 imagefs 不足的阈值达到 2 分钟之后,kubelet 才会 开始 Eviction 的过程。 而 Hard Eviction 模式下,Eviction 过程就会在阈值达到之后立刻开始。

Kubernetes 计算 Eviction 阈值的数据来源,主要依赖于从 Cgroups 读取到 的值,以及使用 cAdvisor(Google开源的一款用于展示和分析容器运行状态的可视化工具。通过在主机上运行CAdvisor用户可以轻松的获取到当前主机上容器的运行统计信息,并以图表的形式向用户展示) 监控到的数据。

而当 Eviction 发生的时候,kubelet 具体会挑选哪些 Pod 进行删除操作,就需要参考这些 Pod 的 QoS 类别了:

  • 首当其冲的,自然是 BestEffort 类别的 Pod。
  • 其次,是属于 Burstable 类别、并且发生“饥饿”的资源使用量已经超出了 requests 的 Pod。
  • 最后,才是 Guaranteed 类别。并且,Kubernetes 会保证只有当 Guaranteed 类别的 Pod 的资源使用量超过了其 limits 的限制,或者宿主机本身正处于 Memory Pressure 状态时,Guaranteed 的 Pod 才可能被选中进行 Eviction 操作。

    当然,对于同 QoS 类别的 Pod 来说,Kubernetes 还会根据 Pod 的优先级来进行进一步 地排序和选择。

    cpuset

    在使用容器的时候,你可以通过设置 cpuset 把容器绑定到某个 CPU 的核上, 而不是像 cpushare 那样共享 CPU 的计算能力。 这种情况下,由于操作系统在 CPU 之间进行上下文切换的次数大大减少,容器里应用的性 能会得到大幅提升。事实上,cpuset 方式,是生产环境里部署在线应用类型的 Pod 时, 非常常用的一种方式。 可是,这样的需求在 Kubernetes 里又该如何实现呢? 其实非常简单:

  • 首先,你的 Pod 必须是 Guaranteed 的 QoS 类型;

  • 然后,你只需要将 Pod 的 CPU 资源的 requests 和 limits 设置为同一个相等的整数值即 可。

    这时候,该 Pod 就会被绑定在 2 个独占的 CPU 核上。当然,具体是哪两个 CPU 核,是由 kubelet 为你分配的。 以上,就是 Kubernetes 的资源模型和 QoS 类别相关的主要内容。

    Pod调度算法

    node污点

    K8s 每个节点上都可以应用一个或多个 taint ,这表示对于那些不能容忍这些 taint 的 pod,是不会被该节点接受的。如果将 toleration 应用于 pod 上,则表示这些 pod 可以(但不要求)被调度到具有相应 taint 的节点上。

    Taint 基本用法

  • 设置污点: kubectl taint node [node] key=value:[effect]

  • 其中[effect] 可取值:[ NoSchedule | PreferNoSchedule | NoExecute ]:
  • NoSchedule :一定不能被调度。
  • PreferNoSchedule:尽量不要调度。
  • NoExecute:不仅不会调度,还会驱逐 Node 上已有的 Pod。
  • 去除污点:kubectl taint node [node] key:[effect]- ```yaml

    比如设置污点:

    kubectl taint node test test=16:NoSchedule
    kubectl taint node test test=16:NoExecute

去除指定key及其effect:

这里的key不用指定value

kubectl taint nodes node_name key:[effect]-

去除指定key所有的effect

kubectl taint nodes node_name key-

示例:

kubectl taint node test test:NoSchedule-
kubectl taint node test test:NoExecute-
kubectl taint node test test-

下面是一个简单的示例:在 node1 上加一个 Taint,该 Taint 的键为 key,值为 value,Taint 的效果是 NoSchedule。这意味着除非 pod 明确声明可以容忍这个 Taint,否则就不会被调度到 node1 上:
```yaml
kubectl taint nodes node1 key=value:NoSchedule

然后需要在 pod 上声明 Toleration。下面的 Toleration 设置为可以容忍具有该 Taint 的 Node,使得 pod 能够被调度到 node1 上:

apiVersion: v1  
kind: Pod  
metadata:  
    name: pod-taints  
spec:  
    tolerations:  
    - key: "key"  
        operator: "Equal"
        value: "value" 
    effect: "NoSchedule"
    containers:  
    - name: pod-taints  
        image: busybox:latest

也可以写成如下:

tolerations:  
- key: "key"  
    operator: "Exists"  
     effect: "NoSchedule"

Toleration 基本用法

pod 的 Toleration 声明中的 key 和 effect 需要与 Taint 的设置保持一致,并且满足以下条件之一:

  • operator 的值为 Exists,这时无需指定 value
  • operator 的值为 Equal 并且 value 相等
  • 如果不指定 operator,则默认值为 Equal。

另外还有如下两个特例:

  • 空的 key 配合 Exists 操作符能够匹配所有的键和值
  • 空的 effect 匹配所有的 effect

上面的例子中 effect 的取值为 NoSchedule,下面对 effect 的值作下简单说明:

  • NoSchedule:如果一个 pod 没有声明容忍这个 Taint,则系统不会把该 Pod 调度到有这个 Taint 的 node 上
  • PreferNoSchedule:NoSchedule 的软限制版本,如果一个 Pod 没有声明容忍这个 Taint,则系统会尽量避免把这个 pod 调度到这一节点上去,但不是强制的。
  • NoExecute:定义 pod 的驱逐行为,以应对节点故障。

NoExecute 这个 Taint 效果对节点上正在运行的 pod 有以下影响:

  • 没有设置 Toleration 的 Pod 会被立刻驱逐
  • 配置了对应 Toleration 的 pod,如果没有为 tolerationSeconds 赋值,则会一直留在这一节点中
  • 配置了对应 Toleration 的 pod 且指定了 tolerationSeconds 值,则会在指定时间后驱逐

    案例

    Pod的节点亲和性是Pod选择节点,而污点是node选择Pod。常见的有两种用法:
    一是运维人员相对节点进行一些配置,那可能需要将这个节点上的Pod都驱逐,这时候就可以在节点上设置污点,此时Pod肯定没有对于的Toleration,就会被驱逐到其他节点。
    二是对于一些具有特殊硬件的节点,普通的Pod不应该被调度到这个节点上,只有特定的Pod(数量很少)可以被调度到这些节点上。

    Pod的优先级

    正常情况下,当一个 Pod 调度失败后,它就会被暂时“搁置”起来,直到 Pod 被更新,或者集群状态发生变化,调度器才会对这个 Pod 进行重新调度。但在有时候,我们希望的是这样一个场景。当一个高优先级的 Pod 调度失败后,该 Pod 并 不会被“搁置”,而是会“挤走”某个 Node 上的一些低优先级的 Pod 。这样就可以保证这个高优先级 Pod 的调度成功。

    apiVersion: scheduling.k8s.io/v1beta1
    kind: PriorityClass
    metadata:
    name: high-priority
    value: 1000000
    globalDefault: false
    description: "This priority class should be used for high priority service pods only."
    

    上面这个 YAML 文件,定义的是一个名叫 high-priority 的 PriorityClass,其中 value 的 值是 1000000 。 Kubernetes 规定,优先级是一个 32 bit 的整数,最大值不超过 1000000000(10 亿, 1 billion),并且值越大代表优先级越高。而超出 10 亿的值,其实是被 Kubernetes 保留 下来分配给系统 Pod 使用的。显然,这样做的目的,就是保证系统 Pod 不会被用户抢占 掉。 而一旦上述 YAML 文件里的 globalDefault 被设置为 true 的话,那就意味着这个 PriorityClass 的值会成为系统的默认值。而如果这个值是 false,就表示我们只希望声明使 用该 PriorityClass 的 Pod 拥有值为 1000000 的优先级,而对于没有声明 PriorityClass 的 Pod 来说,它们的优先级就是 0。 在创建了 PriorityClass 对象之后,Pod 就可以声明使用它了,如下所示:

    apiVersion: v1
    kind: Pod
    metadata:
    name: nginx
    labels:
    env: test
    spec:
    containers:
    - name: nginx
      image: nginx
      imagePullPolicy: IfNotPresent
      priorityClassName: high-priority
    

    可以看到,这个 Pod 通过 priorityClassName 字段,声明了要使用名叫 high-priority 的 PriorityClass。当这个 Pod 被提交给 Kubernetes 之后,Kubernetes 的 PriorityAdmissionController 就会自动将这个 Pod 的 spec.priority 字段设置为 1000000。
    当一个高优先级的 Pod 调度失败的时候,调度器的抢占能力就会被触发。这时,调度器 就会试图从当前集群里寻找一个节点,使得当这个节点上的一个或者多个低优先级 Pod 被 删除后,待调度的高优先级 Pod 就可以被调度到这个节点上。

    调度算法

    Kubernetes 默认调度器调度分为两部分,第一部分是 Predicates,第二部分是 Priorities 。

    Predicates

    Predicates 在调度过程中的作用,可以理解为 Filter,即:它按照调度策略,从当前集群 的所有节点中,“过滤”出一系列符合条件的节点。这些节点,都是可以运行待调度 Pod 的宿主机。 而在 Kubernetes 中,默认的调度策略有如下三种。
    第一种类型,叫作 GeneralPredicates。 顾名思义,这一组过滤规则,负责的是最基础的调度策略。

  • PodFitsResources 计算 的就是宿主机的 CPU 和内存资源等是否够用。

  • PodFitsHost 检查的是,宿主机的名字是否跟 Pod 的 spec.nodeName 一致。
  • PodFitsHostPorts 检查的是,Pod 申请的宿主机端口(spec.nodePort)是不是跟已经被 使用的端口有冲突。
  • PodMatchNodeSelector 检查的是,Pod 的 nodeSelector 或者 nodeAffinity 指定的节 点,是否与待考察节点匹配,等等。

    第二种类型,是与 Volume 相关的过滤规则。

  • NoDiskConflict 检查的条件,是多个 Pod 声明挂载的持久化 Volume 是否有冲 突。比如,AWS EBS 类型的 Volume,是不允许被两个 Pod 同时使用的。所以,当一个 名叫 A 的 EBS Volume 已经被挂载在了某个节点上时,另一个同样声明使用这个 A Volume 的 Pod,就不能被调度到这个节点上了。

  • MaxPDVolumeCountPredicate 检查的条件,则是一个节点上某种类型的持久化 Volume 是不是已经超过了一定数目,如果是的话,那么声明使用该类型持久化 Volume 的 Pod 就不能再调度到这个节点了。
  • VolumeZonePredicate,则是检查持久化 Volume 的 Zone(高可用域)标签,是否与 待考察节点的 Zone 标签相匹配。

第三种类型,是宿主机相关的过滤规则。
这一组规则,主要考察待调度 Pod 是否满足 Node 本身的某些条件。

  • PodToleratesNodeTaints,负责检查的就是我们前面经常用到的 Node 的“污 点”机制。只有当 Pod 的 Toleration 字段与 Node 的 Taint 字段能够匹配的时候,这个 Pod 才能被调度到该节点上。
  • NodeMemoryPressurePredicate,检查的是当前节点的内存是不是已经不够充足,如 果是的话,那么待调度 Pod 就不能被调度到该节点上。

    第四种类型,是 Pod 相关的过滤规则。
    这一组规则,跟 GeneralPredicates 大多数是重合的。而比较特殊的,是 PodAffinityPredicate。

Priorities

在 Predicates 阶段完成了节点的“过滤”之后,Priorities 阶段的工作就是为这些节点打 分。这里打分的范围是 0-10 分,得分最高的节点就是最后被 Pod 绑定的最佳节点。
LeastRequestedPriority 选择空闲资源(CPU 和 Memory)最多的宿主机。
BalancedResourceAllocation 选择的,其实是调度完成后,所有节点里各种资源 分配最均衡的那个节点,从而避免一个节点上 CPU 被大量分配、而 Memory 大量剩余的 情况。
有 NodeAffinityPriority、TaintTolerationPriority 和 InterPodAffinityPriority 这三种 Priority。顾名思义,它们与前面的 PodMatchNodeSelector、 PodToleratesNodeTaints 和 PodAffinityPredicate 这三个 Predicate 的含义和计算方法 是类似的。但是作为 Priority,一个 Node 满足上述规则的字段数目越多,它的得分就会越 高。

Pause容器

在k8s中,pause容器作为pod中其他容器的父容器(parent container),它有两个核心特质:

  • 作为每个pod中共享Linux Namespace的基础
  • 启用共享PID namespace之后,作为每个pod中PID为1的进程,负责回收僵尸进程。

    共享namespace

    pause容器作为pod中所有容器的父容器,是第一个启动的容器,当pause容器正常启动后,其他业务负载相关的容器就会加入pause容器的namespace,从而同一pod下的其他容器都共享这个pause容器的命名空间,相互之间以localhost访问,构成统一的整体。
    比如,下面的命令启动一个pause容器:
    $ docker run -d --name pause -p 8080:80 gcr.io/google_containers/pause-amd64:3.0
    
    然后,启动一个业务容器(比如nginx),通过—net命令加入pause容器的Network namespace中,通过—pid命令加入其PID namespace中,通过—ipc加入其IPC namespace中:
    $ docker run -d --name nginx --net=container:pause --ipc=container:pause --pid=container:pause nginx
    
    按同样的方式再启动一个业务容器(比如ghost,这是一个博客应用)
    $ docker run -d --name ghost --net=container:pause --ipc=container:pause --pid=container:pause ghost
    
    那么,同一个pod内的不同业务容器间就可以直接通过localhost进行通信了,如下图所示:
    image.png ```bash [root@tengxunyun1412 ~]# curl localhost:8080 <!DOCTYPE html>

    Welcome to nginx!

    If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

我们在宿主机上访问8080端口,会发现其实是转发到了nginx容器上,这说明nginx和pause处于一个网络空间中。
<a name="L8l2I"></a>
## 管理进程
我们知道,docker是一个单进程模型,不具备进程的管理能里,所以开发者需要自己赋予 entrypoint 进程管理多进程的能力,而 Kubernetes 通过pause容器来管理同一个Pod中的容器。<br />k8s 可以将多个容器编排到一个 pod 里面,共享同一个 Linux Namespace。这项技术的本质是使用 k8s 提供一个 pause 镜像,也就是说先启动一个 pause 容器,相当于实例化出 Namespace,然后其他容器加入这个 Namespace 从而实现 Namespace 的共享。<br />我们来介绍一下 pause。pause 是 k8s 在 1.16 版本引入的技术,要使用 pause,我们只需要在 pod 创建的 yaml 中指定 shareProcessNamespace 参数为 true,如下:
```yaml
# pod_shareProcessNamespace.yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: mynginx
    app: tomcat
  name: nginx-tomcat-share
  namespace: dev
spec:
    shareProcessNamespace: true
  containers:
  - image: nginx
    name: mynginx
    # Always Never IfNotPresent可选
    imagePullPolicy: Always
  - image: tomcat:8.5.68
    name: mytomcat
    imagePullPolicy: IfNotPresent
[root@k8s-master pod]# kubectl exec -it nginx-tomcat-share --container=mytomcat -- /bin/bash
root@nginx-tomcat-share:/usr/local/tomcat# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0    972     4 ?        Ss   08:29   0:00 /pause
root           8  0.0  0.1  10664  6168 ?        Ss   08:29   0:00 nginx: master process nginx -g daemon off;
101           39  0.0  0.0  11068  2460 ?        S    08:29   0:00 nginx: worker process
101           40  0.0  0.0  11068  2460 ?        S    08:29   0:00 nginx: worker process
root          41  2.3  2.0 3331172 79280 ?       Ssl  08:29   0:01 /usr/local/openjdk-8/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/c
root          86  0.0  0.0   5756  3580 pts/0    Ss   08:30   0:00 /bin/bash
root          93  0.0  0.0   9396  3108 pts/0    R+   08:30   0:00 ps aux

我们可以看到 pod 中的 1 号进程变成了 /pause,其他容器的 entrypoint 进程都变成了 1 号进程的子进程。这个时候开始逐渐逼近事情的本质了:/pause 进程是如何处理 将孤儿进程的父进程置为 1 号进程进而避免僵尸进程的呢?pause 镜像的源码如下:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

static void sigdown(int signo) {
    psignal(signo, "Shutting down, got signal");
    exit(0);
}
// 关注1
static void sigreap(int signo) {
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

int main(int argc, char **argv) {
    int i;
    for (i = 1; i < argc; ++i) {
        if (!strcasecmp(argv[i], "-v")) {
            printf("pause.c %s\n", VERSION_STRING(VERSION));
            return 0;
        }
    }

    if (getpid() != 1)
        /* Not an error because pause sees use outside of infra containers. */
        fprintf(stderr, "Warning: pause should be the first process\n");

    if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
        return 1;
    if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
        return 2;
    // 关注2
    if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
                                               .sa_flags = SA_NOCLDSTOP},
                  NULL) < 0)
        return 3;

    for (;;)
        pause(); // 编者注:该系统调用的作用是wait for signal
    fprintf(stderr, "Error: infinite loop terminated\n");
    return 42;
}

重点关注一下void sigreap(int signo){...}if (sigaction(SIGCHLD,...) ,这个就是父子进程之间异步通信的方式。即子进程结束之后会向父进程发送 SIGCHLD 信号,基于此父进程注册一个 SIGCHLD 信号的处理函数来进行子进程的资源回收就可以了。
SIGCHLD 信号的处理函数核心就是这一行 while (waitpid(-1, NULL, WNOHANG) > 0) ,其中各参数示意如下:

  • -1:meaning wait for any child process.
  • NULL:?
  • WNOHANG :return immediately if no child has exited.

    Pod的创建流程

    image.png
  1. 用户通过kubectl命名发起请求。
  2. apiserver通过对应的kubeconfig进行认证,认证通过后将yaml中的po信息存到etcd。
  3. Controller-Manager通过apiserver的watch接口发现了pod信息的更新,执行该资源所依赖的拓扑结构整合,整合后将对应的信息交给apiserver,apiserver写到etcd,此时pod已经可以被调度了。
  4. Scheduler同样通过apiserver的watch接口更新到pod可以被调度,通过算法给pod分配节点,并将pod和对应节点绑定的信息交给apiserver,apiserver写到etcd,然后将pod交给kubelet。
  5. kubelet收到pod后,调用CNI接口给pod创建pod网络,调用CRI接口去启动容器,调用CSI进行存储卷的挂载。
  6. 网络,容器,存储创建完成后pod创建完成,等业务进程启动后,pod运行成功。