2

通过 xDS 实现 Envoy 动态配置

 3 years ago
source link: https://studygolang.com/articles/32700
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在云原生时代,容器取代虚拟机成为承载应用工作负载的主要形式。虚拟机生命周期相对较长,可能有数天,但是容器少则几分钟。这就要求负载均衡器必须能适应这种动态性。

Envoy 通过 xDS 实现了其动态配置,来应对不断变化的基础架构。

xDS 简介

Envoy通过文件系统或查询管理服务器发现其各种动态资源。这些发现服务及其相应的API统称为xDS。

资源类型

xDS API中的每个配置资源都有与之关联的类型。资源类型遵循 版本控制方案 。目前V2版本已经停止开发,不过会有一年的维护期。V3版本是目前主力版本。

支持以下v3 xDS资源类型:

格式为 http://type.googleapis.com/ <资源类型>–例如,用于集群资源的type.googleapis.com/envoy.api.v3.Cluster。在来自Envoy的各种请求和管理服务器的响应中,都声明了资源类型URL。

这些API 实际上通过 proto3 Protocol Buffers 定义。

流式gRPC订阅

Envoy 通过订阅(_subscription_)方式来获取资源,如监控指定路径下的文件、启动 gRPC 流或轮询 REST-JSON URL。后两种方式会发送 DiscoveryRequest 请求消息,发现的对应资源则包含在响应消息 DiscoveryResponse 中。

3qA3emn.png!mobile

其中流式gRPC订阅是最常使用的。

流式gRPC使用的xDS传输协议有四种变体:

  1. State of the World (Basic xDS):SotW,每种资源类型的单独gRPC流
  2. 增量xDS:每种资源类型的增量独立gRPC流
  3. 聚合发现服务(ADS):SotW,所有资源类型的聚合流
  4. 增量ADS:所有资源类型的增量聚合流

如何实现一个简单的控制平面

社区提供了两种语言的实现,在我们编写自己的控制平面时,可以直接使用:

比如 go-control-plane 提供了由多个不同控制平面实现共享的基础结构。该库提供的组件是:

  • API服务器 _:_一种基于gRPC的通用API服务器,可实现 data-plane-api中 定义的xDS API 。API服务器负责将配置更新推送到Envoy。消费者应该能够在生产部署中导入该go库并按原样使用API​​服务器。
  • 配置缓存 _:_该库将在内存中缓存Envoy配置,以对Envoy提供快速响应。此库的使用者有责任将数据写入到高速缓存,并在必要时使高速缓存无效。高速缓存将基于预定义的哈希函数进行键控,该哈希函数的键基于 Node信息

目前,此存储库将不会处理将平台(例如服务,服务实例等)的特定于资源的表示转换为Envoy样式的配置。

下面我们通过 go-control-plane 实现一个简单的Envoy控制平面,并且采用的是第三种xDS变体。

1:数据平面Envoy配置

虽然Envoy接受控制平面的动态资源,但是Envoy的启动需要一些静态配置,也就是引导文件。完整引导文件如下:

node:
  id: node-1
  cluster: edge-gateway

admin:
  access_log_path: /dev/stdout
  address:
    socket_address: { address: 127.0.0.1, port_value: 9901 }

dynamic_resources:
  ads_config:
    # allows limiting the rate of discovery requests.
    # for edge cases with very frequent requests or due to a bug.
    rate_limit_settings:
      max_tokens: 10
      fill_rate: 3
    # we use v3 xDS framing
    transport_api_version: V3
    # over gRPC
    api_type: GRPC
    grpc_services:
      - envoy_grpc:
          cluster_name: xds_cluster
  # Use ADS for LDS and CDS; request V3 clusters and listeners.
  lds_config: {ads: {}, resource_api_version: V3}
  cds_config: {ads: {}, resource_api_version: V3}

static_resources:
  clusters:
  - name: xds_cluster
    connect_timeout: 0.25s
    type: STATIC
    lb_policy: ROUND_ROBIN
    # as we are using gRPC xDS we need to set the cluster to use http2
    http2_protocol_options: {}
    upstream_connection_options:
      # important:
      # configure a TCP keep-alive to detect and reconnect to the admin
      # server in the event of a TCP socket half open connection
      # the default values are very conservative, so you will want to tune them.
      tcp_keepalive: {}
    load_assignment:
      cluster_name: xds_cluster
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 9977

引导文件包含两个 ConfigSource 消息,一个指示如何获取 侦听器 资源,另一个指示如何获取 集群 资源。它还包含一个单独的 ApiConfigSource 消息,该消息指示如何与ADS服务器通信,只要 ConfigSource 消息(在引导文件或从管理服务器获取的 侦听器集群 资源中)包含 AggregatedConfigSource 消息,就会使用该消息。

在使用xDS的gRPC客户端中,仅支持ADS,并且引导文件包含ADS服务器的名称,该名称将用于所有资源。 侦听器集群 资源中的 ConfigSource 消息必须包含 AggregatedConfigSource 消息。

那么Envoy按照该引导文件启动后,会与127.0.0.1:9977 通信,获取监听器和集群资源。

