作者:至简

软件是以持续迭代的方式去不断演进的。某种程度上,我们并不担心软件不完善,但担心软件的迭代速度太慢而影响了完善的速度。在分布式软件领域,如何快速、安全地验证新的软件版本一直是大家所关心并探索的。服务网格(Service Mesh)的出现将这个领域的探索推向了新的高度。“泳道”这一概念在分布式软件领域并非新词,只不过,这次我们是以服务网格为基础技术去构建,充分发挥云原生技术天然具备灵活治理流量的优势。
本文分享了阿里云内部所沉淀的全链路流量打标与路由的能力,做出服务网格技术新体验的同时,很好地兑现了服务网格的新价值。

概念与场景

图 1 以 Istio 官方所提供的 Bookinfo 示例程序为例示例说明了使用场景中的关键概念。其中紫色的圆边方框代表了 Envoy。图中所有泳道的性质是一样的,不同的命名只是为了区分细分场景或用户。

• 基线(baseline):指业务所有服务都部署到了这一环境中。基线可以来自真实的生产环境,也可以专为开发工作而构建的与生产环境完全独立的环境。

• 流量泳道(traffic lane):代表了一个与基线环境相隔离的软环境,通过给机器(即 Kubernetes 中的 Pod)打标签的方法,将机器加入到该泳道中。显然,加入泳道的机器在网络层面与基线中的机器是互通的。

• 流量回退(traffic fallback):泳道中所部署的服务数量并非要求与基线环境完全一致,当泳道中并不存在调用链中所依赖的其他服务时,流量需要回退至基线环境,进一步在必要的时候返流泳道。比如,图 1 中 dev1 泳道中并不存在 productpage 服务所依赖的 reviews 服务,因此需要让流量回退到基线中的 reviews 服务(图中深蓝色线所示),紧接着基线中的 reviews 服务需要将流量打回 dev1 泳道中的 ratings 服务。

• 流量标识透传(traffic label passthrough):所有服务边上的 Sidecar 都需要有能力将调入请求中所携带的流量标签自动放到由这一请求所分叉出的每一个调出请求中去,以便实现全链路流量标识透传和按流量标识路由,否则泳道与基线间的流量无法来回穿梭。
• 入口服务(entrance service):指流量进入泳道时所触达的第一个服务。图 1 中代表服务的图形在左边框边上打上三角形标识的就代表它是一个入口服务。

在这里插入图片描述

图1

泳道技术可以运用于如下场景:

• 单个服务的日常开发或多个服务间的日常开发联调。开发者建立泳道,将增加了新功能的服务部署到泳道中,基于流量的特征通过定义规则将测试流量引入泳道中进行验证。由于泳道只需部署被测试服务的新版本,省去了搭建全链路测试环境的麻烦。在这一场景中,需要注意测试流量所存在的数据落盘问题,处理好开发与联调过程中所留下的脏数据。

• 全链路灰度。对于涉及重大功能上线中的多个服务,可以通过泳道以全链路灰度的方式做更为全面的功能验证。全链路功能验收通过后,再将服务的新版本发布至基线。

• 关键业务重保。类似零售场景的业务(比如 POS 机收银),我们不希望因为软件的故障而引发巨大的舆情,这时可以通过泳道对业务流量进行隔离实现重保。

技术实现

### 流量打标方案与实现

在运用泳道技术时,根据流量打标的位置不同而存在三种不同的方案。值得指出,虽然方案有所不同,但就服务网格而言的技术实现是完全一致的,将方案罗列出来是为了帮助读者更好地理解。

图 2 示例说明了方案一。在这个方案中,流量进入服务网格的 Ingress 网关之前还有一级网关,我们姑且称之为 API 网关(比如,Nginx)。通常 API 网关可以根据流量的特征,在转发收到的请求前先加上额外的头,从而完成对流量的打标动作。图中对于特定的流量将增加名为 x-asm-traffic-lane: dev1 的 HTTP 头,代表需要将流量打到 dev1 泳道中。在这个方案,服务网格中的 Envoy 无需有任何流量打标动作。

在这里插入图片描述

图 2

图 3 示例说明了方案二。本方案中,客户端的流量直接打到服务网格的 Ingress 网关上。由 Ingress 网关根据流量的特征通过 Istio 原生的 VirtualService 匹配规则识别出后,在转发请求前加上名为 x-asm-traffic-lane 的 HTTP 头,随后将流量路由到相应的泳道。

在这里插入图片描述

图 3

图 4 示例说明了方案三。本质上,这一方案与方案二是完全一样的,同样通过 Istio 原生的 VirtualService 匹配规则识别出相应的流量后加上名为 x-asm-traffic-lane 的 HTTP 头。唯一的不同在于,方案二中的 Envoy 的角色是 Ingress,而方案三中 Envoy 的角色是 Sidecar。
在这里插入图片描述

