kubernetes资源管理

一 资源对象与API群组

kubernetes系统的API Server基于HTTP/HTTPS协议接收并响应客户端的操作请求,它提供了一种基于资源的RESTful风格的编程接口,把集群各种组件都抽象成标准的RESTful资源,并支持通过标准的HTTP方法、以json为数据序列化方案进行资源管理操作。

1.1 kubernetes的资源对象类型

以资源的主要功能作为分类标准,kubernetes的API对象大体可分为工作负载、发现与负载均衡、配置与存储、集群和元数据几个类别。它们基本上都是围绕一个核心目的而设计:如何更好的运行和丰富pod资源,从而为容器化应用提供更灵活和更完善的操作与管理组件。

工作负载型资源用于确保pod资源对象更好地运行容器化应用。具有同一种负载的各pod对象需要以负载均衡的方式服务于各请求,而各种容器化应用需要彼此发现以完成工作协同。pod资源具有生命周期,存储型资源能够为重构的pod对象提供持久化数据存储机制,配置型资源能够让共享同一配置的pod资源从中统一获取配置改动信息。这些资源为配置中心为管理容器化应用的配置文件提供了极为便捷的管理机制。集群型资源为管理集群本身的工作特性提供了配置接口,而元数据型资源用于配置集群内部其它资源的行为。

1.2 kubernetes的资源对象

一个应用通常需要多个资源的支撑,例如用Deployment资源编排应用实例,用ConfigMap和Secret资源保存应用配置、用Service或Ingress资源暴露服务、用Volume资源提供持久化存储等。

1.2.1 工作负载型资源

pod用于承载容器化应用,代表着kubernetes之上工作负载的表现形式。它负责运行容器,并为容器解决环境性的依赖,例如向容器注入临时或持久化的存储空间、配置信息或秘钥数据等。而诸如滚动更新、扩容和缩容一类的编排任务则有控制器对象负责,专用于pod编排任务的控制器可统称为pod控制器。

应用程序分为无状态和有状态两种类型,无状态应用中的每个pod实例均可被其它同类实例所取代,但有状态应用的每个pod实例均有其独特性,必须单独标识和管理,因而他们分别由两种不同类型的pod控制器进行管理。例如RelicationController、ReplicaSet和Deployment负责管理无状态应用,StatefulSet则用于管控有状态类应用。还有些应用较为独特,有些需要在集群中的每个节点上运行单个pod资源,负责收集日志或运行系统服务等任务,该类编排操作由DaemonSet控制器对象进行,而需要在正常完成后退出故无需始终处于运行状态任务的编排工作则隶属于Job控制器对象。CronJob控制器对象还能为Job型的任务提供定期执行机制。

  • RelicationController:用于确保每个pod副本在任一时刻均能满足目标数量,即它用于保证每个容器或容器组总是运行并可访问;它是上一代的无状态pod应用控制器。
  • ReplicaSet:新一代RelicationController,它与RelicationController唯一不同之处在于支持的标签选择器不同,RelicationController只支持“等值选择器”,而ReplicaSet还支持基于集合的选择器。
  • Deployment:用于管理无状态的持久化应用,例如HTTP服务等,它用于为pod和ReplicaSet提供声明式更新,是构建在ReplicaSet之上的、更为高级的控制器。
  • StatefulSet:用于管理有状态的持久化应用,例如数据库服务程序,与Deployment的不同支持在于,StatefulSet会为每个pod创建一个独有的持久性标识符,并会确保个pod间的顺序性。
  • DaemonSet:用于确保每个节点都运行了某pod的一个副本,包括后来新增的节点;而节点移除将导致pod回收;Daemonset常用于运行各类系统级守护进程,例如kube-proxy和Flannel网络插件,以及日志收集和邻近系统的Agent应用,例如fluentd、Logstash、Prometheus的Node Exporter等。
  • Job:用于管理运行完成后即可终止的应用,例如批处理作业任务;job创建一个或多个pod,并确保其符合目标数量,直到应用完成而终止。

1.2.2 发现与负载均衡

service是kubernetes标准的资源类型之一,用于为工作负载实例提供固定的访问入口及负载均衡服务,它把每个可用后端实例定义为Endpoint资源对象,通过IP地址和端口等属性映射至pod实例或相应的服务端点。Ingress资源则为工作负载提供7层代理及负载均衡功能。

1.2.3 配置与存储

Docker容器分层联合挂载的方式决定了其系统文件无法在容器内部存储持久化数据,因而容器引擎引入外部存储卷的方式来解决此类问题,相应地,kubernetes支持在pod级别附加Volume资源对象为容器添加可用的外部存储。kubernetes支持众多类型的存储设备或存储系统,例如GlusterFS、Ceph RBD和Flocker等,还可以通过FlexVOLUME及CSI(Container Storage Interface)存储接口扩展支持更多类型的存储系统。

另外,基于镜像运行容器应用时,其配置信息在镜像制作时以硬编码的方式置入,所以很难为不同的环境定制所需的配置。Docker使用环境变量等作为解决方案,但需要在容器启动时将配置作为变量传入,且无法在运行时修改。kubernetes的ConfigMap资源能够以环境变量或存储卷的方式接入pod资源的容器中,并可被多个同类的pod共享引用,从而做到一次修改,多出生效。不过这种方式不适用于存储敏感数据,例如证书、私钥和密码等,那是另一个资源类型Secret的功能。

1.2.4 集群型资源

kubernetes还存在一些用于定义集群自身配置信息的资源类型,它们不属于任何名称空间且仅应该由集群管理员操作,常用的集群型资源有如下几种:

  • Namespace:名称空间,为资源对象的名称空间提供了限定条件或作用范围,它为使用同一集群的多个团队或项目提供了逻辑上的隔离机制,降低或消除了资源对象名称冲突的可能性。
  • Node:kubernetes并不能直接管理其工作节点,但它会把由管理员添加进来的任何形式的工作节点映射为一个Node资源对象,因而节点名称在集群中必须唯一。
  • Role:角色,隶属于名称空间,代表名称空间级别由规则组成的权限集合,可被RoleBinding引用。
  • ClusterRole:集群角色,隶属于集群而非名称空间,代表集群级别的、由规则组成的权限集合,可被RoleBinding和ClusterRoleBinding引用。
  • RoleBinding:用于将Role中的许可权限绑定在一个或一组用户之上,从而完成用户授权,它隶属于且仅能作用于名称空间级别。
  • ClusterRoleBinding:将ClusterRole中定义的许可权限绑定在一个或一组用户之上,通过引用全局名称空间的ClusterRole将集群级别的权限授予指定用户。

