Dapr 集成 Flomesh 实现跨集群服务调用

背景

随着技术和架构的不断演进,有着多运行时的态势:现代应用程序的基础能力不断地以独立运行时的方式从应用程序分离出来。这其中就有分布式应用运行时和服务网格两种运行时,今天这篇文章就为大家介绍 Dapr 与 Flomesh 服务网格的集成进行跨集群的服务调用来实现“真正的”多集群互联互通。

多集群

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

Dapr

Dapr[1] 是一个分布式应用工具包,通过提供简单而稳定的 API 实现应用程序和外围功能组件的解耦合,让开发人员可以聚焦在业务功能的研发。同时与外围组件的解耦,也使得应用程序更加的便携、更加云原生,企业可以轻松低成本地将应用迁移到不同的环境中。

Dapr 工具包提供了丰富的功能,如服务调用、弹性策略、状态存储、发布/订阅、绑定、分布式锁、名称解析等,但对于高级的服务治理功能如灰度、跨集群服务调用没有支持。

Flomesh 服务网格

微服务架构兴起之后,随着规模越来越大,服务治理的难度和碎片化显著提升,服务网格的出现使得这些问题迎刃而解。服务网格是一个处理服务间通讯的专用的基础设施层,通过它可以透明地添加可观测性、流量管理和安全性等功能,而无需将其添加到你的代码中。

Flomesh 服务网格使用可编程代理 Pipy[2] 为核心提供东西、南北向的流量管理。通过基于 L7 的流量管理能力,突破计算环境间的网络隔离,建议一个虚拟的平面网络,使不同计算环境中应用可以互相通信。可以想象,Flomesh 服务网格是覆盖多集群的“大网格”。

Dapr 集成 Flomesh 实现跨集群服务调用

示例介绍

服务端 NodeApp 是个 Dapr 应用, 在 Dapr hello-kubernetes 示例[3] 中的 NodeApp 基础上做了修改,返回响应时会显示当前的集群名;客户端 curl 用于向 NodeApp 发送请求,但并没有声明为 Dapr 应用。

Dapr 集成 Flomesh 实现跨集群服务调用

NodeApp 中有三个 endpoint:

  • • GET /ports 返回当前应用可访问的端口

  • • POST /neworder 创建新的订单

  • • GET /order 查询订单

下面的演示中会从集群的创建开始,一步步介绍环境的配置、各个组件的安装和配置、应用的部署等等。

一键安装脚本

我们也准备脚本进行一键安装和快速的体验,免除环境和组件配置的繁琐。可以 访问 GitHub 获取脚本[4] 内容。

使用该脚本之前,需要确保系统装已经安装了 Docker 和 kubectl。脚本运行时会进行检查,并安装 k3dhelmjqpv 等工具。

  • • flomesh.sh - 不提供任何参数,脚本会创建 4 个集群、完成环境安装配置并运行演示

  • • flomesh.sh -h - 打印帮助信息

  • • flomesh.sh -i - 创建 4 个集群、完成环境的安装和配置

  • • flomesh.sh -d - 运行演示

  • • flomesh.sh -r - 清理演示相关的资源

  • • flomesh.sh -u - 删除所有集群

执行下面的命令,即可完成环境安装配置和演示的运行。

curl -sL https://raw.githubusercontent.com/addozhang/flomesh-dapr-demo/main/flomesh.sh | bash -

逐步演示

前提条件

进行演示,我们需要如下的工具:

  • • Docker

  • • Kubectl

  • • K3d

  • • Helm

  • • kubectx

创建多集群

获取本机 IP 地址作为集群间的通信地址。

export HOST_IP=10.0.0.13

执行下面创建 4 个集群:control-planecluster-1cluster-2 和 cluster-3

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 "${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

helm repo add fsm https://charts.flomesh.iohelm repo updateexport FSM_NAMESPACE=flomeshexport FSM_VERSION=0.2.1-alpha.3for 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_NAMESPACE --timeout=120sdone

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

kubectx k3d-control-planesleep 1PORT=81for CLUSTER_NAME in cluster-1 cluster-2 cluster-3do  kubectl apply -f - <<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-edge

下载 CLI

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

