Kubernetes 跨集群流量调度实战

时间:2022-12-27 17:02:06

Kubernetes 跨集群流量调度实战

背景

Kubernetes 问世于 2015 年,从一开始秉持着松耦合和可扩展的设计理念,也因此带来了 Kubernetes 生态的蓬勃发展。但这些大部分先限制在单一集群内,然后由于种种原因和目的企业内部创建的集群越来越多,比如单集群故障、监管要求、异地多机房可用区容灾、出于敏捷、降本考虑的混合云、多云部署、单一集群的承载能力受限、多版本 Kubernetes 集群共存等。

多集群之后除了提升管理的难度外,首当其冲的就是多集群间的流量调度,这也是多集群部署的基础。没有跨集群的通信,多集群的收益也会大打折扣。

年初的时候我们曾在 Flomesh 多集群流量调度 一文中介绍过多集群流量调度的方案,这次我们对其进行了升级,将 FSM 与我们 服务网格产品 集成实现流量在多个集群间的*调度。

在这个方案中主要关注几个点:

  • 跨集群的服务发现:跨集群服务的名称保持一直
  • 流量管理及调度:集群间流量的管控
  • 跨集群通信:服务的访问链路
  • DNS:跨集群服务的 DNS 解析
  • 社区规范和标准:MCS(Multi Cluster Service API)、Ingress、Gateway API、SMI(Service Mesh Interface)

目前的方案还是 beta 版本,预计会在下个月的版本中正式发布。

FSM 介绍

Kubernetes 跨集群流量调度实战

FSM 是 Flomesh 的另一款开源产品,用于 Kubernetes 南北向的流量管理。FSM 以可编程代理 Pipy 为核心,提供了 Ingress 管理器、Gateway API* 实现、负载均衡器以及跨集群的服务注册发现等功能。FSM 的使用,曾经在多篇文章中介绍过:

参与跨集群调度的集群,我们称之为集群联邦。一个集群要加入集群联邦,需要在 控制平面 所在的集群中 注册 该集群。

集群的注册是通过 CRD Cluster 来完成的。

Cluster CRD