1.2.5 元数据型资源

此类资源对象用于为集群内部的其它资源对象配置其行为或特性,例如HPA资源对象可用于控制自动伸缩工作负载类型对象的规模,Pod模板提供了创建pod对象的预制模板,而LimitRange可为名称空间内的pod应用设置其CPU和内存等系统资源的数量限制等。

1.3 资源在API中的组织形式

资源对象代表了系统上持久实体,而kubernetes用这些持久实体来表达集群的状态,包括容器化的应用程序正运行于那些节点,每个应用程序有那些资源可用,以及每个应用程序各自的行为策略,例如重启、升级及容错策略等。一个对象可能会由对个资源组合而成,用户可对这些资源执行增删改查等管理操作。kubernetes通常使用标准的RESTful术语描述API概念。

  • 资源类型是指在URL中使用的名称,例如pods、namespace和service等,其URL格式为GROUP/VERSION/RESOURCE。
  • 每个资源类型都有一个对于的JSON表示格式,称为一个种类;客户端创建对象时,一般要以json规范提交对象的配置信息。
  • Kind代表资源对象所属的类型,例如Namespace、Deployment、Service及pod等,而这些资源类型又大体分为3个类别。
    • 对象类:对象标识kubernetes系统上的持久化实体,一个对象可能包含多个资源,客户端可以对其执行多种操作;Namespace、Deployment、Service及pod等都属于这个类别。
    • 列表类:通常是指同一类型资源的集合,例如PodLists和NodeLists等。
    • 简单类:常用于在对象上执行某种特殊操作,或管理非持久化的实体,例如/binding或/status等。

kubernetes绝大多数的API资源都属于对象型类别,代表集群中某个抽象组件的实例。有一小部分的API资源类型为虚拟类型,用于表达一类操作。所有的对象型资源都拥有一个独有的名称标识已完成幂等的创建及获取操作,但虚拟型资源无须获取或不依赖于幂等性时也可以不使用专用标识符。

二 对象类资源配置规范

创建kubernetes对象时,必须提供描述该对象期望状态的规范以及相关的基本信息,例如对象名称、标签和注解等元数据,并以json格式提交给API Server。在大多数情况下,用户以YAML格式的配置清单定义对象的相关信息,并由kubectl在发起API请求时将其自动换位为JSON格式。

API Server接收和返回的所有json对象都遵循同一个模式,即他们都具有kind和apiVersion字段,分别用于标识对象所属的资源类型、API群组及相关版本,可合称为类型元数据(TypeMeta)。同时,大多数的对象或列表类型的资源还会有metadata、spec和status这3个嵌套型的字段。其中metadata字段为资源提供元数据信息,例如名称、隶属的名称空间和标签等,因而也称为对象元数据(ObjectMeta);space字段则由用户负责声明对象期望状态的字段,不同资源类型的期望状态描述方式各不相同,因此其嵌套支持的字段也不尽相同而status字段则记录活动对象的当前状态信息,也称为观察状态,它由kubernetes系统自行维护,对用户来说为只读字段,不需要在配置清单中提供,而是查询集群中的对象时由API Server在相应中返回。

每个资源类型代表一种格式的数据结构,它接收并返回该格式的对象数据,同时,一个对象也可嵌套多个独立的小对象,并支持每个小对象的单独管理操作。例如对pod类型的资源来说,用户可创建、更新或删除Pod对象,所以他们可被单独操作。需要注意的是status对象由kubernetes系统单独进行自动更新,且不支持用户手动操作。

2.1 对象元数据

metadata字段内嵌多个字段以定义对象的元数据,这些字段大体可分为必要字段和可选字段两类。名称空间级别的资源的必须字段如下。

  • namespace:当前对象隶属于的名称空间,默认值default。
  • name:当前对象的名称标识,同一名称空间下同一类型的对象名称必须唯一。
  • uid:当前对象的唯一标识符,其唯一性仅发生在特定的时间段和名称空间中,主要用于区别拥有同样名字的已删除和重新创建的同一个名称对象。

可选字段通常是指那些或由kubernetes系统自定维护和设置,或存在默认值,或本身允许使用空值等一类的字段,常用的可选字段有如下几个。

  • labels:该资源对象的标签,键值型数据,常被用作标签选择器的挑选条件。
  • annotations:非标识目的的元数据,键值格式,但不可作为标签选择器的挑选条件,其意义或解释方式一般由客户端自行定义。
  • resourceVersion:对象的内部版本标识符,用于让客户端确定对象的变动与否。
  • generation:用于标识当前对象目标状态的代别。
  • creationTimestamp:当前对象创建时的时间戳。
  • deletionTimestamp:当前字段删除时的时间戳。

配置清单中未在metadata字段中明确定义的可选嵌套字段,会由一系列的finalizer组件以自动填充。而当用户需要强制对资源创建的目标资源对象进行校检或者修改时,则要使用initialize组件完成,例如,为每个待创建的pod对象添加一个Sidecar容器。另外,不同的资源类型也会存在一些其专有的嵌套字段,例如ConfugMap资源还支持使用clusterName等。

2.2 资源的期望状态

kubernetes API中定义的大部分资源都有spec和status两个字段:前一个是声明式API风格的期望状态,由用户负责定义而由系统读取;后一个是系统写入的实际观测到的状态,可被用户读取,以了解API对象的实际状况。控制器是kubernetes的核心组件之一,负责将用户通过spec字段声明的的API对象状态真实反映到集群之上,尤其是创建和更新操作,并持续确保系统观测到并写入status的实际状态符合用户期望的状态。例如Deployment是负责编排无状态应用的声明式控制器,当创建一个具体的Deployment对象时,需要在其spec字段中指定期望运行的pod副本数量、匹配pod对象的标签选择器以及创建pod的模板等。Deployment控制器读取该对象的spec字段中的定义过滤出符合标签选择器的所有pod对象,并对比该类pod对象的总数与用户期望的副本数量,进行多退少补操作,多出的pod会被删除,少的将根据pod模板进行创建,而后将结果保存于Deployment对象的status字段中。随后,Deployment控制器在其控制循环中持续监控status字段与spec的差异,任何不同都将由控制器启动相应操作进行对齐。