将服务网格 osm-edge 安装到集群 cluster-1cluster-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
kubectl get svc -n defaultNAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGEkubernetes   ClusterIP   10.43.0.1    <none>        443/TCP   43h

放行 pod 中访问 apiserver 的流量,不经过 sidecar。

for CLUSTER_NAME in cluster-1 cluster-2 cluster-3do  kubectx k3d-${CLUSTER_NAME}  kubectl patch meshconfig osm-mesh-config -n $OSM_NAMESPACE -p '{"spec":{"traffic":{"outboundIPRangeExclusionList":["10.43.0.1/32"]}}}'  --type=mergedone

安装 Dapr

将 Dapr 安装到集群 cluster-1cluster-2 和 cluster-3

for CLUSTER_NAME in cluster-1 cluster-2 cluster-3do  kubectx k3d-${CLUSTER_NAME}  dapr init --kubernetes \  --enable-mtls=false \  --waitdone

查看组件运行状态。

dapr status -k  NAME                   NAMESPACE    HEALTHY  STATUS   REPLICAS  VERSION  AGE  CREATED  dapr-placement-server  dapr-system  True     Running  1         1.9.6    2m   2023-02-09 10:36.51  dapr-operator          dapr-system  True     Running  1         1.9.6    2m   2023-02-09 10:36.51  dapr-dashboard         dapr-system  True     Running  1         0.11.0   2m   2023-02-09 10:36.51  dapr-sentry            dapr-system  True     Running  1         1.9.6    2m   2023-02-09 10:36.51  dapr-sidecar-injector  dapr-system  True     Running  1         1.9.6    2m   2023-02-09 10:36.51

查看组件 Service 及端口。

kubectl get svc -n dapr-systemNAME                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)              AGEdapr-placement-server   ClusterIP   None            <none>        50005/TCP,8201/TCP   5h50mdapr-sidecar-injector   ClusterIP   10.43.12.213    <none>        443/TCP              5h50mdapr-webhook            ClusterIP   10.43.103.31    <none>        443/TCP              5h50mdapr-dashboard          ClusterIP   10.43.172.156   <none>        8080/TCP             5h50mdapr-api                ClusterIP   10.43.126.14    <none>        80/TCP               5h50mdapr-sentry             ClusterIP   10.43.41.10     <none>        80/TCP               5h50m

不拦截 dapr 组件和 redis 端口的流量。

for CLUSTER_NAME in cluster-1 cluster-2 cluster-3do  kubectx k3d-${CLUSTER_NAME}  kubectl patch meshconfig osm-mesh-config -n $OSM_NAMESPACE -p '{"spec":{"traffic":{"outboundPortExclusionList":[50005,8201,6379]}}}'  --type=mergedone

部署 Redis

version: '3'services:  redis:    image: redis:latest    container_name: redis    ports:      - 6379:6379    volumes:      - ./data:/data    command: redis-server --appendonly yes --requirepass changeme

创建示例命名空间

export NAMESPACE=dapr-testfor CLUSTER_NAME in cluster-1 cluster-2 cluster-3do  kubectx k3d-${CLUSTER_NAME}  kubectl create namespace $NAMESPACE  osm namespace add $NAMESPACEdone

注册 Redis State Store 组件

export NAMESPACE=dapr-testfor CLUSTER_NAME in cluster-1 cluster-2 cluster-3do  kubectx k3d-${CLUSTER_NAME}  kubectl create secret generic redis -n $NAMESPACE --from-literal=redis-password=changeme  kubectl apply -n $NAMESPACE -f - <<EOFapiVersion: dapr.io/v1alpha1kind: Componentmetadata:  name: statestorespec:  type: state.redis  version: v1  metadata:  - name: redisHost    value: 10.0.0.13:6379  - name: redisPassword    secretKeyRef:      name: redis      key: redis-passwordauth:  secretStore: kubernetesEOFdone

部署示例应用

在集群 cluster-1 和 cluster-3 的 httpbin 命名空间(由网格管理,会注入 sidecar)下,部署 nodeapp 应用。

