上篇指路:https://segmentfault.com/a/1190000044913808
上篇文章的结尾,我提到了 “Envoy 社区并非对此毫无察觉。针对这个问题,不少解法被提了出来”。其中一个解法是 VHDS(Virtual Host Discovery Service)。
VHDS
VHDS 协议通过 delta xDS 来动态订阅特定的路由配置,并根据需要请求必要的 Virtual Host。使用 VHDS 将允许 Envoy 实例订阅和退订控制面内部存储的 Virtual Host 列表,而不是随路由配置发送所有 Virtual Host。控制面将监视此列表,并使用它来过滤发送至单个 Envoy 实例的配置,使其只包含已订阅的 Virtual Host。
听起来是挺不错的,居然可以做到每实例级别的按需订阅,真正实现了按需分配的伟大愿景。
如果这套方案真的能行,那么接下来我应该会放几个基于官方文档的示例,然后本文就此了结。熟悉我的读者会知道,我不可能为一件引用官方文档就能解决的事情专门写一篇文章,那样太没有意思了。要是使用 VHDS 就能解决问题,那么全量路由变更就不会是 Envoy 的一个待解决的关键技术点。
在推理小说中,小说家往往会在文中抛出一个伪解答,然后等到快到小说结尾时才给出真正的解答。在抵达作者提供的“真相”之前,让我们看看为什么 VHDS 在当下是一个“伪解答”。
VHDS 的破绽
VHDS 是按需订阅的,那么它是怎么知道什么时候需要订阅呢?让我们引用一下官方文档:
If a route for the contents of a host/authority header cannot be resolved, the active stream is paused while a DeltaDiscoveryRequest is sent. When a DeltaDiscoveryResponse is received where one of the aliases or the name in the response exactly matches the resource_names_subscribe entry from the DeltaDiscoveryRequest, the route configuration is updated, the stream is resumed, and processing of the filter chain continues.
在 API 调用的语境下,stream 基本上和一个请求是一个同义词。可以这么理解订阅过程:
- 数据面发现客户端请求的路由不存在
- 数据面向控制面发起一个 DeltaDiscoveryRequest 请求缺少的路由,控制面返回 DeltaDiscoveryResponse
- 数据面判断所需的数据是否在 DeltaDiscoveryResponse 里,如果在,继续处理请求
这里面有个局限性,就是整个请求链路将不仅仅是数据面的事情,而是会涉及到控制面。整条链路会比较长,过程中涉及到许多机制(stream 的 pause/resume,delta xDS 等等),不好管控,而且容易出问题。
比方说,假设有个需求是要监控路由的生效时间。如果数据面是按需拉取配置的,那么这个需求就不好通过衡量下发的路由在数据面上生效的时间点来实现,因为配置是否生效取决于客户端是否有请求某个数据面。当然你不能坚称路由在控制面上转换成某种表示形式就已经是“生效了”。因为如果控制面突然崩溃了,那么所有已经转换好却没有被客户端访问到的路由,就不会发布到数据面上,直到控制面重新提供服务为止。从用户的角度看就是控制面不可用会造成路由配置丢失。
另外,VHDS 的长链路意味着潜在出 bug 的环节会比较多。比如 当请求带 body 时,第一个请求无法被正确继续处理。
自然,我们也可以像 CDN 那样使用预热之类的方式来绕过 VHDS 的长链路局限性。不过 VHDS 并不是唯一的解决方案,所以为什么要在同一棵树上吊死呢?
SRDS
SRDS 在官网上的介绍是这样的:
The Scoped Route Discovery Service (SRDS) API allows a route table to be broken up into multiple pieces. This API is typically used in deployments of HTTP routing with massive route tables in which simple linear searches are not feasible.
粗看会以为它是用来加速路由匹配的,但是它可以把路由拆散,这不就能解决全量路由的问题嘛。
阿里云开源的 Higress 就用了这套方案来应对路由颗粒度过大的问题。在网上能找到 环界云计算基于 Higress 解决他们 10 万级别路由的痛点的文章 。这年头,不少公司对外宣传的方案只适合老老实实待在宣传稿里(抑或发发 Paper),实际上自己都没有落地。能开源出来,并在其他公司里落地解决他们的痛点,真的了不起。我曾就这个话题和该文章的作者交流过,可以确信 Higress 的方案真的能解决问题。
为了更好地了解 Higress 基于 SRDS 的方案,笔者阅读了 Higress 的代码,并请教了 Higress 的维护者。其实当时他在调研时也看过 VHDS,但是由于担心 VHDS 需要将流量穿透到控制面,而且实现 VHDS 的工作量比较大,所以选择了潜力更大的 SRDS。
SRDS 工作方式大致如下:
- 控制面生成一系列名为 ScopedRouteConfiguration 的 KV 对,K 为请求特征,V 为 RDS 资源名。
- 和之前一个 RDS 对应一个 HCM 不同,控制面可以生成大量的小粒度 RDS。比如原本一个 HCM 里会有一个名为 all-in-one 的 RDS,现在可以生成 all-in-one-0,all-in-one-1 等一系列 RDS。
- 对于每个请求,Envoy 会提取请求特征,作为 K 去查找匹配的 ScopedRouteConfiguration,然后根据找到的 ScopedRouteConfiguration 的 V 去查找对应的 RDS (all-in-one-0 等等)。
也可以参考文档里具体的例子:https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rou...
不过 Envoy 目前的 SRDS 还只支持提取 header 特征,而且只支持 KV 查找,所以实际上直接用原生的 Envoy 是没办法解决路由颗粒度过大的问题。
Higress 在原生的 Envoy 上做了两点修改来应对这个问题:
- 引入两种请求特征提取类,可以提取请求的端口和 Host。Higress 使用 Port+Host 来作为 K。
- 通过一个 recomputation 的过程,如果 KV 查找没有找到 RDS,那么去掉 K 里面的一个子域名,重新查找 RDS。相当于实现了 wildcard host match。
这个 recomputation 的过程,我个人觉得还是挺复杂的,当初看了几遍才看懂。笔者有个想法,现在 higress 的 envoy 侧 SRDS 需要 recompute 的过程,这是因为 scope key builder 使用 KV 查找,而 host match 需要处理 wildcard 的场景。如果我们额外加一个 scope host matcher,不是用 KV 查找而是直接用 host match,那代码可以更加简化。
直接使用 Port+Host 来作为 K 有一个风险,就是如果 SNI 和 Host 不一致的情况下,会导致 SNI 相关的策略被绕过。比如域名 A 上有客户端证书校验,那么攻击者可以使用另一个域名 B 作为 SNI,然后使用域名 A 作为 Host,就能无需提供域名 A 的客户端证书即可访问到域名 A 的上游。Higress 通过引入 AllowServerNames 这个 HCM 配置来解决该问题。如果开启了客户端证书校验,那么 AllowServerNames 中会检测 Host 是否在 SNI 白名单之中。当然不考虑 SNI 的做法其实和 Envoy 的机制还是有点冲突的,因为 Envoy 有可能会配置客户端证书校验以外的四层策略,而这时候就不好保证访问域名 A 的所有请求一定应用了域名 A 的四层策略。
Higress 目前没有实现 RDS 的增量变更,所以事实上在路由变更后,还是会把全量的 RDS 推送到 Envoy 上。你可能会惊讶,说了那么多,绕了一大圈,最后还是没有避免全量 RDS 推送…… 这里 Envoy 其实有一个缓存:https://github.com/envoyproxy/envoy/blob/c39207e670bd916ae18874a889a43fe2e6fd815e/source/common/router/route_config_update_receiver_impl.cc#L54,每个 RDS 的 hash 值不变的话,就不会重新初始化 RDS。 之前一个 RDS 包含全部路由时,这个功能用途不大,任意路由变更都会导致缓存失效。在 RDS 粒度变小之后,即使推送的是全量,数据面变更的也只是增量部分。Higress 维护者认为这里做 delta xDS 的好处不大,仅限于节省带宽以及控制面内存。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。