xDS REST 和 gRPC 协议

Envoy 通过文件系统或查询一个或多个管理服务器来发现其各种动态资源。这些发现服务及其相应的 API 统称为 xDS。资源是通过 订阅 请求的,方法是指定要监视的文件系统路径、启动 gRPC 流或轮询 REST-JSON URL。后两种方法涉及发送带有 DiscoveryRequest proto 负载的请求。资源在所有方法中都以 DiscoveryResponse proto 负载的形式传递。我们在下面讨论每种类型的订阅。

资源类型

xDS API 中的每个配置资源都与一个类型相关联。资源类型遵循 版本控制方案。资源类型与下面描述的传输无关。

以下 v3 xDS 资源类型受支持

以下出现了 类型 URL 的概念,其形式为 type.googleapis.com/<resource type> – 例如,type.googleapis.com/envoy.config.cluster.v3.Cluster 用于 Cluster 资源。在 Envoy 的各种请求和管理服务器的响应中,都声明了资源类型 URL。

Protoc-Gen-Validate 注释

单个 xDS 资源类型的 protobuf 消息使用 protoc-gen-validate (PGV) 的注释,这些注释指示语义约束,用于在客户端接收资源时验证资源的内容。

客户端不需要使用这些 PGV 注释来验证资源(例如,Envoy 执行此验证,但 gRPC 不执行)。此外,PGV 注释并非旨在成为客户端执行的验证检查的详尽列表;客户端可能由于与 PGV 注释无关的原因而拒绝资源。

通常,PGV 注释并非旨在由控制平面或 xDS 代理直接使用。在某些情况下,控制平面可能希望使用 PGV 注释来执行验证,以此作为更早地在配置管道中捕获问题的途径(例如,在资源被添加到控制平面之前,甚至在资源被发送到任何客户端之前,拒绝无效的输入)。但是,随着 xDS API 的发展,PGV 注释也会不断发展,使 PGV 注释不那么严格不被视为 API 中的重大更改。因此,通常情况下,控制平面无法假设其所有客户端都使用与控制平面相同的 xDS proto 文件版本进行编译,这意味着它无法知道客户端是否实际使用与服务器相同的验证。这会导致服务器拒绝客户端接受的资源的问题。

文件系统订阅

传递动态配置的最简单方法是在 ConfigSource 中指定的知名路径处放置动态配置。Envoy 将使用 inotify(macOS 上为 kqueue)来监视文件更改,并在更新时解析文件中的 DiscoveryResponse proto。二进制 protobuf、JSON、YAML 和 proto 文本是 DiscoveryResponse 的支持格式。

文件系统订阅没有机制可以确认/拒绝更新,除了统计计数器和日志。如果配置更新被拒绝,xDS API 的最后一个有效配置将继续适用。

流式 gRPC 订阅

API 流程

对于典型的 HTTP 路由场景,客户端配置的核心资源类型是 ListenerRouteConfigurationClusterClusterLoadAssignment。每个 Listener 资源可能指向一个 RouteConfiguration 资源,该资源可能指向一个或多个 Cluster 资源,而每个 Cluster 资源可能指向一个 ClusterLoadAssignment 资源。

Envoy 在启动时获取所有 ListenerCluster 资源。然后,它获取 ListenerCluster 资源所需的任何 RouteConfigurationClusterLoadAssignment 资源。实际上,每个 ListenerCluster 资源都是 Envoy 配置树的一部分的根节点。

非代理客户端(例如 gRPC)可能从仅获取其感兴趣的特定 Listener 资源开始。然后,它获取这些 Listener 资源所需的 RouteConfiguration 资源,然后获取这些 RouteConfiguration 资源所需的任何 Cluster 资源,然后获取这些 Cluster 资源所需的 ClusterLoadAssignment 资源。实际上,原始的 Listener 资源是客户端配置树的根节点。

xDS 传输协议的变体

四种变体

通过流式 gRPC 使用的 xDS 传输协议有四种变体,涵盖了两个维度的所有组合。

第一个维度是世界状态 (SotW) 与增量。SotW 方法是 xDS 使用的最初机制,其中客户端必须在每次请求中指定其感兴趣的所有资源名称,并且对于 LDS 和 CDS 资源,服务器必须在每次请求中返回客户端已订阅的所有资源。这意味着如果客户端已经订阅了 99 个资源并想要添加一个额外的资源,则它必须发送包含所有 100 个资源名称的请求,而不仅仅是新的资源名称。对于 LDS 和 CDS 资源,服务器必须通过发送所有 100 个资源来响应,即使已订阅的 99 个资源没有发生变化。这种机制可能会造成可扩展性限制,这就是引入了增量协议变体的原因。增量方法允许客户端和服务器仅指示相对于其先前状态的增量(即,客户端可以说它想要添加或删除对特定资源名称的订阅,而无需重新发送未发生变化的资源名称,而服务器只能发送已发生更改的资源的更新)。增量协议还提供了一种延迟加载资源的机制。有关增量协议的详细信息,请参阅下面的 增量 xDS