2.3 获取资源配置清单格式文档

在定义资源配置清单时,尽管apiVersion、kind和metadata有据可依,但spec字段对不同的资源来说千差万别,用户难以事先全部掌握,这就不得不参考kubernetes API文档了解各字段的详尽使用说明。通过kubectl explain 可以打印出对应资源的一级字段及其使用格式。例如打印pod资源的一级字段以及使用格式:kubectl explain pods 。

每个对象的spec字段的文档通常会包含kind、version、resource、description和fields几个小节,其中fields一节给出了可嵌套使用的字段、数据类型及功能描述。各字段的数据类型遵循json规范,包括数值型、字符串、布尔型、数组或列表、对象和空值等,其中数值型还可以分为整型、分数和指数3种。其中对象类型的字段还可以嵌套其它字段,且每个对象的文档也支持单独打印。例如打印pod二级字段以及使用格式:kubectl expain pods.spec 。

2.4 资源对象管理方式

kubernetes的API Server遵循声明式编程范式而设计,侧重于构建程序逻辑而无需描述其实现流程,用户只需要设置期望的状态,系统既能依赖相应的控制器自动确定需要执行的操作,以确保达到用户期望的状态。事实上,声明式API和控制器模式也是kubernetes的编程等功能赖以实现的根本。

另一种对于类型称为命令式编程的范式,代码侧重于通过创建一种告诉计算机如何执行操作的算法或步骤来更改程序状态的语句,它与硬件的工作方式密切相关,通常代码将使用条件语句、循环和类继承等控制结构。为了便于用户使用,kubectl命令也支持命令式编排功能,用户直接通过命令及其选项就能完成对象管理操作。

kubectl的命令也可大体分为3类:命令式命令(imperative command)、命令式对象配置(imperative object configuration)和声明式对象配置(declarative object configuration)。

2.4.1 命令式命令

命令式命令是指将实施于目标对象的操作以选项及选项参数的方式提供给kubectl命令,并直接操作kubernetes集群中的活动对象,因而无法提供之前配置的历史记录。这是在集群中允许一次性任务的最简单办法。常用的命令式命令如下:

  • 创建对象:run、expose、autoscale和create <objecttype> [<subetype>] <instancename>。
  • 更新对象:scale、annotate、label、set <field>、edit和patch。
  • 删除对象:delete <type>/<name>。
  • 查看对象:get、describe和logs。

2.4.2 命令式对象配置

命令式对象配置管理方式包括create、delete、get和replace等命令。与命令式命令不同,它通过资源配置清单读取要管理的目标资源对象,通用格式为kubectl create|delete|replace|get -f <filename|url>,其中filename和url分别是指以本地文件路径或URL来指定配置清单文件。命令式对象配置的管理操作直接作用于活动对象,因而即时修改配置清单中极小的一部分内容,在使用replace命令进行对象更新时也将会导致整个对象被完全替换。所以,混合使用命令式命令进行清单文件带外修改时,必然导致用户丢失活动对象的当前状态。 

2.4.3 声明式对象配置

声明式对象配置并不直接指明要进行的对象管理操作,而是提供配置清单文件给kubernetes系统,并委托来跟踪活动对象的状态变动。资源对象的创建、删除及修改操作全部通过唯一的命令kubectl apply完成,且每次操作时,提供给命令的配置信息都将保存在对象的注解信息中,它通过对比检查活动对象的当前状态、注解中的配置信息及资源清单中的配置信息进行变更合并,从而实现仅吸怪变动字段的高级补丁机制。

声明式对象配置支持针对目录进行操作,通过在目录中存储多个对象配置文件,然后由apply命令递归地创建、更新或删除这些对象、它保留对活动对象所做的更改操作,但并不会将更改合并回对象配置文件中。例外,kubectl diff命令提供了预览要更待的配置的方法。

  • 创建:kubectl apply -f <directory>/。
  • 更新:kubectl apply -f <directory>/,可以先使用kubectl diff -f <directory>/命令预览。
  • 删除:kubectl apply -f <directory>/ --prune -l your=label,建议使用命令式的方法kubectl delete -f  <filename>。
  • 查看:命令式的方式kubectl get -f <filename|url> -o yaml。

命令式对象配置相较于声明式对象配置来说,其缺点还在于同一目录下的配置文件必须同时进行同一种操作,例如要么都创建,要么都更新等,而且其他用户的更新也必须一并反应在配置文件中,否则将在下一次的变动中被覆盖。但声明式对象配置并无此要求或限制,它仅作用于自己生命的各对象,事先不存在的对象会被创建,而已存在对象则会被保留或者修改以吻合声明中的定义,具体操作取决于活动对象的当前状态与用户声明状态的对比结果。

三 名称空间

由多个独立的团体或项目共享使用的kubernetes集群或许会运行大量彼此无关的工作负载,即时使用需要相对较小,部署的对象数量也可能很快变得难以管理,进而降低操作相应速度并增加发生危险错误的可能性。kubernetes使用名称空间来帮助降低这种复杂性:通过一个抽象组件将存在管理关系的对象组织成一个逻辑担心进行统一管控,可用于对集群中的任意对象组进行分、筛选和管理。

kubernetes名称空间并不能实现pod间的通信隔离,它仅用于限制资源对象名称的作用域。

3.1 名称空间的作用

名称空间的核心功能在于限制集群上对象名称的作用域:同一类型的资源对象名称在同一名称空间中必须唯一,但不同名称空间却可为同一类型的资源使用相同名称。对于某些场景,这种功能有着非常实用的价值,例如名称空间可用于分割不同应用程序的生命周期环境,从而能够在每个环境中维护具有相同名称的同一类型的资源对象。名称空间本身并不具有网络隔离和访问限制等功能,但它可以作为网络访问控制策略(NetworkPolicy)、授权策略(RBAC)、资源限制策略(ResourceQuota)和pod安全策略(PodSecurityPolicy)等管理逻辑的承载组件,这也是支撑集群多租户机制的基础组件。

