pod学习4

污点、容忍度

给了节点选则的主动权,我们给节点打一个污点,不容忍的 pod 就运行不上来,污点就是定义在 节点上的键值属性数据,可以定决定拒绝那些 pod;

taints 是键值数据,用在节点上,定义污点;

tolerations 是键值数据,用在 pod 上,定义容忍度,能容忍哪些污点

节点亲和性是 pod 属性,pod 亲和性也是 pod 属性;但是污点是节点的属性,污点定义在 nodeSelector 上

kubectl get nodes god62 -o yaml        #查询详细信息

kubectl explain node.spec.taints

effect  -required- #定义排斥等级 key -required-

timeAdded

value

taints 的 effect 用来定义对 pod 对象的排斥等级(效果): 


NoSchedule:仅影响调度过程,当 pod 能容忍这个节点污点,就可以调度到当前节点,后来这个节点的污点改 了,加了一个新的污点,使得之前调度的 pod 不能容忍了,那这个 pod 会怎么处理,对现存的 pod 对象不产生影响

NoExecute: 既影响调度过程,又影响现存的 pod 对象,如果现存的 pod 不能容忍节点后来加的污点,这个 pod 就会被驱逐

PreferNoSchedule:    最好不,也可以,是 NoSchedule 的柔性版本

kubectl explain nodes.spec.taints

在 pod 对象定义容忍度的时候支持两种操作:

1.等值密钥:key 和 value 上完全匹配

2.存在性判断:key 和 effect 必须同时匹配,value 可以是空

在 pod 上定义的容忍度可能不止一个,在节点上定义的污点可能多个,需要琢个检查容忍度和污 点能否匹配,每一个污点都能被容忍,才能完成调度,如果不能容忍怎么办,那就需要看 pod 的 容忍度了

#查看 master 这个节点是否有污点,显示如下:

kubectl describe nodes  god63 

上面可以看到 master 这个节点的污点是 Noschedule所以我们创建的 pod 都不会调度到 master 上,因为我们创建的 pod 没有容忍度

kubectl describe pods kube-apiserver-god63 -n kube-system

可以看到这个 pod 的容忍度是 NoExecute,则可以调度到 god63 上

1 管理节点污点

kubectl taints --help

把 62 当成是生产环境专用的,其他 node 是测试的

kubectl taint node god62 node-type=production:NoSchedule 

给 god62 打污点,pod 如果不能容忍就不会调度过来

#看到 kubectl describe nodes god62

#Taints: node-type=production:NoSchedule


vi pod-taint.yaml 

apiVersion: v1

kind: Pod

metadata:

  name: taint-pod

  namespace: default

  labels:

    tomcat:  tomcat-pod

spec:

  containers:

  - name:  taint-pod

    ports:

    - containerPort: 8080

    image: tomcat:8.5-jre8-alpine

    imagePullPolicy: IfNotPresent

#更新

kubectl apply -f pod-taint.yaml

 kubectl get pods -o wide 显示如下:

taint-pod running god64

可以看到都被调度到 god64 上了,因为 god62 这个节点打了污点,而我们在创建 pod 的时候没有容忍度,所以 god62 上不会有 pod 调度上去的

kubectl delete -f pod-taint.yaml

#加上以后已经存在的pod也会撵走

kubectl taint node god64 node-type=dev:NoExecute

#删除污点

kubectl taint node god64 node-type-

#查看是否删除污点

kubectl describe nodes god64


operator: "Equal".   精确匹配

operator: "Exists"。  模糊匹配

kubectl delete pods myapp-deploy

# cat pod-demo-1.yaml

apiVersion: v1

kind: Pod

metadata:

  name: myapp-deploy

  namespace: default

  labels:

    app: myapp

    release: canary

spec:

  containers:

  - name: myapp

    image: ikubernetes/myapp:v1

    ports:

    - name: http

      containerPort: 80

  tolerations:

  - key: "node-type"

    operator: "Exists"

    value: "production" 

    effect: "NoExecute"  

    tolerationSeconds: 3600

# tolerationSeconds: 3600 表示可以多存活 3600 秒。Pod 才被驱逐 

# kubectl apply -f pod-demo-1.yaml 

# kubectl get pods

myapp-deploy 1/1 Pending 0 11s god62