第二个维度是使用单独的 gRPC 流来处理每种资源类型,还是将所有资源类型聚合到单个 gRPC 流上。前一种方法是 xDS 使用的最初机制,它提供了最终一致性模型。后一种方法是在需要明确控制排序的环境中添加的。有关详细信息,请参阅下面的 最终一致性注意事项

因此,xDS 传输协议的四种变体是

  1. 世界状态(基本 xDS):SotW,为每种资源类型使用单独的 gRPC 流

  2. 增量 xDS:增量,为每种资源类型使用单独的 gRPC 流

  3. 聚合发现服务 (ADS):SotW,为所有资源类型聚合流

  4. 增量 ADS:增量,为所有资源类型聚合流

每个变体的 RPC 服务和方法

对于非聚合协议变体,每种资源类型都有一个单独的 RPC 服务。每个 RPC 服务都可以为 SotW 和增量协议变体提供一种方法。以下是每种资源类型的 RPC 服务和方法

  • 侦听器:侦听器发现服务 (LDS)

    • SotW:ListenerDiscoveryService.StreamListeners

    • 增量:ListenerDiscoveryService.DeltaListeners

  • 路由配置:路由发现服务 (RDS)

    • SotW:RouteDiscoveryService.StreamRoutes

    • 增量:RouteDiscoveryService.DeltaRoutes

  • 范围路由配置:范围路由发现服务 (SRDS)

    • SotW:ScopedRouteDiscoveryService.StreamScopedRoutes

    • 增量:ScopedRouteDiscoveryService.DeltaScopedRoutes

  • 虚拟主机:虚拟主机发现服务 (VHDS)

    • SotW:N/A

    • 增量:VirtualHostDiscoveryService.DeltaVirtualHosts

  • 集群:集群发现服务 (CDS)

    • SotW:ClusterDiscoveryService.StreamClusters

    • 增量:ClusterDiscoveryService.DeltaClusters

  • 集群负载分配:端点发现服务 (EDS)

    • SotW:EndpointDiscoveryService.StreamEndpoints

    • 增量:EndpointDiscoveryService.DeltaEndpoints

  • 密钥:密钥发现服务 (SDS)

    • SotW:SecretDiscoveryService.StreamSecrets

    • 增量:SecretDiscoveryService.DeltaSecrets

  • 运行时:运行时发现服务 (RTDS)

    • SotW: RuntimeDiscoveryService.StreamRuntime

    • 增量: RuntimeDiscoveryService.DeltaRuntime

在聚合协议变体中,所有资源类型都复用在一个 gRPC 流上,其中每个资源类型都被视为聚合流中的独立逻辑流。实际上,它只是通过将每个资源类型的请求和响应视为单个聚合流上的独立子流,将所有上述独立 API 组合到一个流中。聚合协议变体的 RPC 服务和方法是

  • SotW: AggregatedDiscoveryService.StreamAggregatedResources

  • 增量: AggregatedDiscoveryService.DeltaAggregatedResources

对于所有 SotW 方法,请求类型是 DiscoveryRequest,响应类型是 DiscoveryResponse

对于所有增量方法,请求类型是 DeltaDiscoveryRequest,响应类型是 DeltaDiscoveryResponse

配置使用哪种变体

在 xDS API 中,ConfigSource 消息指示如何获取特定类型的资源。如果 ConfigSource 包含 gRPC ApiConfigSource,它会指向管理服务器的上游集群;这将为每个 xDS 资源类型启动一个独立的双向 gRPC 流,可能指向不同的管理服务器。如果 ConfigSource 包含一个 AggregatedConfigSource,它会告诉客户端使用 ADS

目前,客户端预计会收到一些本地配置,告诉它如何获取 ListenerCluster 资源。 Listener 资源可能包含一个 ConfigSource,指示如何获取 RouteConfiguration 资源,而 Cluster 资源可能包含一个 ConfigSource,指示如何获取 ClusterLoadAssignment 资源。

客户端配置

在 Envoy 中,bootstrap 文件包含两个 ConfigSource 消息,一个指示如何获取 Listener 资源,另一个指示如何获取 Cluster 资源。它还包含一个单独的 ApiConfigSource 消息,指示如何联系 ADS 服务器,该服务器将在 ConfigSource 消息(在 bootstrap 文件中或在从管理服务器获取的 ListenerCluster 资源中)包含 AggregatedConfigSource 消息时使用。

