文章 87
评论 0
浏览 460507
OpenKruise插件

OpenKruise插件

一、OpenKruise介绍

OpenKruise是一个基于Kubernetes的扩展套件,他提供的绝大部分能力都是基于CRD扩展来定义,他们不存在任何外部依赖,可以运行在任意纯净的Kubernetes集群中。简单来说OpenKruise对于Kubernetes是一个辅助扩展角色。Kubernetes自身已经提供了一些应用部署管理的功能,比如一些基础工作负载。 但对于大规模应用与集群的场景,这些基础功能是远远不够的。OpenKruise可以被很容易地安装到任意Kubernetes集群中,它弥补了Kubernetes在应用部署、升级、防护、运维等领域的不足。

  • OpenKruise包含了一系列增强版本的Workloads(工作负载),比如CloneSet、Advanced StatefulSet、Advanced DaemonSet、BroadcastJob等,它们不仅支持类似于 Kubernetes 原生 Workloads 的基础功能,还提供了如原地升级、可配置的扩缩容/发布策略、并发操作等。其中,原地升级是一种升级应用容器镜像甚至环境变量的全新方式,它只会用新的镜像重建 Pod 中的特定容器,整个 Pod 以及其中的其他容器都不会被影响。因此它带来了更快的发布速度,以及避免了对其他 Scheduler、CNI、CSI 等组件的负面影响。
  • OpenKruise提供了多种通过旁路管理应用sidecar容器、多区域部署的方式,“旁路”意味着你可以不需要修改应用的Workloads来实现它们。比如,SidecarSet能帮助你在所有匹配的Pod创建的时候都注入特定的sidecar容器,甚至可以原地升级已经注入的sidecar容器镜像、并且对Pod中其他容器不造成影响。而WorkloadSpread可以约束无状态Workload扩容出来Pod的区域分布,赋予单一workload的多区域和弹性部署的能力。
  • OpenKruise在为应用的高可用性防护方面也做出了很多努力。目前它可以保护你的Kubernetes资源不受级联删除机制的干扰,包括CRD、Namespace、以及几乎全部的Workloads类型资源。相比于Kubernetes原生的PDB只提供针对Pod Eviction的防护,PodUnavailableBudget能够防护 Pod Deletion、Eviction、Update 等许多种 voluntary disruption 场景。
  • OpenKruise 也提供了很多高级的运维能力来帮助你更好地管理应用。你可以通过ImagePullJob来在任意范围的节点上预先拉取某些镜像,或者指定某个 Pod中的一个或多个容器被原地重启。

1.1 系统架构

它是一个Kubernetes的标准扩展套件,目前包括 kruise-managerkruise-daemon 两个组件。 PaaS 平台可以通过使用 OpenKruise 提供的这些扩展功能,来使得应用部署、管理流程更加强大与高效。

alt

所有 OpenKruise 的功能都是通过 Kubernetes API 来提供

$ kubectl get crd | grep kruise.io
advancedcronjobs.apps.kruise.io            2022-09-01T02:24:53Z
broadcastjobs.apps.kruise.io               2022-09-01T02:24:53Z
clonesets.apps.kruise.io                   2022-09-01T02:24:53Z
containerrecreaterequests.apps.kruise.io   2022-09-01T02:24:53Z
daemonsets.apps.kruise.io                  2022-09-01T02:24:53Z
imagepulljobs.apps.kruise.io               2022-09-01T02:24:53Z
nodeimages.apps.kruise.io                  2022-09-01T02:24:53Z
persistentpodstates.apps.kruise.io         2022-09-01T02:24:53Z
podunavailablebudgets.policy.kruise.io     2022-09-01T02:24:53Z
resourcedistributions.apps.kruise.io       2022-09-01T02:24:53Z
sidecarsets.apps.kruise.io                 2022-09-01T02:24:53Z
statefulsets.apps.kruise.io                2022-09-01T02:24:53Z
uniteddeployments.apps.kruise.io           2022-09-01T02:24:53Z
workloadspreads.apps.kruise.io             2022-09-01T02:24:53Z

Kruise-manager 是一个运行 controller 和 webhook 中心组件,它通过 Deployment 部署在 kruise-system 命名空间中。