还是显示 pending,因为我们使用的是 equal(等值匹配),所以 key 和 value,effect 必须和 node 节点定义的污点完全匹配才可以,把上面配置 effect: "NoExecute"变成

effect: "NoSchedule";

tolerationSeconds: 3600 这行去掉

 kubectl delete -f pod-demo-1.yaml 

kubectl apply -f pod-demo-1.yaml 

 kubectl get pods

myapp-deploy 1/1 running 0 11s god62

上面就可以调度到 god62 上了,因为在 pod 中定义的容忍度能容忍 node 节点上的污点


tolerations:

- key: "node-type"

operator: "Exists" value: ""

effect: "NoSchedule"

只要对应的键是存在的,exists,其值被自动定义成通配符

# kubectl apply -f pod-demo-1.yaml 

# kubectl get pods

发现还是调度到 god62 上

myapp-deploy 1/1 running 0 11s god62

再次修改:

tolerations:

- key: "node-type"

operator: "Exists"

value: ""

effect: ""

有一个 node-type 的键,不管值是什么,不管是什么效果,都能容忍 

 kubectl apply -f pod-demo-1.yaml

 kubectl get pods -o wide 显示如下: myapp-deploy running god64

可以看到 god62 和 god64 节点上都有可能有 pod 被调度


Pod 状态和重启策略

常见的 pod 状态

Pod 的 status 定义在 PodStatus 对象中,其中有一个 phase 字段。它简单描述了 Pod 在其生 命周期的阶段。熟悉 Pod 的各种状态对我们理解如何设置 Pod 的调度策略、重启策略是很有必要 的。下面是 phase 可能的值,也就是 pod 常见的状态:

挂起(Pending):我们在请求创建 pod 时,条件不满足,调度没有完成,没有任何一个节点能满 足调度条件,已经创建了 pod 但是没有适合它运行的节点叫做挂起,调度没有完成,处于 pending 的状态会持续一段时间:包括调度 Pod 的时间和通过网络下载镜像的时间。 运行中(Running):Pod 已经绑定到了一个节点上,Pod 中所有的容器都已被创建。至少有一 个容器正在运行,或者正处于启动或重启状态。

成功(Succeeded):Pod 中的所有容器都被成功终止,并且不会再重启。

失败(Failed):Pod 中的所有容器都已终止了,并且至少有一个容器是因为失败终止。也就是 说,容器以非 0 状态退出或者被系统终止。

未知(Unknown):未知状态,所谓 pod 是什么状态是 apiserver 和运行在 pod 节点的 kubelet 进行通信获取状态信息的,如果节点之上的 kubelet 本身出故障,那么 apiserver 就连 不上 kubelet,得不到信息了,就会看 Unknown

扩展:还有其他状态,如下:

Evicted 状态:出现这种情况,多见于系统内存或硬盘资源不足,可 df-h 查看 docker 存储所在 目录的资源使用情况,如果百分比大于 85%,就要及时清理下资源,尤其是一些大文件、docker 镜像。

CrashLoopBackOff:容器曾经启动了,但可能又异常退出了

Error 状态:Pod 启动过程中发生了错误

pod 重启策略

Pod 的重启策略(RestartPolicy)应用于 Pod 内的所有容器,并且仅在 Pod 所处的 Node 上由 kubelet 进行判断和重启操作。当某个容器异常退出或者健康检查失败时,kubelet 将根据 RestartPolicy 的设置来进行相应的操作。

Pod 的重启策略包括 Always、OnFailure 和 Never,默认值为 Always。 Always:当容器失败时,由 kubelet 自动重启该容器。 OnFailure:当容器终止运行且退出码不为 0 时,由 kubelet 自动重启该容器。 Never:不论容器运行状态如何,kubelet 都不会重启该容器。 


 vim pod.yaml

apiVersion: v1

kind: Pod

metadata:

  name: tomcat-pod

  namespace: default

  labels:

    tomcat:  tomcat-pod

spec:

  containers:

  - name:  tomcat-pod-java

    ports:

    - containerPort: 8080

    image: tomcat:8.5-jre8-alpine

    imagePullPolicy: IfNotPresent


Pod 生命周期

 kubectl explain pods.spec.initContainers


