• Kubernetes(五) Pod控制器详解


    Pod控制器详解

    本章主要介绍Pod控制器的详细使用

    1. Pod控制器介绍

    在kubernetes中,按照pod的创建方式可以将其分为2类:

    1. 自主式pod:kubernetes直接创建出来的pod,这种pod删除后就没有了,也不会重建
    2. 控制器创建的pod:通过控制器创建的pod,这种pod删除后还会自动重建

    什么是Pod控制器?

    Pod控制器是管理pod的中间层,使用了pod控制器之后,我们只需要告诉pod控制器,需要多少个pod就可以了。他会创建出满足条件的pod,并确保每个pod处于用户期望的状态。如果pod在运行中出现故障,控制器就会基于指定策略重启或重建pod。

    在kubernetes中,有很多种类型的pod控制器,每种都有它适合的场景,常见有:

    • ReplicationController:比较原始的pod控制器,已经被废弃,由ReplicaSet替代
    • ReplicaSet:保证指定数量pod运行,并支持pod数量变更,镜像版本变更
    • Deployment:通过ReplicaSet来控制pod,并支持滚动升级、版本回退
    • Horizontal Pod Autoscaler:可以根据集群负载自动调整Pod的数量,实现削峰填谷
    • DaemonSet:在集群中的指定Node上都运行一个副本,一般用于守护进程类的任务
    • Job:它创建出来的pod只要完成任务就立即退出,用于执行一次性任务
    • Cronjob:它创建的pod会周期性的执行,用于执行周期性任务
    • StatefulSet:管理有状态应用

    2. ReplicaSet

    ReplicaSet的主要作用是保证一定数量的pod能够正常运行,它会持续监听这些pod运行状态,一旦pod发生故障,就会重启或重建。同时它还支持对pod数量的扩缩容和版本镜像的升降级。

    ReplicaSet 如下图所示:

    常规配置:

    apiVersion: apps/v1
    kind: ReplicaSet
    metadata:
      name:
      namespace:
      labels:
         controller: rs
    spec:
      replicas: 3                 #副本数量
      selector:                   #选择器,通过它指定控制器管理哪些 pod 
         matchLabels:
            app: nginx-pod
         matchExpressions:
            - {key: app, operator: In, values: [nginx-pod]}
      template:                            # 模板,当副本数量不足时,会根据下面的模板创建pod副本
         metadata:
            labels:
               app: nginx-pod
         spec:
            containers:
            - name: nginx
              image: nginx:1.17.1
              ports:
              - containerPort: 80

    这里需要了解的配置就是spec下面的几个选项:

    • replicas:

    指定副本数量,也就是当前rs创建出来的pod数量,默认为1

    • selector:

    选择器,它的作用是建立pod控制器和pod之间的关联关系,采用的Label Selector机制。在pod模板上也需要定义label,这样在控制器上定义选择器,就可以表明当前控制器能管理哪些pod了

    • template:模板,当前控制器创建pod所使用的模板,里面是pod定义

    部署

    示例:

    $ cat yamls/pc-replicaset.yaml
    apiVersion: apps/v1
    kind: ReplicaSet
    metadata:
      name: pc-replicaset
      namespace: dev
      labels:
         controller: rs
    spec:
      replicas: 3                 #副本数量
      selector:
         matchLabels:
            app: nginx-pod
         matchExpressions:
            - {key: app, operator: In, values: [nginx-pod]}
      template:                            # 模板,当副本数量不足时,会根据下面的模板创建pod副本
         metadata:
            labels:
               app: nginx-pod
         spec:
            containers:
            - name: nginx
              image: nginx:1.17.1
              ports:
              - containerPort: 80

    查看创建的rs:

    $ kubectl get rs -n dev -o wide
    NAME            DESIRED   CURRENT   READY   AGE    CONTAINERS   IMAGES         SELECTOR
    pc-replicaset     3                    3               3       115s        nginx        nginx:1.17.1   app=nginx-pod,app in (nginx-pod)

    扩缩容

    有2种方法:

    1. 直接编辑

    $ kubectl edit rs -n dev
    replicaset.apps/pc-replicaset edited

    2. kubescale rs 命令

    $ kubectl scale rs pc-replicaset --replicas=4 -n dev
    replicaset.apps/pc-replicaset scaled

    镜像版本升降级

    同样

    1. 直接编辑

    $ kubectl edit rs -n dev
    replicaset.apps/pc-replicaset edited
    
    $ kubectl get rs pc-replicaset -n dev -o wide
    NAME            DESIRED   CURRENT   READY   AGE     CONTAINERS   IMAGES         SELECTOR
    pc-replicaset   4         4         4       5m39s   nginx        nginx:1.17.2   app=nginx-pod,app in (nginx-pod)

    2. kubect set image 命令

    $ kubectl set image rs pc-replicaset nginx=nginx:1.17.1 -n dev
    replicaset.apps/pc-replicaset image updated

    删除

    使用kubectl delete 命令可以删除rs以及它管理的pod。Kubernetes在删除rs前,会将rs的replicascaler调整为0,等待所有pod被删除后,再执行rs对象的删除。

    如果希望仅仅删除rs对象(保留pod),可以使用kubectl delete 命令时添加 --cascade=false(不推荐)

    当然,删除yaml的形式也是可以的

    3. Deployment

    为了更好地解决服务编排的问题,kubernetes在V1.2 版本开始,引入了Deployment控制器。这种控制器并不直接管理pod,而是通过管理ReplicaSet来间接管理Pod。即:Deployment管理ReplicaSet,ReplicaSet管理Pod。所以Deployment比ReplicaSet功能更强大。

    Deployment主要功能有:

    1. 支持ReplicaSet的所有功能
    2. 支持发布的停止、继续
    3. 支持版本滚动升级和版本回退

    Deployment资源主要配置:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: 
      namespace:
      labels:
        controller: deploy
    spec:
      replicas: 3
      revisionHistoryLimit: 3                      # 保留历史版本,默认为10
      paused: false                                       # 暂停部署,默认是false
      progressDeadlineSeconds: 600       # 部署超时时间(s),默认是600
      strategy:                                              # 策略
        type: RollingUpdate                         # 滚动更新策略
        rollingUpdate:                                  # 滚动更新
          maxSurge: 30%                               # 最大额外可以存在的副本数,可以为百分比或整数
          maxUnavailable: 30%                    # 最大不可用状态的Pod的最大值,可以为百分比或整数
      selector:
        matchLabels:
          app: nginx-pod
        matchExpressions:
          - {key: app, operator: In, values: [nginx-pod]}
      template:
        metadata:
          labels:
            app: nginx-pod
        spec:
          containers:
          - name: nginx
            image: nginx:1.17.1
            ports:
            - containerPort: 80
        

    扩缩容

    1. kubectl scale deploy pc-deployment --replicas=5 -n dev

    2. kubectl edit deploy pc-deployment -n dev

    镜像更新

    Deployment支持2种镜像更新策略:重建更新和滚动更新(默认),可以通过strategy选项进行配置。

    重建更新就是一次性删除所有老版本pod,然后重建。滚动就是先删一部分,启动一部分,中间状态是新版本与老版本pod均存在,最终只有新版本pod存在。

    strategy:       # 指定新的pod替换旧pod的策略,支持2个属性
    type:            # 支持2个属性
        Recreate:    # 在创建出新的Pod之前会kill掉所有已存在的pod
        RollingUpdate:     # 滚动更新,kill一部分启动一部分,在更新过程中,存在2个版本的pod
     rollingUpdate:     # 当type为RollingUpdate时生效,用于设置它的参数
         maxUnavailable:    # 用来指定在升级过程中不可用 Pod 的最大数量,默认为25%
         maxSurge:        # 用来指定在升级过程中可以超过期望的Pod的最大数量,默认为 25%

    1. 重建更新

    测试配置项:

    $ cat yamls/pc-deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
       name: pc-deployment
       namespace: dev
       labels:
          controller: deploy
    spec:
       replicas: 3
       strategy:
          type: Recreate
       selector:
          matchLabels:
              app: nginx-pod
       template:
          metadata:
             labels:
                app: nginx-pod
          spec:
             containers:
             - name: nginx
               image: nginx:1.17.1
               ports:
               - containerPort: 80

    变更镜像:

    $ kubectl set image deployment pc-deployment nginx=nginx:1.17.2 -n dev
    deployment.apps/pc-deployment image updated
    
    $ kubectl get pods -n dev -o wide –w
    
    
    NAME                             READY   STATUS    R
    pc-deployment-7d7dd5499b-5wdlc   1/1     Running   0
    pc-deployment-7d7dd5499b-czpzs   1/1     Running   0
    pc-deployment-7d7dd5499b-dgvp5   1/1     Running   0
    …
    pc-deployment-7d7dd5499b-5wdlc   1/1     Terminating
    pc-deployment-7d7dd5499b-dgvp5   1/1     Terminating
    pc-deployment-7d7dd5499b-czpzs   1/1     Terminating
    ….
    pc-deployment-7bbbd589d5-kkdcs   0/1     Pending    
    pc-deployment-7bbbd589d5-kkdcs   0/1     Pending    
    pc-deployment-7bbbd589d5-xtw4l   0/1     Pending    
    …
    pc-deployment-7bbbd589d5-kkdcs   0/1     ContainerCreating
    pc-deployment-7bbbd589d5-xtw4l   0/1     ContainerCreating
    pc-deployment-7bbbd589d5-ztfbw   0/1     ContainerCreating
    …
    pc-deployment-7bbbd589d5-xtw4l   1/1     Running    
    pc-deployment-7bbbd589d5-ztfbw   1/1     Running    
    pc-deployment-7bbbd589d5-kkdcs   1/1     Running    

    从这个部署记录可以看到,recreate 的策略是一次性全部terminate,然后启动新版本pod

     

    2. 滚动更新

    更改配置:

    spec:
       replicas: 3
       strategy:
          type: RollingUpdate
          rollingUpdate:
            maxUnavailable: 25%
            maxSurge: 25%

    变更 image 后打印的输出:

    $ kubectl get pods -n dev -o wide -w
    NAME                                                    READY   STATUS     
    pc-deployment-866fdcbd54-9xwrb   0/1     Pending           
    pc-deployment-866fdcbd54-9xwrb   0/1     Pending      
    pc-deployment-866fdcbd54-9xwrb   0/1     ContainerCreating  
    pc-deployment-866fdcbd54-9xwrb   1/1     Running            
    pc-deployment-7bbbd589d5-xbnmw   1/1     Terminating        
    pc-deployment-866fdcbd54-wjs8n   0/1     Pending            
    pc-deployment-866fdcbd54-wjs8n   0/1     Pending            
    pc-deployment-866fdcbd54-wjs8n   0/1     ContainerCreating  
    pc-deployment-7bbbd589d5-xbnmw   0/1     Terminating        
    pc-deployment-7bbbd589d5-xbnmw   0/1     Terminating        
    pc-deployment-7bbbd589d5-xbnmw   0/1     Terminating        
    pc-deployment-866fdcbd54-wjs8n   1/1     Running            
    pc-deployment-7bbbd589d5-2cjvp   1/1     Terminating        
    pc-deployment-866fdcbd54-ls9b8   0/1     Pending            
    pc-deployment-866fdcbd54-ls9b8   0/1     Pending            
    pc-deployment-866fdcbd54-ls9b8   0/1     ContainerCreating  
    pc-deployment-7bbbd589d5-2cjvp   0/1     Terminating        
    pc-deployment-866fdcbd54-ls9b8   1/1     Running            
    pc-deployment-7bbbd589d5-f44sx   1/1     Terminating        
    pc-deployment-7bbbd589d5-f44sx   0/1     Terminating        
    pc-deployment-7bbbd589d5-2cjvp   0/1     Terminating        
    pc-deployment-7bbbd589d5-2cjvp   0/1     Terminating        
    pc-deployment-7bbbd589d5-f44sx   0/1     Terminating        
    pc-deployment-7bbbd589d5-f44sx   0/1     Terminating        

    可以看到在过程中是启动一个停止一个。

    滚动更新其实是通过2ReplicaSet 完成的,新版本pod全在新的rs中启动。旧版本pod在旧rs中陆续关闭。旧版本的ReplicaSet 不会被删掉,这是为版本回退而做的设计。

    查看rs数量,可以看到第二次实验有2rs

    $ kubectl get rs -n dev
    NAME                       DESIRED   CURRENT   READY   AGE
    pc-deployment-7bbbd589d5   0         0         0       32m
    pc-deployment-7d7dd5499b   0         0         0       32m
    pc-deployment-866fdcbd54   3         3         3       30m

    3. 版本回退

    Deployment支持版本升级过程中的暂停、继续功能以及版本回退等功能。

    kubectl rollout 是版本升级相关的功能,支持的选项有:

    • status:显示当前升级状态
    • history:显示升级历史记录
    • pause:暂停版本升级过程
    • resume:继续已经暂停的版本升级过程
    • restart:重启版本升级过程
    • undo:回滚到上一版本(可以使用--to-version回滚到指定版本)
    #查看deployment 升级的当前状态
    $ kubectl rollout status deploy pc-deployment -n dev
    deployment "pc-deployment" successfully rolled out
    
    # 查看版本升级历史
    $ kubectl rollout history deploy pc-deployment -n dev
    deployment.apps/pc-deployment
    REVISION  CHANGE-CAUSE
    1         <none>
    2         <none>
    3         <none>
    
    这里change-cause为<none> 是因为我们在部署 deployment的时候未添加--record的选项
    
    加上 kubectl apply -f yamls/pc-deployment.yaml --record 后再次尝试的结果:
    $ kubectl rollout history deploy pc-deployment -n dev
    deployment.apps/pc-deployment
    REVISION  CHANGE-CAUSE
    1         kubectl apply --filename=yamls/pc-deployment.yaml --record=true
    2         kubectl apply --filename=yamls/pc-deployment.yaml --record=true
    
    # 版本回退,若未指定版本号,默认回退到上一版本
    $ kubectl rollout undo deploy pc-deployment -n dev
    deployment.apps/pc-deployment rolled back
    
    # 其实这个回退版本另一方面又是一个新版本(版本1 现在变为了版本4):
    $ kubectl rollout history deploy pc-deployment -n dev
    deployment.apps/pc-deployment
    REVISION  CHANGE-CAUSE
    2         kubectl apply --filename=yamls/pc-deployment.yaml --record=true
    3         kubectl apply --filename=yamls/pc-deployment.yaml --record=true

    金丝雀发布

    Deployment支持更新过程中的控制,如“暂停(pause)”或继续(resume)”更新操作。

    比如有一批新的Pod资源创建完成后立即暂停更新过程。此时,仅存在一部分新版本的应用,主体部分还是旧版本。然后,再筛选一小部分的用户请求路由到新版本的Pod应用,继续观察能否稳定地按期望方式运行。确定没问题后再继续完成余下Pod资源滚动更新,否则立即回滚更新操作。这就是金丝雀发布。

    # 更新deployment版本,并配置暂停deployment
    $ kubectl set image deploy pc-deployment nginx=nginx:1.17.4 -n dev && kubectl rollout pause deployment pc-deployment -n dev
    deployment.apps/pc-deployment image updated
    deployment.apps/pc-deployment paused
    
    # 查看 rs
    $ kubectl get rs -n dev
    NAME                       DESIRED   CURRENT   READY   AGE
    pc-deployment-56f77b8695   1         1         1       18s
    pc-deployment-7d7dd5499b   3         3         3       35m
    
    可以看到老版本是仍有3个,新版本仅有1个
    
    # 查看 deployment status
    $ kubectl rollout status deploy pc-deployment -n dev
    Waiting for deployment "pc-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
    
    后续可以将部分流量先导入到新的rs中进行测试,最终没有任何问题后,可以继续rollout的流程:
    $ kubectl rollout resume deployment pc-deployment -n dev
    deployment.apps/pc-deployment resumed
    
    $ kubectl rollout status deploy pc-deployment -n dev
    Waiting for deployment "pc-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
    
    Waiting for deployment spec update to be observed...
    Waiting for deployment spec update to be observed...
    Waiting for deployment "pc-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
    Waiting for deployment "pc-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
    Waiting for deployment "pc-deployment" rollout to finish: 2 out of 3 new replicas have been updated...
    Waiting for deployment "pc-deployment" rollout to finish: 2 out of 3 new replicas have been updated...
    Waiting for deployment "pc-deployment" rollout to finish: 2 out of 3 new replicas have been updated...
    Waiting for deployment "pc-deployment" rollout to finish: 1 old replicas are pending termination...
    Waiting for deployment "pc-deployment" rollout to finish: 1 old replicas are pending termination...
    deployment "pc-deployment" successfully rolled out

    4. Horizontal Pod AutoscalerHPA

    前面都是通过kubectl scale进行的手动扩容,在生产中更多的是自动化扩展。通过HPA可以实现此续期。

    HPA可以获取每个pod利用率,然后和HPA中定义的指标进行对比,同时计算需要伸缩的具体指,最后实现pod数量调整。

    HPA与之前的Deployment一样,也属于一种kubernetes 资源对象,它通过追踪分析目标pod的负载变化情况,来确定是否需要针对性地调整目标pod的副本数。

    4.1. 安装metrics-server

    metrics-server可以用来收集集群中的资源使用情况。

    $ git clone -b v0.3.6 https://github.com/kubernetes-incubator/metrics-server
    $ cd metrics-server/deploy/1.8+/
    
    # 修改metrics-server-deployment.yaml 文件以下内容
        spec:
          hostNetwork: true
          serviceAccountName: metrics-server
          volumes:
          # mount in tmp so we can safely use from-scratch images and/or read-only containers
          - name: tmp-dir
            emptyDir: {}
          containers:
          - name: metrics-server
            image: registry.cn-hangzhou.aliyuncs.com/google_containers/metrics-server-amd64:v0.3.6
            imagePullPolicy: Always
            volumeMounts:
            - name: tmp-dir
              mountPath: /tmp
            args:
            - --kubelet-insecure-tls
            - --kubelet-preferred-address-types=InternalIP,Hostname,InternalDNS,ExternalDNS,ExternalIP
    
    # 部署
    kubectl apply -f ./
    
    # 验证
    $ kubectl get pods -n kube-system
    metrics-server-5f55b696bd-xmgkp   1/1     Running   0          2m2s

     

    部署完成后即可查看资源使用情况

    # 查看资源使用情况
    $ kubectl top node
    NAME                                                                     CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
    ip-10-0-1-217.cn-north-1.compute.internal    51m                2%            657Mi                  19%
    ip-10-0-2-30.cn-north-1.compute.internal      42m                2%            565Mi                  16%

    4.2. 准备deploymentservice

    # 创建deployment
    $ kubectl get deploy,pod -n dev
    NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/nginx-dep   1/1     1            1           61s
    
    NAME                             READY   STATUS    RESTARTS   AGE
    pod/nginx-dep-755c49cf64-9hrfn   1/1     Running   0          61s
    
    # 准备service
    $ kubectl expose deploy nginx-dep --type=NodePort --port 80 --target-port=80 -n dev
    service/nginx-dep exposed

    4.3. 部署HPA

    $ cat yamls/pc-hpa.yaml
    apiVersion: autoscaling/v1
    kind: HorizontalPodAutoscaler
    metadata:
        name: pc-hpa
        namespace: dev
    spec:
        minReplicas: 1
        maxReplicas: 10
        targetCPUUtilizationPercentage: 3       # 此处用3仅是为了测试
        scaleTargetRef:                                         # 指定要扩展的target deployment
            apiVersion: apps/v1
            kind: Deployment
            name: nginx-dep
    
    # 部署后
    $ kubectl get hpa -n dev
    NAME     REFERENCE                       TARGETS        MINPODS   MAXPODS   REPLICAS   AGE
    pc-hpa   Deployment/nginx-dep   <unknown>/3%   1               10                  0              11s
    
    => 这里 unknown 表示当前的使用情况,仍在计算中,需要等待一会儿出数据
    $ kubectl get hpa -n dev
    NAME     REFERENCE              TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
    pc-hpa   Deployment/nginx-dep   0%/3%     1         10        1          21m
    
    如果hpa一直报invalid metrics (1 invalid out of 1), first error is: failed to get cpu utilization: missing request for cpu,说明 deploy中没有配置 CPU requests。需要执行下面的命令:
    kubectl patch deployment nginx-dep -p='{"spec":{"template":{"spec":{"containers":[{"name":"nginx","resources":{"requests":{"cpu":"200m"}}}]}}}}' -n dev

    开始压测,并观察hpadeploy的结果:

    $ kubectl get hpa -n dev -o wide -w
    NAME     REFERENCE              TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
    pc-hpa   Deployment/nginx-dep   0%/3%     1         10        1          32m
    pc-hpa   Deployment/nginx-dep   3%/3%     1         10        1          32m
    pc-hpa   Deployment/nginx-dep   9%/3%     1         10        1          33m
    
    $ kubectl get deploy -n dev
    NAME        READY   UP-TO-DATE   AVAILABLE   AGE
    nginx-dep   3/3     3            3           51m
    
    # 停掉一段时间后
    $ kubectl get deploy -n dev
    NAME        READY   UP-TO-DATE   AVAILABLE   AGE
    nginx-dep   1/1     1            1           63m

    5. DaemonSet

    DaemonSet类型的控制器可以保证集群中每一台(或指定)节点上都运行一个副本,一般适合于日志收集、节点监控等常见。也就是说,如果一个pod提供的功能是节点级别的(每个节点都需要且仅需要一个),那么这类Pod就适合使用DaemonSet类型的控制器创建。

    DaemonSet控制器的特点:

    • 每当向集群中添加一个节点时,指定的pod副本也会添加到该节点上
    • 每当节点从集群中移除时,pod也就被垃圾回收了

    6. Job

    Job主要用于负责批量处理(一次处理多个任务)、短暂的、一次性任务。有以下2个特点:

    1. 当job创建的pod执行成功结束时,job将记录成功结束后的pod数量
    2. 当成功结束的pod达到指定的数量时,job将完成执行

    主要属性有:

    spec:
      completions: 1                          # 指定job需要成功运行Pods的数量。默认为1
      parallelism: 1                            # 指定job在任一时刻应该并发运行Pods的数量
      activeDeadlineSeconds: 30    # job可运行的时间期限,若是超时仍未结束,则系统会尝试终止
      backoffLimit: 6                          # job失败后重试次数,默认为6 
      manualSelector: true              # 是否使用selector选择器选择pod,默认false
      selector:                        
        matchLabels:
        app: counter-pod
        matchExpressions:
          - {key: app, operator: In, values: [counter-pod]}

    7. CronJobCJ

    Cronjob控制器以Job控制器资源为其管控对象,并借助它管理pod资源对象,Job控制器定义的作业任务在其控制器资源创建之后便立即执行。但CronJob可以以类似于Linux操作系统的方式实现周期性地在某时间点运行。

    主要属性有:

    spec:
      schedule:                                        # cron格式的作业调度运行时间点,控制任务什么时间执行
        concurrencyPolicy:                     # 并发执行策略,定义前一次作业未完成时是否运行后一次
        failedJobHistoryLimit:                # 为失败的任务执行那个保留的历史记录数,默认为1
        successfulJobHistoryLimit:        # 为成功的任务执行保留的历史记录数,默认为3
        startingDeadlineSeconds:         # 启动作业错误的超时时长
        jobTemplate:                               # job控制器模板,用于为cronjob控制器生成job对象
          metadata:
            spec:
              completions: 1
              parallelism: 1
              activeDeadlineSeconds: 30
              ...
              template:
                metadata:
                  labels:
                    app: counter-pod
                spec:
                  restartPolicy: Never
                containers:
                - name: counter
                  ...
          

  • 相关阅读:
    HDU 4571 Travel in time(最短路径+DP)(2013 ACM-ICPC长沙赛区全国邀请赛)
    第十四届华中科技大学程序设计竞赛 C Professional Manager【并查集删除/虚点】
    第十四届华中科技大学程序设计竞赛 B Beautiful Trees Cutting【组合数学/费马小定理求逆元/快速幂】
    二分最化最值问题(二)
    二分最化最值问题(一)
    CONTINUE...?【构造/分析】
    Doki Doki Literature Club
    King of Karaoke
    Peak
    ZOJ18th省赛 Lucky 7
  • 原文地址:https://www.cnblogs.com/zackstang/p/14528477.html
Copyright © 2020-2023  润新知