$ kubectl get pod -n kruise-system -l control-plane=controller-manager
NAME                                         READY   STATUS    RESTARTS   AGE
kruise-controller-manager-5df958b8c7-29xlv   1/1     Running   0          3m10s
kruise-controller-manager-5df958b8c7-wmqr4   1/1     Running   0          3m10s

除了 controller 之外,kruise-controller-manager-xxx 中还包含了针对 Kruise CRD 以及 Pod 资源的 admission webhook。Kruise-manager 会创建一些 webhook configurations 来配置哪些资源需要感知处理、以及提供一个 Service 来给 kube-apiserver 调用。

$ kubectl get svc -n kruise-system
NAME                     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
kruise-webhook-service   ClusterIP   172.20.84.74   <none>        443/TCP   4m16s

上述的 kruise-webhook-service 非常重要,是提供给 kube-apiserver 调用的。

它通过 DaemonSet 部署到每个 Node 节点上,提供镜像预热、容器重启等功能。

$ kubectl get pod -n kruise-system -l control-plane=daemon
NAME                  READY   STATUS    RESTARTS   AGE
kruise-daemon-4f2dh   1/1     Running   0          5m1s
kruise-daemon-4fhtz   1/1     Running   0          5m1s
kruise-daemon-4k2fp   1/1     Running   0          5m1s
kruise-daemon-4sdzz   1/1     Running   0          5m1s
kruise-daemon-5cm9b   1/1     Running   0          5m1s
kruise-daemon-7lr9j   1/1     Running   0          5m1s
kruise-daemon-8rnzw   1/1     Running   0          5m1s
kruise-daemon-bcpn4   1/1     Running   0          5m1s
kruise-daemon-f99jj   1/1     Running   0          5m1s
kruise-daemon-ftp22   1/1     Running   0          5m1s
kruise-daemon-g67jb   1/1     Running   0          5m1s
kruise-daemon-l6dpt   1/1     Running   0          5m1s
kruise-daemon-mm6sn   1/1     Running   0          5m1s
kruise-daemon-mskn9   1/1     Running   0          5m1s
kruise-daemon-nn56x   1/1     Running   0          5m1s
kruise-daemon-q57ws   1/1     Running   0          5m1s
kruise-daemon-qvmgr   1/1     Running   0          5m1s
kruise-daemon-rsj2w   1/1     Running   0          5m1s
kruise-daemon-ssfzv   1/1     Running   0          5m1s
kruise-daemon-z48z5   1/1     Running   0          5m1s
kruise-daemon-z76px   1/1     Running   0          5m1s

1.2 OpenKruise部署

OpenKruise要求在 Kubernetes >= 1.16 以上版本的集群中安装和使用。建议采用 helm v3.5+ 来安装 Kruise。

#添加仓库
helm repo add openkruise https://openkruise.github.io/charts/
#下载chart包
helm pull openkruise/kruise
#解压包
tar xf kruise-1.2.0.tgz
#修改配置主要修改values.yaml中镜像
  image:
    repository: 192.168.6.77:5000/kruise-manager                             
    tag: v1.2.0
#部署
helm install kruise .
#验证
kubectl get pod -n kruise-system

以上部署都已默认配置部署,如果需要修改请查看官方文档:https://openkruise.io/zh/docs/installation#%E5%8F%AF%E9%80%89-chart-%E5%AE%89%E8%A3%85%E5%8F%82%E6%95%B0

二、OpenKruise使用详解

2.1 原地升级

原地升级是 OpenKruise 提供的核心功能之一,目前支持原地升级的Workload:

  • CloneSet
  • Advanced StatefulSet
  • Advanced DaemonSet
  • SidecarSet

重建升级时我们要删除旧 Pod、创建新 Pod:

  • Pod 名字和 uid 发生变化,因为它们是完全不同的两个 Pod 对象(比如 Deployment 升级)
  • Pod 名字可能不变、但 uid 变化,因为它们是不同的 Pod 对象,只是复用了同一个名字(比如 StatefulSet 升级)
  • Pod 所在 Node 名字发生变化,因为新 Pod 很大可能性是不会调度到之前所在的 Node 节点的
  • Pod IP 发生变化,因为新 Pod 很大可能性是不会被分配到之前的 IP 地址的