Envoy 中当前的限制是,任何 xDS Cluster 资源应首先在 Bootstrap 配置的 static_resources 字段中指定,然后再指定任何依赖于 xDS 集群的静态 Cluster 资源。否则会导致 Envoy 初始化速度变慢(有关详细信息,请参阅 GitHub 问题)。例如,如果某个集群依赖于 xDS Cluster 来使用 SDS 配置传输套接字上的秘密,则应首先在 static_resources 字段中指定 xDS Cluster,然后再指定具有传输套接字秘密的集群。

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

xDS 传输协议

传输 API 版本

除了上面描述的资源类型版本之外,xDS 线程协议还与它关联一个传输版本。这为 DiscoveryRequestDiscoveryResponse 等消息提供类型版本控制。它还被编码在 gRPC 方法名称中,因此服务器可以根据客户端调用的方法来确定客户端使用的版本。

基本协议概述

每个 xDS 流都从客户端的 DiscoveryRequest 开始,该请求指定要订阅的资源列表、与订阅资源相对应的类型 URL、节点标识符以及可选的资源类型实例版本,该版本指示客户端已看到的资源类型的最新版本(有关详细信息,请参阅 ACK/NACK 和资源类型实例版本)。

然后,服务器将发送 DiscoveryResponse,其中包含自客户端指示已看到的最后一个资源类型实例版本以来发生更改的任何客户端已订阅的资源。服务器可以在任何时候发送其他响应,以反映订阅的资源的变化。

每当客户端收到新响应时,它都会发送另一个请求,指示响应中的资源是否有效(有关详细信息,请参阅 ACK/NACK 和资源类型实例版本)。

所有服务器响应都将包含 nonce,并且客户端的所有后续请求都必须将 response_nonce 字段设置为在该流上从服务器收到的最新 nonce。这允许服务器确定给定请求与哪个响应相关联,从而避免了 SotW 协议变体中的各种竞争条件。请注意,nonce 仅在单个 xDS 流的上下文中有效;它不会在流重启后保留。

只有流上的第一个请求保证携带节点标识符。同一流上的后续发现请求可能会携带空节点标识符。无论同一流上的发现响应是否被接受,这都适用。如果节点标识符在流中出现多次,则它应该始终相同。因此,检查第一个消息以获取节点标识符就足够了。

ACK/NACK 和资源类型实例版本

每个 xDS 资源类型都有一个版本字符串,指示该资源类型的版本。每当该类型的一种资源发生变化时,版本就会发生变化。

在 xDS 服务器发送的响应中,version_info 字段指示该资源类型的当前版本。然后,客户端向服务器发送另一个请求,其中 version_info 字段指示客户端看到的最新有效版本。这提供了一种方法,让服务器确定它发送的版本何时被客户端认为是无效的。

(在 增量协议变体 中,资源类型实例版本由服务器在 system_version_info 字段中发送。但是,客户端实际上并不使用此信息来传达哪些资源有效,因为增量 API 变体为此目的提供了一种单独的机制。)

资源类型实例版本对于每个资源类型都是独立的。当使用聚合协议变体时,即使所有资源类型都在同一个流上发送,每个资源类型也有自己的版本。

资源类型实例版本对于每个 xDS 服务器也是独立的(其中 xDS 服务器由唯一的 ConfigSource 标识)。从多个 xDS 服务器获取特定类型的资源时,每个 xDS 服务器对版本的理解都会有所不同。

请注意,资源类型的版本不是单个 xDS 流的属性,而是资源本身的属性。如果流中断,并且客户端创建了新流,则客户端在新流上的初始请求应指示客户端在先前流上看到的最新版本。服务器可以通过不重新发送客户端已经在先前流上看到的资源来决定优化,但前提是他们知道客户端没有订阅它以前没有订阅的新资源。例如,当唯一订阅是通配符订阅时,服务器通常可以安全地对 LDS 和 CDS 执行此优化,并且在客户端始终订阅完全相同的资源集的环境中,这样做也是安全的。

EDS 请求示例可能为

version_info:
node: { id: envoy }
resource_names:
- foo
- bar
type_url: type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
response_nonce:

管理服务器可以立即回复,也可以在请求的资源可用时回复 DiscoveryResponse,例如

version_info: X
resources:
- foo ClusterLoadAssignment proto encoding
- bar ClusterLoadAssignment proto encoding
type_url: type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
nonce: A

处理完 DiscoveryResponse 后,Envoy 将在流上发送新的请求,指定最后成功应用的版本和管理服务器提供的随机数。该版本为 Envoy 和管理服务器提供了一个关于当前应用配置的共享概念,以及一个 ACK/NACK 配置更新的机制。

ACK

如果更新已成功应用,则 version_info 将为 X,如序列图中所示

Version update after ACK

NACK

如果 Envoy 拒绝了配置更新 X,它将回复 error_detail 并提供其先前版本,在本例中为初始的空版本。 error_detail 包含有关消息字段中填充的精确错误消息的更多详细信息

