有了多集群服务和跨集群的流量调度之后,使用 Kubernetes 的方式会发生很大的变化。流量的管理不再限制单一集群内,而是横向跨越了多个集群。最重要的是这一切"静悄悄地"发生,对应用来说毫无感知。
就拿 Kubernetes 版本升级来说吧。记得曾经经历过集群的原地升级:团队的几个人经过多次、多个环境的演练,还要在凌晨的时候进行生产环境的升级。幸好最后是有惊无险,整个过程的体验就像是下图一样:为飞行中的飞机换引擎。
解决了跨集群的流量调度之后,一切就会变得简单:只需重新建个集群,慢慢将应用迁移到新的集群,让乘客来个"空中转机"。
方案
与之前所做的示例类似,整个方案的核心仍然是跨集群的服务调用:服务可以像使用本地 Service 一样使用多集群 Service。
升级过程中,创建新版本的 Kubernetes 集群,其他的中间件复用现有的,这样少了数据同步等问题。剩下的便是,调整 CD 的流程将服务"同时部署到新的集群"。然后再通过 全局流量策略 慢慢放少部分流量到新的集群中进行测试,边测试边调整流量。两个集群的服务也可保持一段时间观察稳定性,然后再慢慢减少原集群的实例直至所有实例下线。
接下来,我们使用进行下示例演示如何使用 Flomesh 服务网格 实现集群的平滑升级。
演示
环境准备
如上面的图展示的那样,我们先创建两个集群:control-plane
、1-23
和 1-25
。 集群 1-23
就是我们现有的集群,1-25
就是最新版本的集群。
集群 | 对外访问地址 | api-server 对外端口 | LB 对外端口 | 描述 |
---|---|---|---|---|
control-plane | HOST_IP(192.168.1.110) | 6444 | N/A | 控制平面集群 |
1-23 | HOST_IP(192.168.1.110) | 6445 | 81 | 应用集群 |
1-25 | HOST_IP(192.168.1.110) | 6446 | 82 | 应用集群 |
工具准备
- Docker
- K3d
- kubectl
- jq
环境搭建
创建一个 bridge
类型的网络 multi-clusters
,所有的容器都工作在这个网络中。
shell
docker network create multi-clusters
获取主机 IP 地址,这个地址将作为集群的访问地址。
shell
export HOST_IP=$(if [ "$(uname)" == "Darwin" ]; then ipconfig getifaddr en0; else ip -o route get to 8.8.8.8 | sed -n 's/.*src \([0-9.]\+\).*/\1/p'; fi)
搭建集群
使用 k3d 来创建这 3 个 集群。
shell
API_PORT=6444 #6445 6446
PORT=80 #81 82
for CLUSTER_NAME in control-plane 1-23 1-25
do
if [ "$CLUSTER_NAME" = "control-plane" ]; then
IMAGE_TAG="v1.25.15-k3s2"
elif [ "$CLUSTER_NAME" = "1-23" ]; then
IMAGE_TAG="v1.23.8-k3s2"
elif [ "$CLUSTER_NAME" = "1-25" ]; then
IMAGE_TAG="v1.25.15-k3s2"
fi
k3d cluster create ${CLUSTER_NAME} \
--image docker.io/rancher/k3s:${IMAGE_TAG} \
--api-port "${HOST_IP}:${API_PORT}" \
--port "${PORT}:80@server:0" \
--servers-memory 4g \
--k3s-arg "--disable=traefik@server:0" \
--timeout 120s \
--wait
((API_PORT=API_PORT+1))
((PORT=PORT+1))
done
保存集群的 kubeconfig
shell
kubeconfig_cp=${KUBECONFIG_CP:-"/tmp/cp.kubeconfig"}
kubeconfig_123=${KUBECONFIG_C1:-"/tmp/123.kubeconfig"}
kubeconfig_125=${KUBECONFIG_C2:-"/tmp/125.kubeconfig"}
k0="kubectl --kubeconfig ${kubeconfig_cp}"
k1="kubectl --kubeconfig ${kubeconfig_123}"
k2="kubectl --kubeconfig ${kubeconfig_125}"
k3d kubeconfig get control-plane >"${kubeconfig_cp}"
k3d kubeconfig get 1-23 >"${kubeconfig_123}"
k3d kubeconfig get 1-25 >"${kubeconfig_125}"
安装 FSM
下载 FSM CLI
shell
system=$(uname -s | tr '[:upper:]' '[:lower:]')
arch=$(uname -m | sed -E 's/x86_/amd/' | sed -E 's/aarch/arm/')
release=v1.1.4
curl -L https://github.com/flomesh-io/fsm/releases/download/$release/fsm-$release-$system-$arch.tar.gz | tar -vxzf -
./$system-$arch/fsm version
sudo cp ./$system-$arch/fsm /usr/local/bin/fsm
在 3 个集群中安装 FSM。
shell
export FSM_NAMESPACE=fsm-system
export FSM_MESH_NAME=fsm
for CONFIG in kubeconfig_cp kubeconfig_123 kubeconfig_125
do
DNS_SVC_IP="$(kubectl --kubeconfig ${!CONFIG} get svc -n kube-system -l k8s-app=kube-dns -o jsonpath='{.items[0].spec.clusterIP}')"
KUBECONFIG=${!CONFIG} fsm install \
--mesh-name "$FSM_MESH_NAME" \
--fsm-namespace "$FSM_NAMESPACE" \
--set=fsm.fsmIngress.enabled=true \
--set=fsm.localDNSProxy.enable=true \
--set=fsm.localDNSProxy.primaryUpstreamDNSServerIPAddr="${DNS_SVC_IP}" \
--timeout=900s
sleep 2
kubectl --kubeconfig ${!CONFIG} wait --for=condition=ready pod --all -n $FSM_NAMESPACE --timeout=120s
done
加入集群组
将集群 1-23
和 1-25
纳入集群 control-plane
的管理。 不管是新集群还是旧集群,如果要进行跨集群的服务调用,都是要加入集群组的。
在集群 control-plane
为两个集群创建 CR Cluster
,同时提供入口的 IP 地址 gatewayHost
和端口 gatewayPort
,以及服务网格所在的命名空间 fsmNamespace
。最终要的还有集群的 kubeconfig
,用于访问集群。
shell
PORT=81
for CLUSTER_NAME in 1-23 1-25
do
kubectl --kubeconfig ${kubeconfig_cp} apply -f - <<EOF
apiVersion: flomesh.io/v1alpha1
kind: Cluster
metadata:
name: ${CLUSTER_NAME}
spec:
gatewayHost: ${HOST_IP}
gatewayPort: ${PORT}
fsmNamespace: ${FSM_NAMESPACE}
kubeconfig: |+
$(k3d kubeconfig get ${CLUSTER_NAME} | sed 's|^| |g' | sed "s|0.0.0.0|$HOST_IP|g")
EOF
((PORT = PORT + 1))
done
查看集群组的情况:
shell
$k0 get cluster
NAME REGION ZONE GROUP GATEWAY HOST GATEWAY PORT MANAGED MANAGED AGE AGE
1-23 default default default 10.0.2.4 81 True 10s 11s
1-25 default default default 10.0.2.4 82 True 9s 11s
部署实例应用
在集群 1-23
的 httpbin
命名空间(由网格管理,会注入 sidecar)下,部署 httpbin
应用。这里的 httpbin
应用由 Pipy 实现,会返回当前的集群名,并提示被网格管理。
shell
NAMESPACE=httpbin
CLUSTER_NAME="1-23"
kubectl --kubeconfig ${kubeconfig_123} create namespace ${NAMESPACE}
KUBECONFIG=${kubeconfig_123} fsm namespace add ${NAMESPACE}
kubectl --kubeconfig ${kubeconfig_123} 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
EOF
sleep 3
kubectl --kubeconfig ${kubeconfig_123} wait --for=condition=ready pod -n ${NAMESPACE} --all --timeout=60s
在命名空间 curl
下部署 curl
应用,这个命名空间也是被网格管理的,注入的 sidecar 会完全流量的跨集群调度。
shell
export NAMESPACE=curl
kubectl --kubeconfig ${kubeconfig_123} create namespace ${NAMESPACE}
KUBECONFIG=${kubeconfig_123} fsm namespace add ${NAMESPACE}
kubectl --kubeconfig ${kubeconfig_123} 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 --kubeconfig ${kubeconfig_123} wait --for=condition=ready pod -n ${NAMESPACE} --all --timeout=60s
测试
在集群 1-23
中的 curl
向 httpbin
发送请求。
shell
curl_client="$(kubectl --kubeconfig ${kubeconfig_123} get pod -n curl -l app=curl -o jsonpath='{.items[0].metadata.name}')"
kubectl --kubeconfig ${kubeconfig_123} exec "${curl_client}" -n curl -c curl -- curl -s http://httpbin.httpbin:8080/
看到下面的输出,说明服务正常。
shell
Hi, I am from 1-23 and controlled by mesh!
导出服务
服务的导出就是向集群组注册服务,执行下面的命令将集群 1-23
中的服务 httpbin
注册到集群组。注意,该命令是在集群 1-23
中执行的。
shell
export NAMESPACE_MESH=httpbin
kubectl --kubeconfig ${kubeconfig_123} apply -f - <<EOF
apiVersion: flomesh.io/v1alpha1
kind: ServiceExport
metadata:
namespace: ${NAMESPACE_MESH}
name: httpbin
spec:
serviceAccountName: "*"
rules:
- portNumber: 8080
path: "/httpbin"
pathType: Prefix
EOF
sleep 1
此时,我们的系统如下图所示。
升级迁移
新集群中部署应用
有了新版本的集群之后,我们慢慢向新集群迁移服务。在集群 1-25
中部署 httpbin
服务。
shell
NAMESPACE=httpbin
CLUSTER_NAME="1-25"
kubectl --kubeconfig ${kubeconfig_125} create namespace ${NAMESPACE}
KUBECONFIG=${kubeconfig_125} fsm namespace add ${NAMESPACE}
kubectl --kubeconfig ${kubeconfig_125} 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 --kubeconfig ${kubeconfig_125} wait --for=condition=ready pod -n ${NAMESPACE} --all --timeout=60s
导出服务
向集群组注册集群 1-25
的服务 httpbin
。
shell
export NAMESPACE_MESH=httpbin
kubectl --kubeconfig ${kubeconfig_125} apply -f - <<EOF
apiVersion: flomesh.io/v1alpha1
kind: ServiceExport
metadata:
namespace: ${NAMESPACE_MESH}
name: httpbin
spec:
serviceAccountName: "*"
rules:
- portNumber: 8080
path: "/httpbin"
pathType: Prefix
EOF
sleep 1
发现多集群服务
回到集群 1-23
,查看 ServiceImports
httpbin
,可以看到已经发现了集群 1-25
注册的服务。其中 82
就是新集群访问入口的端口。
shell
kubectl --kubeconfig ${kubeconfig_123} get serviceimports httpbin -n httpbin -o jsonpath='{.spec}' | jq
{
"ports": [
{
"endpoints": [
{
"clusterKey": "default/default/default/1-25",
"target": {
"host": "10.0.2.4",
"ip": "10.0.2.4",
"path": "/httpbin",
"port": 82
}
}
],
"port": 8080,
"protocol": "TCP"
}
],
"serviceAccountName": "*",
"type": "ClusterSetIP"
}
但此时在 curl
发送请求,并不会收到集群 1-25
中的响应。在多集群流量调度中,默认的全局流量策略是 Locality
,也就是只调用本集群的服务,因此集群外的节点并不会参与请求的处理。
shell
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
我们创建一个 ActiveActive
的全局流量策略。注意,这里设置 targets
的时候我们加上了代表权重的字段 weight
。 将其设置为 50,表示将 50/150 的流量导入集群 1-25
中,记住本集群的权重总是 100。
shell
kubectl --kubeconfig ${kubeconfig_123} apply -n httpbin -f - <<EOF
apiVersion: flomesh.io/v1alpha1
kind: GlobalTrafficPolicy
metadata:
name: httpbin
spec:
lbType: ActiveActive
targets:
- clusterKey: default/default/default/1-25
weight: 50
EOF
这次我们请求 20 次。
shell
curl_client="$(kubectl --kubeconfig ${kubeconfig_123} get pod -n curl -l app=curl -o jsonpath='{.items[0].metadata.name}')"
for i in {1..20}; do kubectl --kubeconfig ${kubeconfig_123} exec "${curl_client}" -n curl -c curl -- curl -s http://httpbin.httpbin:8080/ ; done
在结果中可以看到有 1/3 的流量进入到了新集群。
css
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-23 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
接下来,下线旧集群中的服务实例。
shell
kubectl --kubeconfig ${kubeconfig_123} delete deploy,svc httpbin -n httpbin
再次请求,所有的流量都会进入新的集群。
最后的工作就是慢慢得将 curl
服务也迁移到新的集群中,进而是所有的服务都迁移完成之后,下线旧的集群。
总结
自维护的 Kubernetes 集群升级不是一件容易的事情,原地升级风险高,尤其是升级控制面。不管是蓝绿还是金丝雀升级,都面临着流量跨集群的问题:流量除了从入口进入还会有其他的途径,比如消息系统,定时任务等等。
解决了流量的跨集群调度问题后,这些问题都迎刃而解。剩下的问题就是如何让迁移做到自动化、可控的迁移了。