用户指南

最佳实践

API 文档

ServiceGroup 使用指南

最近更新时间:2022-05-20 18:29:46

功能特点

  • 边缘计算场景中,往往会在同一个集群中管理多个边缘站点,每个边缘站点内有一个或多个计算节点。
  • 同时希望在每个站点中都运行一组有业务逻辑联系的服务,每个站点内的服务是一套完整的功能,可以为用户提供服务。
  • 由于受到网络限制,有业务联系的服务之间不希望或者不能跨站点访问。

由于以上边缘计算的 3 个特点,腾讯云边缘容器专门设计了一套 ServiceGroup 的自定义资源逻辑,来解决边缘容器在多地域场景下遇到的应用分发和服务治理的问题。

操作场景

ServiceGroup 可以便捷地在共属同一个集群的不同机房或区域中各自部署一组服务,并且使得各个服务间的请求在本机房或本地域内部即可完成,避免服务跨地域访问。

原生 Kubernetes 无法控制 Deployment 的 Pod 创建的具体节点位置,需要通过统筹规划节点的亲和性来间接完成。当边缘站点数量以及需要部署的服务数量过多时,管理和部署方面的极为复杂,甚至仅存在理论上的可能性。与此同时,为了将服务间的相互调用限制在一定范围,业务方需要为各个 Deployment 分别创建专属的 Service,管理方面的工作量巨大且极容易出错并引起线上业务异常。

ServiceGroup 针对此场景设计,用户仅需使用 ServiceGroup 提供的 DeploymentGrid 和 ServiceGrid 两种 TKE Edge 自研 Kubernetes 资源,即可方便地将服务分别部署到这些节点组中,并进行服务流量管控,同时还可保证各区域服务数量及容灾。

本文以详细的案例结合具体的实现原理,来详细说明 ServiceGroup 的使用场景以及需要关注的细节问题。

关键概念

整体架构

基本概念

关于 NodeUnit 和 NodeGroup 最新版设计可以参考 边缘节点池和边缘节点池分类设计文档

NodeUnit(边缘节点池)

  • NodeUnit 通常是位于同一边缘站点内的一个或多个计算资源实例,需要保证同一 NodeUnit 中的节点内网是通的。
  • ServiceGroup 组中的服务运行在一个 NodeUnit 之内。
  • ServiceGroup 允许用户设置服务在一个 NodeUnit 中运行的 pod 数量。
  • ServiceGroup 能够把服务之间的调用限制在本 NodeUnit 内。

NodeGroup(边缘节点池分类)

  • NodeGroup 包含一个或者多个 NodeUnit。
  • 保证在集合中每个 NodeUnit 上均部署 ServiceGroup 中的服务。
  • 集群中增加 NodeUnit 时自动将 ServiceGroup 中的服务部署到新增 NodeUnit。

ServiceGroup

ServiceGroup 包含一个或者多个业务服务。适用场景如下:

  • 业务需要打包部署。
  • 业务需要在每一个 NodeUnit 中运行起来并且保证 pod 数量。
  • 业务需要将服务之间的调用控制在同一个 NodeUnit 中,不能将流量转发到其他 NodeUnit。
注意:

ServiceGroup 是一种抽象资源,一个集群中可以创建多个 ServiceGroup。

ServiceGroup 涉及的资源类型包括如下三类:

DeploymentGrid 的格式与 Deployment 类似,字段就是原先 deployment 的 template 字段,比较特殊的是 gridUniqKey 字段,该字段指明了节点分组的 label 的 key 值:

apiVersion: superedge.io/v1
kind: DeploymentGrid
metadata:
  name:
  namespace:
spec:
  gridUniqKey: <NodeLabel Key>
  <deployment-template>

操作步骤

以在边缘部署 echo-service 为例,我们希望在多个节点组内分别部署 echo-service 服务,需要如下操作:

确定 ServiceGroup 唯一标识

此步骤为逻辑规划,不需要做任何实际操作。我们将目前要创建的 ServiceGroup 逻辑标记使用的 UniqKey 为 location

将边缘节点分组

如下图,我们以一个边缘集群为例,将集群中的节点添加到边缘节点池以及边缘节点池分类中。本文示例以公有云界面来操作,SuperEdge 开源用户可以参考 边缘节点池和边缘节点池分类设计文档 使用 CRD 进行操作完成下面的步骤。

此集群包含 6 个边缘节点,分别位于北京/广州 2 个地域,节点名为bj-1bj-2bj-3gz-1gz-2gz-3

然后我们分别创建 2 个 NodeUnit(边缘节点池):beijingguangzhou,分别将相应的节点加入对应的 NodeUnit(边缘节点池)中,如下图:

最后,我们创建名称为 location 的 NodeGroup(边缘节点池分类),将beijingguangzhou 这两个边缘节点池划分到location这个分类中,如下图:

进行上述操作后,每个节点上会被打上相应的标签,节点 gz-2 的标签如下图所示:

注意:

上一步中,label 的 key 就是 NodeGroup 的名字,同时与 ServiceGroup 的 UniqKey 一致,value 是 NodeUnit 的唯一 key,value 相同的节点表示属于同一个 NodeUnit。
如果同一个集群中有多个 NodeGroup 请为每一个 NodeGroup 分配不同的 UniqKey,部署 ServiceGroup 相关资源的时候会通过 UniqKey 来绑定指定的 NodeGroup 进行部署。

无状态 ServiceGroup

部署 DeploymentGrid

apiVersion: superedge.io/v1
kind: DeploymentGrid
metadata:
  name: deploymentgrid-demo
  namespace: default
spec:
  gridUniqKey: location
  template:
    replicas: 2
    selector:
      matchLabels:
        appGrid: echo
    strategy: {}
    template:
      metadata:
        creationTimestamp: null
        labels:
          appGrid: echo
      spec:
        containers:
        - image: superedge/echoserver:2.2
          name: echo
          ports:
          - containerPort: 8080
            protocol: TCP
          env:
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          resources: {}

部署 ServiceGrid

apiVersion: superedge.io/v1
kind: ServiceGrid
metadata:
  name: servicegrid-demo
  namespace: default
spec:
  gridUniqKey: location
  template:
    selector:
      appGrid: echo
    ports:
    - protocol: TCP
      port: 80
      targetPort: 8080

gridUniqKey 字段设置为了 location,所以我们在将节点分组时采用 label 的 key 为 location;这时,bejingguangzhou 的 NodeUnit 内都有了 echo-service 的 deployment 和对应的 pod,在节点内访问统一的 service-name 也只会将请求发向本组的节点。

[~]# kubectl get dg
NAME                  AGE
deploymentgrid-demo   9s

[~]# kubectl get deployment
NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
deploymentgrid-demo-beijing     2/2     2            2           14s
deploymentgrid-demo-guangzhou   2/2     2            2           14s

[~]# kubectl get pods -o wide
NAME                                             READY   STATUS    RESTARTS   AGE     IP           NODE   NOMINATED NODE   READINESS GATES
deploymentgrid-demo-beijing-65d669b7d-v9zdr      1/1     Running   0          6m51s   10.0.1.72    bj-3   <none>           <none>
deploymentgrid-demo-beijing-65d669b7d-wrx7r      1/1     Running   0          6m51s   10.0.0.70    bj-1   <none>           <none>
deploymentgrid-demo-guangzhou-5d599854c8-hhmt2   1/1     Running   0          6m52s   10.0.0.139   gz-2   <none>           <none>
deploymentgrid-demo-guangzhou-5d599854c8-k9gc7   1/1     Running   0          6m52s   10.0.1.8     gz-3   <none>           <none>

#从上面可以看到,对于一个 deploymentgrid,会分别在每个 nodeunit 下创建一个 deployment,他们会通过 <deployment>-<nodeunit>名称的方式来区分

[~]# kubectl get svc
NAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
kubernetes             ClusterIP   172.16.33.1     <none>        443/TCP             47h
servicegrid-demo-svc   ClusterIP   172.16.33.231   <none>        80/TCP              3s

[~]# kubectl describe svc servicegrid-demo-svc
Name:              servicegrid-demo-svc
Namespace:         default
Labels:            superedge.io/grid-selector=servicegrid-demo
                   superedge.io/grid-uniq-key=location
Annotations:       topologyKeys: ["location"]
Selector:          appGrid=echo
Type:              ClusterIP
IP Families:       <none>
IP:                172.16.33.231
IPs:               <none>
Port:              <unset>  80/TCP
TargetPort:        8080/TCP
Endpoints:         10.0.0.139:8080,10.0.0.70:8080,10.0.1.72:8080 + 1 more...
Session Affinity:  None
Events:            <none>

#从上面可以看到,对于一个 servicegrid,都会创建一个 <servicename>-svc 的标准 Service;
#!!!注意,这里的 Service 对应的后端 Endpoint 仍然为所有 pod 的 endpoint 地址,这里并不会按照 nodeunit 进行 endpoint 筛选

# 在 beijing 地域的 pod 内执行下面的命令
[~]# curl 172.16.33.231|grep "node name"
        node name:      gz-2
...
# 这里会随机返回 gz-2 或者 gz-3 的 node 名称,并不会跨 NodeUnit 访问到 bj-1 或者 bj-3

# 在 guangzhou 地域的 pod 执行下面的命令
[~]# curl 172.16.33.231|grep "node name"
        node name:      bj-3
# 这里会随机返回 bj-1 或者 bj-3 的 node 名称,并不会跨 NodeUnit 访问到 gz-2 或者 gz-3

另外,对于部署了 DeploymentGrid 和 ServiceGrid 后才添加进集群的节点组,该功能会在新的节点组内自动创建指定的 deployment。

原理解析

下面来简单介绍一些 ServiceGroup 如何实现不同 NodeUnit 地域的 Service 访问的流量闭环的,如下图:

原理剖析:

  • 当创建一个 DeploymentGrid 的时候,通过云端的 application-grid-controller 服务,会分别在每个 NodeUnit 上生成一个单独的标准 Deployment。(例如 deploymentgrid-demo-beijing-XXXXX)
  • 当创建相应的 ServiceGrid 的时候,会在集群中创建一个标准的 Service,如上图servicegrid-demo-svc
  • 此时为标准的 Deployment 和标准 Service,这两个行为无法根据 NodeUnit 实现流量闭环。此时需要添加application-grid-wrapper 组件。
  • 从上图可以看到application-grid-wrapper 组件部署在每一个边缘 node 上,同时边缘侧kube-proxy 会通过application-grid-wrapper和 apiserver 通信,获取相应资源信息;这里application-grid-wrapper会监听 ServiceGrid 的 CRD 信息,同时在获取到对应的 Service 的 Endpoint 信息后,就会根据所在 NodeUnit 的节点信息进行筛选,将不在同一 NodeUnit 的 Node 上的 Endpoint 剔除,传递给kube-proxy更新 iptables 规则。下述示例为bj-3北京地域节点上的 iptables 规则:
    -A KUBE-SERVICES -d 172.16.33.231/32 -p tcp -m comment --comment "default/servicegrid-demo-svc: cluster IP" -m tcp --dport 80 -j KUBE-SVC-MLDT4NC26VJPGLP7
    
    -A KUBE-SVC-MLDT4NC26VJPGLP7 -m comment --comment "default/servicegrid-demo-svc:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-VB3QR2E2PUKDLHCW
    
    -A KUBE-SVC-MLDT4NC26VJPGLP7 -m comment --comment "default/servicegrid-demo-svc:" -j KUBE-SEP-U5ZEIIBVDDGER3DI
    
    -A KUBE-SEP-U5ZEIIBVDDGER3DI -p tcp -m comment --comment "default/servicegrid-demo-svc:" -m tcp -j DNAT --to-destination 10.0.1.72:8080
    
    -A KUBE-SEP-VB3QR2E2PUKDLHCW -p tcp -m comment --comment "default/servicegrid-demo-svc:" -m tcp -j DNAT --to-destination 10.0.0.70:8080
  • 从上面规则分析可以看到,beijing地域中,172.16.33.231 的 ClusterIP 只会分流到10.0.1.7210.0.0.70两个后端 Endpooint 上,对应两个 pod:deploymentgrid-demo-beijing-65d669b7d-v9zdrdeploymentgrid-demo-beijing-65d669b7d-wrx7r,而且不会添加上guangzhou地域的两个 IP 10.0.0.139 和 10.0.1.8,按照此逻辑,可以在不同的 NodeUnit 中实现流量闭环能力。
    需注意以下2个场景
    • DeploymentGrid + 标准 Service 能否实现流量闭环?
      不可以。通过上述的分析,如果是标准的 Service,这个 Service:Endpoint 列表并不会被application-grid-wrapper 来监听处理,因此这里会获取全量的 Endpoint 列表,kube-proxy更新 iptables 规则的时候就会添加相应规则,将流量导出到其他 NodeUnit 中。

    • DeploymentGrid + Headless Service 是否可以实现流量闭环?
      可以根据下面的 Yaml 文件部署一个 Headless Service,同样使用 ServiceGrid 的模板来创建:
      apiVersion: superedge.io/v1
      kind: ServiceGrid
      metadata:
        name: servicegrid-demo
        namespace: default
      spec:
        gridUniqKey: location
        template:
          clusterIP: None
          selector:
            appGrid: echo
          ports:
          - protocol: TCP
            port: 8080
            targetPort: 8080
      获取 Service 信息如下:
      Name:              servicegrid-demo-svc
      Namespace:         default
      Labels:            superedge.io/grid-selector=servicegrid-demo
                         superedge.io/grid-uniq-key=location
      Annotations:       topologyKeys: ["location"]
      Selector:          appGrid=echo
      Type:              ClusterIP
      IP Families:       <none>
      IP:                None
      IPs:               <none>
      Port:              <unset>  8080/TCP
      TargetPort:        8080/TCP
      Endpoints:         10.0.0.139:8080,10.0.0.70:8080,10.0.1.72:8080 + 1 more...
      Session Affinity:  None
      Events:            <none
      这里能够看到,仍然能够获取 4 个 Endpoint 的信息,同时 ClusterIP 的地址为空,在集群中的任意 Pod 里通过 nslookup 获取的 Service 地址就是 4 个后端 Endpoint,如下:
      [~]# nslookup servicegrid-demo-svc.default.svc.cluster.local
      Server:         169.254.20.11
      Address:        169.254.20.11#53
      
      Name:   servicegrid-demo-svc.default.svc.cluster.local
      Address: 10.0.1.8
      Name:   servicegrid-demo-svc.default.svc.cluster.local
      Address: 10.0.1.72
      Name:   servicegrid-demo-svc.default.svc.cluster.local
      Address: 10.0.0.70
      Name:   servicegrid-demo-svc.default.svc.cluster.local
      Address: 10.0.0.139
      所以如果通过这种形式访问 servicegrid-demo-svc.default.svc.cluster.local 任然会访问到其余地域的 NodeUnit 内,无法实现流量闭环。
      同时,由于 Deployment 后端 pod 无状态,会随时重启更新,Pod 名称会随机变化,同时 Deployment 的 Pod 不会自动创建 DNS 地址,因此一般我们不会建议 Deployment + Headless Service 这样配合使用;而是推荐大家使用 StatefulsetGrid + Headless Service 的方式
      通过上述的分析可得:如果访问 Service 的行为会通过 kube-proxy的 iptables 规则去进行转发,同时 Service 是 SerivceGrid 类型,会被application-grid-wrapper监听的话,就可以实现区域流量闭环;如果是通过 DNS 获取的实际 Endpoint IP 地址,就无法实现流量闭环。

