云原生技术公开课学习笔记:应用编排与管理:核心原理、Deployment

三、应用编排与管理:核心原理

1、资源元信息

在这里插入图片描述

Kubernetes的资源对象组成:主要包括了Spec、Status两部分。其中Spec部分用来描述期望的状态,Status部分用来描述观测到的状态

Kubernetes的元数据部分。该部分主要包括了用来识别资源的标签:Labels;用来描述资源的注解:Annotations;用来描述多个资源之间相互关系的OwnerReference

1)、labels

在这里插入图片描述

Labels是一种具有标识型的Key:Value元数据。标签主要用来筛选资源和组合资源,可以使用类似于SQL查询select,来根据Label查询相关的资源

2)、Selector

最常见的Selector就是相等型Selector

在这里插入图片描述

假设系统中有四个Pod,每个Pod都有标识系统层级和环境的标签,通过Tie:front这个标签,可以匹配左边栏的Pod,相等型Selector还可以包括多个相等条件,多个相等条件之间是逻辑与的关系

在这里插入图片描述

通过Tie=front,Env=dev的Selector,可以筛选出所有Tie=front,而且Env=dev的Pod,也就是上图中左上角的Pod。另外一种Selector是集合型Selector,在例子中,Selector筛选所有环境是test或者gray的Pod

在这里插入图片描述

除了in的集合操作外,还有notin集合操作,比如tie notin(front,back),将会筛选所有tie不是front且不是back的Pod。另外,也可以根据是否存在某lable的筛选,如:Selector release,筛选所有带release标签的Pod。集合型和相等型的Selector,也可以用,来连接,同样的标识逻辑与的关系

3)、Annotations

Annotations一般是系统或者工具用来存储资源的非标示性信息,可以用来扩展资源的spec/status的描述

在这里插入图片描述

4)、Ownereference

在这里插入图片描述

Ownereference一般就是指集合类的资源,比如说Pod集合,就有replicaset、statefulset

集合类资源的控制器会创建对应的归属资源。比如:replicaset控制器在操作中会创建Pod,被创建Pod的Ownereference就指向了创建Pod的replicaset,Ownereference使得用户可以方便地查找一个创建资源的对象,另外,还可以用来实现级联删除的效果

2、kubectl查看和修改Kubernetes元数据

pod1.yaml

apiVersion: v1
kind: Pod
metadata:
  name: nginx1
  namespace: default
  labels:
    env: dev
    tie: front  
spec:
  containers:
  - name : nginx
    image: nginx:1.8
    ports:
    - containerPort: 80      

pod2.yaml

apiVersion: v1
kind: Pod
metadata:
  name: nginx2
  namespace: default
  labels:
    env: dev
    tie: front  
spec:
  containers:
  - name : nginx
    image: nginx:1.8
    ports:
    - containerPort: 80      

创建两个pod

hanxiantaodeMBP:yamls hanxiantao$ kubectl get pods
No resources found in default namespace.
hanxiantaodeMBP:yamls hanxiantao$ kubectl apply -f pod1.yaml 
pod/nginx1 created
hanxiantaodeMBP:yamls hanxiantao$ kubectl apply -f pod2.yaml 
pod/nginx2 created

查看所有pod的标签

hanxiantaodeMBP:yamls hanxiantao$ kubectl get pods --show-labels
NAME     READY   STATUS    RESTARTS   AGE   LABELS
nginx1   1/1     Running   0          62s   env=dev,tie=front
nginx2   1/1     Running   0          58s   env=dev,tie=front

查看nginx1这个pod的详细信息

hanxiantaodeMBP:yamls hanxiantao$ kubectl get pods nginx1 -o yaml | less

修改pod的label,改为env=test