No version update after NACK

在序列图中,使用以下格式来缩写消息

  • DiscoveryRequest: (V=version_info,R=resource_names,N=response_nonce,T=type_url)

  • DiscoveryResponse: (V=version_info,R=resources,N=nonce,T=type_url)

NACK 后,API 更新可能会在新的版本 Y 上成功

ACK after NACK

服务器检测 NACK 的首选机制是查看客户端发送的请求中 error_detail 字段是否存在。一些旧的服务器可能会通过查看请求中的版本和随机数来检测 NACK:如果请求中的版本与服务器使用该随机数发送的版本不一致,则客户端已拒绝最新版本。但是,此方法不适用于 LDS 和 CDS 以外的 API,因为这些 API 的客户端可能会动态更改他们订阅的资源集,除非服务器以某种方式安排了每当任何一个客户端订阅新资源时都增加资源类型实例版本。具体来说,请考虑以下示例

detecting NACK from error_detail instead of version and nonce

ACK 和 NACK 语义摘要

  • xDS 客户端应为从管理服务器收到的每个 DiscoveryResponse 发送 ACKNACKresponse_nonce 字段告诉服务器哪个响应与 ACKNACK 相关联。

  • ACK 表示配置更新成功,并包含 version_infoDiscoveryResponse 中。

  • NACK 表示配置失败,由 error_detail 字段的存在指示。 version_info 指示客户端正在使用的最新版本,尽管在客户端从现有版本订阅了新资源并且该新资源无效的情况下,该版本可能不是旧版本(参见上面的示例)。

何时发送更新

管理服务器仅应在 DiscoveryResponse 中的资源发生变化时,才向 Envoy 客户端发送更新。Envoy 在接受或拒绝任何 DiscoveryResponse 后,立即使用包含 ACK/NACKDiscoveryRequest 响应。如果管理服务器提供相同的资源集,而不是等待发生变化,则会导致客户端和管理服务器上的不必要工作,这会对性能产生严重影响。

在一个流中,新的 DiscoveryRequests 将覆盖任何具有相同资源类型的先前 DiscoveryRequests。这意味着管理服务器只需要对每个流上任何给定资源类型的最新 DiscoveryRequest 做出响应。

客户端如何指定要返回的资源

xDS 请求允许客户端指定一组资源名称,作为关于客户端感兴趣的资源的提示。在 SotW 协议变体中,这是通过 resource_namesDiscoveryRequest 中指定的;在增量协议变体中,这是通过 resource_names_subscriberesource_names_unsubscribe 字段在 DeltaDiscoveryRequest 中指定的。

通常(有关例外情况,请参见下文),请求必须指定客户端感兴趣的资源名称集。如果存在请求的资源,则管理服务器必须提供这些资源。客户端将静默忽略任何未明确请求的提供资源。当客户端发送更改请求资源集的新请求时,服务器必须重新发送任何新请求的资源,即使它以前在未被请求的情况下发送了这些资源,并且这些资源自那时起没有发生变化。如果资源名称列表变为空,则意味着客户端不再对任何指定类型的资源感兴趣。

对于 ListenerCluster 资源类型,还有一个“通配符”订阅,在订阅特殊名称 * 时触发。在这种情况下,服务器应使用特定于站点的业务逻辑来确定客户端感兴趣的完整资源集,通常基于客户端的 node 标识。

由于历史原因,如果客户端发送了对给定资源类型的请求,但从未明确订阅过任何资源名称(即,在 SotW 中,该资源类型在流上的所有请求都具有空的 resource_names 字段,或者在增量中,从未发送过对该资源类型具有非空 resource_names_subscribe 字段的流请求),服务器应该将该请求与客户端显式订阅 * 的方式相同。但是,一旦客户端显式订阅了资源名称(无论它是否为 * 或任何其他名称),则此旧语义将不再可用;此时,清除已订阅资源列表被解释为取消订阅(参见 Unsubscribing From Resources),而不是订阅 *

例如,在 SotW 中

  • 客户端发送具有未设置的 resource_names 的请求。服务器将此解释为订阅 *

  • 客户端发送具有设置为 *Aresource_names 的请求。服务器将此解释为继续对 * 的现有订阅,并添加对 A 的新订阅。

  • 客户端发送具有设置为 Aresource_names 的请求。服务器将此解释为取消订阅 * 并继续对 A 的现有订阅。

  • 客户端发送一个请求,resource_names 未设置。服务器将其解释为取消订阅 A(即,客户端现在已取消订阅所有资源)。虽然此请求与第一个请求相同,但它不被解释为通配符订阅,因为之前在该流上有一个针对该资源类型的请求设置了 resource_names 字段。

