跨集群流量调度实现 Kubernetes 集群金丝雀升级

Posted dotNET跨平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了跨集群流量调度实现 Kubernetes 集群金丝雀升级相关的知识,希望对你有一定的参考价值。

有了多集群服务和跨集群的流量调度之后,使用 Kubernetes 的方式会发生很大的变化。流量的管理不再限制单一集群内,而是横向跨越了多个集群。最重要的是这一切“静悄悄地”发生,对应用来说毫无感知。

就拿 Kubernetes 版本升级来说吧。记得曾经经历过集群的原地升级:团队的几个人经过多次、多个环境的演练,还要在凌晨的时候进行生产环境的升级。幸好最后是有惊无险,整个过程的体验就像是下图一样:为飞行中的飞机换引擎。

图片来自网络

解决了跨集群的流量调度之后,一切就会变得简单:只需重新建个集群,慢慢将应用迁移到新的集群,让乘客来个“空中转机”。

方案

与之前所做的示例类似,整个方案的核心仍然是跨集群的服务调用:服务可以像使用本地 Service 一样使用多集群 Service。

Kubernetes 集群金丝雀升级

升级过程中,创建新版本的 Kubernetes 集群,其他的中间件复用现有的,这样少了数据同步等问题。剩下的便是,调整 CD的流程将服务“同时部署到新的集群”。然后再通过 全局流量策略 慢慢放少部分流量到新的集群中进行测试,边测试边调整流量。两个集群的服务也可保持一段时间观察稳定性,然后再慢慢减少原集群的实例直至所有实例下线。

接下来,我们使用进行下示例演示。

演示

环境准备

如上面的图展示的那样,我们先创建两个集群:control-plane1-23 和 1-25。集群 1-23 就是我们现有的集群,1-25 就是最新版本的集群。

集群对外访问地址api-server 对外端口LB 对外端口描述
control-planeHOST_IP(192.168.1.110)6444N/A控制平面集群
1-23HOST_IP(192.168.1.110)644581应用集群
1-25HOST_IP(192.168.1.110)644682应用集群

环境搭建

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

export HOST_IP=192.168.1.110

搭建集群

使用 k3d 来创建这 3 个 集群。

API_PORT=6444 #6445 6446
PORT=80 #81 82
for CLUSTER_NAME in control-plane 1-23 1-25
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" \\
    --timeout 120s \\
    --wait
    ((API_PORT=API_PORT+1))
    ((PORT=PORT+1))
done

安装 FSM

在 3 个集群中安装 FSM。

helm repo update
export FSM_NAMESPACE=flomesh
exportFSM_VERSION=0.2.0-alpha.9
for CLUSTER_NAME in control-plane 1-23 1-25
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 --timeout=120s
done

加入集群组

将集群 1-23 和 1-25 纳入集群 control-plane 的管理。不管是新集群还是旧集群,如果要进行跨集群的服务调用,都是要加入集群组的。

export HOST_IP=192.168.1.110
kubectx k3d-control-plane
sleep 1
PORT=81
for CLUSTER_NAME in 1-23 1-25
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

system=$(uname -s | tr [:upper:] [:lower:])
arch=$(dpkg --print-architecture)
release=v1.3.0-beta.1
curl -L https://github.com/flomesh-io/osm-edge/releases/download/$release/osm-edge-$release-$system-$arch.tar.gz | tar -vxzf -
./$system-$arch/osm version
cp ./$system-$arch/osm /usr/local/bin/

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

export OSM_NAMESPACE=osm-system
export OSM_MESH_NAME=osm
for CLUSTER_NAME in 1-23 1-25
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

部署实例应用

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

NAMESPACE=httpbin
CLUSTER_NAME="1-23"

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
EOF

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

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

export NAMESPACE=curl
kubectx k3d-1-23
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

测试

在集群 1-23 中的 curl 向 httpbin 发送请求。

kubectx k3d-1-23
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/

看到下面的输出,说明服务正常。

