流量路由,顾名思义就是将具有某些属性特征的流量,路由到指定的目标。流量路由是流量治理中重要的一环,本节内容将会介绍流量路由常见的场景、流量路由技术的原理以及实现。
流量路由的业务场景
我们可以基于流量路由标准来实现各种业务场景,如标签路由、金丝雀发布、同机房优先路由等。
标签路由
标签路由是按照标签为维度对目标负载进行划分,符合条件的流量匹配至对应的目标,从而实现标签路由的能力。当然基于标签路由的能力,赋予标签各种含义我们就可以实现各种流量路由的场景化能力。
金丝雀发布
金丝雀发布是一种降低在生产中引入新软件版本的风险的技术,方法是在将更改推广到整个基础架构并使其可供所有人使用之前,缓慢地将更改推广到一小部分用户。金丝雀发布是一种在黑与白之间,能够平滑过渡的一种发布方式。让一部分用户继续用旧版本,一部分用户开始用新版本,如果用户对新版本没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到新版本上面来。一直都有听说,安全生产三板斧的概念:可灰度、可观测、可回滚。那么灰度发布能力就是帮助企业软件做到快速迭代验证的必备能力。在K8s中金丝雀发布的最佳实践如下:第一步:新建灰度 Deployment,部署新版本的镜像,打上新版本的标签。第二步:配置针对新版本的标签路由规则。第三步:验证成功,扩大灰度比例。第四步:若验证成功,将稳定版本的应用更新成最新镜像;若验证失败,把灰度的 Deployment 副本数调整到 0 或删除该 Deployment。
全链路灰度
当企业的发展,微服务的数量会逐渐增多。在有一定规模的一定数量的微服务情况下,一次发版可能涉及到的服务数量会比较多,微服务链路也相当较长。全链路灰度可以保证特定的灰度流量可以路由到所有涉及到的灰度版本中。
同可用区优先路由
当企业的对稳定性的要求变高时,企业的应用会选择部署在多个可用区中提高应用的可用性,避免某个可用区出现问题后导致影响应用的可用性。当应用在不同的可用区部署时,应用间跨可用区调用可能会被因为远距离调用造成的网络延迟影响,同可用区优先路由会让我们的Consumer应用优先调用当前可用区内的Provider应用,可以很好地减少这种远距离调用造成的影响,同时当某个可用区出现问题后,我们只需在流量入口处将当前可用区的流量隔离掉,其他可用区的流量不会访问至当前可用区的节点,可以很好地控制某个可用区出现问题后的影响面。
流量路由能力实现的场景众多,上面只是列举了一些典型的场景,下面我们将从流量路由原理入手,剖析流量路由的实现与技术细节。
流量路由原理
需要实现上述所提的流量路由的场景,那么对于Consumer应用来说,同一个 Provider 应用的不同节点之间是有一些特殊的标识。金丝雀发布场景来说,新版本代码所部署的节点需要被标上成新版本的标识;同机房优先路由来说,Provider节点要被标识上机房的信息;全链路灰度场景来说,灰度环境的节点需要被带上灰度标。因此,我们需要在Provider服务注册的过程中,就在注册到注册中心的地址信息中带上治理场景所需的标识。
首先介绍一下节点打标的能力,我们以 Apache Dubbo 为例子,其中 Dubbo 服务节点的地址信息使用 URL 模型来承载。
class URL implements Serializable {
protected String protocol;
// by default, host to registry
protected String host;
// by default, port to registry
protected int port;
protected String path;
private final Map<String, String> parameters;
}
举个简单的例子,假如 Consumer 收到这样一条 dubbo://10.29.0.102:20880/GreetingService?tag=gray&az=az_1 地址信息,表示 GreetingService 服务使用的是 dubbo 协议,服务绑定的 ip 与 port 分别为 10.29.0.102 跟 20880,该地址携带上了 tag=gray、az=az_1 这样两条元数据信息,分别表示当前节点的标签为灰度,当前节点所处的可用区(az:Availability Zone 为云上的机房的可用区概念)为 az_1 。那么节点打标的能力其实就比较明确了,我们在服务提供者向注册中心注册服务地址之前,我们在当前服务提供者的地址信息上增加需要增加的元数据信息比如 `verion = gray`,比如在 Apache Dubbo 的 URL 中增加 paramters 信息,一般来说元数据信息都是 k-v 的 map 结构,这样框架向注册中心注册该节点时会为其添加需要的标签信息`verison=gray`。
到目前为止,我们算是搞明白了 Consumer 收到的 Provider 的地址列表长什么样子。假设 Consumer 收到了 如下图所示 GreetingService 服务的6条地址,那么我们该如何进行选择呢?
算是进入到正题,我们看一下 Dubbo 是如何实现流量路由能力的。
Dubbo 是通过Router模型来实现服务路由的。Dubbo 在收到注册中心同步过来的 Provider URL 之后会生成对应的 Invoker ,Invoker 列表我们可以理解为就是可以调用的Provider节点列表的抽象。Router 的 Route 方法会将传入的 Invoker 列表按照路由规则进行路由筛选,筛选出符合路由规则的服务提供者,即符合路由规则的 Invoker 列表。
public interface Router extends Comparable<Router> {
<T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
每个 Router 服务路由包含一条路由规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者;Dubbo 的调用可以由多个 Router 服务路由构成,比如我们希望当前的 Consumer 流量访问到在同时符合灰度发布以及同可用区优先调用路由规则的节点上。
多个服务路由如同流水线一样,形成一条路由链。Dubbo 使用了RouterChain模型来承载路由链的抽象的。
public class RouterChain<T> {
public List<Invoker<T>> route(URL url, Invocation invocation) {
List<Invoker<T>> finalInvokers = invokers;
for (Router router : routers) {
finalInvokers = router.route(finalInvokers, url, invocation);
}
return finalInvokers;
}
}
其中用户可以按照需求增加路由链中的Router,并且路由链的 Route 方法是循环调用每个 Router 的 Route 方法。并且上一个 Router 的输出 Invoker 列表会做为下一个 Router 的输入。介绍到这里,大家可能对下图会有一个更加深刻的理解了。
路由的整体模型大家已经理解了,我们来重点看一下具体的 Router 服务路由是如何实现的。Router 的 Route 方法会在每次请求调用时被执行,Route 方法有关键的两个入参 invokers 跟 invocation,invokers 是可调用的服务提供者节点列表的抽象。invocation 是当前调用流量的请求上下文的抽象,我们可以从中读到请求中携带的参数、上下文等内容。Route 方法会在每次调用时候根据请求中的上下文信息结合路由规则计算出当前请求需要匹配的目标节点特征,并遍历当前的地址列表,根据目标节点特征进行地址过滤。筛选出目标节点的地址列表,是输入地址列表的子集,然后传递给下一个 Router。
Router 的 Route 方法逻辑的伪代码如下:
List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) {
Tag = invocation.getTag;
List<Invoker<T>> targetInvokers = new List<>();
for (Invoker invoker: invokers) {
if (invoker.match(Tag)) {
targetInvokers.add(invoker);
}
}
return targetInvokers;
}
其中Tag代表目标节点特征的抽象,invokers 为输入地址列表,targetInvokers为输出地址列表即当前Router服务路由的结果。
我们可以根据 Router 的 SPI 扩展 Dubbo 的路由能力,实现我们自定义的服务路由能力。