在增量模式下

  • 客户端发送一个请求,resource_names_subscribe 未设置。服务器将其解释为订阅 *

  • 客户端发送一个请求,resource_names_subscribe 设置为 A。服务器将其解释为继续对 * 的现有订阅并添加对 A 的新订阅。

  • 客户端发送一个请求,resource_names_unsubscribe 设置为 *。服务器将其解释为取消订阅 * 并继续对 A 的现有订阅。

  • 客户端发送一个请求,resource_names_unsubscribe 设置为 A。服务器将其解释为取消订阅 A(即,客户端现在已取消订阅所有资源)。虽然已订阅的资源集现在为空,就像在初始请求之后一样,但它不被解释为通配符订阅,因为之前在该流上有一个针对该资源类型的请求设置了 resource_names_subscribe 字段。

客户端行为

Envoy 将始终对 ListenerCluster 资源使用通配符订阅。但是,其他 xDS 客户端(例如使用 xDS 的 gRPC 客户端)可能会显式订阅这些资源类型的特定资源名称,例如,如果它们只有一个单例监听器并且已经从某些带外配置中知道它的名称。

将资源分组到响应中

在增量协议变体中,服务器在其自己的响应中发送每个资源。这意味着,如果服务器之前发送了 100 个资源并且只有一个资源发生了更改,它可能会发送一个仅包含更改的资源的响应;它不需要重新发送 99 个未发生更改的资源,并且客户端不应删除未发生更改的资源。

在 SotW 协议变体中,除了 ListenerCluster 之外的所有资源类型,都以与增量协议变体相同的方式分组到响应中。但是,ListenerCluster 资源类型处理方式不同:服务器必须包含世界完整状态,这意味着必须包含客户端需要的相关类型的所有资源,即使它们自上次响应以来没有发生更改。这意味着,如果服务器之前发送了 100 个资源并且只有一个资源发生了更改,它必须重新发送所有 100 个资源,即使是 99 个未修改的资源。

请注意,所有协议变体都对整个命名资源的单元进行操作。没有机制可以提供命名资源中重复字段的增量更新。最值得注意的是,目前没有机制可以增量更新 EDS 响应中的单个端点。

重复资源名称

服务器发送一个包含相同资源名称两次的单个响应是错误的。客户端应 NACK 包含多个相同资源名称实例的响应。

删除资源

在增量协议变体中,服务器通过响应的 removed_resources 字段向客户端发出信号,表明应删除资源。这告诉客户端从其本地缓存中删除资源。

在 SotW 协议变体中,删除资源的标准更为复杂。对于 ListenerCluster 资源类型,如果新响应中不存在先前看到的资源,则表示该资源已被删除,并且客户端必须删除它;不包含任何资源的响应表示删除该类型的所有资源。但是,对于其他资源类型,API 没有提供任何机制让服务器告诉客户端资源已被删除;相反,删除由父资源更改为不再引用子资源来隐式指示。例如,当客户端收到一个 LDS 更新,删除了先前指向 RouteConfiguration A 的 Listener 时,如果没有其他 Listener 指向 RouteConfiguration A,则客户端可以删除 A。对于那些资源类型,空 DiscoveryResponse 从客户端的角度来看实际上是无操作的。

知道何时请求的资源不存在

SotW 协议变体没有提供任何明确的机制来确定何时请求的资源不存在。

对于 ListenerCluster 资源类型,响应必须包含客户端请求的所有资源。但是,客户端可能无法仅根据响应中的缺失来确定资源不存在,因为更新的传递是最终一致的:如果客户端最初发送对资源 A 的请求,然后发送对资源 AB 的请求,然后看到一个仅包含资源 A 的响应,客户端无法推断出资源 B 不存在,因为响应可能是在第一个请求的基础上发送的,在服务器看到第二个请求之前。

对于其他资源类型,因为每个资源都可以在其自己的响应中发送,因此无法从下一个响应中知道新请求的资源是否存在,因为下一个响应可能是对之前已订阅的其他资源的无关更新。

因此,客户端预计在发送对新资源的请求后使用超时(推荐持续时间为 15 秒),之后,如果它们没有收到资源,它们将认为请求的资源不存在。在 Envoy 中,这在 资源预热 期间针对 RouteConfigurationClusterLoadAssignment 资源执行。

请注意,即使在客户端请求时请求的资源不存在,该资源也可能在任何时间被创建。管理服务器必须记住客户端请求的资源集,如果这些资源中的一个在以后出现,服务器必须向客户端发送更新,通知它新资源。最初看到不存在的资源的客户端必须为该资源在任何时间被创建做好准备。

取消订阅资源

在增量协议变体中,可以通过 resource_names_unsubscribe 字段取消订阅资源。

