Kubernetes 跨集群流量调度实战
背景
Kubernetes 问世于 2015 年,从一开始秉持着松耦合和可扩展的设计理念,也因此带来了 Kubernetes 生态的蓬勃发展。但这些大部分先限制在单一集群内,然后由于种种原因和目的企业内部创建的集群越来越多,比如单集群故障、监管要求、异地多机房可用区容灾、出于敏捷、降本考虑的混合云、多云部署、单一集群的承载能力受限、多版本 Kubernetes 集群共存等。
多集群之后除了提升管理的难度外,首当其冲的就是多集群间的流量调度,这也是多集群部署的基础。没有跨集群的通信,多集群的收益也会大打折扣。
年初的时候我们曾在 Flomesh 多集群流量调度 一文中介绍过多集群流量调度的方案,这次我们对其进行了升级,将 FSM[1] 与我们 服务网格产品 集成实现流量在多个集群间的自由调度。
在这个方案中主要关注几个点:
- • 跨集群的服务发现:跨集群服务的名称保持一致
- • 流量管理及调度:集群间流量的管控
- • 跨集群通信:服务的访问链路
- • DNS:跨集群服务的 DNS 解析
- • 社区规范和标准:MCS(Multi Cluster Service API)、Ingress、Gateway API、SMI(Service Mesh Interface)
目前的方案还是 beta 版本,预计会在下个月的版本中正式发布。
FSM 介绍
FSM 是 Flomesh 的另一款开源产品,用于 Kubernetes 南北向的流量管理。FSM 以可编程代理 Pipy 为核心,提供了 Ingress 管理器、Gateway API* 实现、负载均衡器以及跨集群的服务注册发现等功能。
参与跨集群调度的集群,我们称之为集群联邦。一个集群要加入集群联邦,需要在 控制平面 所在的集群中 注册 该集群。
集群的注册是通过 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:
ServiceExport
和 ServiceImport
CRD
在跨集群的服务注册方面,FSM 参考了 KEP-1645: Multi-Cluster Services API[2] 中的 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-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
整体架构
集群介绍
这里我们需要创建 4 个集群,使用 k3d 在本地创建 4 个集群,分别是 control-plane
、cluster-1
、cluster-2
和 cluster-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-1
、cluster-2
和 cluster-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
cat <<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-1
、cluster-2
和 cluster-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-1
和 cluster-3
的 httpbin
命名空间(由网格管理,会注入 sidecar)下,部署 httpbin
应用。这里的 httpbin
应用由 Pipy[3] 实现,会返回当前的集群名,并提示被网格管理。
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
:正常情况下也会代理到其他集群,类似多活。
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 - <<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 的升级会变得更加容易;单集群的容量限制不再成为问题。
在后面的文章中,我们还会继续介绍非服务网格管理的服务如何进行跨集群的流量调度、跨集群情况下的访问控制,以及更高级的流量管理等内容。