init 容器Pod 里面可以有一个或者多个容器,部署应用的容器可以称为主容器,在创建 Pod 时候,Pod 中可以有一个或多个先于主容器启动的 Init 容器,这个 init 容器就可以称为初始化容器,初始化容 器一旦执行完,它从启动开始到初始化代码执行完就退出了,它不会一直存在,所以在主容器启动 之前执行初始化,初始化容器可以有多个,多个初始化容器是要串行执行的,先执行初始化容器 1,在执行初始化容器 2 等,等初始化容器执行完初始化就退出了,然后再执行主容器,主容器一 退出,pod 就结束了,主容器退出的时间点就是 pod 的结束点,它俩时间轴是一致的;

Init 容器就是做初始化工作的容器。可以有一个或多个,如果多个按照定义的顺序依次执行,只有 所有的初始化容器执行完后,主容器才启动。由于一个 Pod 里的存储卷是共享的,所以 Init Container 里产生的数据可以被主容器使用到,Init Container 可以在多种 K8S 资源里被使用 到,如 Deployment、DaemonSet, StatefulSet、Job 等,但都是在 Pod 启动时,在主容器启 动前执行,做初始化工作。

Init 容器与普通的容器区别是:

1、Init 容器不支持 Readiness,因为它们必须在 Pod 就绪之前运行完成

2、每个 Init 容器必须运行成功,下一个才能够运行

3、如果 Pod 的 Init 容器失败,Kubernetes 会不断地重启该 Pod,直到 Init 容器成功为止,

然而,如果 Pod 对应的 restartPolicy 值为 Never,它不会重新启动。

例:定义一个初始化容器,用来修改主容器的 pod 主机的内核参数 #查看 initContainers 怎么定义

# kubectl explain pods.spec.initContainers initContainers:

-  name: increase-vm-max-map

image: busybox

command: ["sysctl", "-w", "vm.max_map_count=262144"] securityContext:

        privileged: true

-  name: increase-fd-ulimit

      image: busybox

command: ["sh", "-c", "ulimit -n 65536"] 

securityContext:

              privileged: true  #不开启特权模式不能改内核参数

主容器 1、容器钩子

初始化容器启动之后,开始启动主容器,在主容器启动之前有一个 post start hook(容器启动后 钩子)和 pre stop hook(容器结束前钩子),无论启动后还是结束前所做的事我们可以把它放两 个钩子,这个钩子就表示用户可以用它来钩住一些命令,来执行它,做开场前的预设,结束前的清 理,如 awk 有 begin,end,和这个效果类似; postStart:该钩子在容器被创建后立刻触发,通知容器它已经被创建。如果该钩子对应的 hook (钩子)handler(检测)执行失败,则该容器会被杀死,并根据该容器的重启策略决定是否要重 启该容器,这个钩子不需要传递任何参数。

preStop:该钩子在容器被删除前触发,其所对应的 hook handler 必须在删除该容器的请求发 送给 Docker daemon 之前完成。在该钩子对应的 hook handler 完成后不论执行的结果如何, Docker daemon 会发送一个 SGTERN 信号量给 Docker daemon 来删除该容器,这个钩子不 需要传递任何参数。

在 k8s 中支持两类对 pod 的检测,第一类叫做 livenessprobe(pod 存活性探测): 存活探针主要作用是,用指定的方式检测 pod 中的容器应用是否正常运行,如果检测失败,则认 为容器不健康,那么 Kubelet 将根据 Pod 中设置的 restartPolicy 来判断 Pod 是否要进行重启 操作,如果容器配置中没有配置 livenessProbe,Kubelet 将认为存活探针探测一直为成功状 态。

第二类是状态检 readinessprobe(pod 就绪性探测):用于判断容器中应用是否启动完成,当探 测成功后才使 Pod 对外提供网络访问,设置容器 Ready 状态为 true,如果探测失败,则设置容 器的 Ready 状态为 false。


创建 pod 需要经过哪些阶段?

当用户创建 pod 时,这个请求给 apiserver,apiserver 把创建请求的状态保存在 etcd 中; 接下来 apiserver 会请求 scheduler 来完成调度,如果调度成功,会把调度的结果(如调度到哪 个节点上了,运行在哪个节点上了,把它更新到 etcd 的 pod 资源状态中)保存在 etcd 中,一旦 存到 etcd 中并且完成更新以后,如调度到 god64 上,那么 god64 节点上的 kubelet 通 过 apiserver 当中的状态变化知道有一些任务被执行了,所以此时此 kubelet 会拿到用户创建时 所提交的清单,这个清单会在当前节点上运行或者启动这个 pod,如果创建成功或者失败会有一 个当前状态,当前这个状态会发给 apiserver,apiserver 在存到 etcd 中;在这个过程中,etcd 和 apiserver 一直在打交道,不停的交互,scheduler 也参与其中,负责调度 pod 到合适的node 节点上,这个就是 pod 的创建过程

pod 在整个生命周期中有非常多的用户行为:

1、初始化容器完成初始化

2、主容器启动后可以做启动后钩子

3、主容器结束前可以做结束前钩子

4、在主容器运行中可以做一些健康检测,如 livenessprobe,readnessprobe

Pod 高级用法:Pod 容器探测深度讲解

容器钩子:postStart 和 preStop postStart:容器创建成功后,运行前的任务,用于资源部署、环境准备等。 preStop:在容器被终止前的任务,用于优雅关闭应用程序、通知其他系统等。

Pod 高级用法:Pod 容器探测深度讲解

容器钩子:postStart 和 preStop postStart:容器创建成功后,运行前的任务,用于资源部署、环境准备等。 preStop:在容器被终止前的任务,用于优雅关闭应用程序、通知其他系统等。

演示 postStart 和 preStop 用法 ......

containers:

- image: sample:v2

name: war lifecycle:

postStart: exec:

command:

“cp”

- “/sample.war” - “/app”

prestop: httpGet:

host: monitor.com path: /waring port: 8080 scheme: HTTP

......

以上示例中,定义了一个 Pod,包含一个 JAVA 的 web 应用容器,其中设置了 PostStart 和 PreStop 回调函数。即在容器创建成功后,复制/sample.war 到/app 文件夹中。而在容器终止 之前,发送 HTTP 请求到 http://monitor.com:8080/waring,即向监控系统发送警告。

优雅的删除资源对象

当用户请求删除含有 pod 的资源对象时(如 RC、deployment 等),K8S 为了让应用程序优雅关 闭(即让应用程序完成正在处理的请求后,再关闭软件),K8S 提供两种信息通知: 1)、默认:K8S 通知 node 执行 docker stop 命令,docker 会先向容器中 PID 为 1 的进程发送 系统信号 SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间, 或者默认超时时间(30s),会继续发送 SIGKILL 的系统信号强行 kill 掉进程。

2)、使用 pod 生命周期(利用 PreStop 回调函数),它执行在发送终止信号之前。 默认情况下,所有的删除操作的优雅退出时间都在 30 秒以内。kubectl delete 命令支持-- grace-period=的选项,以运行用户来修改默认值。0 表示删除立即执行,并且立即从 API 中删pod。在节点上,被设置了立即结束的的 pod,仍然会给一个很短的优雅退出时间段,才会开 始被强制杀死。如下:

spec:

containers:

- name: nginx-demo

image: centos:nginx lifecycle:

preStop: exec:

# nginx -s quit gracefully terminate while SIGTERM triggers a quick exit

command: ["/usr/local/nginx/sbin/nginx","-s","quit"] ports:

- name: http containerPort: 80


探测:livenessProbe 和 readinessProbe

livenessProbe:存活性探测 许多应用程序经过长时间运行,最终过渡到无法运行的状态,除了重启,无法恢复。通常情况下, K8S 会发现应用程序已经终止,然后重启应用程序 pod。有时应用程序可能因为某些原因(后端 服务故障等)导致暂时无法对外提供服务,但应用软件没有终止,导致 K8S 无法隔离有故障的 pod,调用者可能会访问到有故障的 pod,导致业务不稳定。K8S 提供 livenessProbe 来检测应 用程序是否正常运行,并且对相应状况进行相应的补救措施。

readinessProbe:就绪性探测

在没有配置 