有状态 ServiceGroup

部署 StatefulSetGrid

apiVersion: superedge.io/v1
kind: StatefulSetGrid
metadata:
  name: statefulsetgrid-demo
  namespace: default
spec:
  gridUniqKey: location
  template:
    selector:
      matchLabels:
        appGrid: echo
    serviceName: "servicegrid-demo-svc"
    replicas: 3
    template:
      metadata:
        labels:
          appGrid: echo
      spec:
        terminationGracePeriodSeconds: 10
        containers:
        - image: superedge/echoserver:2.2
          name: echo
          ports:
          - containerPort: 8080
            protocol: TCP
          env:
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          resources: {}

注意:

template 中的 serviceName 设置成即将创建的 service 名称。

部署 ServiceGrid

apiVersion: superedge.io/v1
kind: ServiceGrid
metadata:
  name: servicegrid-demo
  namespace: default
spec:
  gridUniqKey: location
  template:
    selector:
      appGrid: echo
    ports:
    - protocol: TCP
      port: 80
      targetPort: 8080

gridUniqKey 字段设置为了 location,因此这个 NodeGroup 依然包含beijingguangzhou两个 NodeUnit,每个 NodeUnit 内都有了 echo-service 的 statefulset 和对应的 pod,在节点内访问统一的 service-name 也只会将请求发向本组的节点。

[~]# kubectl get ssg
NAME                   AGE
statefulsetgrid-demo   31s

[~]# kubectl get statefulset
NAME                             READY   AGE
statefulsetgrid-demo-beijing     3/3     49s
statefulsetgrid-demo-guangzhou   3/3     49s

[~]# kubectl get pods -o wide
NAME                               READY   STATUS    RESTARTS   AGE     IP           NODE   NOMINATED NODE   READINESS GATES
statefulsetgrid-demo-beijing-0     1/1     Running   0          9s      10.0.0.67    bj-1   <none>           <none>
statefulsetgrid-demo-beijing-1     1/1     Running   0          8s      10.0.1.67    bj-3   <none>           <none>
statefulsetgrid-demo-beijing-2     1/1     Running   0          6s      10.0.0.69    bj-1   <none>           <none>
statefulsetgrid-demo-guangzhou-0   1/1     Running   0          9s      10.0.0.136   gz-2   <none>           <none>
statefulsetgrid-demo-guangzhou-1   1/1     Running   0          8s      10.0.1.7     gz-3   <none>           <none>
statefulsetgrid-demo-guangzhou-2   1/1     Running   0          6s      10.0.0.138   gz-2   <none>           <none>

[~]# kubectl get svc
NAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
kubernetes             ClusterIP   172.16.33.1     <none>        443/TCP             2d2h
servicegrid-demo-svc   ClusterIP   172.16.33.220   <none>        80/TCP              30s

[~]# kubectl describe svc servicegrid-demo-svc
Name:              servicegrid-demo-svc
Namespace:         default
Labels:            superedge.io/grid-selector=servicegrid-demo
                   superedge.io/grid-uniq-key=location
Annotations:       topologyKeys: ["location"]
Selector:          appGrid=echo
Type:              ClusterIP
IP Families:       <none>
IP:                172.16.33.220
IPs:               <none>
Port:              <unset>  80/TCP
TargetPort:        8080/TCP
Endpoints:         10.0.0.136:8080,10.0.0.138:8080,10.0.0.67:8080 + 3 more...
Session Affinity:  None
Events:            <none>

# 在 guangzhou 地域的 pod 中访问 CluserIP,会随机得到 gz-2 gz-3 节点名称,不会访问到 beijing 区域的 Pod;使用 Service 的域名访问效果一致
[~]# curl 172.16.33.220|grep "node name"
        node name:      gz-2
[~]# curl servicegrid-demo-svc.default.svc.cluster.local|grep "node name"
        node name:      gz-3

# 在 beijing 地域的 pod 中访问 CluserIP,会随机得到 bj-1 bj-3 节点名称,不会访问到 guangzhou 区域的 Pod;使用 Service 的域名访问效果一致
[~]# curl 172.16.33.220|grep "node name"
        node name:      bj-1
[~]# curl servicegrid-demo-svc.default.svc.cluster.local|grep "node name"
        node name:      bj-3

我们在guangzhou地域的节点上检查 iptables 规则,代码示例如下:

-A KUBE-SERVICES -d 172.16.33.220/32 -p tcp -m comment --comment "default/servicegrid-demo-svc: cluster IP" -m tcp --dport 80 -j KUBE-SVC-MLDT4NC26VJPGLP7

-A KUBE-SVC-MLDT4NC26VJPGLP7 -m comment --comment "default/servicegrid-demo-svc:" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-Z2EAS2K37V5WRDQC
  -A KUBE-SVC-MLDT4NC26VJPGLP7 -m comment --comment "default/servicegrid-demo-svc:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-PREBTG6M6AFB3QA4
-A KUBE-SVC-MLDT4NC26VJPGLP7 -m comment --comment "default/servicegrid-demo-svc:" -j KUBE-SEP-URDEBXDF3DV5ITUX

-A KUBE-SEP-Z2EAS2K37V5WRDQC -p tcp -m comment --comment "default/servicegrid-demo-svc:" -m tcp -j DNAT --to-destination 10.0.0.136:8080
-A KUBE-SEP-PREBTG6M6AFB3QA4 -p tcp -m comment --comment "default/servicegrid-demo-svc:" -m tcp -j DNAT --to-destination 10.0.0.138:8080
-A KUBE-SEP-URDEBXDF3DV5ITUX -p tcp -m comment --comment "default/servicegrid-demo-svc:" -m tcp -j DNAT --to-destination 10.0.1.7:8080

通过 iptables 规则很明显的可以看到对 servicegrid-demo-svc的访问分别 redirect 到了 10.0.0.13610.0.0.13810.0.1.7这 3 个地址,分别对应的就是 guangzhou 地域的 3 个 pod 的 IP 地址。可以看到,如果是 StatefulsetGrid + 标准 ServiceGrid 访问方式的话,其原理和上面的 DeploymentGrid 原理一致,都是通过application-grid-wrapper配合kube-proxy修改 iptables 规则来实现的,没有任何区别。

StatefusetGrid + Headless Service 支持

StatefulSetGrid 目前支持使用 Headless service 配合 Pod FQDN的方式进行闭环访问,如下所示:

部署 Headless Service

按照下面的模板部署 Headless Service

apiVersion: superedge.io/v1
kind: ServiceGrid
metadata:
  name: servicegrid-demo
  namespace: default
spec:
  gridUniqKey: location
  template:
    clusterIP: None
    selector:
      appGrid: echo
    ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080

[~]# kubectl describe svc servicegrid-demo-svc
Name:              servicegrid-demo-svc
Namespace:         default
Labels:            superedge.io/grid-selector=servicegrid-demo
                   superedge.io/grid-uniq-key=location
Annotations:       topologyKeys: ["location"]
Selector:          appGrid=echo
Type:              ClusterIP
IP Families:       <none>
IP:                None
IPs:               <none>
Port:              <unset>  8080/TCP
TargetPort:        8080/TCP
Endpoints:         10.0.0.136:8080,10.0.0.138:8080,10.0.0.67:8080 + 3 more...
Session Affinity:  None
Events:            <none>

这里可以看到 Service 的 ClusterIP 为空,Endpoint 仍然包含 6 个 pod 的 IP 地址,同时,如果通过域名查询 servicegrid-demo-svc.default.svc.cluster.local会得到下面的信息:

[~]# nslookup servicegrid-demo-svc.default.svc.cluster.local
Server:         169.254.20.11
Address:        169.254.20.11#53

Name:   servicegrid-demo-svc.default.svc.cluster.local
Address: 10.0.1.7
Name:   servicegrid-demo-svc.default.svc.cluster.local
Address: 10.0.0.136
Name:   servicegrid-demo-svc.default.svc.cluster.local
Address: 10.0.0.138
Name:   servicegrid-demo-svc.default.svc.cluster.local
Address: 10.0.0.69
Name:   servicegrid-demo-svc.default.svc.cluster.local
Address: 10.0.1.67
Name:   servicegrid-demo-svc.default.svc.cluster.local
Address: 10.0.0.67

此时如果我们通过标准的servicegrid-demo-svc.default.svc.cluster.local来访问服务,仍然会跨 NodeUnit 随机访问不同的 Endpoint 地址,无法实现流量闭环。

如何在一个 NodeUnit 内支持 Statefulset 标准访问方式?

在一个标准的 K8s 环境中,按照 Statefulset 的标准使用方式,我们会使用一种逻辑来访问 Statefulset 中的 Pod ,类似Statefulset-0.SVC.NS.svc.cluster.local 这样的格式,例如我们想要使用statefulsetgrid-demo-0.servicegrid-demo-svc.default.svc.cluster.local来访问这个 Statefulset 中的 Pod-0,SuperEdge 针对这个场景进行了多 NodeUnit 的能力适配。

由于 Statefulset 的 Pod 都有独立的 DNS 域名,可以通过 FQDN 方式来访问单独的 pod,例如可以查询statefulsetgrid-demo-beijing-0域名:

[~]# nslookup statefulsetgrid-demo-beijing-0.servicegrid-demo-svc.default.svc.cluster.local
Server:         169.254.20.11
Address:        169.254.20.11#53

