pod 深入理解
pod 容器生命周期
pod 的几种状态
可以使用命令kubectl get pod -w
实时监控查看 pod 的状态
running
:正常运行状态Pending
:资源分配不对的时候会挂起,出现此状态Terminating
:某个节点突然关机,上面的 pod 就会是这种状态ContainerCreating
:容器创建的时候OOMKilled
:当要求的内存超过限制的时候,k8s 会把这个容器 kill 后重启ErrImagePull
:宿主机上不了网,镜像拉取失败
生命周期
每个 Pod 里运行着一个特殊的被称之为 Pause 的容器,其他容器则为业务容器,这些业务容器共享 Pause 容器的网络栈和 Volume 挂载卷,因此他们之间通信和数据交换更为高效。在设计时可以充分利用这一特性,将一组密切相关的服务进程放入同一个 Pod 中;同一个 Pod 里的容器之间仅需通过localhost 就能互相通信。
Pause 容器,又叫 Infra 容器
比如说现在有一个 Pod,其中包含了一个容器 A 和一个容器 B Pod ip,它们两个就要共享 Network Namespace。在 k8s 里的解法是这样的:它会在每个 Pod 里,额外起一个 Infra container 小容器来共享整个 Pod 的 Network Namespace。
Infra container 是一个非常小的镜像,大概 700KB 左右,是一个 C 语言写的、永远处于 “暂停” 状态的容器。由于有了这样一个 Infra container 之后,其他所有容器都会通过 Join Namespace 的方式加入到 Infra container 的 Network Namespace 中。
所以说一个 Pod 里面的所有容器,它们看到的网络视图是完全一样的。即:它们看到的网络设备、IP 地址、Mac 地址等等,跟网络相关的信息,其实全是一份,这一份都来自于 Pod 第一次创建的这个 Infra container。这就是 Pod 解决网络共享的一个解法。
由于需要有一个相当于说中间的容器存在,所以整个 Pod 里面,必然是 Infra container 第一个启动。并且整个 Pod 的生命周期是等同于 Infra container 的生命周期的,与容器 A 和 B 是无关的。这也是为什么在 k8s 里面,它是允许去单独更新 Pod 里的某一个镜像的,即:做这个操作,整个 Pod 不会重建,也不会重启,这是非常重要的一个设计。
Pod hook(钩子)是由 k8s 管理的 kubelet 发起的,当容器中的进程启动前或者容器中的进程终止之前运行,这是包含在容器的生命周期之中。可以同时为 Pod 中的所有容器都配置 hook。
init 容器
官网:https://kubernetes.io/zh-cn/docs/concepts/workloads/pods/init-containers/
Init 容器是一种特殊容器,在 Pod 内的应用容器启动之前运行。Init 容器可以包括一些应用镜像中不存在的实用工具和安装脚本。
每个 pod 中可以包含多个容器, 应用运行在这些容器里面,同时 Pod 也可以有一个或多个先于应用容器启动的 Init 容器。
Init 容器与普通的容器非常像,除了如下两点:
- 它们总是运行到完成。
- 每个都必须在下一个启动之前成功完成。
如果 Pod 的 Init 容器失败,kubelet 会不断地重启该 Init 容器直到该容器成功为止。 然而,如果 Pod 对应的 restartPolicy
值为 “Never”,并且 Pod 的 Init 容器失败, 则 k8s 会将整个 Pod 状态设置为失败。
为 Pod 设置 Init 容器需要在 Pod 规约 中添加 initContainers
字段, 该字段以 Container 类型对象数组的形式组织,和应用的 containers
数组同级相邻。
Init 容器的状态在 status.initContainerStatuses
字段中以容器状态数组的格式返回 (类似 status.containerStatuses
字段)。
下面我们编写一个案例测试 init 容器
- 编写 yaml 文件
my-init.yaml
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: 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
[root@k8s-master k8s]# vim my-init.yaml
[root@k8s-master k8s]# kubectl apply -f my-init.yaml
pod/myapp-pod created
- 查看 pod
[root@k8s-master k8s]# kubectl get pod
NAME READY STATUS RESTARTS AGE
myapp-pod 0/1 Init:0/2 0 86s
可以看到我们的这个 pod 没有运行成功,这是因为 init 容器没有启动成功
Init 容器会等待至发现名称为 mydb
和 myservice
的服务。
- 接下来我们创建这两个服务,编写 yaml 文件
init-svc.yaml
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
- 启动两个服务,查看 pod 状态
[root@k8s-master k8s]# vim init-svc.yaml
[root@k8s-master k8s]# kubectl apply -f init-svc.yaml
service/myservice created
service/mydb created
[root@k8s-master k8s]# kubectl get pod
NAME READY STATUS RESTARTS AGE
myapp-pod 1/1 Running 0 5m13s
可以看到 pod 已经顺利启动了
总结
Init 容器与普通的容器非常像,区别是每个都必须在下一个容器启动之前成功完成。
如果 Pod 的 Init 容器失败,kubelet 会不断地重启该 Init 容器直到该容器成功为止。 然而,如果 Pod 对应的 restartPolicy 值为 “Never”,k8s 不会重新启动 Pod。容器重启策略默认是Always。
为 Pod 设置 Init 容器需要在 Pod 的 spec 中添加 initContainers 字段, 该字段以 Container类型对象数组的形式组织,和应用的 containers 数组同级相邻。
Init 容器支持应用容器的全部字段和特性,包括资源限制、数据卷和安全设置。
如果为一个 Pod 指定了多个 Init 容器,这些容器会按顺序逐个运行。 每个 Init 容器必须运行成功,下一个才能够运行。当所有的 Init 容器运行完成时, k8s 才会为 Pod 初始化应用容器并像平常一样运行。
容器探针
官网:https://kubernetes.io/zh-cn/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
当你使用 k8s 的时候,有没有遇到过 Pod 在启动后一会就挂掉然后又重新启动这样的恶性循环?你有没有想过 k8s 是如何检测 pod 是否还存活?虽然容器已经启动,但是 k8s 如何知道容器的进程是否准备好对外提供服务了呢?
k8s 探针(Probe)是用于检查容器运行状况的一种机制。探针可以检查容器是否正在运行,容器是否能够正常响应请求,以及容器内部的应用程序是否正常运行等。在 k8s 中,探针可以用于确定容器的健康状态,如果容器的健康状态异常,k8s 将会采取相应的措施,例如重启容器或将其从服务中删除。
为什么需要容器探针?
容器探针可以确保您的容器在任何时候都处于可预测的状态。
如果没有容器探针,那么容器对于 k8s 平台而言,就处于一个黑盒状态。下面是没有使用容器探针可能出现的一些 case:
- 容器未启动,负载均衡就把流量转发给容器,导致请求大量异常
- 容器内服务不可用/发生异常,负载均衡把流量转发给容器,导致请求大量异常
- 容器已经不正常工作(如容器死锁导致的应用程序停止响应),k8s 平台本身无法感知,不能即时低重启容器。
探测类型
针对运行中的容器,kubelet 可以选择是否执行以下三种探针,以及如何针对探测结果作出反应:
分别是Liveness Probe(存活探针)、Readiness Probe(就绪探针)、Startup Probe(启动探针)
# livenessProbe
用于检查容器是否正在运行,如果Liveness Probe检查失败,则Kubernetes将重启容器
# readinessProbe
用于检查容器是否能够正常响应请求,如果Readiness Probe检查失败,则Kubernetes将停止将流量发送到该容器
# startupProbe
用于检查容器内部的应用程序是否已经启动并且已经准备好接受流量,如果Startup Probe检查失败,则Kubernetes将重启容器
探针资源 yaml 中的常用的配置,如下。可根据具体的需求去设置,心跳机制
spec:
containers:
# 就绪探针
readinessProbe:
# 检测方式
httpGet:
# 超时时间
timeoutSeconds:
# 延迟时间
initialDelaySeconds:
# 失败次数限制
failureThreshold:
# 每多少秒检测一次
periodSeconds:
# 存活探针
livenessProbe:
# 检测方式
httpGet:
# 超时时间
timeoutSeconds:
# 延迟时间
initialDelaySeconds:
# 失败次数限制
failureThreshold:
# 每多少秒检测一次
periodSeconds:
探针机制
使用探针来检查容器有四种不同的方法。 每个探针都必须准确定义为这四种机制中的一种:请求-响应。
#exec
在容器内执行指定命令。如果命令退出时返回码为 0 则认为诊断成功。
#httpGet
对容器的 IP 地址上指定端口和路径执行 HTTP GET 请求。如果响应的状态码大于等于 200 且小于 400,则诊断被认为是成功的。
#tcpSocket
对容器的 IP 地址上的指定端口执行 TCP 检查。如果端口打开,则诊断被认为是成功的。如果远程系统 (容器)在打开连接后立即将其关闭,这算作是健康的。
#grpc
使用 gRPC 执行一个远程过程调用。 目标应该实现 gRPC健康检查。 如果响应的状态是"SERVING",则认为诊断成功。 gRPC 探针是一个 alpha 特性,只有在你启用了"GRPCContainerProbe"特性门控时才能使用。
探测结果
不管是那种每次探测都将获得以下三种结果之一:
Success
(成功)容器通过了诊断。Failure
(失败)容器未通过诊断。Unknown
(未知)诊断失败,因此不会采取任何行动。
测试
- 编辑 yaml 文件
my-liveness.yaml
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness-exec
spec:
containers:
- name: liveness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 600
livenessProbe:
exec: # exec方式检测
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5 # 容器启动成功后5s,开始执行探针检测
periodSeconds: 5 # 每隔5s检测一次
从 yaml 文件中可以看出:首先创建目录/tmp/healthy
,每隔5秒进行探针检测,检测目录是否存在,pod 创建完成后30s,删除目录,这时候探针就检测不到了,重复检测,在600秒后,pod 结束
- 启动 pod
[root@k8s-master k8s]# vim my-liveness.yaml
[root@k8s-master k8s]# kubectl apply -f my-liveness.yaml
pod/liveness-exec configured
- 查看 pod 描述信息
kubectl describe pod liveness-exec
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m26s default-scheduler Successfully assigned default/liveness-exec to k8s-node2
Normal Pulling 61s kubelet Pulling image "busybox"
Normal Pulled 45s kubelet Successfully pulled image "busybox" in 15.275s (15.275s including waiting)
Normal Created 45s kubelet Created container liveness
Normal Started 45s kubelet Started container liveness
Warning Unhealthy 1s (x3 over 11s) kubelet Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
Normal Killing 1s kubelet Container liveness failed liveness probe, will be restarted
可以看到,容器在创建30秒后Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
无法打开这个目录了,这时候容器探针检测失败,重新启动
我们这个案例是使用exec
机制(在容器内执行指定命令)来检测探针是否成功,官网还有 httpGet 请求和 tcpSocket,自行测试即可
节点亲和性
问题:每次pod调度都是随机事件,不知道pod会被调度到哪一个节点
Scheduler 是 k8s 的调度器,主要的任务是把定义的 pod 分配到集群的节点上。听起来非常简单,但有很多要考虑的问题:
- 公平:如何保证每个节点都能被分配资源
- 资源高效利用:集群所有资源最大化被使用
- 效率:调度的性能要好,能够尽快地对大批量的 pod 完成调度工作
- 灵活:允许用户根据自己的需求控制调度的逻辑
Scheduler 是作为单独的程序运行的,启动之后会一直监听 API Server ,获取 Pod.Spec.NodeName
为空的 pod,对每个 pod 都会创建一个 binding,表明该 pod 应该放到哪个节点上。
调度的几个部分
- 首先是过滤掉不满足条件的节点,这个过程称为 predicate(预选);
- 然后对通过的节点按照优先级排序,这个是 priority(优选);
- 最后从中选择优先级最高的节点。
如果中间任何一步骤有错误,就直接返回错误(先预选,后优选)
Predicate(预选)有一系列的算法可以使用:
- PodFitsResources:节点上剩余的资源是否大于 pod 请求的资源
- PodFitsHost:如果 pod 指定了 NodeName,检查节点名称是否和 NodeName 匹配
- PodFitsHostPorts:节点上已经使用的 port 是否和 pod 申请的 port 冲突
- PodSelectorMatches:过滤掉和 pod 指定的 label 不匹配的节点
- NoDiskConflict:已经 mount 的 volume 和 pod 指定的 volume 不冲突,除非它们都是只读
如果在 predicate 过程中没有合适的节点,pod 会一直在 pending 状态(pending:等待),不断重试调度,直到有节点满足条件。经过这个步骤,如果有多个节点满足条件,就继续 priorities 过程:按照优先级大小对节点排序。
优先级由一系列键值对组成,键是该优先级项的名称,值是它的权重(该项的重要性)。这些优先级选项包括:
-
LeastRequestedPriority:通过计算 CPU 和 Memory 的使用率来决定权重,使用率越低权重越高。换句话说,这个优先级指标倾向于资源使用比例更低的节点
-
BalancedResourceAllocation:节点上 CPU 和 Memory 使用率越接近,权重越高。这个应该和上面的一起使用,不应该单独使用
-
ImageLocalityPriority:倾向于已经有要使用镜像的节点,镜像总大小值越大,权重越高
通过算法对所有的优先级项目和权重进行计算,得出最终的结果
pod 与 node 的亲和性
pod.spec.nodeAffinity
-
preferredDuringSchedulingIgnoredDuringExecution(优先执行计划):软策略
-
requiredDuringSchedulingIgnoredDuringExecution(要求执行计划):硬策略
(preferred:首选,较喜欢 required:需要,必须)
键值运算关系
- In:label 的值在某个列表中
- NotIn:label 的值不在某个列表中
- Gt:label 的值大于某个值
- Lt:label 的值小于某个值
- Exists:某个 label 存在
- DoesNotExist:某个 label 不存在
#软硬策略(先满足硬策略再满足软策略)
apiVersion: v1
kind: Pod
metadata:
name: affinity2
labels:
app: node-affinity-pod
spec:
containers:
- name: with-node-affinity
image: nginx
affinity:
# 可以编写多个亲和性策略
nodeAffinity: #node亲和性
# 硬亲和性限制
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname # 标签键名
operator: NotIn #键值运算关系 ,NotIn:label的值不在某个列表中。 表示不是node02节点就可运行
values:
- k8s-node02 # 标签键值
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1 #权重,权重越大越亲和(多个软策略的情况)
preference:
matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- k8s-node03 # 期望是node03
pod 与 pod 的亲和性
pod.spec.affinity.podAffinity
亲和性 podAntiAffinity
反亲和性
- preferredDuringSchedulingIgnoredDuringExecution:软策略
- requiredDuringSchedulingIgnoredDuringExecution:硬策略
要有选择中的 Pod —— 才可以说是 Pod 间 —— 也就是要有 labelSelector(若没有设置此字段,表示没有选中的 Pod)
topology 就是拓扑的意思,这里指的是一个拓扑域,是指一个范围的概念,比如一个 Node、一个机柜、一个机房或者是一个地区(如杭州、上海)等,实际上对应的还是 Node 上的标签。
topologyKey 可以理解为 Node 的 label,比如默认所有节点都会有 kubernetes.io/hostname 这 label,相应的值为节点名称,如 master01 节点的 label 为 kubernetes.io/hostname=master01,这种情况每个节点对应的值都不同。
pod亲和性调度需要各个相关的 pod 对象运行于"同一位置", 而反亲和性调度则要求他们不能运行于"同一位置",这里指定“同一位置” 是通过 topologyKey 来定义的。
在我这个集群中,找到我期望的pod启动节点:pod亲和性策略:标签: app=node-affinity-pod
反亲和性 podAntiAffinity
Taint 和 Toleration(污点和容忍)
所谓污点就是故意给某个节点服务器上设置个污点参数,那么你就能让生成 pod 的时候使用相应的参数去避开有污点参数的 node 服务器。而容忍呢,就是当资源不够用的时候,即使这个 node 服务器上有污点,那么只要 pod 的 yaml 配置文件中写了容忍参数,最终 pod 还是会容忍的生成在该污点服务器上。默认 master 节点是 NoSchedule(不会调度任何新的 Pod)。
污点(Taint)是应用在节点机器 node 上之上的,从这个名字就可以看出来,是为了排斥 pod 所存在的。
容忍度(Toleration)是应用于 Pod 上的,允许(但并不要求)Pod 调度到带有与之匹配的污点的节点上。
Taint(污点)和 Toleration(容忍)可以作用于 node 和 pod 上,其目的是优化 pod 在集群间的调度,这跟节点亲和性类似,只不过它们作用的方式相反,具有 taint 的 node 和 pod 是互斥关系,而具有节点亲和性关系的 node 和 pod 是相吸的。另外还有可以给 node 节点设置 label,通过给 pod 设置 nodeSelector
将 pod 调度到具有匹配标签的节点上。
Taint 和 toleration 相互配合,可以用来避免 pod 被分配到不合适的节点上。每个节点上都可以应用一个或多个 taint ,这表示对于那些不能容忍这些 taint 的 pod,是不会被该节点接受的。如果将 toleration 应用于 pod 上,则表示这些 pod 可以(但不要求)被调度到具有相应 taint 的节点上。
基本操作
查看某个节点的 Taint 信息
kubectl describe node nodename
[root@k8s-master01 ~]# kubectl describe node k8s-node01
......
Taints: <none> # 关注这个地方即可 ---没有设置过污点的节点属性中的参数是这样的Taints: <none>
......
为 node 设置 taint
使用 kubectl taint
命令可以给某个 Node 节点设置污点,Node 被设置上污点之后就和 Pod 之间存在了一种相斥的关系,可以让 Node 拒绝 Pod 的调度执行,甚至将 Node 已经存在的 Pod 驱逐出去。
每个污点有一个 key 和 value 作为污点的标签,其中 value 可以为空,effect 描述污点的作用。当前 taint effect 支持如下三个选项:
# NoSchedule:表示k8s将不会将Pod调度到具有该污点的Node上
# PreferNoSchedule:表示k8s将尽量避免将Pod调度到具有该污点的Node上
# NoExecute:表示k8s将不会将Pod调度到具有该污点的Node上,同时会将Node上已经存在的Pod驱逐出去,赶到其它节点上
这也是为什么生成的pod不会分配到master的原因(因为天生就打了这个污点NoSchedule)
接下来我们给节点node1
增加一个污点,它的键名是key1
,键值是value1
,效果是NoSchedule
。 这表示只有拥有和这个污点相匹配的容忍度的 Pod 才能够被分配到node1
这个节点。
为 node1 设置 taint:
kubectl taint nodes k8s-node1 key1=value1:NoSchedule
删除上面的 taint:
# 去除污点,最后一个"-"代表删除
kubectl taint nodes k8s-node1 key1:NoSchedule-
为 pod 设置 toleration
只要在 pod 的 spec 中设置 tolerations 字段即可,可以存在多个 key
,如下所示:
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoExecute"
- key: "node.alpha.kubernetes.io/unreachable"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 6000
value
的值可以为NoSchedule
、PreferNoSchedule
或NoExecute
。tolerationSeconds
是当 pod 需要被驱逐时,可以继续在 node 上运行的时间。
多个污点匹配原则
可以给一个节点添加多个污点,也可以给一个 Pod 添加多个容忍度设置。
k8s 处理多个污点和容忍度的过程就像一个过滤器:从一个节点的所有污点开始遍历, 过滤掉那些 Pod 中存在与之相匹配的容忍度的污点。余下未被过滤的污点的 effect 值决定了 Pod 是否会被分配到该节点。
例如,假设您给一个节点添加了如下污点:
kubectl taint nodes node1 key1=value1:NoSchedule
kubectl taint nodes node1 key1=value1:NoExecute
kubectl taint nodes node1 key2=value2:NoSchedule
假定有一个 Pod,它有两个容忍度:
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoExecute"
上述 Pod 不会被分配到 node1 节点,因为其没有容忍度和第三个污点相匹配。
但是如果在给节点添加上述污点之前,该 Pod 已经在上述节点运行, 那么它还可以继续运行在该节点上,因为第三个污点是三个污点中唯一不能被这个 Pod 容忍的。
总结:
通过污点和容忍度,可以灵活地让 Pod 避开某些节点或者将 Pod 从某些节点驱逐。
如果您想将某些节点专门分配给特定的一组用户使用,您可以给这些节点添加一个污点(即, kubectl taint nodes nodename dedicated=groupName:NoSchedule
), 然后给这组用户的 Pod 添加一个相对应的 toleration。
拥有上述容忍度的 Pod 就能够被分配到上述专用节点,同时也能够被分配到集群中的其它节点。
在部分节点配备了特殊硬件(比如 GPU)的集群中, 我们希望不需要这类硬件的 Pod 不要被分配到这些特殊节点,以便为后继需要这类硬件的 Pod 保留资源。
要达到这个目的,可以先给配备了特殊硬件的节点添加 taint (例如 kubectl taint nodes nodename special=true:NoSchedule
或 kubectl taint nodes nodename special=true:PreferNoSchedule
), 然后给使用了这类特殊硬件的 Pod 添加一个相匹配的 toleration。
指定调度节点
亲和性和污点,容忍都比较含蓄。
指定调度节点是绝对指定目标,我就要这个 node
Pod.spec.nodeName
将 Pod 直接调度到指定的 Node 节点上,会跳过 Scheduler 的调度策略,该匹配规则是强制匹配
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: myweb
spec:
replicas: 7
template:
metadata:
labels:
app: nginx
spec:
nodeName: k8s-node01 #指定全在node01
containers:
- name: myweb
image: nginx
ports:
- containerPort: 80
Pod.spec.nodeSelector
:通过 k8s 的 label-selector 机制选择节点,由调度器调度策略匹配 label,而后调度 Pod 到目标节点,该匹配规则属于强制约束
首先在 master 节点给需要调度的目标 node 打上 label,并在创建 pod 的 yaml 文件中指定 label 来使得 pod 创建的时候调度到指定标签的 node 节点。
# 首先给指定的 node 节点加上标签
kubectl label nodes kube-node01 disk=ssd
kubectl get nodes kube-node01 --show-labels |grep disk
# 编写 yaml 文件引入标签
[root@kube-master scheduler]# vim nodeLabels-test.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-busybox
spec:
replicas: 1
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
containers:
- name: busybox
image: busybox
imagePullPolicy: IfNotPresent
nodeSelector: # 定义 nodeSelector 参数
disk: ssd # 引入标签
指定在某台节点启动的命令: nodename 、nodeSelector(需要再node上指定labels)