hanxiantaodeMBP:yamls hanxiantao$ kubectl label pods nginx1 env=test
error: 'env' already has a value (dev), and --overwrite is false
hanxiantaodeMBP:yamls hanxiantao$ kubectl label pods nginx1 env=test --overwrite
pod/nginx1 labeled
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pods --show-labels
NAME     READY   STATUS    RESTARTS   AGE     LABELS
nginx1   1/1     Running   0          5m39s   env=test,tie=front
nginx2   1/1     Running   0          5m35s   env=dev,tie=front

去掉pod的tie这个标签

hanxiantaodeMBP:yamls hanxiantao$ kubectl label pods nginx1 tie-
pod/nginx1 labeled
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pods --show-labels
NAME     READY   STATUS    RESTARTS   AGE     LABELS
nginx1   1/1     Running   0          6m58s   env=test
nginx2   1/1     Running   0          6m54s   env=dev,tie=front

通过label来筛选pod

hanxiantaodeMBP:yamls hanxiantao$ kubectl get pods --show-labels -l env=test
NAME     READY   STATUS    RESTARTS   AGE     LABELS
nginx1   1/1     Running   0          7m33s   env=test
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pods --show-labels -l env=test,env=dev
No resources found in default namespace.
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pods --show-labels -l env=dev,tie=front
NAME     READY   STATUS    RESTARTS   AGE    LABELS
nginx2   1/1     Running   0          8m4s   env=dev,tie=front
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pods --show-labels -l 'env in (test,dev)'
NAME     READY   STATUS    RESTARTS   AGE     LABELS
nginx1   1/1     Running   0          8m36s   env=test
nginx2   1/1     Running   0          8m32s   env=dev,tie=front

添加annotate

hanxiantaodeMBP:yamls hanxiantao$ kubectl annotate pods nginx1 my-annotate='my comment, ok'
pod/nginx1 annotated
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pods -o yaml | less
apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"labels":{"env":"dev","tie":"front"},"name":"nginx1","namespace":"default"},"spec":{"containers":[{"image":"nginx:1.8","name":"nginx","ports":[{"containerPort":80}]}]}}
    my-annotate: my comment, ok
  creationTimestamp: "2020-12-24T00:18:18Z"
  labels:
    env: test

rs.yaml

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-replicasets
  namespace: default
  labels:
    env: prod
spec:
  replicas: 2
  selector:
   matchLabels:
    env: prod
  template:
    metadata:
      labels:
        env: prod
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80       

通过创建replicaset对象来创建pod,pod中会包含ownerReference信息

hanxiantaodeMBP:yamls hanxiantao$ kubectl apply -f rs.yaml 
replicaset.apps/nginx-replicasets created
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pods 
NAME                      READY   STATUS    RESTARTS   AGE
nginx-replicasets-ld4n6   1/1     Running   0          2m3s
nginx-replicasets-xvr6k   1/1     Running   0          2m3s
nginx1                    1/1     Running   0          27m
nginx2                    1/1     Running   0          27m
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pods nginx-replicasets-ld4n6 -o yaml | less
  name: nginx-replicasets-ld4n6
  namespace: default
  ownerReferences:
  - apiVersion: apps/v1
    blockOwnerDeletion: true
    controller: true
    kind: ReplicaSet
    name: nginx-replicasets
    uid: 5beab4c4-3aae-4c6c-a3f2-3822a2e3ba3a
  resourceVersion: "567337"
  selfLink: /api/v1/namespaces/default/pods/nginx-replicasets-ld4n6
  uid: 9dbcafce-72ec-4088-a0ab-3eccaba95c2d
spec:
  containers:
  - image: nginx:1.7.9
    imagePullPolicy: IfNotPresent
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP
    resources: {
    
    }
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: default-token-6c86p
      readOnly: true

3、控制器模式

1)、控制循环

在这里插入图片描述

控制型模式最核心的就是控制循环的概念。在控制循环中包括了控制器,被控制的系统,以及能够观测系统的传感器,三个逻辑组件