图 4

流量一旦完成打标后,由服务网格中的每一个 Envoy 基于流量标和控制面下发的配置做全链路的标透传和按标路由。

流量标识透传

图 5 示例说明了服务网格中一个服务与边上的 Envoy (Sidecar)间的流量细节。
在这里插入图片描述
图 5

从 Envoy 的视角来看,包含对流入和流出两种流量的转发。I1 是流入的流量,收到后将转发给本地的 Svc A;O1 是流出的流量(因为处理 l1 的需要而去调用别的服务所引发),收到后将转发给外部的被调用的服务。流入与流出只与请求(request)相关,与请求对应的响应(response)没有关系。显然,一个流入的请求可能导致有多个流出的请求发生(即“分叉”),这完全取决于 Svc A 的具体业务逻辑。

泳道技术所需解决的核心点在于,当流入的流量打上了相应的标签后,如何让由其所分叉出的每一个流出的流量都带上同样的标签,我们采用的方案是结合链路追踪技术(比如,OpenTelemetry)去解决。链路追踪技术是通过 traceId 去唯一标识一条调用链树,为根请求分配并带上全网唯一的 traceId 后,之后由其所分叉出的所有新调用都得带上值完全一样的 HTTP 头,换句话说服务开发者需要在编程的过程中确保这一头被传播到后续的服务调用中(比如,调用 OpenTelemetry 的 SDK 完成头传播)。换句话说,运用泳道技术的前提是需要每个服务都采用了链路追踪技术,作为微服务构架的最佳实践之一这一先决条件很容易满足。回到图 5,Svc A 收到 I2 请求并对之处理时需要发起 O1 调用,此时需要确保 I2 中的 traceId 头传播到 O1 请求中,这是 Svc A 的开发者需要特别注意的一个细节。

一旦服务网格中所有服务的请求都带上了 traceId,那么通过 Envoy 实现全链路的流量标透传就是非常简单的事了。大体分这几大步骤:

• Envoy 内部构建一张映射表,记录 traceId 与流量标的映射。比如,图 5 所示的流量标是放在 x-asm-traffic-lane 这一 HTTP 头中的。x-asm-traffic-lane: dev1 代表的流量标是 dev1,x-asm-traffic-lane: canary 代表的流量标是 canary。
• 当请求 I1 进到 Envoy 时,Envoy 基于请求中所带的 traceId 和流量标,在映射表中增加一条映射记录。
• Envoy 对于收到的每一个 O1 请求,基于请求中的 traceId 从映射表中找到对应的流量标并将之增加到 O2 请求中再转发出去。

通过服务网格基于 traceId 打标的技术方案的好处在于,将流量打标的动作和流量标传递做到完全与服务解耦,将这一能力下沉到本来擅长流量治理的服务网格中,让流量调度的灵动性得以进一步解锁。

流量标识与 traceld 的定义

我们在 Istio 已有 CR 的基础上,新增了 TrafficLabel 这个全新的 CRD。选择新增而非直接对 VirtualService 做扩展的原因是,VirtualService 的设计之初是应用维度的,当一个业务复杂到全链路有很多应用需要放到泳道中时,就得对每个应用的 VirtualService 进行变更,背后所导致的时效性和可操作性会是一个问题。扩展 VirtualService 去实现的另一种思路是,让 VirtualService 具备配置全局规则的能力,而这就要用到规则的合并机制,这一点从实操层面也有问题。Istio 社区对于多个 VirtualService 进行合并的需求有不少讨论,目前只在网关上支持合并,对于 Sidecar 则不提供这一能力,原因在于担心合并的顺序不同而导致故障。

图 6 示例说明了如何使用 TrafficLabel 这一 CR 在 istio-system 根命名空间定义全局有效的流量打标方法。其中定义了名为 x-asm-traffic-lane 的标签,作为 HTTP 请求的头用于存放流量标识(比如,dev1、dev2、canary 等),以及 traceId 基于 x-request-id 获得。用户可以根据自己所选型的链路追踪系统的具体实现加以设置。图中设置为从 x-request-id 头中获得,是因为 Envoy 实现了通过 x-request-id 做全网链路唯一性标识的功能。采用 x-request-id 作为映射表键意味着,我们可以直接用 Istio 开源社区所提供的 Bookinfo 示例程序去演示泳道的效果,因为 Bookinfo 中的所有服务都做了 x-request-id 头从调入请求到调出请求的传播。

apiVersion: istio.alibabacloud.com/v1beta1
kind: TrafficLabel
metadata:
  name: global-traffic-label
spec:
  rules:
  - labels:
      - name: x-asm-traffic-lane
    protocols: "http"
    traceIdHeader: x-request-id
  hosts:
    - "*"

图6

按流量标路由