但是对于原地升级,我们仍然复用同一个 Pod 对象,只是修改它里面的字段。因此:

  • 可以避免如 调度分配 IP分配、挂载盘 等额外的操作和代价
  • 更快的镜像拉取,因为开源复用已有旧镜像的大部分 layer 层,只需要拉取新镜像变化的一些 layer
  • 当一个容器在原地升级时,Pod 中的其他容器不会受到影响,仍然维持运行

提供了三种升级方式:

  • ReCreate: 控制器会删除旧 Pod 和它的 PVC,然后用新版本重新创建出来。
  • InPlaceIfPossible: 控制器会优先尝试原地升级 Pod,如果不行再采用重建升级。
  • InPlaceOnly: 控制器只允许采用原地升级。因此,用户只能修改上一条中的限制字段,如果尝试修改其他字段会被 Kruise 拒绝。
apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
spec:
# ...
  updateStrategy:
    type: InPlaceIfPossible
# ...
  1. 修改 app-image:v1 镜像,会触发原地升级。
  2. 修改 annotations 中 app-config 的 value 内容,会触发原地升级。
  3. 同时修改上述两个字段,会在原地升级中同时更新镜像和环境变量。
  4. 直接修改 env 中 APP_NAME 的 value 内容或者新增 env 等其他操作,会触发 Pod 重建升级。

2.2 CloneSet

CloneSet控制器提供了高效管理无状态应用的能力,它可以对标原生的 Deployment,但 CloneSet 提供了很多增强功能。

文件示例

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
metadata:
  labels:
    app: cloneset-dev
  name: cloneset-dev
  namespace: kri-dev
spec:
  replicas: 1
  scaleStrategy:
    podsToDelete:
    - cloneset-dev-r25n8
  selector:
    matchLabels:
      app: cloneset-dev
  template:
    metadata:
      labels:
        app: cloneset-dev
    spec:
      containers:
      - name: net
        image: 192.168.6.77:5000/alpine:net
      - name: alpine
        image: 192.168.6.77:5000/riped/alpine:3.8
        command:
        - sh
        - -c
        - "sleep 360000"
        volumeMounts:
        - name: cloneset-data
          mountPath: /data1
  volumeClaimTemplates:
    - metadata:
        name: cloneset-data
      spec:
        accessModes: [ "ReadWriteMany" ]
        storageClassName: "managed-nfs-storage"
        resources:
          requests:
            storage: 1Gi

1.支持PVC模板

CloneSet 允许用户配置 PVC 模板 volumeClaimTemplates,用来给每个 Pod 生成独享的 PVC,这是 Deployment 所不支持的。 如果用户没有指定这个模板,CloneSet 会创建不带 PVC 的 Pod。

  • 每个被自动创建的PVC会有一个ownerReference指向CloneSet,因此CloneSet被删除时,它创建的所有Pod和PVC都会被删除。
  • 每个被CloneSet创建的Pod和PVC,都会带一个 apps.kruise.io/cloneset-instance-id: xxx 的 label。关联的Pod和PVC会有相同的 instance-id,且它们的名字后缀都是这个 instance-id
  • 如果一个Pod被CloneSet controller缩容删除时,这个Pod关联的PVC都会被一起删掉。
  • 如果一个Pod被外部直接调用删除或驱逐时,这个Pod关联的PVC还都存在;并且CloneSet controller发现数量不足重新扩容时,新扩出来的 Pod 会复用原Pod的instance-id 并关联原来的PVC。
  • 当Pod被重建升级时,关联的PVC会跟随Pod一起被删除、新建。
  • 当Pod被原地升级时,关联的PVC会持续使用。
  • 注意必须配置访问模式,否则会报错。

2.指定Pod缩容

当一个 CloneSet 被缩容时,有时候用户需要指定一些 Pod 来删除。这对于 StatefulSet 或者 Deployment 来说是无法实现的,因为 StatefulSet 要根据序号来删除 Pod,而 Deployment/ReplicaSet 目前只能根据控制器里定义的排序来删除。