kubernetes API使用Namespace资源类型来标识名称空间,它自身是集群集群级别的资源,不能嵌套于其它名称空间,但kubernetes的绝大多数资源类型都隶属于名称空间级别,例如pod、Service、Deployment和StatefulSet等。而集群级别的资源类型除了Namespace外还有Node和PersistentVolume等,它们的名称标识作用域为集群全局。

默认情况下,kubernetes集群会内置如下4个名称空间,其中第4个名称空间是为kubelet引入租约机制后才新增的。

  • defalut:创建名称空间级别的资源对象,但未指定从属的名称空间将默认是要defalut名称空间。
  • kube-public:用于为集群上的所有用户提供一个公共可用的名称空间,保留给集群使用,以防某些资源在整个集群中公开可见;该名称空间的公共属性仅是约定,并非强制要求。
  • kube-system:用不部署与kubernetes系统自身相关的组件,例如kube-proxy、CNI网络插件,甚至是kube-apiserver、kube-controller-manager和kube-scheduler等控制平面组件的静态pod等;不建议在该名称空间中运行与系统无关的工作负载。
  • kube-node-lease:目前是专用于放置kubelet lease对象的名称空间,这些对象对于kubernetes系统自身健康运行直观重要;因而同样不建议在该名称空间中运行与系统无关的工作负载。
~# kubectl get ns
NAME                   STATUS   AGE
default                Active   31d
kube-node-lease        Active   31d
kube-public            Active   31d
kube-system            Active   31d

3.2 管理Namespace资源

3.2.1 命令式命令

命令式命令的管理通常是针对集群上的活动对象直接进行,用户或管理员针对指定的资源运行create、get、describe、edit和delete命令即能完成资源的创建、查看、编辑和删除等基础管理操作。例如创建myns名称空间。

  • 创建名称空间:kubectl create namespace myns。
  • 查看名称空间信息:kubectl get namespace myns -o yaml。
  • 打印对象详细信息:kubectl describe namespace myns。
  • 编辑对象信息:kubectl edit namespace myns。
  • 删除对象:kubectl delete namespace myns。 

3.2.2 命令式对象配置

命令式对象配置管理机制,是将资源操作命令作用于资源对象的配置清单进行的管理操作。常用的命令create、delete、replace、get和describe等。

Namespace资源对象的配置格式文档可通过kubectl explain namespace命令得到。

配置清单示例:

~# cat ns-demo.yaml
apiVersion:v1
kind:Namespace
metada:
  name:demo
spce:
  finalizers:
  - kubernetes

Namespace对象仅支持在spec中定义期望使用的终结器(finalizer,也称为垃圾收集器),用于让检测这在删除名称空间时清楚相关的资源,它是个可选字段,且目前仅支持使用kubernetes作为其属性值。在namespace对象上指定了不存在的终结器时并不影响创建操作,但删除该Namespace对象的操作将会被卡主,即删除操作将一直停留在Terminating状态。

  • 创建namespace:kubectl apply -f ns-demo.yaml。
  • 查看namespace:kubectl get -f ns-demo.yaml。
  • 替换namespace:kubectl replace --force -f ns-demo-v2.yaml。
  • 删除namespace:kubectl delete -f ns-demo.yaml。

有时候,名称空间的正常删除操作可能会莫名的卡在Terminating状态。常用的解决办法是获取并保存目标namespace对象的json格式的配置清单,将spec.finzlizers字段的值置空,而后手动终止相应的名称空间对象即可。示例:

  • 获取配置
~# kubectl get namespace/demo -o json > ns-demo-term.json
  • 修改spec.finzlizers的值为空
  • 手动删除名称空间
~# kubectl replace --raw "/api/v1/namespaces/demo/finalize" -f ns-demo-term.json

3.2.3 声明式对象配置

在命令式对象配置管理机制中,若同时指定了多个资源,则这些资源必须进行同一种操作,而且更新操作的replace命令通过完全替代现有活动对象进行资源更新操作,对于生产环境来说,这并非理想的选择。声明式对象配置操作在管理资源对象时会把配置信息保存在目标对象的注解中,并通过比较活动对象的当前配置、前一次管理操作时保存于注解中的配置,以及当前命令提供配置生成更新补丁从而完成活动对象的补丁式更新操作。

四 节点资源

kubernetes把工作节点也抽象成了名为Node的API资源,但在现实中,工作是指需要运行kubelet、kube-proxy和以docker或containerd为代表的容器运行时这三个关键组件的物理服务器或虚拟机,其核心任务在于以pod形式运行工作负载,而这些工作负载将消耗节点上的计算机资源,必要时还会占用一定的存储资源,显然在创建一个pod资源对象时,kubernetes内部无法真正的构建出这个主机设备来,而是仅创建了一个资源对象来代表该主机,真正的主机设备需要由集群外部的服务器商创建或由管理员手动创建。

4.1 节点心跳与节点租约

节点控制器负载Node对象生命周期中的多个管理任务,包括节点注册到集群时的CIDR分配、与服务器交互已维护可用节点列表,以及监控节点的健康状态等。对于每一个Node对象,节点控制器都会根据其元数据字段metadata.name执行健康状态检查,已验证节点是否可用。可用节点意味着它已经运行了kubelet、kube-proxy和容器运行时,满足了运行pod的基本条件。对于那些不可用节点,kubernetes将持续对其健康状态检查知道变为可用节点或由管理员手动删除相应的Noe对象为止。

kubelet是运行于节点之上的主代理程序,它负责从API Server接收并执行由自身承载的pod管理任务,并需要向Master上报自身运行状态以维持集群正常运行。在kubernetes 1.13版本之前,节点心跳通过NodeStatus信息每10秒发送一次。如果在参数node-monitor-grace-period指定的时长内(默认为40秒)没有收到心跳信息,节点控制器将把相应节点标记为NotReady,而如果在参数pod-eviction-timeout指定的时长内仍然没有收到节点的心跳信息,则节点控制器将开始从该节点驱逐pod对象。

考虑到节点之上维持的容器镜像和存储卷等信息,NodeStatus信息在节点上有着较多镜像和存储卷的场景中可能会变得较大,进而也必将影响到etcd的存储效率。因此在kubernetes 1.13版本引入了与NodeStatus协同工作的更加轻便且可扩展的心跳指示器——节点租约(node lease)。每个节点的kubelet负载在专用的名称空间kube-node-lease中创建一个与节点同名的Lease对象以标识心跳信息,该资源隶属于名为coordination.k8s.io的新内置API群组。