在 SotW 协议变体中,每个请求都必须包含 resource_names 字段中订阅的完整资源名称列表,因此取消订阅一组资源是通过发送一个包含仍在订阅的所有资源名称但没有包含取消订阅的资源名称的新请求来完成的。例如,如果客户端先前已订阅资源 AB 但希望取消订阅 B,它必须发送一个仅包含资源 A 的新请求。

请注意,对于 ListenerCluster 资源类型,如果客户端正在使用“通配符”订阅(有关详细信息,请参阅 客户端如何指定要返回的资源),则订阅的资源集由服务器而不是客户端决定,因此客户端无法单独取消订阅这些资源;它只能取消订阅整个通配符。

在单个流上请求多个资源

对于 EDS/RDS,Envoy 可以为每种类型的资源生成一个独立的流(例如,如果每个 ConfigSource 都有其自己的独立上游集群用于管理服务器),或者当这些资源请求都发送到同一个管理服务器时,可以将多个资源请求合并在一起。虽然这取决于具体的实现细节,但管理服务器应该能够在每个请求中处理一个或多个 resource_names (对于给定资源类型)。以下两个时序图都适用于获取两个 EDS 资源 {foo, bar}

Multiple EDS requests on the same stream Multiple EDS requests on distinct streams

资源更新

如上所述,Envoy 可能会更新它在每个 DiscoveryRequest 中向管理服务器提供的 resource_names 列表,该请求用于 ACK/NACK 特定的 DiscoveryResponse。此外,Envoy 可能会在稍后以给定 version_info 处发出额外的 DiscoveryRequests,以向管理服务器更新新的资源提示。例如,如果 Envoy 处于 EDS 版本 X,并且只知道集群 foo,但随后接收到 CDS 更新,并且还了解到了 bar,则它可能会为 X 发出一个额外的 DiscoveryRequest,其中 {foo,bar} 作为 resource_names

CDS response leads to EDS resource hint update

这里可能存在一种竞争条件;如果在 Envoy 在 X 处发出资源提示更新后,但在管理服务器处理该更新之前,它以新版本 Y 进行了回复,则资源提示更新可能会被解释为通过提供 X version_info 来拒绝 Y。为了避免这种情况,管理服务器提供了 nonce,Envoy 使用它来指示每个 DiscoveryResponse 所对应的特定 DiscoveryRequest

EDS update race motivates nonces

管理服务器不应为任何 DiscoveryRequest 发送 DiscoveryResponse,这些请求具有过时的 nonce。nonce 在 Envoy 在 DiscoveryResponse 中收到更新的 nonce 之后就会变得过时。管理服务器不需要发送更新,直到它确定有新的版本可用。版本早期的请求也会变得过时。它可能会处理多个版本下的 DiscoveryRequests,直到有新的版本准备就绪。

Requests become stale

上述资源更新顺序的一个含义是,Envoy 不期望为它发出的每个 DiscoveryRequests 都收到 DiscoveryResponse

资源预热

ClustersListeners 在能够提供服务之前需要经历预热。此过程发生在 Envoy 初始化 期间以及 ClusterListener 更新时。当管理服务器提供 ClusterLoadAssignment 响应时,Cluster 的预热才算完成。类似地,当管理服务器提供 RouteConfiguration 时,Listener 的预热才算完成(如果监听器引用了 RDS 配置)。管理服务器预计在预热期间提供 EDS/RDS 更新。如果管理服务器没有提供 EDS/RDS 响应,Envoy 将不会在初始化阶段初始化自身,并且通过 CDS/LDS 发送的更新将不会生效,直到提供 EDS/RDS 响应。

注意

Envoy 特定的实现说明

  • 即使端点没有变化,只要管理服务器提供新的 ClusterLoadAssignment 响应,Cluster 的预热就会完成。如果运行时标志 envoy.restart_features.use_eds_cache_for_ads 被设置为 true,Envoy 将在资源预热超时后使用缓存的 ClusterLoadAssignment(如果存在)用于集群。

  • 即使管理服务器没有发送 Listener 引用的 RouteConfiguration 的响应,Listener 的预热也会完成。Envoy 将使用之前发送的 RouteConfiguration 来完成 Listener 的预热。管理服务器必须仅在 RouteConfiguration 响应发生变化或过去从未发送过时才发送该响应。

最终一致性考虑

由于 Envoy 的 xDS API 具有最终一致性,因此在更新期间可能会出现短暂的流量下降。例如,如果仅通过 CDS/EDS 了解到集群 XRouteConfiguration 引用了集群 X,然后在提供 Y 的 CDS/EDS 更新之前,被调整为集群 Y,那么在 Envoy 实例了解到 Y 之前,流量将被黑洞化。

对于某些应用程序来说,短暂的流量下降是可以接受的,客户端或其他 Envoy 侧车进行的重试会隐藏这种下降。对于不能容忍下降的其他场景,可以通过提供包含 XY 的 CDS/EDS 更新,然后进行将 X 重定向到 Y 的 RDS 更新,最后进行将 X 删除的 CDS/EDS 更新来避免流量下降。