外界通过修改资源spec来控制资源,控制器比较资源spec和status,从而计算一个diff,diff最后会用来决定执行对系统进行什么样的控制操作,控制操作会使得系统产生新的输出,并被传感器以资源status形式上报,控制器的各个组件将都会是独立自主地运行,不断使系统向spec表示终态趋近

2)、Sensor

在这里插入图片描述

控制循环中逻辑的传感器主要由Reflector、Informer、Indexer三个组件构成

Reflector通过List和Watch K8s server来获取资源的数据。List用来在Controller重启以及Watch中断的情况下,进行系统资源的全量更新;而Watch则在多次List之间进行增量的资源更新;Reflector在获取新的资源数据后,会在Delta队列中塞入一个包括资源对象信息本身以及资源对象事件类型的Delta记录,Delta队列中可以保证同一个对象在队列中仅有一条记录,从而避免Reflector重新List和Watch的时候产生重复的记录

Informer组件不断地从Delta队列中弹出delta记录,然后把资源对象交给indexer,让indexer把资源记录在一个缓存中,缓存在默认设置下是用资源的命名空间来做索引的,并且可以被Controller Manager或多个Controller所共享。之后,再把这个事件交给事件的回调函数

控制循环中的控制器组件主要由事件处理函数以及worker组成,事件处理函数之间会相互关注资源的新增、更新、删除的事件,并根据控制器的逻辑去决定是否需要处理。对需要处理的事件,会把事件关联资源的命名空间以及名字塞入一个工作队列中,并且由后续的worker池中的一个Worker来处理,工作队列会对存储的对象进行去重,从而避免多个Woker处理同一个资源的情况

Worker在处理资源对象时,一般需要用资源的名字来重新获得最新的资源数据,用来创建或者更新资源对象,或者调用其他的外部服务,Worker如果处理失败的时候,一般情况下会把资源的名字重新加入到工作队列中,从而方便之后进行重试

3)、控制循环例子——扩容

在这里插入图片描述

ReplicaSet是一个用来描述无状态应用的扩缩容行为的资源,ReplicaSet controler通过监听ReplicaSet资源来维持应用希望的状态数量,ReplicaSet中通过selector来匹配所关联的Pod,在这里考虑ReplicaSet rsA的,replicas从2被改到3的场景

在这里插入图片描述

首先,Reflector会watch到ReplicaSet和Pod两种资源的变化。发现ReplicaSet发生变化后,在delta队列中塞入了对象是rsA,而且类型是更新的记录

Informer一方面把新的ReplicaSet更新到缓存中,并与Namespace nsA作为索引。另外一方面,调用Update的回调函数,ReplicaSet控制器发现ReplicaSet发生变化后会把字符串的nsA/rsA字符串塞入到工作队列中,工作队列后的一个Worker从工作队列中取到了nsA/rsA这个字符串的key,并且从缓存中取到了最新的ReplicaSet数据

Worker通过比较ReplicaSet中spec和status里的数值,发现需要对这个ReplicaSet进行扩容,因此ReplicaSet的Worker创建了一个Pod,这个pod中的Ownereference取向了ReplicaSet rsA

在这里插入图片描述

然后Reflector Watch到的Pod新增事件,在delta队列中额外加入了Add类型的deta记录,一方面把新的Pod记录通过Indexer存储到了缓存中,另一方面调用了ReplicaSet控制器的Add回调函数,Add回调函数通过检查pod ownerReferences找到了对应的ReplicaSet,并把包括ReplicaSet命名空间和字符串塞入到了工作队列中

ReplicaSet的Woker在得到新的工作项之后,从缓存中取到了新的ReplicaSet记录,并得到了其所有创建的Pod,因为ReplicaSet的状态不是最新的,也就是所有创建Pod的数量不是最新的。因此在此时ReplicaSet更新status使得spec和status达成一致

4、控制器模式总结

在这里插入图片描述

Kubernetes所采用的控制器模式,是由声明式API驱动的。确切来说,是基于对Kubernetes资源对象的修改来驱动的

Kubernetes资源之后,是关注该资源的控制器。这些控制器将异步的控制系统向设置的终态驱近

这些控制器是自主运行的,使得系统的自动化和无人值守成为可能

因为Kubernete的控制器和资源都是可以自定义的,因此可以方便的扩展控制器模式。特别是对于有状态应用,我们往往通过自定义资源和控制器的方式,来自动化运维操作。这个也就是operator的场景

四、应用编排与管理:Deployment

1、需求来源

在这里插入图片描述

如果直接管理集群中所有的Pod,应用A、B、C的Pod,其实是散乱地分布在集群中

现在有以下的问题:

  • 首先,如何保证集群内可用Pod的数量?也就是说我们应用A四个Pod如果出现了一些宿主机故障,或者一些网络问题,如何能保证它可用的数量?
  • 如何为所有Pod更新镜像版本?我们是否要某一个Pod去重建新版本的Pod?
  • 然后在更新过程中,如何保证服务的可用性?
  • 以及更新过程中,如果发现了问题,如何快速回滚到上一个版本?

在这里插入图片描述

通过Deployment将应用A、B、C分别规划到不同的Deployment中,每个Deployment其实是管理的一组相同的应用Pod,这组Pod我们认为它是相同的一个副本,那么Deployment能帮我们做什么事情呢?

1)首先,Deployment定义了一种Pod期望数量,比如说应用A,我们期望Pod数量是四个,那么这样的话,controller就会持续维持Pod数量为期望的数量。当我们与Pod出现了网络问题或者宿主机问题的话,controller能帮我们恢复,也就是新扩出来对应的Pod,来保证可用的Pod数量与期望数量一致

2)配置Pod发布方式,也就是说controller会按照用户给定的策略来更新Pod,而且更新过程中,也可以设定不可用Pod数量在多少范围内

3)如果更新过程中发生问题的话,即所谓一键回滚,也就是说你通过一条命令或者一行修改能够将Deployment下面所有Pod更新为某一个旧版本

2、用例解读

1)、Deployment语法

在这里插入图片描述

apiVersion:apps/v1,也就是说Deployment当前所属的组是apps,版本是v1

Deployment作为一个K8s资源,它有自己的metadata元信息,这里定义的Deployment.name是nginx-deployment

Deployment.spec中首先要有一个核心的字段,即replicas,这里定义期望的Pod数量为三个;selector其实是Pod选择器,那么所有扩容出来的Pod,它的Labels必须匹配selector层上的image.labels,也就是app:nginx

2)、查看Deployment状态

在这里插入图片描述

可以通过kubectl get deployment,看到Deployment总体的一个状态

  • DESIRED:期望的Pod数量是3个
  • CURRENT:当前实际Pod数量是3个
  • UP-TO-DATE:其实是到达最新的期望版本的Pod数量
  • AVAILABLE:这个其实是运行过程中可用的Pod数量。这里AVAILABLE并不简单是可用的,也就是Ready状态的,它其实包含了一些可用超过一定时间长度的Pod
  • AGE:deployment创建的时长,如上图Deployment就是已经创建了80分钟

3)、查看Pod

在这里插入图片描述

Pod名字格式最前面一段:nginx-deployment,其实是Pod所属 Deployment.name;中间一段:template-hash,这里三个Pod是一样的,因为这三个Pod其实都是同一个template中创建出来的;最后一段,是一个random的字符串

通过get.pod可以看到,Pod的ownerReferences即Pod所属的controller资源,并不是Deployment,而是一个ReplicaSet。这个ReplicaSet的name,其实是nginx-deployment加上pod.template-hash。所有的Pod都是ReplicaSet创建出来的,而ReplicaSet它对应的某一个具体的Deployment.template版本

4)、更新镜像

在这里插入图片描述

首先kubectl后面有一个set image固定写法,这里指的是设定镜像;其次是一个deployment.v1.apps,这里也是一个固定写法,写的是我们要操作的资源类型,deployment是资源名、v1是资源版本、apps是资源组,这里也可以简写为deployment或者deployment.apps,比如说写为deployment的时候,默认将使用apps组v1版本