CloneSet 允许用户在缩小 replicas 数量的同时,指定想要删除的 Pod 名字。

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
#...
spec:
  replicas: 1   #指定缩容后的副本
  scaleStrategy:  #添加   
    podsToDelete:  #指定缩容被删除的pod名称
    - cloneset-dev-r25n8
#...

如果你只把 Pod 名字加到 podsToDelete,但没有修改 replicas 数量,那么控制器会先把指定的 Pod 删掉,然后再扩一个新的 Pod。 另一种直接删除 Pod 的方式是在要删除的 Pod 上打 apps.kruise.io/specified-delete: true 标签。

kubectl label pod -n kri-dev cloneset-dev-dkbmb apps.kruise.io/specified-delete=true

3.扩容相关

CloneSet 扩容时可以指定 ScaleStrategy.MaxUnavailable 来限制扩容的步长,以达到服务应用影响最小化的目的。 它可以设置为一个绝对值或者百分比,如果不填,则 Kruise 会设置为默认值为 nil,即表示不设限制。

该字段可以配合 Spec.MinReadySeconds 字段使用。

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
metadata:
#..
spec:
  replicas: 10
  minReadySeconds: 15
  scaleStrategy:
    maxUnavailable: 1
#..

上述配置能达到的效果是:在扩容时,只有当上一个扩容出的 Pod 已经 Ready 超过15秒后,CloneSet 才会执行创建下一个 Pod 的操作。

2.3 Advanced StatefulSet

这个控制器基于原生StatefulSet上增强了发布能力,比如 maxUnavailable 并行发布、原地升级等。

如果你对原生StatefulSet不是很了解,我们强烈建议你先阅读它的文档(在学习 Advanced StatefulSet 之前):

注意 Advanced StatefulSet 是一个 CRD,kind 名字也是 StatefulSet,但是 apiVersion 是 apps.kruise.io/v1beta1。 这个 CRD 的所有默认字段、默认行为与原生 StatefulSet 完全一致,除此之外还提供了一些 optional 字段来扩展增强的策略。

示例文件:

apiVersion: v1
kind: Service
metadata:
  name: statefulset
  namespace: kri-dev
spec:
  clusterIP: None  
  selector:
    app: statefulset
---
apiVersion: apps.kruise.io/v1beta1
kind: StatefulSet  
metadata:   
  name: statefulset
  namespace: kri-dev
spec:       
  replicas: 3     
  selector: 
    matchLabels:  
      app: statefulset
  serviceName: statefulset
  template:             
    metadata:
      labels:
        app: statefulset
    spec:
      containers:
      - name: net
        image: 192.168.6.77:5000/alpine:net
      - name: alpine
        image: 192.168.6.77:5000/riped/alpine:3.8
        command:
        - sh
        - -c
        - "sleep 360000"

1.MaxUnavailable最大不可用

Advanced StatefulSet 在 RollingUpdateStatefulSetStrategy 中新增了 maxUnavailable 策略来支持并行 Pod 发布,它会保证发布过程中最多有多少个 Pod 处于不可用状态。注意,maxUnavailable 只能配合 podManagementPolicy 为 Parallel 来使用。

apiVersion: apps.kruise.io/v1beta1
kind: StatefulSet
metadata:
#...
spec:
  podManagementPolicy: Parallel
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 50%
#...

2.原地升级

Advanced StatefulSet 增加了 podUpdatePolicy 来允许用户指定重建升级还是原地升级。

  • ReCreate: 控制器会删除旧 Pod 和它的 PVC,然后用新版本重新创建出来。
  • InPlaceIfPossible: 控制器会优先尝试原地升级 Pod,如果不行再采用重建升级。具体参考下方阅读文档。
  • InPlaceOnly: 控制器只允许采用原地升级。因此,用户只能修改上一条中的限制字段,如果尝试修改其他字段会被 Kruise 拒绝。

我们还在原地升级中提供了 graceful period 选项,作为优雅原地升级的策略。用户如果配置了 gracePeriodSeconds 这个字段,控制器在原地升级的过程中会先把 Pod status 改为 not-ready,然后等一段时间(gracePeriodSeconds),最后再去修改 Pod spec 中的镜像版本。 这样,就为 endpoints-controller 这些控制器留出了充足的时间来将 Pod 从 endpoints 端点列表中去除。