通常,为了避免流量下降,更新的顺序应遵循“先做再破”模型,其中

  • CDS 更新(如果有)必须始终先推送。

  • EDS 更新(如果有)必须在相应的集群的 CDS 更新之后到达。

  • LDS 更新必须在相应的 CDS/EDS 更新之后到达。

  • 与新添加的监听器相关的 RDS 更新必须在 CDS/EDS/LDS 更新之后到达。

  • 与新添加的 RouteConfigurations 相关的 VHDS 更新(如果有)必须在 RDS 更新之后到达。

  • 然后可以删除过时的 CDS 集群和相关的 EDS 端点(不再被引用的端点)。

如果未添加新的集群/路由/监听器,或者如果在更新期间可以暂时丢弃流量,则可以独立推送 xDS 更新。请注意,在 LDS 更新的情况下,监听器将在它们接收流量之前进行预热,即,如果已配置,将通过 RDS 获取依赖的路由。在添加/删除/更新集群时,集群会被预热。另一方面,路由不会被预热,即,管理平面必须确保路由引用的集群已就位,然后才能推送路由的更新。

TTL

如果管理服务器变得不可达,Envoy 收到的最后一个已知配置将一直保留,直到连接重新建立。对于某些服务来说,这可能不理想。例如,在故障注入服务的情况下,管理服务器在错误时间崩溃可能会使 Envoy 处于不可取的状态。TTL 设置允许 Envoy 在与管理服务器失去联系后,在指定的时间段后删除一组资源。例如,这可以用来在无法再联系到管理服务器时终止故障注入测试。

对于支持 xds.config.supports-resource-ttl 客户端功能的客户端,可以在每个 Resource 上指定一个 TTL 字段。每个资源都将有其自己的 TTL 过期时间,届时该资源将过期。每个 xDS 类型可能对处理这种过期有不同的方式。

要更新与 Resource 关联的 TTL,管理服务器会以新的 TTL 重新发送该资源。要删除 TTL,管理服务器会以未设置 TTL 字段的方式重新发送该资源。

为了允许轻量级的 TTL 更新(“心跳”),可以发送一个响应,该响应提供一个 Resource,其中 resource 未设置,并且版本与最近发送的版本匹配,可以用来更新 TTL。这些资源不会被视为资源更新,而只会被视为 TTL 更新。

SotW TTL

为了使用带有 SotW xDS 的 TTL,相关资源必须包裹在 Resource 中。这允许设置与 SotW 的 Delta xDS 相同的 TTL 字段,而无需更改 SotW API。SotW 也支持心跳:响应中任何类似心跳资源的资源都将仅用于更新 TTL。

此功能受 xds.config.supports-resource-in-sotw 客户端功能控制。

聚合发现服务

当管理服务器分布时,难以提供上述关于排序的保证以避免流量下降。ADS 允许单个管理服务器通过单个 gRPC 流来交付所有 API 更新。这提供了仔细排序更新以避免流量下降的能力。使用 ADS,单个流用于多个独立的 DiscoveryRequest/DiscoveryResponse,这些请求通过类型 URL 多路复用。对于任何给定的类型 URL,上述 DiscoveryRequestDiscoveryResponse 消息的排序适用。示例更新序列可能如下所示

EDS/CDS multiplexed on an ADS stream

每个 Envoy 实例都提供了一个 ADS 流。

ADS 配置的示例最小 bootstrap.yaml 片段是

node:
  # set <cluster identifier>
  cluster: envoy_cluster
  # set <node identifier>
  id: envoy_node

dynamic_resources:
  ads_config:
    api_type: GRPC
    grpc_services:
    - envoy_grpc:
        cluster_name: ads_cluster
  cds_config:
    ads: {}
  lds_config:
    ads: {}

static_resources:
  clusters:
  - name: ads_cluster
    type: STRICT_DNS
    load_assignment:
      cluster_name: ads_cluster
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                # set <ADS management server address>
                address: my-control-plane
                # set <ADS management server port>
                port_value: 777
    # It is recommended to configure either HTTP/2 or TCP keepalives in order to detect
    # connection issues, and allow Envoy to reconnect. TCP keepalive is less expensive, but
    # may be inadequate if there is a TCP proxy between Envoy and the management server.
    # HTTP/2 keepalive is slightly more expensive, but may detect issues through more types
    # of intermediate proxies.
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http2_protocol_options:
            connection_keepalive:
              interval: 30s
              timeout: 5s
    upstream_connection_options:
      tcp_keepalive: {}

增量 xDS