第三部分是要更新的deployment的name,也就是我们的nginx-deployment;再往后的nginx其实指的是template,也就是Pod中的container.name;这里可以注意到:一个Pod中,其实可能存在多个container,而我们指定想要更新的镜像的container.name,就是nginx

最后,指定我们这个容器期望更新的镜像版本,这里指的是nginx: 1.9.1。如上图所示:当执行完这条命令之后,可以看到deployment中的template.spec已经更新为nginx: 1.9.1

5)、快速回滚

在这里插入图片描述

通过kubectl执行的话,其实是kubectl rollout undo这个命令,可以回滚到Deployment上一版本;通过rollout undo加上to-revision来指定可以回滚到某一个具体的版本

6)、DeploymeStatus

在这里插入图片描述

deploymentStatus中描述的三个其实是它的conversion状态,也就是Processing、Complete以及Failed

以Processing为例:Processing指的是Deployment正在处于扩容和发布中。比如说Processing状态的deployment,它所有的replicas及Pod副本全部达到最新版本,而且是available,这样的话,就可以进入complete状态。而complete状态如果发生了一些扩缩容的话,也会进入processing这个处理工作状态

如果在处理过程中遇到一些问题:比如说拉镜像失败了,或者说readiness probe检查失败了,就会进入failed状态;如果在运行过程中即complete状态,中间运行时发生了一些pod readiness probe检查失败,这个时候deployment也会进入failed状态。进入failed状态之后,除非所有点replicas均变成available,而且是updated最新版本,deployment才会重新进入complete状态

3、操作演示

deployment-case.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
   matchLabels:
    app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
hanxiantaodeMBP:yamls hanxiantao$ kubectl create -f deployment-case.yaml 
deployment.apps/nginx-deployment created
hanxiantaodeMBP:yamls hanxiantao$ kubectl get deployment nginx-deployment
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3/3     3            3           15s
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pod
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-5d59d67564-528s8   1/1     Running   0          30s
nginx-deployment-5d59d67564-6znl8   1/1     Running   0          30s
nginx-deployment-5d59d67564-q47cp   1/1     Running   0          30s
hanxiantaodeMBP:yamls hanxiantao$ kubectl get replicaset
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-5d59d67564   3         3         3       99s

升级nginx为nginx:1.9.1

hanxiantaodeMBP:yamls hanxiantao$ kubectl set image deployment nginx-deployment nginx=nginx:1.9.1
deployment.apps/nginx-deployment image updated
hanxiantaodeMBP:yamls hanxiantao$ kubectl edit deployment nginx-deployment
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:1.9.1
        imagePullPolicy: IfNotPresent
        name: nginx
        ports:
        - containerPort: 80
          protocol: TCP
        resources: {
    
    }
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {
    
    }
      terminationGracePeriodSeconds: 30
hanxiantaodeMBP:yamls hanxiantao$ kubectl get pod
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-69c44dfb78-6v2gv   1/1     Running   0          2m2s
nginx-deployment-69c44dfb78-gzbnf   1/1     Running   0          83s
nginx-deployment-69c44dfb78-j7wwl   1/1     Running   0          85s
hanxiantaodeMBP:yamls hanxiantao$ kubectl get replicaset
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-5d59d67564   0         0         0       8m33s
nginx-deployment-69c44dfb78   3         3         3       5m47s

回滚到上一版本

hanxiantaodeMBP:yamls hanxiantao$ kubectl rollout undo deployment nginx-deployment
deployment.apps/nginx-deployment rolled back
hanxiantaodeMBP:yamls hanxiantao$ kubectl get replicaset
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-5d59d67564   3         3         3       12m
nginx-deployment-69c44dfb78   0         0         0       9m15s

4、架构设计

1)、管理模式

在这里插入图片描述