更重要的是,如果使用 InPlaceIfPossibleInPlaceOnly 策略,必须要增加一个 InPlaceUpdateReady readinessGate,用来在原地升级的时候控制器将 Pod 设置为 NotReady。

apiVersion: apps.kruise.io/v1beta1
kind: StatefulSet  
metadata:   
  name: statefulset
  namespace: kri-dev
spec:       
  podManagementPolicy: Parallel
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 50%
      podUpdatePolicy: InPlaceIfPossible
      inPlaceUpdateStrategy:
        gracePeriodSeconds: 10
  replicas: 5     
  selector: 
    matchLabels:  
      app: statefulset
  serviceName: statefulset
  template:             
    metadata:
      labels:
        app: statefulset
    spec:
      readinessGates:
      - conditionType: InPlaceUpdateReady
      containers:
      - name: net
        image: 192.168.6.77:5000/alpine:net
      - name: alpine
        image: 192.168.6.77:5000/riped/alpine:3.8 
        command:
        - sh
        - -c
        - "sleep 360000"

2.4 BroadcastJob

这个控制器将 Pod 分发到集群中每个 node 上,类似于 DaemonSet, 但是 BroadcastJob 管理的 Pod 并不是长期运行的 daemon 服务,而是类似于 Job的任务类型 Pod。最终在每个 node 上的 Pod 都执行完成退出后,BroadcastJob 和这些 Pod 并不会占用集群资源。 这个控制器非常有利于做升级基础软件、巡检等过一段时间需要在整个集群中跑一次的工作。此外,BroadcastJob 还可以维持每个 node 跑成功一个 Pod 任务。如果采取这种模式,当后续集群中新增 node 时 BroadcastJob 也会分发 Pod 任务上去执行。

示例文件:

apiVersion: apps.kruise.io/v1alpha1
kind: BroadcastJob
metadata:
  name: broadcastjob
  namespace: kri-dev
spec:
  template:
    spec:
      containers:
        - name: ps
          image: 192.168.6.77:5000/alpine:net
          command: 
          - sh
          - -c 
          - ps 
      restartPolicy: Never
      hostPID: true
  completionPolicy:
    type: Always
    ttlSecondsAfterFinished: 30  #pod执行完成多少秒后被删除
    activeDeadlineSeconds: 10    #pod执行多久被判断为失败

验证状态

[root@k8s-master-1-kty-sc test]# kubectl get bcj -n kri-dev 
NAME           DESIRED   ACTIVE   SUCCEEDED   FAILED   AGE
broadcastjob   20        0        20          0        99s

2.5 AdvancedCronJob

AdvancedCronJob 是对于原生 CronJob 的扩展版本。 后者根据用户设置的 schedule 规则,周期性创建 Job 执行任务,而 AdvancedCronJob 的 template 支持多种不同的job资源。

示例文件:

apiVersion: apps.kruise.io/v1alpha1
kind: AdvancedCronJob
metadata:
  name: advancedcronjob
  namespace: kri-dev
spec:
  schedule: "*/1 * * * *"
  template:
    broadcastJobTemplate:
      spec:
        template:
          spec:
            containers:
              - name: ps
                image: 192.168.6.77:5000/alpine:net
                command: 
                - sh
                - -c 
                - ps 
            restartPolicy: Never
            hostPID: true
        completionPolicy:
          type: Always
          activeDeadlineSeconds: 10
          ttlSecondsAfterFinished: 30

2.6 SidecarSet

这个控制器支持通过 admission webhook 来自动为集群中创建的符合条件的 Pod 注入 sidecar 容器。 这个注入过程和 istio的自动注入方式很类似。 除了在 Pod 创建时候注入外,SidecarSet 还提供了为运行时 Pod 原地升级其中已经注入的 sidecar 容器镜像的能力。

示例文件:

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
  name: test-sidecarset
  namespace: kri-dev
