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

Envoy 通过 xDS 实现了其动静配置,来应答一直变动的基础架构。

xDS 简介

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

资源类型

xDS API中的每个配置资源都有与之关联的类型。资源类型遵循版本控制计划。目前V2版本曾经进行开发,不过会有一年的保护期。V3版本是目前主力版本。

反对以下v3 xDS资源类型:

  • envoy.config.listener.v3.Listener
  • envoy.config.route.v3.RouteConfiguration
  • envoy.config.route.v3.ScopedRouteConfiguration
  • envoy.config.route.v3.VirtualHost
  • envoy.config.cluster.v3.Cluster
  • envoy.config.endpoint.v3.ClusterLoadAssignment
  • envoy.extensions.transport_sockets.tls.v3.Secret
  • envoy.service.runtime.v3.Runtime

格局为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中。

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

流式gRPC应用的xDS传输协定有四种变体:

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

如何实现一个简略的管制立体

社区提供了两种语言的实现,在咱们编写本人的管制立体时,能够间接应用:

  • go-control-plane
  • java-control-plane

比方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-gatewayadmin:  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 疏导程序

Contour应用init-container来为Envoy生成一个动态疏导程序配置文件,该文件批示在哪里能够找到xDS服务。xDS服务器是管制立体中的第二个组件。

总结

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

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