readinessProbe 的资源对象中,pod 中的容器启动完成后,就认为 pod 中的应用程 序可以对外提供服务,该 pod 就会加入相对应的 service,对外提供服务。但有时一些应用程序 启动后,需要较长时间的加载才能对外服务,如果这时对外提供服务,执行结果必然无法达到预期 效果,影响用户体验。比如使用 tomcat 的应用程序来说,并不是简单地说 tomcat 启动成功就可 以对外提供服务的,还需要等待 spring 容器初始化,数据库连接上等等。

目前 LivenessProbe 和 ReadinessProbe 两种探针都支持下面三种探测方法: 1、ExecAction:在容器中执行指定的命令,如果执行成功,退出码为 0 则探测成功。 2、TCPSocketAction:通过容器的 IP 地址和端口号执行 TCP 检 查,如果能够建立 TCP 连 接,则表明容器健康。

3、HTTPGetAction:通过容器的 IP 地址、端口号及路径调用 HTTP Get 方法,如果响应的状 态码大于等于 200 且小于 400,则认为容器健康

探针探测结果有以下值: 1、Success:表示通过检测。 2、Failure:表示未通过检测。 3、Unknown:表示检测没有正常进行。

Pod 探针相关的属性:

探针(Probe)有许多可选字段,可以用来更加精确的控制 Liveness 和 Readiness 两种探针的行为 initialDelaySeconds: Pod 启动后首次进行检查的等待时间,单位“秒”。 periodSeconds: 检查的间隔时间,默认为 10s,单位“秒”。

timeoutSeconds: 探针执行检测请求后,等待响应的超时时间,默认为 1s,单位“秒”。 successThreshold: 表示探针的成功的阈值,在达到该次数时,表示成功。默认值为 1,

表示只要成功一次,就算成功了。

failureThreshold: 探测失败的重试次数,重试一定次数后将认为失败,在 readiness 探

针中,Pod 会被标记为未就绪,默认为 3,最小值为 1。

两种探针区别:

ReadinessProbe 和 livenessProbe 可以使用相同探测方式,只是对 Pod 的处置方式不同: readinessProbe 当检测失败后,将 Pod 的 IP:Port 从对应的 EndPoint 列表中删除。 livenessProbe 当检测失败后,将杀死容器并根据 Pod 的重启策略来决定作出对应的措施。


污点删除

kubectl taint nodes god62 node-type-

kubectl taint nodes god64 node-type-


Pod探针使用示例:

1、LivenessProbe 探针使用示例(1)、通过exec方式做健康探测示例文件 liveness-exec.yaml

apiVersion: v1

kind: Pod

metadata:

 name: liveness-exec

 labels:

 app: liveness

spec:

 containers:

 - name: liveness

 image: busybox

 args: #创建测试探针探测的文件

 - /bin/sh

 - -c

 - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600

 livenessProbe:

 initialDelaySeconds: 10 #延迟检测时间

 periodSeconds: 5 #检测时间间隔

 exec:

 command:

 - cat

 - /tmp/healthy

容器启动设置执行的命令:

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

  容器在初始化后,首先创建一个 /tmp/healthy 文件,然后执行睡眠命令,睡眠 30 秒,到时间后执行删除 /tmp/healthy 文件命令。而设置的存活探针检检测方式为执行 shell 命令,用 cat 命令输出 healthy 文件的内容,如果能成功执行这条命令,存活探针就认为探测成功,否则探测失败。在前 30 秒内,由于文件存在,所以存活探针探测时执行 cat /tmp/healthy 命令成功执行。30 秒后 healthy 文件被删除,所以执行命令失败,Kubernetes 会根据 Pod 设置的重启策略来判断,是否重启 Pod。

(2)、通过HTTP方式做健康探测示例文件 liveness-http.yaml

apiVersion: v1

kind: Pod

metadata:

 name: liveness-http

 labels:

 test: liveness

spec:

 containers:

 - name: liveness

 image: mydlqclub/springboot-helloworld:0.0.1

 livenessProbe:

 initialDelaySeconds: 20 #延迟加载时间

 periodSeconds: 5 #重试时间间隔

 timeoutSeconds: 10 #超时时间设置

 httpGet:

 scheme: HTTP

 port: 8081

 path: /actuator/health

上面 Pod 中启动的容器是一个 SpringBoot 应用,其中引用了 Actuator 组件,提供了 /actuator/health 健康检查地址,存活探针可以使用HTTPGet 方式向服务发起请求,请求 8081 端口的 /actuator/health 路径来进行存活判断:


任何大于或等于200且小于400的代码表示探测成功。

任何其他代码表示失败。


如果探测失败,则会杀死 Pod 进行重启操作。httpGet探测方式有如下可选的控制字段:

scheme: 用于连接host的协议,默认为HTTP。

host:要连接的主机名,默认为Pod IP,可以在http request head中设置host头部。

port:容器上要访问端口号或名称。

path:http服务器上的访问URI。

httpHeaders:自定义HTTP请求headers,HTTP允许重复headers。

(3)、通过TCP方式做健康探测示例文件 liveness-tcp.yaml

apiVersion: v1

kind: Pod

metadata:

 name: liveness-tcp

 labels:

 app: liveness

spec:

 containers:

 - name: liveness

 image: nginx

 livenessProbe:

 initialDelaySeconds: 15

 periodSeconds: 20

 tcpSocket:

 port: 80

TCP 检查方式和 HTTP 检查方式非常相似,在容器启动 initialDelaySeconds 参数设定的时间后,kubelet 将发送第一个 livenessProbe 探针,尝试连接容器的 80 端口,如果连接失败则将杀死 Pod 重启容器。

2、ReadinessProbe 探针使用示例


Pod 的ReadinessProbe (就绪型探测)探针使用方式和 LivenessProbe (存活型探测)探针探测方法一样,也是支持三种,只是一个是用于探测应用的存活,一个是判断是否对外提供流量的条件。这里用一个 Springboot 项目,设置 ReadinessProbe 探测 SpringBoot 项目的 8081 端口下的/actuator/health 接口,如果探测成功则代表内部程序以及启动,就开放对外提供接口访问,否则内部应用没有成功启动,暂不对外提供访问,直到就绪探针探测成功。

示例文件 readiness-exec.yaml

  apiVersion: v1

kind: Service

metadata:

 name: springboot

 labels:

 app: springboot

spec:

 type: NodePort

 ports:

 - name: server

 port: 8080

 targetPort: 8080

 nodePort: 31180

 - name: management

 port: 8081

 targetPort: 8081

 nodePort: 31181

 selector:

 app: springboot

---

apiVersion: v1

kind: Pod

metadata:

 name: springboot

 labels:

 app: springboot

spec:

 containers:

 - name: springboot

 image: mydlqclub/springboot-helloworld:0.0.1

 ports:

 - name: server

 containerPort: 8080

 - name: management

 containerPort: 8081

 readinessProbe:

 initialDelaySeconds: 20     #延迟加载时间

 periodSeconds: 5               #重试时间间隔

 timeoutSeconds: 10          #超时时间设置

 httpGet:

 scheme: HTTP

 port: 8081

 path: /actuator/health

3、ReadinessProbe + LivenessProbe 配合使用示例一般程序中需要设置两种探针结合使用,并且也要结合实际情况,来配置初始化检查时间和检测间隔,下面列一个简单的 SpringBoot 项目的Deployment 例子。


apiVersion: v1

kind: Service

metadata:

 name: springboot

 labels:

 app: springboot

spec:

 type: NodePort

 ports:

 - name: server

 port: 8080

 targetPort: 8080

 nodePort: 31180

 - name: management

 port: 8081

 targetPort: 8081

 nodePort: 31181

 selector:

 app: springboot

---

apiVersion: apps/v1

kind: Deployment

metadata:

 name: springboot

 labels:

 app: springboot

spec:

 replicas: 1

 selector:

 matchLabels:

 app: springboot

 template:

 metadata:

 name: springboot

 labels:

 app: springboot

 spec:

 containers:

 - name: readiness

 image: mydlqclub/springboot-helloworld:0.0.1

 ports:

 - name: server 

 containerPort: 8080

 - name: management

 containerPort: 8081

 readinessProbe:

 initialDelaySeconds: 20 

 periodSeconds: 5 

 timeoutSeconds: 10 

 httpGet:

 scheme: HTTP

 port: 8081

 path: /actuator/health

 livenessProbe:

 initialDelaySeconds: 30 

 periodSeconds: 10 

 timeoutSeconds: 5 

 httpGet:

 scheme: HTTP

 port: 8081

 path: /actuator/health

你可能感兴趣的:(pod学习4)