root@k8s-master-01:~# kubectl get leases -n kube-node-lease
NAME              HOLDER            AGE
192.168.174.100   192.168.174.100   31d
192.168.174.101   192.168.174.101   31d
192.168.174.102   192.168.174.102   31d
192.168.174.106   192.168.174.106   31d
192.168.174.107   192.168.174.107   31d
192.168.174.108   192.168.174.108   31d

节点租约与NodeStatus协同工作的逻辑如下:

  • kubelet定期更新自己的lease对象,默认10秒;
  • kubelet定期(默认为10秒)计算一次NodeStatus,但并不直接上报给Master;
  • 仅NodeStatus发生变动,或者已经超过了由参数node-status-update-period指定时长(默认5分钟)时,kubelet将发送NodeStatus心跳给Master;

无论是NodeStatus还是Lease对象的更新,都会被节点控制器视为给定的kubelet健康状态信息,但承载节点信息的NodeStatus的平均频率被大大降低,从而显著降低了etcd存储数据的压力,有效改善了系统性能。

4.2 节点状态

查看代码
~# kubectl describe nodes 192.168.174.106
Name:               192.168.174.106
Roles:              node
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=192.168.174.106
                    kubernetes.io/os=linux
                    kubernetes.io/role=node
Annotations:        node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Thu, 11 Nov 2021 11:51:55 +0800
Taints:             <none>
Unschedulable:      false
Lease:
  HolderIdentity:  192.168.174.106
  AcquireTime:     <unset>
  RenewTime:       Sun, 12 Dec 2021 20:44:00 +0800
Conditions:
  Type                 Status  LastHeartbeatTime                 LastTransitionTime                Reason                       Message
  ----                 ------  -----------------                 ------------------                ------                       -------
  NetworkUnavailable   False   Sun, 12 Dec 2021 17:44:58 +0800   Sun, 12 Dec 2021 17:44:58 +0800   CalicoIsUp                   Calico is running on this node
  MemoryPressure       False   Sun, 12 Dec 2021 20:43:36 +0800   Fri, 10 Dec 2021 14:00:25 +0800   KubeletHasSufficientMemory   kubelet has sufficient memory available
  DiskPressure         False   Sun, 12 Dec 2021 20:43:36 +0800   Fri, 10 Dec 2021 14:00:25 +0800   KubeletHasNoDiskPressure     kubelet has no disk pressure
  PIDPressure          False   Sun, 12 Dec 2021 20:43:36 +0800   Fri, 10 Dec 2021 14:00:25 +0800   KubeletHasSufficientPID      kubelet has sufficient PID available
  Ready                True    Sun, 12 Dec 2021 20:43:36 +0800   Fri, 10 Dec 2021 14:09:16 +0800   KubeletReady                 kubelet is posting ready status. AppArmor enabled
Addresses:
  InternalIP:  192.168.174.106
  Hostname:    192.168.174.106
Capacity:
  cpu:                2
  ephemeral-storage:  19475088Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             2006368Ki
  pods:               1100
Allocatable:
  cpu:                2
  ephemeral-storage:  17948241072
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             1699168Ki
  pods:               1100
System Info:
  Machine ID:                 3c54aabcc0cb4377bb76b9da76b7c43f
  System UUID:                76e04d56-16ab-7e92-89aa-af0f8921d7fa
  Boot ID:                    a303f8bd-48e2-4368-b719-394cc7f077df
  Kernel Version:             5.4.0-91-generic
  OS Image:                   Ubuntu 20.04.3 LTS
  Operating System:           linux
  Architecture:               amd64
  Container Runtime Version:  docker://20.10.10
  Kubelet Version:            v1.21.6
  Kube-Proxy Version:         v1.21.6
PodCIDR:                      10.200.4.0/24
PodCIDRs:                     10.200.4.0/24
Non-terminated Pods:          (5 in total)
  Namespace                   Name                                         CPU Requests  CPU Limits  Memory Requests  Memory Limits  Age
  ---------                   ----                                         ------------  ----------  ---------------  -------------  ---
  kube-system                 calico-kube-controllers-6f8794d6c4-8tvwl     0 (0%)        0 (0%)      0 (0%)           0 (0%)         24d
  kube-system                 calico-node-c2f2q                            250m (12%)    0 (0%)      0 (0%)           0 (0%)         31d
  kube-system                 coredns-8568fcb45d-8lrz2                     100m (5%)     0 (0%)      70Mi (4%)        170Mi (10%)    24d
  kubernetes-dashboard        dashboard-metrics-scraper-c5f49cc44-ws7cq    0 (0%)        0 (0%)      0 (0%)           0 (0%)         28d
  kubernetes-dashboard        kubernetes-dashboard-688994654d-9cwwz        0 (0%)        0 (0%)      0 (0%)           0 (0%)         24d
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests    Limits
  --------           --------    ------
  cpu                350m (17%)  0 (0%)
  memory             70Mi (4%)   170Mi (10%)
  ephemeral-storage  0 (0%)      0 (0%)
  hugepages-1Gi      0 (0%)      0 (0%)
  hugepages-2Mi      0 (0%)      0 (0%)
Events:              <none>

节点状态信息可使用kubectl describe nodes [NODE]命令打印,它通常包括节点地址(Addresses)、系统属性(system info)、租约(lease)、污点(Taints)、不可调度性(Unschedulable)、状况(conditions)、系统容量(capacity)、已分资源量(Allocated resource)、可分配容量(Allocatable)和pod的可用地址池(PodCIDR和PodCIDRs)等。

节点地址包括节点IP地址和主机名,其中节点地址包括InternalIP和ExternalIP,前者用于集群内部通信,后者是能够从集群外部路由并访问的IP地址。Pod可用地址池是指为当前节点中允许的pod对象分配IP地址的CIDR格式的网络,若存在多个可用CIDR网络,则将其保存在PodCIDRs属性中。系统信息用户描述主机ID、主机操作系统及标识(UUID)、内核版本、平台架构、kubelet程序版本、kube-proxy程序版本和容器运行时及版本等节点通用信息,它们由kubelet从其所在的节点收集而来。