Hi, I am from 1-23 and controlled by mesh!

导出服务

服务的导出就是向集群组注册服务,执行下面的命令将集群 1-23 中的服务 httpbin 注册到集群组。注意,该命令是在集群 1-23 中执行的。

export NAMESPACE_MESH=httpbin
CLUSTER_NAME="1-23"
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
EOF
sleep 1

此时,我们的系统如下图所示。

集群升级.002

升级迁移

新集群中部署应用

有了新版本的集群之后,我们慢慢向新集群迁移服务。在集群 1-25 中部署 httpbin 服务。

NAMESPACE=httpbin
CLUSTER_NAME="1-25"

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

导出服务

向集群组注册集群 1-25 的服务 httpbin

export NAMESPACE_MESH=httpbin
CLUSTER_NAME="1-25"
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
EOF
sleep 1

回到集群 1-23,查看 ServiceImports httpbin,可以看到已经发现了集群 1-25 注册的服务。

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

  "ports": [
    
      "endpoints": [
        
          "clusterKey": "default/default/default/1-25",
          "target": 
            "host": "192.168.1.110",
            "ip": "192.168.1.110",
            "path": "/1-25/httpbin-mesh",
            "port": 82
          
        
      ],
      "port": 8080,
      "protocol": "TCP"
    
  ],
  "serviceAccountName": "*",
  "type": "ClusterSetIP"

但此时在 curl发送请求,并不会收到集群 1-25 中的响应。还记得 上篇 中提到过,默认的全局流量策略是 Locality,因此集群外的节点并不会参与请求的处理。

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 将其设置为 10,表示将 1/11 的流量导入集群 1-25 中,记住本集群的权重总是 100

kubectx k3d-1-23
kubectl 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: 10
EOF

这次我们请求 20 次,结果也正如我们所期望的,只有 1-2 次请求进入集群 1-25

for i in 1..20; do kubectl exec "$curl_client" -n curl -c curl -- curl -s http://httpbin.httpbin:8080/ ; done
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-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!
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!
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-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!
Hi, I am from 1-23 and controlled by mesh!

我们将集群 1-25 的权重设置为 900,因此会有 90% 的流量进入集群 1-25

kubectx k3d-1-23
kubectl 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: 900
EOF

然后请求 10 次,会看到 9 次的请求都进入到集群 1-25,也就是 90% 的流量分流到了其他集群。

for i in 1..10; do kubectl exec "$curl_client" -n curl -c curl -- curl -s http://httpbin.httpbin:8080/ ; done
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-25 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!
Hi, I am from 1-25 and controlled by mesh!

接着就是停止集群 1-23 中的实例,使所有流量都分流到外集群。

kubectl scale deploy httpbin -n httpbin --replicas 0

最后的工作就是慢慢得将 curl 服务也迁移到新的集群中,进而是所有的服务都迁移完成之后,下线旧的集群。

总结

自维护的 Kubernetes 集群升级不是一件容易的事情,原地升级风险高,尤其是升级控制面。不管是蓝绿还是金丝雀升级,都面临着流量跨集群的问题:流量除了从入口进入还会有其他的途径,比如消息系统,定时任务等等。

解决了流量的跨集群调度问题后,这些问题都迎刃而解。剩下的问题就是如何让迁移做到自动化、可控的迁移了。

引用链接

[1] Pipy: https://flomesh.io/pipy

以上是关于跨集群流量调度实现 Kubernetes 集群金丝雀升级的主要内容,如果未能解决你的问题,请参考以下文章

Kubernetes:如何实现跨集群节点均匀调度分布Pod(Pod拓扑分布约束)

Kubernetes:如何实现跨集群节点均匀调度分布Pod(Pod拓扑分布约束)

Kubernetes:如何实现跨集群节点均匀调度分布Pod(Pod拓扑分布约束)

教程|使用Istio在Kubernetes集群中实现金丝雀部署

使用Karmada实现Helm应用的跨集群部署

跨集群服务——实现Kubernetes应用的高可用