export NAMESPACE=dapr-testfor CLUSTER_NAME in cluster-1 cluster-2  cluster-3do  kubectx k3d-${CLUSTER_NAME}  kubectl apply -n $NAMESPACE -f - <<EOFkind: ServiceapiVersion: v1metadata:  name: nodeapp  labels:    app: nodespec:  selector:    app: node  ports:  - protocol: TCP    port: 3000    targetPort: 3000---apiVersion: apps/v1kind: Deploymentmetadata:  name: nodeapp  labels:    app: nodespec:  replicas: 1  selector:    matchLabels:      app: node  template:    metadata:      labels:        app: node      annotations:        dapr.io/enabled: "true"        dapr.io/app-id: "nodeapp"        dapr.io/app-port: "3000"        dapr.io/enable-api-logging: "true"    spec:      containers:      - name: node        image: addozhang/dapr-nodeapp        env:        - name: APP_PORT          value: "3000"        - name: CLUSTER_NAME          value: ${CLUSTER_NAME}        ports:        - containerPort: 3000        imagePullPolicy: AlwaysEOFdone

在集群 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      annotations:        dapr.io/enabled: "true"        dapr.io/app-id: "curl"        dapr.io/enable-api-logging: "true"        dapr.io/log-level: "debug"    spec:      serviceAccountName: curl      containers:      - image: curlimages/curl        imagePullPolicy: IfNotPresent        name: curl        command: ["sleep", "365d"]EOFsleep 3kubectl wait --for=condition=ready pod -n ${NAMESPACE} --all --timeout=60s

导出服务

export NAMESPACE=dapr-testfor CLUSTER_NAME in cluster-1 cluster-3do  kubectx k3d-${CLUSTER_NAME}  kubectl apply -n $NAMESPACE -f - <<EOFapiVersion: flomesh.io/v1alpha1kind: ServiceExportmetadata:  name: nodeappspec:  serviceAccountName: '*'  pathRewrite:    from: '^/nodeapp/?'    to: '/'  rules:    - portNumber: 3000      path: '/nodeapp'      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}/ports"  echo '-----------------------------------'done

测试

切换到集群 cluster-2 在 curl pod 中发起请求进行测试。

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

发送请求访问 nodeapp 时报错,这是因为在 cluster-2 中并未部署 nodeapp 应用。默认情况下,在不指定跨集群的流量策略时,只会尝试调用本地服务,不会将流量调度到其他的集群。

kubectl exec "${curl_client}" -n curl -c curl -- curl -s http://nodeapp.dapr-test:3000/portscommand terminated with exit code 7

设置流量策略

跨集群的流量策略支持三种:

  • • Locality:只使用本集群的服务,也是默认类型。这就是为什么我们不提供任何全局策略的时候,访问 nodeapp 应用会失败,因为在集群 cluster-2 中并没有该服务。

  • • FailOver:当本集群访问失败时才会代理到其他集群,也就是常说的故障迁移,类似主备。

  • • ActiveActive:正常情况下也会代理到其他集群,类似多活。

接下来我们创建并应用如下的 ActiveActive 策略,同样是在 cluster-2 集群中进行操作。

kubectl apply -n dapr-test -f - <<EOFapiVersion: flomesh.io/v1alpha1kind: GlobalTrafficPolicymetadata:  name: nodeappspec:  lbType: ActiveActive  targets:    - clusterKey: default/default/default/cluster-1      weight: 100    - clusterKey: default/default/default/cluster-3      weight: 100EOF

再次尝试发送请求,可以收到成功的响应。

kubectl exec "${curl_client}" -n curl -c curl -- curl -s http://nodeapp.dapr-test:3000/ports{"DAPR_HTTP_PORT":"3500","DAPR_GRPC_PORT":"50001"}, from cluster: cluster-3

这时请求 /neworder 尝试写入订单数据。

kubectl exec "${curl_client}" -n curl -c curl -- curl -si --request POST --data '{"data":{"orderId":"42"}}' --header Content-Type:application/json --header dapr-app-id:nodeapp http://nodeapp.dapr-test:3000/newordercreated order via cluster: cluster-1

多次请求 /order,可以发现请求被转发到了不同的集群进行处理。

kubectl exec "${curl_client}" -n curl -c curl -- curl -s http://nodeapp.dapr-test:3000/order{"orderId":"42"}, from cluster: cluster-3kubectl exec "${curl_client}" -n curl -c curl -- curl -s http://nodeapp.dapr-test:3000/order{"orderId":"42"}, from cluster: cluster-1