spec:
  updateStrategy:  #更新策略
    type: RollingUpdate  #滚动更新
    maxUnavailable: 20%  #最大不可用数量
  injectionStrategy:  #暂停注入
    paused: true
  selector:
    matchLabels:
      zhangzhuo: zhangzhuo
  containers:
  - name: sidecar1
    image: 192.168.6.77:5000/alpine:net
    command: ["sleep", "999d"] # do nothing at all
    podInjectPolicy: BeforeAppContainer   #注入方式,默认为BeforeAppContainer注入到源pod的containers前面,AfterAppContainer后面
    shareVolumePolicy:                    #共享注入pod的所有挂载卷
      type: enabled
    transferEnv:                          #共享容器环境变量
    - sourceContainerName: main           #那个容器
      envName: PROXY_IP                   #那个变量

2.7 WorkloadSpread

WorkloadSpread能够将workload的Pod按一定规则分布到不同类型的Node节点上,赋予单一workload多区域部署和弹性部署的能力。

常见的一些规则包括:

  • 水平打散(比如按host、az等维度的平均打散)。
  • 按指定比例打散(比如按比例部署Pod到几个指定的 az 中)。
  • 带优先级的分区管理,比如:
    • 优先部署到ecs,资源不足时部署到eci。
    • 优先部署固定数量个pod到ecs,其余到eci。
  • 定制化分区管理,比如:
    • 控制workload部署不同数量的Pod到不同的cpu架构上。
    • 确保不同的cpu架构上的Pod配有不同的资源配额。

示例文件如下:

apiVersion: apps.kruise.io/v1alpha1
kind: WorkloadSpread
metadata:
  name: workloadspread-demo
spec:
  targetRef:  #管理的资源
    apiVersion: apps/v1 | apps.kruise.io/v1alpha1   
    kind: Deployment | CloneSet
    name: workload-xxx
  subsets:
    - name: subset-a  #区域名称
      requiredNodeSelectorTerm:  #强制匹配到某个zone
        matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
              - zone-a
    preferredNodeSelectorTerms: #尽量匹配到某个zone
      - weight: 1
        preference:
        matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
              - another-node-label-value
      maxReplicas: 3  #该subset所期望调度的最大副本数
      tolerations:   #改区域下的pod容忍度
      - key: "key1"
        operator: "Equal"
        value: "value1"
        effect: "NoSchedule"
      patch:    #定制改subset中pod配置
        metadata:
          labels:
            xxx-specific-label: xxx
    - name: subset-b
      requiredNodeSelectorTerm:
        matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
              - zone-b
  scheduleStrategy:  #调度策略配置,workload严格按照subsets定义分布,Adaptive不严格规定。默认为Fixed
    type: Adaptive | Fixed  
    adaptive:
      rescheduleCriticalSeconds: 30

WorkloadSpread 功能默认是关闭的,你需要在安装/升级 Kruise 的时候打开 feature-gate:WorkloadSpread

helm upgrade kruise --set featureGates="WorkloadSpread=true" .

1.弹性部署

zone-a固定部署2个Pod,其余部署到zone-b中。

apiVersion: apps.kruise.io/v1alpha1
kind: WorkloadSpread
metadata:
  name: ws-demo
  namespace: kri-dev
spec:
  targetRef: #选择匹配的控制器
    apiVersion: apps.kruise.io/v1alpha1
    kind: CloneSet
    name: cloneset-dev
  subsets:  #调度配置
  - name: zone-a #zone-a定义为只允许调度2个pod副本
    requiredNodeSelectorTerm:
      matchExpressions:
      - key: topology.application.deploy/zone 
        operator: In
        values:
        - zone-a
    maxReplicas: 2
    patch: #pod注入label
      metadata:
        labels:
          topology.application.deploy/zone: zone-a
  - name: zone-b #zone-b定义为其余pod调度到zone-b
    requiredNodeSelectorTerm:
      matchExpressions:
      - key: topology.application.deploy/zone 
        operator: In
        values:
        - zone-b
    patch: #pod注入lable
      metadata:
        labels:
          topology.application.deploy/zone: zone-b

标题:OpenKruise插件
作者:Carey
地址:HTTPS://zhangzhuo.ltd/articles/2022/09/01/1662022769895.html

生而为人

取消