系统容器用于描述节点上的总体可用资源:CPU、临时存储、大内存页、内存空间,以及可以调度到节点上的pod对象的最大数量等,可分配容量则描述这些资源剩余总量。为了便于同kubernetes上的资源区分开来,CPU和内存通常被称为计算资源,而临时存储空间则被称为存储资源。

已分配资源量用于描述当前节点上CPU、内存和临时存储资源的已分配比例。kubernetes系统上的每个pod在创建时可分别声明其计算资源及存储资源的需求量(request)和限制量(limit),需求量标识运行时一个pod必须确保的某项最新资源,而限制量标识该pod能够申请占用的某项资源的上限。在已分配资源量中,Request代表所有Pod对象声明的资源需求量之和所占节点资源的比例,而Limits则表示所有Pod对象声明的资源限制之和所占节点资源的比例。

kubernetes调度器负责确保每个节点上所有pod对象的总容量需求不会超过节点拥有容量,它计算已有的资源占用量时包括所有由Scheduler调度以及kubelet自身启动的pod对象,但不包括由容器运行时自行启动的容器,也不包括容器外运行的任何进程。若需要为非pod进程显示保留资源,需要使用--system-reserved和--reserved-cpus等选项进行定义。

conditions字段则用于描述当前节点所处的境况或者条件每个条件都需要用布尔值来表达其满足与否的状态。

  • Ready:True标示节点健康且准备好接收pod,False表示不健康且无法接收pod,Unknown表示在最近的40秒内未收到节点的NodeStatus。
  • MemoryPressure:True表示内存资源存在压力,可用空间不足,否则为False。
  • DiskPressure:True表示磁盘存在压力,即磁盘可用量低,否则为False。
  • PIDPressure:True表示进程数量存在压力,即进程过多,否则为False。
  • NetworkUnavailable:True表示网络配置不正确,否则为False。

一旦Ready条件的值不为True的时长超过kube-controller-manager程序的--pod-eviction-timeout选项指定的值(默认5分钟),则改节点上的所有pod对象都将被节点控制器计算删除。不过,当kubelet无法同Master通信时,必然无法接收到删除pod对象的指令,但调度器可能已经将这些pod的替代实例指派到了其它健康的节点之上运行。

五 标签与标签选择器

标签是kubernetes极具特色的功能之一。它是附加在kubernetes任何资源对象之上的键值型数据,常用于标签选择器的匹配度检查,从而完成资源筛选。kubernetes系统的部分基础功能的实现也要依赖标签和标签选择器,例如Service筛选并关联后端pod对象,有ReplicaSet、StatefulSet和Deployment等控制器过滤并关联后端pod对象等,从而提升用户的资源管理效率。

5.1 资源标签

标签可在资源创建时直接指定,也可随时按需添加在活动对象上。一个对象可拥有不止一个标签,而同一个标签也可加至多个对象之上。在实践中,可以为资源附加多个不用维度的标签以实现灵活地资源分组管理功能,例如版本标签、环境标签、分层架构标签等,用于交叉标识同一个资源所属的不同版本、环境及架构层级等。

  • 版本标签:"release":"stable","release":"canary","release":"beta"。
  • 环境标签:"environment":"dev","environment":"release","environment":"prod"。
  • 应用标签:"app":"ui","app":"pc"。

标签中的键名通常由键前缀和键名组成,其格式形式如KEY_PREFIX/KEY_NAME,键前缀为可选部分。键名至多能使用63个字符,支持字母、数字、连接后(-)、下划线(_)、点号(.)等字符,且只能以字符或数字开头。而键前缀必须为DNS子域名格式,且不能超过253个字符。省略键前缀时,键将被视为用户的私有数据。由kubernetes系统组件或第三方组件自动为用户资源添加的键必须使用键前缀,kubernetes.io/os和k8s.kubernetes.io/arch前缀预留给了kubernetes的核心组件使用。

标签的键值必须不能多于63个字符,键值要么为空,要么以字母或数字开头及结尾,且中间只能使用字母、数字、连接号(-)、下划线(_)、或点号(.)等字符。

  • 查看标签信息:kubectl get namespace [namespace] --show-labels。
  • 查看多个标签信息:kubectl get namespace [namespace] -L key1 key2。
  • 管理标签:kubectl label namespace/[namespace] key=value --overwrite。

5.2 标签选择器

标签选择器用于表达标签的查询条件或选择标准,目前kubernetes API支持两个选择器,基于等值关系(equality=based)的标签选择器与基于集合关系(set-based)的标签选择器。同时,在指定多个选择器时需要以逗号分割,各选择器之间循序逻辑"与",即必要要满足所有条件,而且空值的选择器将不选择任何对象。

基于等值关系的标签选择器的可用操作符有=、==和!=,其中前两个意义相同,都表示等值关系,最后一个表示不等。

基于集合的标签选择器则根据标签名的一组值进行筛选,它支持in、notin和exists这3中操作符。

  • KEY in(VALUE1,VALUE1,...):指定键名的值存在于给定的列表中即满足条件。
  • KEY notin(VALUE1,VALUE1,...):指定键名的值不存在于给定列表中即满足条件。
  • KEY:所有存在此键名标签的资源。
  • !KEY:所有不存在此键名标签的资源。

kubectl get命令的“-l”选项能够指定使用标签选择器筛选目标资源。

为了避免叹号(!)被shell解析器解析必须要为此类型表达式使用单引号。

此外,kubernetes的诸多资源对象必须以标签选择的方式关联到pod资源对象,例如Service资源在spec字段中嵌套使用selector字段定义标签过滤器,而Deployment和StatefulSet等资源在selector字段中通过matchLables和matchExpressions构造复杂的标签选择机制。

  • matchLables:直接给定键值对指定标签选择器。
  • matchExpressions:基于表达式指定的标签选择器列表,每个选择器形如{key:KEY_NAME,operator:OPERATOR,value:[]value1,value2,...}选择器列表间为"逻辑与"关系;使用In或NoIn操作符时,其values必须为分空的字符串列表,而使用Exists或DostNotExist时,其value必须为空。

六 资源注解

除了标签之外,kubernetes的API对象还支持使用资源注解(annotations)。类似于标签,注解也是键值型数据,不过它不能用作标签,也不能用于挑选API对象,资源注解的核心目的是为资源提供元数据信息,但注解的值不受字符数量限制,它可大可小可以是结构化或非结构化形式,也支持在标签中禁用其它字符。