Name:   statefulsetgrid-demo-beijing-0.servicegrid-demo-svc.default.svc.cluster.local
Address: 10.0.0.67

因此可以开始考虑一种方式,是不是可以抛弃掉 NodeUnit 的标记,直接使用statefulsetgrid-demo-0.servicegrid-demo-svc.default.svc.cluster.local的域名来访问本地域的 Statefulset 内的 Pod-0,如下:

  • beijing地域访问的就是statefulsetgrid-demo-beijing-0.servicegrid-demo-svc.default.svc.cluster.local 这个 Pod 的 IP。
  • guangzhou地域访问的就是statefulsetgrid-demo-guangzhou-0.servicegrid-demo-svc.default.svc.cluster.local 这个 Pod 的 IP。

上图中使用 CoreDNS 两条记录指向相同的 Pod IP ,这个能力就可以实现上述的标准访问需求。因此 SuperEdge 在产品层面提供了相应的能力,在公有云产品上需要在控制台手动开启,如下图(如果是 SuperEdge 开源用户,需要独立部署下面的 Daemonset 服务):

当使能 Edge Headless开关后,边缘侧节点上都会部署一个服务 statefulset-grid-daemon, 这个服务会来更新节点侧的 CoreDNS 信息。

edge-system   statefulset-grid-daemon-8gtrz      1/1     Running   0          7h42m   172.16.35.193   gz-3   <none>           <none>
edge-system   statefulset-grid-daemon-8xvrg      1/1     Running   0          7h42m   172.16.32.211   gz-2   <none>           <none>
edge-system   statefulset-grid-daemon-ctr6w      1/1     Running   0          7h42m   192.168.10.15   bj-3   <none>           <none>
edge-system   statefulset-grid-daemon-jnvxz      1/1     Running   0          7h42m   192.168.10.12   bj-1   <none>           <none>
edge-system   statefulset-grid-daemon-v9llj      1/1     Running   0          7h42m   172.16.34.168   gz-1   <none>           <none>
edge-system   statefulset-grid-daemon-w7lpt      1/1     Running   0          7h42m   192.168.10.7    bj-2   <none>           <none>

现在,在某个 NodeUnit 内使用统一 headless service 访问形式,例如访问如下 DNS 获取的 IP 地址:

{StatefulSet}-{0..N-1}.SVC.default.svc.cluster.local

实际就会访问这个 NodeUnit 下具体 pod 的 FQDN 地址获取的是同一 Pod IP:

{StatefulSet}-{NodeUnit}-{0..N-1}.SVC.default.svc.cluster.local

例如,在beijing地域访问 statefulsetgrid-demo-0.servicegrid-demo-svc.default.svc.cluster.localDNS 的 IP 地址和 statefulsetgrid-demo-beijing-0.servicegrid-demo-svc.default.svc.cluster.local域名返回的 IP 地址是一样的,如下图:

# 在 guangzhou 地域执行 nslookup 指令
[~]# nslookup statefulsetgrid-demo-0.servicegrid-demo-svc.default.svc.cluster.local
Server:         169.254.20.11
Address:        169.254.20.11#53

Name:   statefulsetgrid-demo-0.servicegrid-demo-svc.default.svc.cluster.local
Address: 10.0.0.136

[~]# nslookup statefulsetgrid-demo-guangzhou-0.servicegrid-demo-svc.default.svc.cluster.local
Server:         169.254.20.11
Address:        169.254.20.11#53

Name:   statefulsetgrid-demo-guangzhou-0.servicegrid-demo-svc.default.svc.cluster.local
Address: 10.0.0.136

# 在 beijing 地域执行 nslookup 指令
[~]# nslookup statefulsetgrid-demo-0.servicegrid-demo-svc.default.svc.cluster.local
Server:         169.254.20.11
Address:        169.254.20.11#53

Name:   statefulsetgrid-demo-0.servicegrid-demo-svc.default.svc.cluster.local
Address: 10.0.0.67

[~]# nslookup statefulsetgrid-demo-beijing-0.servicegrid-demo-svc.default.svc.cluster.local
Server:         169.254.20.11
Address:        169.254.20.11#53

Name:   statefulsetgrid-demo-beijing-0.servicegrid-demo-svc.default.svc.cluster.local
Address: 10.0.0.67

每个 NodeUnit 通过相同的 headless service 只会访问本 NodeUnit 内的 pod:

# 在 guangzhou 区域执行下面的命令
[~]# curl statefulsetgrid-demo-0.servicegrid-demo-svc.default.svc.cluster.local:8080 | grep "pod name:"
        pod name:       statefulsetgrid-demo-guangzhou-0
[~]# curl statefulsetgrid-demo-1.servicegrid-demo-svc.default.svc.cluster.local:8080 | grep "pod name:"
        pod name:       statefulsetgrid-demo-guangzhou-1
[~]# curl statefulsetgrid-demo-2.servicegrid-demo-svc.default.svc.cluster.local:8080 | grep "pod name:"
        pod name:       statefulsetgrid-demo-guangzhou-2