2:编写控制平面

本次控制平面要实现的功能是控制Envoy实现灰度发布。由于 go-control-plane 已经帮我们做了很多事情,所以我们唯一需要做的就是将灰度相关的设置转换为Envoy样式的配置。

由于代码较长,我们只贴出核心的代码:

func makeRoute(routeName string, weight uint32, clusterName1, clusterName2 string) *route.RouteConfiguration {
    routeConfiguration := &route.RouteConfiguration{
        Name: routeName,
        VirtualHosts: []*route.VirtualHost{{
            Name:    "local_service",
            Domains: []string{"*"},
        }},
    }
    switch weight {
    case 0:
        routeConfiguration.VirtualHosts[0].Routes = []*route.Route{{
            Match: &route.RouteMatch{
                PathSpecifier: &route.RouteMatch_Prefix{
                    Prefix: "/",
                },
            },
            Action: &route.Route_Route{
                Route: &route.RouteAction{
                    ClusterSpecifier: &route.RouteAction_Cluster{
                        Cluster: clusterName1,
                    },
                    HostRewriteSpecifier: &route.RouteAction_HostRewriteLiteral{
                        HostRewriteLiteral: UpstreamHost,
                    },
                },
            },
        }}

    case 100:
        routeConfiguration.VirtualHosts[0].Routes = []*route.Route{{
            Match: &route.RouteMatch{
                PathSpecifier: &route.RouteMatch_Prefix{
                    Prefix: "/",
                },
            },
            Action: &route.Route_Route{
                Route: &route.RouteAction{
                    ClusterSpecifier: &route.RouteAction_Cluster{
                        Cluster: clusterName2,
                    },
                    HostRewriteSpecifier: &route.RouteAction_HostRewriteLiteral{
                        HostRewriteLiteral: UpstreamHost,
                    },
                },
            },
        }}
        // canary-roll out:
    default:
        routeConfiguration.VirtualHosts[0].Routes = []*route.Route{{
            Match: &route.RouteMatch{
                PathSpecifier: &route.RouteMatch_Prefix{
                    Prefix: "/",
                },
            },
            Action: &route.Route_Route{
                Route: &route.RouteAction{
                    ClusterSpecifier: &route.RouteAction_WeightedClusters{
                        WeightedClusters: &route.WeightedCluster{
                            TotalWeight: &wrapperspb.UInt32Value{
                                Value: 100,
                            },
                            Clusters: []*route.WeightedCluster_ClusterWeight{
                                {
                                    Name: clusterName1,
                                    Weight: &wrapperspb.UInt32Value{
                                        Value: 100 - weight,
                                    },
                                },
                                {
                                    Name: clusterName2,
                                    Weight: &wrapperspb.UInt32Value{
                                        Value: weight,
                                    },
                                },
                            },
                        },
                    },
                    HostRewriteSpecifier: &route.RouteAction_HostRewriteLiteral{
                        HostRewriteLiteral: UpstreamHost,
                    },
                },
            },
        }}

    }
    return routeConfiguration
}

由于我们是第三种变体,我们在变更配置的时候,需要所有资源的变更封装成cachev3.Snapshot:

func GenerateSnapshot(weight uint32) cachev3.Snapshot {
    version++
    nextversion := fmt.Sprintf("snapshot-%d", version)
    fmt.Println("publishing version: ", nextversion)
    return cachev3.NewSnapshot(
        nextversion,        // version needs to be different for different snapshots
        []types.Resource{}, // endpoints
        []types.Resource{makeCluster(ClusterName1), makeCluster(ClusterName2)},
        []types.Resource{makeRoute(RouteName, weight, ClusterName1, ClusterName2)},
        []types.Resource{makeHTTPListener(ListenerName, RouteName)},
        []types.Resource{}, // runtimes
        []types.Resource{}, // secrets
    )
}

我们的示例比较简单,但是在生产环境,我们应该考虑哪些内容那?

3:生产环境的控制面

生产环境控制面,则需要实现:

  • 核心xDS服务接口和实现
  • 处理向服务注册表中注册/反注册服务的组件
  • 服务注册表
  • 描述Envoy配置的抽象对象模型(可选)
  • 数据存储区,用于保存配置

比如Contour,实际上只有两个组成其控制平面的组件,但是,由于它仅基于Kubernetes,因此它实际上利用了许多内置的Kubernetes设施,例如Kubernetes API /存储和CRD来驱动配置。

contour
init-container

QrmIBbA.png!mobile

Contour使用 init-container 来为Envoy生成一个静态引导程序配置文件,该文件指示在哪里可以找到xDS服务。xDS服务器是控制平面中的第二个组件。

总结

其实目前诸多基于Envoy的项目,都是采取控制面 + xDS + envoy 的模式。比如Apigateway中的gloo,ambassador,Service Mesh 中的istio,app-mesh等。

各个控制面其实基本上对接各种服务注册中心,然后再根据客户配置的转发规则,转换为xDS资源,通过gRPC流下发到Envoy中。

有疑问加站长微信联系(非本文作者)

eUjI7rn.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK