背景
Kubernetes 问世于 2015 年,从一开始秉持着松耦合和可扩展的设计理念,也因此带来了 Kubernetes 生态的蓬勃发展。但这些大部分先限制在单一集群内,然后由于种种原因和目的企业内部创建的集群越来越多,比如单集群故障、监管要求、异地多机房可用区容灾、出于敏捷、降本考虑的混合云、多云部署、单一集群的承载能力受限、多版本 Kubernetes 集群共存等。
多集群之后除了提升管理的难度外,首当其冲的就是多集群间的流量调度,这也是多集群部署的基础。没有跨集群的通信,多集群的收益也会大打折扣。
年初的时候我们曾在 Flomesh 多集群流量调度 一文中介绍过多集群流量调度的方案,这次我们对其进行了升级,将 FSM 与我们 服务网格产品 集成实现流量在多个集群间的自由调度。
在这个方案中主要关注几个点:
目前的方案还是 beta 版本,预计会在下个月的版本中正式发布。
FSM 介绍
FSM 是 Flomesh 的另一款开源产品,用于 Kubernetes 南北向的流量管理。FSM 以可编程代理 Pipy 为核心,提供了 Ingress 管理器、Gateway API* 实现、负载均衡器以及跨集群的服务注册发现等功能。FSM 的使用,曾经在多篇文章中介绍过:
参与跨集群调度的集群,我们称之为集群联邦。一个集群要加入集群联邦,需要在 控制平面 所在的集群中 注册 该集群。
集群的注册是通过 CRD Cluster 来完成的。
Cluster CRD
在注册集群时,我们提供如下信息:
apiVersion: flomesh.io/v1alpha1kind: Clustermetadata: name: cluster-Aspec: 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:
复制代码
ServiceExport 和 ServiceImport CRD
在跨集群的服务注册方面,FSM 参考了 KEP-1645: Multi-Cluster Services API 中的 ServiceExport 和 ServiceImport 提供了 ServiceExports.flomesh.io 和 ServiceImports.flomesh.io 两个 CRD。前者用于向控制平面注册服务,声明应用可以跨集群提供服务,而后者则用于引用其他集群的服务。而在服务网格 osm-edge 中,将 ServiceImports 导入的服务做服务提供者。
对于加入集群联邦的集群 cluser-A 和 cluster-B 来说,在集群 cluster-A 的命名空间 bar 下存在名为 foo 的 Service,并在在同命名空间下创建同名的 ServiceExport foo。在集群 cluster-B 的命名空间 bar (若不存在,会自动创建) 下会自动创建同名的 ServiceImport 资源。
// in cluster-AapiVersion: v1kind: Servicemetadata: name: foo namespace: barspec: ports: - port: 80 selector: app: foo---apiVersion: flomesh.io/v1alpha1kind: ServiceExportmetadata: name: foo namespace: bar---// in cluster-BapiVersion: flomesh.io/v1alpha1kind: ServiceImportmetadata: name: foo namespace: bar
复制代码
上面的 YAML 片段展示了如何向多集群的控制面注册 foo 服务。下面我们将通过稍微复杂的场景来介绍跨集群的服务注册和流量调度。
演示
前置条件
整体架构
集群介绍
这里我们需要创建 4 个集群,使用 k3d 在本地创建 4 个集群,分别是 control-plane、cluster-1、cluster-2 和 cluster-3。
在创建时,将其 api-server 地址的监听改为上面拿到的主机 IP 地址,这样当从 control-plane 去访问其他集群的 api-server 时,可以通过主机 IP 地址进行访问。这还并不够,还需要为其指定个端口并映射到本地。
注:控制平面集群可以与任一应用集群合并,独立集群只是建议选择。
示例应用介绍
环境搭建
创建一个 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 6447PORT=80 #81 82 83for CLUSTER_NAME in control-plane cluster-1 cluster-2 cluster-3do 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 updateexport FSM_NAMESPACE=flomeshexport FSM_VERSION=0.2.0-alpha.9for CLUSTER_NAME in control-plane cluster-1 cluster-2 cluster-3do 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_NAMESPACEdone
复制代码
加入集群联邦
将集群 cluster-1、cluster-2 和 cluster-3 纳入集群 control-plane 的管理。
export HOST_IP=192.168.1.110kubectx k3d-control-planesleep 1PORT=81for CLUSTER_NAME in cluster-1 cluster-2 cluster-3do cat <<EOFapiVersion: flomesh.io/v1alpha1kind: Clustermetadata: 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-1、cluster-2 和 cluster-3。控制平面不处理应用流量,无需安装。
export OSM_NAMESPACE=osm-systemexport OSM_MESH_NAME=osmfor CLUSTER_NAME in cluster-1 cluster-2 cluster-3do 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-1 和 cluster-3 的 httpbin 命名空间(由网格管理,会注入 sidecar)下,部署 httpbin 应用。这里的 httpbin 应用由 Pipy 实现,会返回当前的集群名,并提示被网格管理。
export NAMESPACE=httpbinfor CLUSTER_NAME in cluster-1 cluster-3do kubectx k3d-${CLUSTER_NAME} kubectl create namespace ${NAMESPACE} osm namespace add ${NAMESPACE} kubectl apply -n ${NAMESPACE} -f - <<EOFapiVersion: apps/v1kind: Deploymentmetadata: name: httpbin labels: app: pipyspec: 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: v1kind: Servicemetadata: name: httpbinspec: ports: - port: 8080 targetPort: 8080 protocol: TCP selector: app: pipy---apiVersion: v1kind: Servicemetadata: name: httpbin-${CLUSTER_NAME}spec: ports: - port: 8080 targetPort: 8080 protocol: TCP selector: app: pipyEOF
sleep 3 kubectl wait --for=condition=ready pod -n ${NAMESPACE} --all --timeout=60sdone
复制代码
在集群 cluster-2 的命名空间 curl 下部署 curl 应用,这个命名空间是被网格管理的,注入的 sidecar 会完全流量的跨集群调度。
export NAMESPACE=curlkubectx k3d-cluster-2kubectl create namespace ${NAMESPACE}osm namespace add ${NAMESPACE}kubectl apply -n ${NAMESPACE} -f - <<EOFapiVersion: v1kind: ServiceAccountmetadata: name: curl---apiVersion: v1kind: Servicemetadata: name: curl labels: app: curl service: curlspec: ports: - name: http port: 80 selector: app: curl---apiVersion: apps/v1kind: Deploymentmetadata: name: curlspec: 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 3kubectl wait --for=condition=ready pod -n ${NAMESPACE} --all --timeout=60s
复制代码
导出服务
导出服务
export NAMESPACE_MESH=httpbinfor CLUSTER_NAME in cluster-1 cluster-3do kubectx k3d-${CLUSTER_NAME} kubectl apply -f - <<EOFapiVersion: flomesh.io/v1alpha1kind: ServiceExportmetadata: namespace: ${NAMESPACE_MESH} name: httpbinspec: serviceAccountName: "*" rules: - portNumber: 8080 path: "/${CLUSTER_NAME}/httpbin-mesh" pathType: Prefix---apiVersion: flomesh.io/v1alpha1kind: ServiceExportmetadata: namespace: ${NAMESPACE_MESH} name: httpbin-${CLUSTER_NAME}spec: serviceAccountName: "*" rules: - portNumber: 8080 path: "/${CLUSTER_NAME}/httpbin-mesh-${CLUSTER_NAME}" pathType: PrefixEOFsleep 1done
复制代码
导出后的服务,FSM 会自动为其创建 Ingress 规则,有了规则之后就可以通过 Ingress 来访问这些服务。
for CLUSTER_NAME_INDEX in 1 3do 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-2kubectl get serviceimports -A
NAMESPACE NAME AGEhttpbin httpbin-cluster-1 13mhttpbin httpbin-cluster-3 13mhttpbin 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:正常情况下也会代理到其他集群,类似多活。
FailOver 和 ActiveActive 策略在使用时,需要通过 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 下创建名为 httpbin 的 GlobalTrafficPolicy 资源。
kubectl apply -n httpbin -f - <<EOFapiVersion: flomesh.io/v1alpha1kind: GlobalTrafficPolicymetadata: name: httpbinspec: lbType: FailOver targets: - clusterKey: default/default/default/cluster-1 - clusterKey: default/default/default/cluster-3EOF
复制代码
设置好策略之后,我们再请求一次试试:
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=httpbinexport CLUSTER_NAME=cluster-2
kubectx k3d-${CLUSTER_NAME}kubectl create namespace ${NAMESPACE}osm namespace add ${NAMESPACE}kubectl apply -n ${NAMESPACE} -f - <<EOFapiVersion: apps/v1kind: Deploymentmetadata: name: httpbin labels: app: pipyspec: 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: v1kind: Servicemetadata: name: httpbinspec: ports: - port: 8080 targetPort: 8080 protocol: TCP selector: app: pipy---apiVersion: v1kind: Servicemetadata: name: httpbin-${CLUSTER_NAME}spec: ports: - port: 8080 targetPort: 8080 protocol: TCP selector: app: pipyEOF
sleep 3kubectl 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 - <<EOFapiVersion: flomesh.io/v1alpha1kind: GlobalTrafficPolicymetadata: name: httpbinspec: lbType: ActiveActive targets: - clusterKey: default/default/default/cluster-1 - clusterKey: default/default/default/cluster-3EOF
复制代码
我们多次请求会发现,三个集群的 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 的升级会变得更加容易;单集群的容量限制不再成为问题。
在后面的文章中,我们还会继续介绍非服务网格管理的服务如何进行跨集群的流量调度、跨集群情况下的访问控制,以及更高级的流量管理等内容。
引用链接
[1] FSM: https://github.com/flomesh-io/fsm
[2] KEP-1645: Multi-Cluster Services API: https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api
[3] Pipy: https://flomesh.io/pipy
评论