资源注解可由用户手动添加,也可由工具程序自动附加并使用,在kubernetes的新版本中计划为某资源引入新字段时常以注解方式提供。以避免字段名称变更、增删等变动给用户带去困扰,一旦确定使用这些新增字段,则将其引入资源规范中并淘汰相关的注解信息。另外。为资源添加注解也可让其它用户快速了解资源的相关信息。例如创建者身份等。

6.1 资源注解常用场景

  • 由声明式配置管理的字段,将这些字段定义为注解,有助于识别由服务器或客户端设定的默认值、系统自动生成的字段以及由自动伸缩系统生成的字段。
  • 构建、发行或镜像相关的信息,例如时间戳、发行ID、Git分支、镜像哈希及仓库地址等。
  • 指定日志、监控、分析或审计仓库的指针。
  • 由客户端库或工具程序生成的用于调试目的的信息,例如名称、版本、构建信息等。
  • 用户或工具程序的来源地信息,例如来自其它生态信息组件的相关对象的URL。
  • 轻量化滚动升级工具的元数据,例如config及checkpoints。
  • 相关人员的电话号码等联系信息,或指向类似信息的可寻址的目录条目,例如网站站点。

6.2 查看资源的注解信息

查看代码
~# kubectl get nodes 192.168.174.106 -o yaml
apiVersion: v1
kind: Node
metadata:
  annotations:
    node.alpha.kubernetes.io/ttl: "0"
    volumes.kubernetes.io/controller-managed-attach-detach: "true"
  creationTimestamp: "2021-11-11T03:51:55Z"
  labels:
    beta.kubernetes.io/arch: amd64
    beta.kubernetes.io/os: linux
    kubernetes.io/arch: amd64
    kubernetes.io/hostname: 192.168.174.106
    kubernetes.io/os: linux
    kubernetes.io/role: node
  name: 192.168.174.106
  resourceVersion: "852508"
  uid: e4e78d38-85b3-4519-89d4-e6f6ec4a196d
spec:
  podCIDR: 10.200.4.0/24
  podCIDRs:
  - 10.200.4.0/24
status:
  addresses:
  - address: 192.168.174.106
    type: InternalIP
  - address: 192.168.174.106
    type: Hostname
  allocatable:
    cpu: "2"
    ephemeral-storage: "17948241072"
    hugepages-1Gi: "0"
    hugepages-2Mi: "0"
    memory: 1699168Ki
    pods: "1100"
  capacity:
    cpu: "2"
    ephemeral-storage: 19475088Ki
    hugepages-1Gi: "0"
    hugepages-2Mi: "0"
    memory: 2006368Ki
    pods: "1100"
  conditions:
  - lastHeartbeatTime: "2021-12-12T09:44:58Z"
    lastTransitionTime: "2021-12-12T09:44:58Z"
    message: Calico is running on this node
    reason: CalicoIsUp
    status: "False"
    type: NetworkUnavailable
  - lastHeartbeatTime: "2021-12-12T13:43:50Z"
    lastTransitionTime: "2021-12-10T06:00:25Z"
    message: kubelet has sufficient memory available
    reason: KubeletHasSufficientMemory
    status: "False"
    type: MemoryPressure
  - lastHeartbeatTime: "2021-12-12T13:43:50Z"
    lastTransitionTime: "2021-12-10T06:00:25Z"
    message: kubelet has no disk pressure
    reason: KubeletHasNoDiskPressure
    status: "False"
    type: DiskPressure
  - lastHeartbeatTime: "2021-12-12T13:43:50Z"
    lastTransitionTime: "2021-12-10T06:00:25Z"
    message: kubelet has sufficient PID available
    reason: KubeletHasSufficientPID
    status: "False"
    type: PIDPressure
  - lastHeartbeatTime: "2021-12-12T13:43:50Z"
    lastTransitionTime: "2021-12-10T06:09:16Z"
    message: kubelet is posting ready status. AppArmor enabled
    reason: KubeletReady
    status: "True"
    type: Ready
  daemonEndpoints:
    kubeletEndpoint:
      Port: 10250
  images:
  - names:
    - 192.168.174.120/baseimages/dashboard@sha256:2d2ac5c357a97715ee42b2186fda39527b826fdd7df9f7ade56b9328efc92041
    - 192.168.174.120/baseimages/dashboard:v2.4.0
    sizeBytes: 221371283
  - names:
    - quay.io/prometheus/prometheus@sha256:ccc801f38fdac43f0ed3e1b0220777e976828d6558f8ef3baad9028e0d1797ae
    - quay.io/prometheus/prometheus:v2.29.1
    sizeBytes: 196072522
  - names:
    - 192.168.174.120/baseimages/calico-node@sha256:5011b14fb2519164d2bd706e56c7082d9082cc0c92c619aad7d457e670103c26
    - 192.168.174.120/baseimages/calico-node:v3.19.2
    - calico/node:v3.19.2
    sizeBytes: 168633019
  - names:
    - 192.168.174.120/baseimages/cadvisor@sha256:3329aedd84d6222e655a7002d06b58db0f173e6a6b4faa65dda6381ef9a8ce2b
    - 192.168.174.120/baseimages/cadvisor:v0.38.8
    sizeBytes: 163766791
  - names:
    - 192.168.174.120/baseimages/calico-cni@sha256:00a619424a20d1b31c0f65d1194484aebe9f90dfc1d4d6396ce706941004860b
    - 192.168.174.120/baseimages/calico-cni:v3.19.2
    - calico/cni:v3.19.2
    sizeBytes: 145858973
  - names:
    - 192.168.174.120/baseimages/calico-kube-controllers@sha256:69828a75e6ecebb6ef2582482e56308b5b6c84275e481ea8024aac674f898ccd
    - 192.168.174.120/baseimages/calico-kube-controllers:v3.19.2
    - calico/kube-controllers:v3.19.2
    sizeBytes: 60606985
  - names:
    - quay.io/brancz/kube-rbac-proxy@sha256:b62289c3f3f883ee76dd4e8879042dd19abff743340e451cb59f9654fc472e4f
    - quay.io/brancz/kube-rbac-proxy:v0.11.0
    sizeBytes: 46556979
  - names:
    - 192.168.174.120/baseimages/coredns@sha256:db4f1c57978d7372b50f416d1058beb60cebff9a0d5b8bee02bfe70302e1cb2f
    - coredns/coredns@sha256:642ff9910da6ea9a8624b0234eef52af9ca75ecbec474c5507cb096bdfbae4e5
    - 192.168.174.120/baseimages/coredns:v1.8.3
    - coredns/coredns:1.8.3
    sizeBytes: 43499235
  - names:
    - 192.168.174.120/baseimages/metrics-scraper@sha256:76eb73afa0198ac457c760887ed7ebfa2f58adc09205bd9667b3f76652077a71
    - 192.168.174.120/baseimages/metrics-scraper:v1.0.7
    sizeBytes: 34446077
  - names:
    - 192.168.174.120/baseimages/calico-pod2daemon-flexvol@sha256:5601fd8584c89f85317aaae918827615c24a166f440e349566a3ff48b2014072
    - 192.168.174.120/baseimages/calico-pod2daemon-flexvol:v3.19.2
    - calico/pod2daemon-flexvol:v3.19.2
    sizeBytes: 21746528
  - names:
    - quay.io/prometheus/node-exporter@sha256:a990408ed288669bbad5b5b374fe1584e54825cde4a911c1a3d6301a907a030c
    - quay.io/prometheus/node-exporter:v1.2.2
    sizeBytes: 21171152
  - names:
    - quay.io/prometheus-operator/prometheus-config-reloader@sha256:61bd63e7bc1aaebd39983d2c118a453e59427ccaa1b188cbadd4d0bded415d17
    - quay.io/prometheus-operator/prometheus-config-reloader:v0.49.0
    sizeBytes: 12425417
  - names:
    - 192.168.174.120/baseimages/alpine@sha256:69704ef328d05a9f806b6b8502915e6a0a4faa4d72018dc42343f511490daf8a
    - 192.168.174.120/baseimages/alpine:latest
    sizeBytes: 5595292
  - names:
    - 192.168.174.120/baseimages/pause-amd64@sha256:2f4b437353f90e646504ec8317dacd6123e931152674628289c990a7a05790b0
    - 192.168.174.120/baseimages/pause-amd64:3.5
    - easzlab/pause-amd64:3.5
    sizeBytes: 682696
  nodeInfo:
    architecture: amd64
    bootID: a303f8bd-48e2-4368-b719-394cc7f077df
    containerRuntimeVersion: docker://20.10.10
    kernelVersion: 5.4.0-91-generic
    kubeProxyVersion: v1.21.6
    kubeletVersion: v1.21.6
    machineID: 3c54aabcc0cb4377bb76b9da76b7c43f
    operatingSystem: linux
    osImage: Ubuntu 20.04.3 LTS
    systemUUID: 76e04d56-16ab-7e92-89aa-af0f8921d7fa