Deployment只负责管理不同版本的ReplicaSet,由ReplicaSet来管理具体的Pod副本数,每个ReplicaSet对应Deployment template的一个版本

如上图所示:Deployment创建ReplicaSet,而ReplicaSet创建Pod。他们的OwnerRef其实都对应了其控制器的资源

2)、Deployment控制器

在这里插入图片描述

所有的控制器都是通过Informer中的Event 做一些Handler和Watch。这个地方Deployment控制器,其实是关注Deployment和ReplicaSet中的event,收到事件后会加入到队列中。而Deployment controller从队列中取出来之后,它的逻辑会判断Check Paused,这个Paused其实是Deployment是否需要新的发布,如果Paused设置为true的话,就表示这个Deployment只会做一个数量上的维持,不会做新的发布

如果Check paused为Yes也就是true的话,那么只会做Sync replicas。也就是说把replicas sync同步到对应的ReplicaSet 中,最后再Update Deployment status,那么controller这一次的ReplicaSet就结束了

如果paused为false的话,它就会做Rollout,也就是通过Create或者是Rolling的方式来做更新,更新的方式其实也是通过Create/Update/Delete这种ReplicaSet来做实现的

3)、ReplicaSet控制器

在这里插入图片描述

当Deployment分配ReplicaSet之后,ReplicaSet控制器本身也是从Informer中watch一些事件,这些事件包含了ReplicaSet和Pod的事件。从队列中取出之后,ReplicaSet controller的逻辑很简单,就只管理副本数。也就是说如果controller发现replicas比Pod数量大的话,就会扩容,而如果发现实际数量超过期望数量的话,就会删除Pod

上面Deployment控制器的图中可以看到,Deployment控制器其实做了更复杂的事情,包含了版本管理,而它把每一个版本下的数量维持工作交给ReplicaSet来做

4)、扩容模拟

在这里插入图片描述

有一个Deployment,它的副本数是2,对应的ReplicaSet有Pod1和Pod2。这时如果我们修改Deployment replicas, controller就会把replicas同步到当前版本的ReplicaSet中,这个ReplicaSet发现当前有2个Pod,不满足当前期望3个,就会创建一个新的Pod3

5)、发布模拟

在这里插入图片描述

Deployment当前初始的template,比如说template1这个版本。template1这个ReplicaSet对应的版本下有三个Pod:Pod1,Pod2,Pod3

这时修改template中一个容器的image, Deployment controller就会新建一个对应template2的ReplicaSet。创建出来之后ReplicaSet会逐渐修改两个ReplicaSet的数量,比如它会逐渐增加ReplicaSet2中replicas的期望数量,而逐渐减少ReplicaSet1中的Pod数量

那么最终达到的效果是:新版本的Pod为Pod4、Pod5和Pod6,旧版本的Pod已经被删除了,这里就完成了一次发布

6)、回滚模拟

在这里插入图片描述

回滚模拟,根据上面的发布模拟可以知道Pod4、Pod5、Pod6已经发布完成。这时发现当前的业务版本是有问题的,如果做回滚的话,不管是通过rollout命令还是通过回滚修改template,它其实都是把template回滚为旧版本的template1

这个时候Deployment会重新修改ReplicaSet1中Pod的期望数量,把期望数量修改为3个,且会逐渐减少新版本也就是ReplicaSet2中的replica数量,最终的效果就是把Pod从旧版本重新创建出来

发布模拟的图中可以看到,其实初始版本中Pod1、Pod2、Pod3是旧版本,而回滚之后其实是Pod7、Pod8、Pod9。就是说它的回滚并不是把之前的Pod重新找出来,而是说重新创建出符合旧版本template的Pod

7)、spec字段解析

在这里插入图片描述

8)、升级策略字段解析

在这里插入图片描述

课程地址:https://edu.aliyun.com/roadmap/cloudnative?spm=5176.11399608.aliyun-edu-index-014.4.dc2c4679O3eIId#suit

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/111808756