增量 xDS 是一个独立的 xDS 端点,它

  • 允许协议以资源/资源名称增量 (“Delta xDS”) 的形式在网络上进行通信。这支持 xDS 资源可扩展性的目标。当单个集群被修改时,管理服务器不需要交付所有 100k 个集群,而只需要交付发生变化的单个集群。

  • 允许 Envoy 按需/延迟请求其他资源。例如,仅在对该集群的请求到达时才请求集群。

增量 xDS 会话始终处于 gRPC 双向流的上下文中。这允许 xDS 服务器跟踪连接到它的 xDS 客户端的状态。目前还没有增量 xDS 的 REST 版本。

在 delta xDS 网络协议中,nonce 字段是必需的,用于将 DeltaDiscoveryResponseDeltaDiscoveryRequest ACK 或 NACK 配对。可选地,响应消息级别的 system_version_info 仅用于调试目的。

DeltaDiscoveryRequest 可以在以下情况下发送

请注意,虽然在请求中可能设置了 response_nonce,但服务器必须尊重对订阅状态的更改,即使 nonce 过期。nonce 可用于将 ack/nack 与服务器响应相关联,但不应用于拒绝过期请求。

在此第一个示例中,客户端连接并接收它 ACK 的第一个更新。第二个更新失败,客户端 NACK 了该更新。稍后,xDS 客户端自发地请求“wc”资源。

Incremental session example

在重新连接时,增量 xDS 客户端可以通过在 initial_resource_versions 中发送已知资源来告知服务器,以避免通过网络重新发送这些资源。由于不假定任何状态从先前流中保留下来,因此重新连接的客户端必须向服务器提供它感兴趣的所有资源名称。

请注意,对于“通配符”订阅(有关详细信息,请参阅 客户端如何指定要返回的资源),请求必须在 resource_names_subscribe 字段中指定 *,或者(传统行为)请求在 resource_names_subscriberesource_names_unsubscribe 中都没有资源。

Incremental reconnect example

资源名称

资源由资源名称或别名标识。如果存在资源的别名,则可以通过 DeltaDiscoveryResponse 中资源的 alias 字段来识别。资源名称将在 DeltaDiscoveryResponse 中资源的 name 字段中返回。

订阅资源

客户端可以在 resource_names_subscribe 字段中发送资源的别名或名称,以订阅资源,该字段位于 DeltaDiscoveryRequest 中。应检查资源的名称和别名,以确定是否已订阅相关实体。

resource_names_subscribe 字段可能包含服务器认为客户端已订阅的资源名称,而且这些名称具有最新的版本。但是,服务器必须仍然在响应中提供这些资源;由于隐藏在服务器之外的实现细节,客户端可能“忘记”了这些资源,尽管它们似乎仍然处于订阅状态。

取消订阅资源

当客户端对某些资源失去兴趣时,它将在 resource_names_unsubscribe 字段中表明这一点,该字段位于 DeltaDiscoveryRequest 中。与 resource_names_subscribe 一样,这些可能是资源名称或别名。

resource_names_unsubscribe 字段可能包含多余的资源名称,服务器认为客户端还没有订阅这些名称。服务器必须干净地处理此类请求;它可以简单地忽略这些虚假的取消订阅。

在大多数情况下(参见下面的例外情况),如果请求除了取消订阅资源之外什么都不做,则服务器不需要发送任何响应;特别是,服务器通常不需要发送带有已取消订阅资源名称的响应,该名称位于 removed_resources 字段中。

但是,上面有一个例外:当客户端具有通配符订阅 (*) 以及对另一个特定资源名称的订阅时,特定资源名称也可能包含在通配符订阅中,因此如果客户端取消订阅该特定资源名称,它不知道是否继续缓存该资源。为了解决这个问题,服务器必须发送一个响应,该响应包含特定资源,该资源位于 removed_resources 字段(如果它不包含在通配符中)或 resources 字段(如果它包含在通配符中)。

了解何时请求的资源不存在

当客户端订阅的资源不存在时,服务器将发送一条 DeltaDiscoveryResponse 消息,该消息在 removed_resources 字段中包含该资源的名称。这允许客户端快速确定资源何时不存在,而无需等待超时,就像 SotW 协议变体中那样。但是,仍然鼓励客户端使用超时来防止管理服务器无法及时发送响应的情况。

REST-JSON 轮询订阅

通过 REST 端点进行同步(长)轮询也适用于 xDS 单例 API。上述消息排序类似,只是没有维护到管理服务器的持久流。预期在任何时候都只有一个未完成的请求,因此响应 nonce 在 REST-JSON 中是可选的。使用 proto3 的 JSON 规范转换DiscoveryRequestDiscoveryResponse 消息进行编码。REST-JSON 轮询不可用。

当轮询周期设置为较小值以实现长轮询时,需要避免发送 DiscoveryResponse,除非底层资源通过 资源更新 发生了变化。