# 在 beijing 区域执行下面的命令
[~]# curl statefulsetgrid-demo-0.servicegrid-demo-svc.default.svc.cluster.local:8080 | grep "pod name:"
        pod name:       statefulsetgrid-demo-beijing-0
[~]# curl statefulsetgrid-demo-1.servicegrid-demo-svc.default.svc.cluster.local:8080 | grep "pod name:"
        pod name:       statefulsetgrid-demo-beijing-1
[~]# curl statefulsetgrid-demo-2.servicegrid-demo-svc.default.svc.cluster.local:8080 | grep "pod name:"
        pod name:       statefulsetgrid-demo-beijing-2

实现原理

上图描述了 StatefulsetGrid+Headless Service 的实现原理,主要就是在边缘节点侧部署了statefulset-grid-daemon的组件,会监听StatefulsetGrid的资源信息;同时刷新边缘侧 CoreDNS 的相关记录,根据所在 NodeUnit 地域,添加{StatefulSet}-{0..N-1}.SVC.default.svc.cluster.local域名记录,和标准的 Pod FQDN 记录 {StatefulSet}-{NodeUnit}-{0..N-1}.SVC.default.svc.cluster.local指向同一 Pod 的 IP 地址。具体如何实现 CoreDNS 域名更新可以参考源代码实现。

按 NodeUnit 灰度

DeploymentGrid 和 StatefulSetGrid 均支持按照 NodeUnit 进行灰度。

重要字段

和灰度功能相关的字段如下:

字段 说明
templatePool 用于灰度的 template 集合
templates NodeUnit 和其使用的 templatePool 中的 template 的映射关系,如果没有指定,NodeUnit 使用 defaultTemplateName 指定的 template
defaultTemplateName 默认使用的 template,如果不填写或者使用"default"就采用 spec.template
autoDeleteUnusedTemplate 默认为 false,如果设置为 true,会自动删除 templatePool 中既不在 templates 中也不在 spec.template 中的 template 模板

使用相同的 template 创建 workload

和上面的 DeploymentGrid 和 StatefulsetGrid 例子完全一致,如果不需要使用灰度功能,则无需添加额外字段。

使用不同的 template 创建 workload

apiVersion: superedge.io/v1
kind: DeploymentGrid
metadata:
  name: deploymentgrid-demo
  namespace: default
spec:
  defaultTemplateName: test1
  gridUniqKey: zone
  template:
    replicas: 1
    selector:
      matchLabels:
        appGrid: echo
    strategy: {}
    template:
      metadata:
        creationTimestamp: null
        labels:
          appGrid: echo
      spec:
        containers:
        - image: superedge/echoserver:2.2
          name: echo
          ports:
          - containerPort: 8080
            protocol: TCP
          env:
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          resources: {}
  templatePool:
    test1:
      replicas: 2
      selector:
        matchLabels:
          appGrid: echo
      strategy: {}
      template:
        metadata:
          creationTimestamp: null
          labels:
            appGrid: echo
        spec:
          containers:
          - image: superedge/echoserver:2.2
            name: echo
            ports:
            - containerPort: 8080
              protocol: TCP
            env:
              - name: NODE_NAME
                valueFrom:
                  fieldRef:
                    fieldPath: spec.nodeName
              - name: POD_NAME
                valueFrom:
                  fieldRef:
                    fieldPath: metadata.name
              - name: POD_NAMESPACE
                valueFrom:
                  fieldRef:
                    fieldPath: metadata.namespace
              - name: POD_IP
                valueFrom:
                  fieldRef:
                    fieldPath: status.podIP
            resources: {}
    test2:
      replicas: 3
      selector:
        matchLabels:
          appGrid: echo
      strategy: {}
      template:
        metadata:
          creationTimestamp: null
          labels:
            appGrid: echo
        spec:
          containers:
          - image: superedge/echoserver:2.3
            name: echo
            ports:
            - containerPort: 8080
              protocol: TCP
            env:
              - name: NODE_NAME
                valueFrom:
                  fieldRef:
                    fieldPath: spec.nodeName
              - name: POD_NAME
                valueFrom:
                  fieldRef:
                    fieldPath: metadata.name
              - name: POD_NAMESPACE
                valueFrom:
                  fieldRef:
                    fieldPath: metadata.namespace
              - name: POD_IP
                valueFrom:
                  fieldRef:
                    fieldPath: status.podIP
            resources: {}
  templates:
    zone1: test1
    zone2: test2
本示例中,NodeUnit zone1 将会使用 test1 template,NodeUnit zone2 将会使用 test2 template,其余 NodeUnit 将会使用 defaultTemplateName 中指定的 template。

多集群分发

支持 DeploymentGrid 和 ServiceGrid 的多集群分发,分发的同时也支持多地域灰度,当前基于的多集群管理方案为 clusternet

特点

  • 支持多集群的按 NodeUnit 灰度。
  • 保证控制集群和被纳管集群应用的强一致和同步更新/删除,做到一次操作,多集群部署。
  • 在控制集群可以看到聚合的各分发实例的状态。
  • 支持节点地域信息更新情况下应用的补充分发:如原先不属于某个 NodeGroup 的集群,更新节点信息后加入了 NodeGroup,控制集群中的应用会及时向该集群补充下发。

前置条件

  • 集群部署了 SuperEdge 中的组件,如果没有 Kubernetes 集群,可以通过 edgeadm 进行创建,如果已有 Kubernetes 集群,可以通过 edageadm 的 addon 部署 SuperEdge 相关组件,将集群转换为一个 SuperEdge 边缘集群。
  • 通过 clusternet 进行集群的注册和纳管。

重要字段

如果要指定某个 DeploymentGrid 或 ServiceGrid 需要进行多集群的分发,则在其 label 中添加superedge.io/fed,并置为"yes"。

使用示例

创建 3 个集群,分别为一个管控集群和 2 个被纳管的边缘集群 A,B,通过 clusternet 进行注册和纳管。其中 A 集群中一个节点添加 zone: zone1 的 label,加入 NodeUnit zone1;集群 B 不加入 NodeGroup。

在管控集群中创建 DeploymentGrid,其中 labels 中添加了 superedge.io/fed: "yes",表示该 DeploymentGrid 需要进行集群的分发,同时灰度指定分发出去的应用在 zone1 和 zone2 中使用不同的副本个数。

apiVersion: superedge.io/v1
kind: DeploymentGrid
metadata:
  name: deploymentgrid-demo
  namespace: default
  labels:
    superedge.io/fed: "yes"
spec:
  defaultTemplateName: test1
  gridUniqKey: zone
  template:
    replicas: 1
    selector:
      matchLabels:
        appGrid: echo
    strategy: {}
    template:
      metadata:
        creationTimestamp: null
        labels:
          appGrid: echo
      spec:
        containers:
        - image: superedge/echoserver:2.2
          name: echo
          ports:
          - containerPort: 8080
            protocol: TCP
          env:
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          resources: {}
  templatePool:
    test1:
      replicas: 2
      selector:
        matchLabels:
          appGrid: echo
      strategy: {}
      template:
        metadata:
          creationTimestamp: null
          labels:
            appGrid: echo
        spec:
          containers:
          - image: superedge/echoserver:2.2
            name: echo
            ports:
            - containerPort: 8080
              protocol: TCP
            env:
              - name: NODE_NAME
                valueFrom:
                  fieldRef:
                    fieldPath: spec.nodeName
              - name: POD_NAME
                valueFrom:
                  fieldRef:
                    fieldPath: metadata.name
              - name: POD_NAMESPACE
                valueFrom:
                  fieldRef:
                    fieldPath: metadata.namespace
              - name: POD_IP
                valueFrom:
                  fieldRef:
                    fieldPath: status.podIP
            resources: {}
    test2:
      replicas: 3
      selector:
        matchLabels:
          appGrid: echo
      strategy: {}
      template:
        metadata:
          creationTimestamp: null
          labels:
            appGrid: echo
        spec:
          containers:
          - image: superedge/echoserver:2.2
            name: echo
            ports:
            - containerPort: 8080
              protocol: TCP
            env:
              - name: NODE_NAME
                valueFrom:
                  fieldRef:
                    fieldPath: spec.nodeName
              - name: POD_NAME
                valueFrom:
                  fieldRef:
                    fieldPath: metadata.name
              - name: POD_NAMESPACE
                valueFrom:
                  fieldRef:
                    fieldPath: metadata.namespace
              - name: POD_IP
                valueFrom:
                  fieldRef:
                    fieldPath: status.podIP
            resources: {}
  templates:
    zone1: test1
    zone2: test2

创建完成后,可以看到在纳管的 A 集群中,创建了对应的 Deployment,而且依照其 NodeUnit 信息,有两个实例。

[root@VM-0-174-centos ~]# kubectl get deploy
NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deploymentgrid-demo-zone1   2/2     2            2           99s
如果在纳管的 A 集群中手动更改了 deployment 的相应字段,会以管控集群为模板更新回来。

B 集群中的一个节点添加 zone: zone2 的 label,将其加入 NodeUnit zone2,管控集群会及时向该集群补充下发 zone2 对应的应用。

[root@VM-0-42-centos ~]# kubectl get deploy
NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deploymentgrid-demo-zone2   3/3     3            3           6s

在管控集群查看 deploymentgrid-demo 的状态,可以看到被聚合在一起的各个被纳管集群的应用状态,便于查看。

status:
  states:
    zone1:
      conditions:
      - lastTransitionTime: "2021-06-17T07:33:50Z"
        lastUpdateTime: "2021-06-17T07:33:50Z"
        message: Deployment has minimum availability.
        reason: MinimumReplicasAvailable
        status: "True"
        type: Available
      readyReplicas: 2
      replicas: 2
    zone2:
      conditions:
      - lastTransitionTime: "2021-06-17T07:37:12Z"
        lastUpdateTime: "2021-06-17T07:37:12Z"
        message: Deployment has minimum availability.
        reason: MinimumReplicasAvailable
        status: "True"
        type: Available
      readyReplicas: 3
      replicas: 3

相关文档

目录