为了支持按流量标路由,需要对 Istio 的 VirtualService 做扩展,让 destination 字段支持用 $x-asm-traffic-lane 这样的变量指定流量的目的地,如下图 7 所示。换句话说,包含 x-asm-traffic-lane: dev2 头的流量会打到 dev2 这个泳道中,背后其实是使用 DestinationRule 定义的名为 dev2 的 subset,如图 8 所示。请注意,图 7 中 VirtualService 中的 $x-asm-traffic-lane 这一名称要与图 6 中 TrafficLabel 内所定义的名保持一致。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  - match:
    - headers:
        end-user:
          exact: dev2
    route:
    - destination:
        host: reviews
        subset: dev2
      fallback:
        case: noinstances|notavailable
        target:
          host: reviews
          subset: baseline
      headers:
        request:
          set:
            x-asm-traffic-lane: dev2
  - route:
    - destination:
        host: reviews
        subset: $x-asm-traffic-lane
      fallback:
        case: noinstances|notavailable
        target:
          host: reviews
          subset: baseline
  - route:
    - destination:
        host: reviews
        subset: baseline

图7

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
spec:
  host: reviews
  subsets:
  - labels:
      version: v2
    name: baseline
  - labels:
      version: v3
    name: dev2

图8

从图 8 中 DestinationRule 的定义不难看出,除了 baseline 外只定义了 dev2 这个泳道,图 7 则是对应情形下的 VirtualService 定义。两者对应的使用场景正是图 1 中的基线和 dev2 泳道。

产品实现

在云原生的大技术背景之下,易用性被放到了聚光灯之下,我们深刻理解背后意味着什么。为此,在设计产品的交互时,努力清空自己所知、站在用户所面临的场景下去思考和优化,在功能性和易用性之间努力做好平衡。

在用户使用泳道前,我们认为他已构建了一个包含所有服务在内的基线环境。在 K8s 中,基线环境通常被部署于特定的命名空间中以便更好地运维和管理其中的服务。用户创建泳道时,只需提供泳道名称。本节接下来的内容以创建名为 dev2 的泳道展开。
在这里插入图片描述

泳道创建好后,需要将服务发布到泳道之中。由于所发布的服务已存于基线环境中并创建了 K8s Service 资源,所以泳道中发布服务其实是创建对应服务下的一个 Deployment,直观的理解就是创建已有服务的另一个软件版本。不难想到,这一发布动作包含确认基线版本,实例数和容器镜像地址。

在这里插入图片描述

当服务发布到泳道中后,需要通过泳道的服务列表确保所有服务都正常启动。此时泳道中并没有流量进入,需要通过配置引流规则才能将基线中的流量引入泳道中。

在这里插入图片描述

引流规则可以基于 HTTP 头、URI、Cookie 的特征去配置,方便我们精确地选择被测流量进入泳道。下图的规则是指将 HTTP 头 end-user 的值为 dev2 的流量引导致 dev2 泳道中。配置规则的同时,需要正确指定入口服务。

在这里插入图片描述

引流规则应用后,即可在网页上以 dev2 用户名登录,从而看到 dev2 泳道中服务所呈现的效果。下面两图分别示例了全基线和 dev2 泳道所看到的页面效果。由于 dev2 泳道中并没有部署 productpage 和 details 两个服务,所以这两个服务会回退为使用基线中的,而最终呈现的效果就是两图中 The Comedy of Errors 和 Book Details 两块的内容是完全一致的。
在这里插入图片描述
在这里插入图片描述

当服务发布到泳道中上线后,可以在泳道的服务列表中方便地查看各服务与基线版本的流量比对情况。帮助开发者更好地了解泳道中服务的运行情况。

在这里插入图片描述

此外,通过服务拓扑图也可以清晰地看到,dev2 泳道中服务的调用情况(图中的 lane-dev2)。
在这里插入图片描述

总结与展望

我们所探索的基于服务网格的泳道技术,让开发者能秒级创建隔离环境用于开发测试或业务重保,通过精确的引流规则将“爆炸半径”控制到最小。很好地兑现了云原生服务网格技术的新体验和新价值。

接下来,我们将进一步以场景化的方式打通泳道和版本灰度的功能,让用户能基于直觉使用好这些功能。功能层面,我们将进一步完善泳道中支持的协议,比如 RocketMQ、Dubbo 3.0 等,通过丰富泳道技术的应用场景去最大程度地发挥其价值。

最后,我们将持续以 Service Mesh as Infra 的理念去构建微服务架构的现代化服务治理平台,和业界伙伴一同加速这一新技术的发展和推广。

作者介绍:李云(花名:至简),阿里云服务网格混合云产品技术负责人。2018 年开始在阿里集团带领团队从事服务网格技术的发展与建设工作,在 QCon 做过多次云原生与服务网格的技术分享。


阿里云云原生
1k 声望302 粉丝