6.3 资源注解管理

查看代码
~# kubectl annotate -h
Update the annotations on one or more resources

All Kubernetes objects support the ability to store additional data with the object as annotations. Annotations are
key/value pairs that can be larger than labels and include arbitrary string values such as structured JSON. Tools and
system extensions may use annotations to store their own data.

Attempting to set an annotation that already exists will fail unless --overwrite is set. If --resource-version is
specified and does not match the current resource version on the server the command will fail.

Use "kubectl api-resources" for a complete list of supported resources.

Examples:

Update pod 'foo' with the annotation 'description' and the value 'my frontend'.

If the same annotation is set multiple times, only the last value will be applied

kubectl annotate pods foo description='my frontend'

Update a pod identified by type and name in "pod.json"

kubectl annotate -f pod.json description='my frontend'

Update pod 'foo' with the annotation 'description' and the value 'my frontend running nginx', overwriting any

existing value.
kubectl annotate --overwrite pods foo description='my frontend running nginx'

Update all pods in the namespace

kubectl annotate pods --all description='my frontend running nginx'

Update pod 'foo' only if the resource is unchanged from version 1.

kubectl annotate pods foo description='my frontend running nginx' --resource-version=1

Update pod 'foo' by removing an annotation named 'description' if it exists.

Does not require the --overwrite flag.

kubectl annotate pods foo description-

Options:
--all=false: Select all resources, including uninitialized ones, in the namespace of the specified resource types.
--allow-missing-template-keys=true: If true, ignore any errors in templates when a field or map key is missing in
the template. Only applies to golang and jsonpath output formats.
--dry-run='none': Must be "none", "server", or "client". If client strategy, only print the object that would be
sent, without sending it. If server strategy, submit server-side request without persisting the resource.
--field-manager='kubectl-annotate': Name of the manager used to track field ownership.
--field-selector='': Selector (field query) to filter on, supports '=', '', and '!='.(e.g. --field-selector
key1=value1,key2=value2). The server only supports a limited number of field queries per type.
-f, --filename=[]: Filename, directory, or URL to files identifying the resource to update the annotation
-k, --kustomize='': Process the kustomization directory. This flag can't be used together with -f or -R.
--list=false: If true, display the annotations for a given resource.
--local=false: If true, annotation will NOT contact api-server but run locally.
-o, --output='': Output format. One of:
json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-as-json|jsonpath-file.
--overwrite=false: If true, allow annotations to be overwritten, otherwise reject annotation updates that
overwrite existing annotations.
--record=false: Record current kubectl command in the resource annotation. If set to false, do not record the
command. If set to true, record the command. If not set, default to updating the existing annotation value only if one
already exists.
-R, --recursive=false: Process the directory used in -f, --filename recursively. Useful when you want to manage
related manifests organized within the same directory.
--resource-version='': If non-empty, the annotation update will only succeed if this is the current
resource-version for the object. Only valid when specifying a single resource.
-l, --selector='': Selector (label query) to filter on, not including uninitialized ones, supports '=', '
', and
'!='.(e.g. -l key1=value1,key2=value2).
--show-managed-fields=false: If true, keep the managedFields when printing objects in JSON or YAML format.
--template='': Template string or path to template file to use when -o=go-template, -o=go-template-file. The
template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].

Usage:
kubectl annotate [--overwrite] (-f FILENAME | TYPE NAME) KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]
[options]

Use "kubectl options" for a list of global command-line options (applies to all commands).

 

posted @ 2022-05-13 16:12  小吉猫  阅读(524)  评论(0编辑  收藏  举报