XDS
通过查询一个或多个管理服务器获取数据以发现动态资源变更,比如 Router、Cluster、EndPoint 等,这些发现服务及其对应的 API 称为 xDS 。xDS 最大的价值就是定义了一套可扩展的通用微服务控制 API,这些API不仅可以做到服务发现,也可以做到路由发现、集群发现,可以说所有配置都能通过发现的方式解决,这是一种全新的解决方案,所以 xDS API 不仅被用在了 Service Mesh 中,也用在了一些 RPC 框架中,比如 gRPC 就是用 xDS 协议做服务发现的。
xDS 包含 LDS(监听器发现服务)、CDS(集群发现服务)、EDS(节点发现服务)、SDS(密钥发现服务)和 RDS(路由发现服务)。
xDS 中每种类型对应一个发现的资源,这些类型数据存储在 xDS 协议的 Discovery Request 和 Discovery Response 的 TypeUrl 字段中, 这个字段按照以下格式存储:
type.googleapis.com/<resource type>
比如 type.googleapis.com/envoy.api.v2.Cluster 就表明是 Cluster 类型的资源,需要按照 Cluster 类型处理数据。
envoy.api.v2.Listener(LDS):对应 Listener 数据类型,包含了监听器的名称、监听端口、监听地址等信息,通过动态更新此类型,可以动态新增监听器或者更新监听器的地址端口等信息。
LDS 的数据结构如下:
{ "name": "...", "address": "{...}", "filter_chains": [], "use_original_dst": "{...}", "per_connection_buffer_limit_bytes": "{...}", "metadata": "{...}", "drain_type": "...", "listener_filters": [], "listener_filters_timeout": "{...}", "continue_on_listener_filters_timeout": "...", "transparent": "{...}", "freebind": "{...}", "socket_options": [], "tcp_fast_open_queue_length": "{...}", "traffic_direction": "...", "udp_listener_config": "{...}", "api_listener": "{...}", "connection_balance_config": "{...}", "reuse_port": "...", "access_log": [] }
envoy.api.v2.RouteConfiguration(RDS):对应 Envoy 中的 Route 类型,用于更新 virtual_hosts,以及 virtual_hosts 包含的路由表信息、路由规则、针对路由的限流、路由级别的插件等,包括路由匹配到的 Cluster。
RDS 的数据结构如下:
{ "name": "...", "virtual_hosts": [], "vhds": "{...}", "internal_only_headers": [], "response_headers_to_add": [], "response_headers_to_remove": [], "request_headers_to_add": [], "request_headers_to_remove": [], "most_specific_header_mutations_wins": "...", "validate_clusters": "{...}" }
envoy.api.v2.Cluster(CDS):对应 Envoy 中的 Cluster 类型,包含了 Cluster 是采用静态配置数据,还是采用动态 EDS 发现的方式,包括 Cluster 的负载均衡策略、健康检查配置等,以及服务级别的插件设置。
CDS 的数据结构如下:
{ "transport_socket_matches": [], "name": "...", "alt_stat_name": "...", "type": "...", "cluster_type": "{...}", "eds_cluster_config": "{...}", "connect_timeout": "{...}", "per_connection_buffer_limit_bytes": "{...}", "lb_policy": "...", "hosts": [], "load_assignment": "{...}", "health_checks": [], "max_requests_per_connection": "{...}", "circuit_breakers": "{...}", "tls_context": "{...}", "upstream_http_protocol_options": "{...}", "common_http_protocol_options": "{...}", "http_protocol_options": "{...}", "http2_protocol_options": "{...}", "extension_protocol_options": "{...}", "typed_extension_protocol_options": "{...}", "dns_refresh_rate": "{...}", "dns_failure_refresh_rate": "{...}", "respect_dns_ttl": "...", "dns_lookup_family": "...", "dns_resolvers": [], "use_tcp_for_dns_lookups": "...", "outlier_detection": "{...}", "cleanup_interval": "{...}", "upstream_bind_config": "{...}", "lb_subset_config": "{...}", "ring_hash_lb_config": "{...}", "original_dst_lb_config": "{...}", "least_request_lb_config": "{...}", "common_lb_config": "{...}", "transport_socket": "{...}", "metadata": "{...}", "protocol_selection": "...", "upstream_connection_options": "{...}", "close_connections_on_host_health_failure": "...", "drain_connections_on_host_removal": "...", "filters": [], "track_timeout_budgets": "..." }
envoy.api.v2.ClusterLoadAssignment(EDS):EDS,也就是我们常说的服务发现。包含服务名、节点信息和 LB 策略等数据。
EDS 的数据结构如下:
{ "cluster_name": "...", "endpoints": [], "policy": "{...}" }
envoy.api.v2.Auth.Secret(SDS):用于发现证书信息,以动态更新证书。早期 Istio 使用变更 TLS 证书文件,然后热重启 Envoy 的方式更新证书,现在通过 SDS 即可动态更新证书。
SDS 的数据结构如下:
{ "name": "...", "tls_certificate": "{...}", "session_ticket_keys": "{...}", "validation_context": "{...}", "generic_secret": "{...}" }
Envoy xDS 协议最早并没有采用 gRPC 流式订阅,而是采用 rest-json 轮询的模式实现,后来因为 gRPC 流式订阅数据更新更加及时,性能也相对高效,所以在 v2 版本转向了 gRPC 的方式更新数据。接下来我们主要看一下 gRPC 流式订阅模式。
gRPC 流式订阅
API 请求顺序
我们先来看一下典型的 HTTP 路由场景,客户端需要先获取 Listener 资源,通过 Listener 资源拿到 Route 的配置。Route 中包含一个或者多个 Cluster 集群资源,通过 Cluster 集群的信息再获取集群节点的信息,这样整个请求链路就完成了。
全量请求和增量请求
传统的 xDS 协议会全量响应订阅数据,对于中途新增的资源订阅来说,这无疑是资源浪费,所以 xDS 新增了增量订阅。也就是说,当出现新的资源时,只需向 Management Server 发送新增的资源,Management Server 也只会返回新增资源的数据。
多条请求流和单条请求流
xDS 协议并不约束在请求多个资源时,多个资源使用同一个请求流,还是每个资源各使用一个请求流,Management Server 应该同时支持这两种模式。
在一个连接请求多个资源
在早期的设计中,xDS 被设计为多个连接,比如 CDS、EDS 分别和 Management Server 建立连接。在后续的改进中,支持在一条连接中按照顺序获取 xDS 中的各种 API,比如先请求 CDS,然后请求 EDS。
xDS 协议详解
通信基础
一个请求信息示例:
version_info: node: { id: envoy } resource_names: - foo - bar type_url: type.googleapis.com/envoy.api.v2.ClusterLoadAssignment response_nonce:
如上述内容,每个请求流都带有一个 version_info 表明版本信息。这个例子中的 version_info 为空,表示这是连接中的第一个请求流,后续会根据 Management Server 推送的 version_info 传递。
Node 中的 ID 则表明机器信息,需要传递机器的唯一标识,可以用机器的 hostname。只有流上的第一个请求需要携带这个字段,后续如果推送发生了变化,也以第一个为准,因为这个值在 Management Server 会被绑定在连接对应的 stream 上。
resource_names 是一个多态信息,在不同的 xDS 类型中表示不同的意思,这里是 Cluster 集群的名称。
type_url 表示 xDS 的类型,这里是 CDS,即集群发现服务。
response_nonce 是 Management Server 推送的响应唯一标识,响应信息中的 Nonce 会作为请求信息中的 response_nonce 发送,就像一会我们将在资源更新部分讲解的一样,Nonce 主要是用来消除 ACK 和 NACK 之间的歧义。在这个例子中,因为是首次请求,response_nonce 为空。
一个响应信息示例:
version_info: X resources: - foo ClusterLoadAssignment proto encoding - bar ClusterLoadAssignment proto encoding type_url: type.googleapis.com/envoy.api.v2.ClusterLoadAssignment nonce: A
version_info:Management Server 响应或者推送给客户端 Envoy 的消息中,都会携带一个最新的版本号。
resources:这个是返回请求信息中 resource_names 对应的 Protobuf 协议的结构体。
type_url:和请求信息中的意思相同。
Nonce:每次响应会携带一个 Nonce 作为唯一标识,用于客户端的 ACK/NACK 或者区分资源更新到底是响应哪个推送数据。
ACK 和 NACK
在收到 Management Server 推送的新版本数据后,Envoy 会响应 ACK 或者 NACK 告知 Management Server 是否更新版本成功。
ACK 代表更新版本成功,这时会携带 Management Server 推送的最新版本号发送 ACK 信息;NACK 代表更新版本失败,这时会携带旧的版本号发送 NACK 信息。
资源更新
如果发现的数据出现变化,依赖发现数据的其他配置就要及时更新。举个简单的例子,比如 CDS 的 Cluster 集群信息发生了变化,那就意味着用户要订阅的服务列表发生了变化,因此需要通过 EDS 的服务发现信息,传递新订阅的集群信息到 EDS。如下图所示 ,比如最早用户只订阅了 foo 这个服务,但是这时 CDS 传递了新的 bar 服务给到了 Envoy,这时就需要往 EDS 发送 foo 和 bar 两个服务的订阅信息。
我们来看一下上述例子在现实中是如何发生的。服务 A 原本依赖服务 foo,但经过了一次版本更新,服务 A 同时依赖服务 foo 和服务 bar,这个时候 CDS 就会推送 foo 和 bar 的信息给到 Envoy,Envoy 就会让 EDS 进行资源更新。
需要注意的是,Discovery Request 除了首次用来进行资源订阅,后面基本上都是用来做 ACK 或者 NACK 确认的。那么上面的资源更新操作是如何进行的呢?
Discovery Request 会在 ACK 之后用相同的 version_info 发送额外的 Discovery Request 信息,让 Management Server 更新资源信息。在上面的例子中,它会为版本 X 额外发送一个 resource_names,作为 {foo, bar} 数据的 Discovery Request 。
但是,需要注意的是,这里可能会发生冲突。比如 Management Server 在收到 V=X 的确认消息后,foo 服务的 EndPoints 信息发生了变化,这时 Management Server 会推送一个新版本 V=Y 的消息给 Envoy,如果 Envoy 发送 V=X 的新资源订阅消息给 Management Server,Management Server 可能会误认为 Envoy 拒绝了 V=Y 的新版本推送。
那么如何解决这个问题呢? Envoy 引入了 Nonce,每个请求和响应都对应唯一的 Nonce,因为 Envoy 的新订阅消息携带的 Nonce 是 A,而 Management Server 返回的 V=Y 的 Nonce 是 B,所以并不会误认为是 Envoy 拒绝了新数据的更新。
实际上在 Istio 的控制面中,也就是这里的 Management Server,并没有完全遵守上面提到的 Nonce 和 version_info 的约定,而是采用了一种更简单、直白的方式解决上面提到的冲突问题。Istio 判断了传递的 resource_names 的 Clusters 信息是否发生变化,如果发生变化,则不认为是 ACK 或者 NACK,直接当作资源更新处理。显然这样的逻辑更易于理解。