在注册集群时,我们提供如下信息:

  • 集群入口的地址(gatewayHost: cluster-A.host)和端口 (gatewayPort: 80
  • 访问集群所要使用的 kubeconfig,包含了 api-server 地址以及证书和秘钥等信息
apiVersion: flomesh.io/v1alpha1
kind: Cluster
metadata:
  name: cluster-A
spec:
  gatewayHost: cluster-A.host
  gatewayPort: 80
  kubeconfig: |+
    ---
    apiVersion: v1
    clusters:
    - cluster:
        certificate-authority-data: 
        server: https://cluster-A.host:6443
      name: cluster-A
    contexts:
    - context:
        cluster: cluster-A
        user: admin@cluster-A
      name: cluster-A
    current-context: cluster-A
    kind: Config
    preferences: {}
    users:
    - name: admin@cluster-A
      user:
        client-certificate-data: 
        client-key-data: 

ServiceExportServiceImport CRD

在跨集群的服务注册方面,FSM 参考了 KEP-1645: Multi-Cluster Services API 中的 ServiceExportServiceImport 提供了 ServiceExports.flomesh.ioServiceImports.flomesh.io 两个 CRD。前者用于向控制平面注册服务,声明应用可以跨集群提供服务,而后者则用于引用其他集群的服务。而在服务网格 osm-edge 中,将 ServiceImports 导入的服务做服务提供者。

对于加入集群联邦的集群 cluser-Acluster-B 来说,在集群 cluster-A 的命名空间 bar 下存在名为 fooService,并在在同命名空间下创建同名的 ServiceExport foo。在集群 cluster-B 的命名空间 bar (若不存在,会自动创建) 下会自动创建同名的 ServiceImport 资源。

// in cluster-A
apiVersion: v1
kind: Service
metadata:
  name: foo
  namespace: bar
spec:
  ports:
  - port: 80
  selector:
    app: foo
---
apiVersion: flomesh.io/v1alpha1
kind: ServiceExport
metadata:
  name: foo
  namespace: bar
---
// in cluster-B
apiVersion: flomesh.io/v1alpha1
kind: ServiceImport
metadata:
  name: foo
  namespace: bar

上面的 YAML 片段展示了如何向多集群的控制面注册 foo 服务。下面我们将通过稍微复杂的场景来介绍跨集群的服务注册和流量调度。

演示

前置条件

  • Kubernets 集群
  • kubectx:用于多个 kubeconfig context (集群)间进行切换
  • k3d:在本地使用容器创建多个 k3s 集群
  • helm:部署 fsm
  • docker:容器中运行 k3s 需要
  • Golang:fsm 处于开发阶段,用来生成 helm chart

整体架构

Kubernetes 跨集群流量调度实战

集群介绍

这里我们需要创建 4 个集群,使用 k3d 在本地创建 4 个集群,分别是 control-planecluster-1cluster-2cluster-3

在创建时,将其 api-server 地址的监听改为上面拿到的主机 IP 地址,这样当从 control-plane 去访问其他集群的 api-server 时,可以通过主机 IP 地址进行访问。这还并不够,还需要为其指定个端口并映射到本地。

注:控制平面集群可以与任一应用集群合并,独立集群只是建议选择。

集群 对外访问地址 api-server 对外端口 LB 对外端口 描述
control-plane HOST_IP(192.168.1.110) 6444 N/A 控制平面集群
cluster-1 HOST_IP(192.168.1.110) 6445 81 应用集群
cluster-2 HOST_IP(192.168.1.110) 6446 82 应用集群
cluster-3 HOST_IP(192.168.1.110) 6447 83 应用集群

示例应用介绍

环境搭建

创建一个 bridge 类型的网络 multi-clusters,所有的容器都工作在这个网络中。

docker network create multi-clusters

获取主机 IP 地址备用,192.168.1.110 是我本机的地址。

export HOST_IP=192.168.1.110

搭建集群

使用 k3d 创建 4 个 集群。

API_PORT=6444 #6444 6445 6446 6447
PORT=80 #81 82 83
for CLUSTER_NAME in control-plane cluster-1 cluster-2 cluster-3
do
  k3d cluster create ${CLUSTER_NAME} \
    --image docker.io/rancher/k3s:v1.23.8-k3s2 \
    --api-port "${HOST_IP}:${API_PORT}" \
    --port "${API_PORT}:6443@server:0" \
    --port "${PORT}:80@server:0" \
    --servers-memory 4g \
    --k3s-arg "--disable=traefik@server:0" \
    --network multi-clusters \
    --timeout 120s \
    --wait
    ((API_PORT=API_PORT+1))
    ((PORT=PORT+1))
done

安装 FSM

在四个集群中安装 FSM。

helm repo update
export FSM_NAMESPACE=flomesh
export FSM_VERSION=0.2.0-alpha.9
for CLUSTER_NAME in control-plane cluster-1 cluster-2 cluster-3
do 
  kubectx k3d-${CLUSTER_NAME}
  sleep 1
  helm install --namespace ${FSM_NAMESPACE} --create-namespace --version=${FSM_VERSION} --set fsm.logLevel=5 fsm fsm/fsm
  sleep 1
  kubectl wait --for=condition=ready pod --all -n $FSM_NAMESPACE
done

加入集群联邦

将集群 cluster-1cluster-2cluster-3 纳入集群 control-plane 的管理。

export HOST_IP=192.168.1.110
kubectx k3d-control-plane
sleep 1
PORT=81
for CLUSTER_NAME in cluster-1 cluster-2 cluster-3
do
  kubectl apply -f - <<EOF
apiVersion: flomesh.io/v1alpha1
kind: Cluster
metadata:
  name: ${CLUSTER_NAME}
spec:
  gatewayHost: ${HOST_IP}
  gatewayPort: ${PORT}
  kubeconfig: |+
`k3d kubeconfig get ${CLUSTER_NAME} | sed 's|^|    |g' | sed "s|0.0.0.0|$HOST_IP|g"`
EOF
((PORT=PORT+1))
done

安装服务网格

下载 osm CLI

将服务网格 osm-edge 安装到集群 cluster-1cluster-2cluster-3。控制平面不处理应用流量,无需安装。

export OSM_NAMESPACE=osm-system
export OSM_MESH_NAME=osm
for CLUSTER_NAME in cluster-1 cluster-2 cluster-3
do
  kubectx k3d-${CLUSTER_NAME}
  DNS_SVC_IP="$(kubectl get svc -n kube-system -l k8s-app=kube-dns -o jsonpath='{.items[0].spec.clusterIP}')"
osm install \
    --mesh-name "$OSM_MESH_NAME" \
    --osm-namespace "$OSM_NAMESPACE" \
    --set=osm.certificateProvider.kind=tresor \
    --set=osm.image.pullPolicy=Always \
    --set=osm.sidecarLogLevel=error \
    --set=osm.controllerLogLevel=warn \
    --timeout=900s \
    --set=osm.localDNSProxy.enable=true \
    --set=osm.localDNSProxy.primaryUpstreamDNSServerIPAddr="${DNS_SVC_IP}"
done

应用部署

部署网格管理的应用

在集群 cluster-1cluster-3httpbin 命名空间(由网格管理,会注入 sidecar)下,部署 httpbin 应用。这里的 httpbin 应用由 Pipy 实现,会返回当前的集群名,并提示被网格管理。

export NAMESPACE=httpbin
for CLUSTER_NAME in cluster-1 cluster-3
do
  kubectx k3d-${CLUSTER_NAME}
  kubectl create namespace ${NAMESPACE}
  osm namespace add ${NAMESPACE}
  kubectl apply -n ${NAMESPACE} -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
  labels:
    app: pipy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pipy
  template:
    metadata:
      labels:
        app: pipy
    spec:
      containers:
        - name: pipy
          image: flomesh/pipy:latest
          ports:
            - containerPort: 8080
          command:
            - pipy
            - -e
            - |
              pipy()
              .listen(8080)
              .serveHTTP(new Message('Hi, I am from ${CLUSTER_NAME} and controlled by mesh!\n'))
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
spec:
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
  selector:
    app: pipy
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin-${CLUSTER_NAME}
spec:
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
  selector:
    app: pipy
EOF

  sleep 3
  kubectl wait --for=condition=ready pod -n ${NAMESPACE} --all --timeout=60s
done

在集群 cluster-2 的命名空间 curl 下部署 curl 应用,这个命名空间是被网格管理的,注入的 sidecar 会完全流量的跨集群调度。

export NAMESPACE=curl
kubectx k3d-cluster-2
kubectl create namespace ${NAMESPACE}
osm namespace add ${NAMESPACE}
kubectl apply -n ${NAMESPACE} -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
  name: curl
---
apiVersion: v1
kind: Service
metadata:
  name: curl
  labels:
    app: curl
    service: curl
spec:
  ports:
    - name: http
      port: 80
  selector:
    app: curl
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: curl
spec:
  replicas: 1
  selector:
    matchLabels:
      app: curl
  template:
    metadata:
      labels:
        app: curl
    spec:
      serviceAccountName: curl
      containers:
      - image: curlimages/curl
        imagePullPolicy: IfNotPresent
        name: curl
        command: ["sleep", "365d"]
EOF

sleep 3
kubectl wait --for=condition=ready pod -n ${NAMESPACE} --all --timeout=60s

导出服务

导出服务

export NAMESPACE_MESH=httpbin
for CLUSTER_NAME in cluster-1 cluster-3
do
  kubectx k3d-${CLUSTER_NAME}
  kubectl apply -f - <<EOF
apiVersion: flomesh.io/v1alpha1
kind: ServiceExport
metadata:
  namespace: ${NAMESPACE_MESH}
  name: httpbin
spec:
  serviceAccountName: "*"
  rules:
    - portNumber: 8080
      path: "/${CLUSTER_NAME}/httpbin-mesh"
      pathType: Prefix
---
apiVersion: flomesh.io/v1alpha1
kind: ServiceExport
metadata:
  namespace: ${NAMESPACE_MESH}
  name: httpbin-${CLUSTER_NAME}
spec:
  serviceAccountName: "*"
  rules:
    - portNumber: 8080
      path: "/${CLUSTER_NAME}/httpbin-mesh-${CLUSTER_NAME}"
      pathType: Prefix
EOF
sleep 1
done

导出后的服务,FSM 会自动为其创建 Ingress 规则,有了规则之后就可以通过 Ingress 来访问这些服务。


for CLUSTER_NAME_INDEX in 1 3
do
  CLUSTER_NAME=cluster-${CLUSTER_NAME_INDEX}
  ((PORT=80+CLUSTER_NAME_INDEX))
  kubectx k3d-${CLUSTER_NAME}
  echo "Getting service exported in cluster ${CLUSTER_NAME}"
  echo '-----------------------------------'
  kubectl get serviceexports.flomesh.io -A
  echo '-----------------------------------'
  curl -s "http://${HOST_IP}:${PORT}/${CLUSTER_NAME}/httpbin-mesh"
  curl -s "http://${HOST_IP}:${PORT}/${CLUSTER_NAME}/httpbin-mesh-${CLUSTER_NAME}"
  echo '-----------------------------------'
done

查看其中的某个 ServiceExports 资源:

kubectl get serviceexports httpbin -n httpbin -o jsonpath='{.spec}' | jq

{
  "loadBalancer": "RoundRobinLoadBalancer",
  "rules": [
    {
      "path": "/cluster-3/httpbin-mesh",
      "pathType": "Prefix",
      "portNumber": 8080
    }
  ],
  "serviceAccountName": "*"
}

导出的服务,可以其他的纳管集群中被导入。比如我们查看集群 cluster-2,可以有多个服务被导入。

kubectx k3d-cluster-2
kubectl get serviceimports -A

NAMESPACE   NAME                AGE
httpbin     httpbin-cluster-1   13m
httpbin     httpbin-cluster-3   13m
httpbin     httpbin             13m

测试

继续保持在 cluster-2 集群(kubectx k3d-cluster-2),我们测试下能否从网格中的 curl 应用访问这些导入的服务。

获取 curl 应用的 pod,后面我们会从这个 pod 发起请求来模拟服务的访问。

curl_client="$(kubectl get pod -n curl -l app=curl -o jsonpath='{.items[0].metadata.name}')"

此时你会发现,无法访问。

kubectl exec "${curl_client}" -n curl -c curl -- curl -s http://httpbin.httpbin:8080/

command terminated with exit code 7

注意这是正常的,默认情况下不会使用其他集群的实例来响应请求,也就是说默认不会调用到其他集群去。那么要如何访问呢,那我们就要清楚全局流量策略 GlobalTrafficPolicy 了。

全局流量策略

注意,所有的全局流量策略都是在使用方这一侧设置的,拿这个 demo 来说就是在集群 cluster-2 这一方进行全局流量策略的设置。所以在开始之前,先切换到 cluster-2 集群:kubectx k3d-cluster-2

全局流量策略通过 CRD GlobalTrafficPolicy 来设置。

type GlobalTrafficPolicy struct {  
   metav1.TypeMeta   `json:",inline"`  
   metav1.ObjectMeta `json:"metadata,omitempty"`  
  
   Spec   GlobalTrafficPolicySpec   `json:"spec,omitempty"`  
   Status GlobalTrafficPolicyStatus `json:"status,omitempty"`  
}
type GlobalTrafficPolicySpec struct {  
   LbType LoadBalancerType `json:"lbType"`  
   LoadBalanceTarget []TrafficTarget `json:"targets"`  
}

全局负载均衡类型 .spec.lbType 一共有三种类型:

  • Locality:只使用本集群的服务,也是默认类型。这就是为什么我们不提供任何全局策略的时候,访问 httpbin 应用会失败,因为在集群 cluster-2 中并没有该服务。
  • FailOver:当本集群访问失败时才会代理到其他集群,也就是常说的故障迁移,类似主备。
  • ActiveActive:正常情况下也会代理到其他集群,类似多活。

FailOverActiveActive 策略在使用时,需要通过 targets 字段来指定备用集群的 id,也就是当故障发生时或者负载均衡可以路由到的集群。 比如,在 集群 cluster-2 中查看导入服务 httpbin/httpbin,可以看到其有两个外集群的 endpoints,注意这里的 endpoints 与原生 endpoints.v1 是不同的概念,会包含更多的信息。此外,还有集群的 id clusterKey

kubectl get serviceimports httpbin -n httpbin -o jsonpath='{.spec}' | jq

{
  "ports": [
    {
      "endpoints": [
        {
          "clusterKey": "default/default/default/cluster-1",
          "target": {
            "host": "192.168.1.110",
            "ip": "192.168.1.110",
            "path": "/cluster-1/httpbin-mesh",
            "port": 81
          }
        },
        {
          "clusterKey": "default/default/default/cluster-3",
          "target": {
            "host": "192.168.1.110",
            "ip": "192.168.1.110",
            "path": "/cluster-3/httpbin-mesh",
            "port": 83
          }
        }
      ],
      "port": 8080,
      "protocol": "TCP"
    }
  ],
  "serviceAccountName": "*",
  "type": "ClusterSetIP"
}

路由类型 Locality

默认情况下的路由类型就是 Locality,就像上面测试的,流量无法调度到其他集群。

kubectl exec "${curl_client}" -n curl -c curl -- curl -s http://httpbin.httpbin:8080/
command terminated with exit code 7

路由类型 FailOver

既然为设置全局流量策略导致访问失败,那我们先开启 FailOver 模式。注意,全局策略流量,要与目标服务名和命名空间一致。 比如,我们要访问 http://httpbin.httpbin:8080/,需要在命名空间 httpbin 下创建名为 httpbinGlobalTrafficPolicy 资源。

kubectl apply -n httpbin -f  - <<EOF
apiVersion: flomesh.io/v1alpha1
kind: GlobalTrafficPolicy
metadata:
  name: httpbin
spec:
  lbType: FailOver
  targets:
    - clusterKey: default/default/default/cluster-1
    - clusterKey: default/default/default/cluster-3
EOF

设置好策略之后,我们再请求一次试试:

kubectl exec "${curl_client}" -n curl -c curl -- curl -s http://httpbin.httpbin:8080/
Hi, I am from cluster-1!

请求成功,请求被代理到了集群 cluster-1 中的服务。再请求一次,又被代理到了集群 cluster-3,符合负载均衡的预期。

kubectl exec "${curl_client}" -n curl -c curl -- curl -s http://httpbin.httpbin:8080/
Hi, I am from cluster-3!

假如我们在集群 cluster-2 的命名空间 httpbin 中部署应用 httpbin,会有什么现象呢?

export NAMESPACE=httpbin
export CLUSTER_NAME=cluster-2

kubectx k3d-${CLUSTER_NAME}
kubectl create namespace ${NAMESPACE}
osm namespace add ${NAMESPACE}
kubectl apply -n ${NAMESPACE} -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
  labels:
    app: pipy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pipy
  template:
    metadata:
      labels:
        app: pipy
    spec:
      containers:
        - name: pipy
          image: flomesh/pipy:latest
          ports:
            - containerPort: 8080
          command:
            - pipy
            - -e
            - |
              pipy()
              .listen(8080)
              .serveHTTP(new Message('Hi, I am from ${CLUSTER_NAME}!\n'))
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
spec:
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
  selector:
    app: pipy
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin-${CLUSTER_NAME}
spec:
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
  selector:
    app: pipy
EOF

sleep 3
kubectl wait --for=condition=ready pod -n ${NAMESPACE} --all --timeout=60s

待应用正常运行之后,这次我们再发请求测试。从结果来看,请求在当前集群中处理完成了。

kubectl exec "${curl_client}" -n curl -c curl -- curl -s http://httpbin.httpbin:8080/ 
Hi, I am from cluster-2!

即使重复多次请求,也都会返回 Hi, I am from cluster-2!,这说明在本集群存在服务时,优先使用本集群的服务处理请求。

有些情况下,我们还希望其他集群也一起参与服务,因为只使用本集群服务的情况下,其他集群的资源不免有些浪费。这就要用到 ActiveActive 路由类型了。

路由类型 ActiveActive

接着上面的状态,我们来测试下 ActiveActive 类型,修改前面创建的策略,将其更新为 ActiveActive

kubectl apply -n httpbin -f  - <<EOF
apiVersion: flomesh.io/v1alpha1
kind: GlobalTrafficPolicy
metadata:
  name: httpbin
spec:
  lbType: ActiveActive
  targets:
    - clusterKey: default/default/default/cluster-1
    - clusterKey: default/default/default/cluster-3
EOF

我们多次请求会发现,三个集群的 httpbin 都会参与服务。这说明负载被均衡代理到多个集群。

kubectl exec "${curl_client}" -n curl -c curl -- curl -s http://httpbin.httpbin:8080/
Hi, I am from cluster-1 and controlled by mesh!
kubectl exec "${curl_client}" -n curl -c curl -- curl -s http://httpbin.httpbin:8080/
Hi, I am from cluster-2!
kubectl exec "${curl_client}" -n curl -c curl -- curl -s http://httpbin.httpbin:8080/
Hi, I am from cluster-3 and controlled by mesh!

总结

上面的示例中,我们演示了如何实现服务的跨集群的流量调度和负载均衡,并尝试了三种不同的全局流量策略:仅本集群调度、故障转移、全局负载均衡。

有了跨集群流量调度的加成,通过混合云、异地多中心等方案避免单集群出现故障导致的系统的可用性问题、满足数据隐私保护的监控要求;多版本的 Kubernetes 共存,Kubernetes 的升级会变得更加容易;单集群的容量限制不再成为问题。

在后面的文章中,我们还会继续介绍非服务网格管理的服务如何进行跨集群的流量调度、跨集群情况下的访问控制,以及更高级的流量管理等内容。