Apache RocketMQ 的 Service Mesh 消息收发和网络模型

什么是service mesh?

service mesh是用于处理服务与服务之间通信的专用基础设施层。 它负责在包含现代化、云原生应用且拓扑复杂的服务之间可靠地传递请求。 实际上,service mesh通过一系列轻量网络代理来实现,这些代理与应用程序代码一起部署,而且对应用程序不感知。 (但我们会看到,这个想法有所不同。)

service mesh作为单独层的概念与云本机应用程序的兴起有关。 在云原生模型中,单个应用程序可能包含数百个服务; 每个服务可能有数千个实例; 并且每个实例可能处于不断变化的状态,因为它们由像Kubernetes这样的协调器动态调度。 这就导致在应用运行过程中,服务通信不仅非常复杂,而且无处不在。 因此,若要保证端到端的性能和可靠性,对服务通信的管理就变得至关重要。

service mesh是一个网络模型吗?

service mesh是在TCP/IP之上的一层抽象网络模型。它假设底层L3/L4网络存在并且能够端到端传送字节(它还假设该网络与环境的其他方面一样的不可靠;因此,service mesh也必须能够处理网络故障)。

在某种程度上,service mesh跟TCP/IP类似。 正如TCP堆栈抽象了在网络端点之间可靠地传递字节的机制一样,service mesh抽象了在服务之间可靠地传递请求的机制。 与TCP一样,service mesh不关心实际有效负载或其编码方式。 像TCP一样,service mesh的工作就是在处理任何故障的同时实现一个高级目标(“从A到B发送一些东西”)。

不同的是,除了保证本身的能用性以外,service mesh还提供了一个统一的、应用层次的功能:在应用程序运行过程中引入可视化和控制。servcie mesh的目标很明确,就是把服务通信从不可见、含蓄的架构领域抽离出来,然后转变成整个生态中的一流成员——这样,就可以对其进行监控、管理和控制。

service mesh能做什么?

在云原生应用程序中可靠地提供请求可能非常复杂。 像Linkerd这样的service mesh通过各种强大的技术来处理这种复杂性:电路中断,延迟感知负载均衡,最终一致(“建议”)服务发现,重试和超时设置。 这些功能必须全部协同工作,这些功能与其运行的复杂环境之间的相互作用可能非常微妙。

举个例子,当通过Linkerd向服务发出请求时,最简单的一个事件时间表如下:

  • 1、Linkerd应用动态路由规则来确定请求者想要的服务。 请求是否应该路由到生产环境或staging环境? 到本地数据中心还是云中的服务? 是路由到已经测试OK的最新版本,还是生产环境中已经审查锅的较旧的版本?所有这些路由规则都是动态可配置的,并且既可以全局应用,也可以应用于任意部分的流量。
  • 2、在找到正确的目的地后,Linkerd从对应的服务发现端点中检索相关的实例池,可能不止一个。 如果这些信息与Linkerd在实践中观察到的信息不同,那么Linkerd会决定要信任哪些信息来源。
  • 3、Linkerd根据各种因素选择最有可能返回快速响应的实例,包括观察到的最近请求的延迟。
  • 4、Linkerd尝试将请求发送到实例,记录结果的延迟和响应类型。
  • 5、如果实例关闭,未响应或无法处理请求,则Linkerd会在另一个实例上重试该请求(但前提是它知道请求是幂等的)。
  • 6、如果实例始终返回错误,则Linkerd会将其从负载均衡池中逐出,以便稍后定期重试(例如,实例可能临时故障)。
  • 7、如果请求的截止日期已过,则Linkerd会主动使请求失败,而不是通过进一步重试来添加负载。
  • 8、Linkerd以度量和分布式跟踪的形式捕获上述行为的各个方面,并将其发送到集中式度量系统。

并且,这仅仅是最简单的情况——Linkerd还可以启动和终止TLS,执行协议升级,动态转换流量以及数据中心之间的故障转移!

需要注意的是,这些功能旨在提供逐点弹性和应用程序范围的弹性。 大规模分布式系统,无论它们如何构建,都有一个明确的特征:小型的、局部的故障很大程度上会升级为系统范围的灾难性故障。 service mesh必须设计成在基础系统接近其极限时通过减少负载和快速失败来防止这些升级。service mesh必须在设计之初就避免这种升级——当底层系统达到极限时,要减少负载和快速失败。

为什么service mesh如此重要?

service mesh不是一个新功能,它只是功能所在位置的一个转变。Web应用程序始终必须管理服务通信的复杂性。在过去的十五年中,服务网格模型的起源可以追溯到这些应用程序的演变过程。

考虑2000年代中型Web应用程序的典型架构:三层应用程序。在此模型中,应用程序逻辑,Web服务逻辑和存储逻辑都是单独的层。层之间的通信虽然复杂,但范围有限 - 毕竟只有两跳。没有“网”,但是每次“跳”之间都存在通信逻辑,这些逻辑在每层的代码中进行处理。

当这种架构方法被推到非常高的规模时,它就开始崩溃了。像谷歌,Netflix和Twitter这样的公司面临着巨大的流量需求,实现了有效的云原生方法的前身:应用层被分成许多服务(有时称为“微服务”),层成为拓扑。在这些系统中,广义的通信层突然变得关键,但通常采用“胖客户端”库的形式 - 推特的Finagle,Netflix的Hystrix,以及谷歌的Stubby就是例证。

某种意义上来说,像Finagle,Stubby和Hystrix这样的库可以成为第一代service mesh。虽然它们特定于周围环境的细节,并且需要使用特定的语言和框架,但它们是用于管理服务到服务通信的专用基础架构的形式,并且(在开源Finagle和Hystrix库的情况下)被其他公司使用。

快进到现代云本机应用程序。云原生模型将许多小型服务的微服务方法与两个额外因素相结合:容器(例如Docker),提供资源隔离和依赖管理;以及编排层(例如Kubernetes),它将底层硬件抽象为同质池。

这三个组件允许应用程序适应自然机制,以便在负载下进行扩展,并处理云环境中始终存在的部分故障。但是,随着数百个服务或数千个实例,以及不时重新调度实例的业务流程层,单个请求通过服务拓扑所遵循的路径可能非常复杂,并且由于容器使每个服务都易于编写在另一种语言中,图书馆方法不再可行。

复杂性和关键性的这种组合激发了对与服务器到服务通信的专用层的需求,该专用层与应用程序代码分离并且能够捕获底层环境的高度动态性质。该层是service mesh。

service mesh的未来

虽然云本地生态系统中的服务网络采用正在迅速增长,但仍有一个广泛而令人兴奋的路线图仍有待探索。 无服务器计算的要求(例如亚马逊的Lambda)直接适用于服务网格的命名和链接模型,并形成其在云原生态系统中的角色的自然扩展。 服务身份和访问策略的角色在云原生环境中仍然非常新生,服务网络很有可能在这里发挥故事的基本部分。 最后,服务网格(如之前的TCP / IP)将继续被推送到底层基础架构中。 正如Linkerd从像Finagle这样的系统发展而来,服务网格作为必须明确添加到云本机堆栈的独立用户空间代理的当前版本也将继续发展。

Service Mesh 下的消息收发

主要流程如下图:



图 1

简述一下 Service Mesh 下 RocketMQ 消息的发送与消费过程:

  • Pilot 获取到 Topic 的路由信息并通过 xDS 的形式下发给数据平面/Envoy ,Envoy 会代理 SDK 向 Broker/Nameserver 发送的所有的网络请求;
  • 发送时,Envoy 通过 request code 判断出请求为发送,并根据 topic 和 request code 选出对应的 CDS,然后通过 Envoy 提供的负载均衡策略选出对应的 Broker 并发送,这里会使用数据平面的 subset 机制来确保选出的 Broker 是可写的;
  • 消费时,Envoy 通过 request code 判断出请求为消费,并根据 topic 和 request code 选出对应的 CDS,然后和发送一样选出对应的 Broker 进行消费(与发送类似,这里也会使用 subset 来确保选出的 Broker 是可读的),并记录相应的元数据,当消息消费 SDK 发出 ACK 请求时会取出相应的元数据信息进行比对,再通过路由来准确将 ACK 请求发往上次消费时所使用的 Broker。

RocketMQ Mesh 化所遭遇的难题

Service Mesh 常常被称为下一代微服务,这一方面揭示了在早期 Mesh 化浪潮中微服务是绝对的主力军,另一方面,微服务的 Mesh 化也相对更加便利,而随着消息队列和一些数据库产品也逐渐走向 Service Mesh,各个产品在这个过程中也会有各自的问题亟需解决,RocketMQ 也没有例外。

有状态的网络模型

RocketMQ 的网络模型比 RPC 更加复杂,是一套有状态的网络交互,这主要体现在两点:

  • RocketMQ 目前的网络调用高度依赖于有状态的 IP;
  • 原生 SDK 中消费时的负载均衡使得每个消费者的状态不可以被忽略。

对于前者,使得现有的 SDK 完全无法使用分区顺序消息,因为发送请求和消费请求 RPC 的内容中并不包含 IP/(BrokerName + BrokerId) 等信息,导致使用了 Mesh 之后的 SDK 不能保证发送和消费的 Queue 在同一台 Broker 上,即 Broker 信息本身在 Mesh 化的过程中被抹除了。当然这一点,对于只有一台 Broker 的全局顺序消息而言是不存在的,因为数据平面在负载均衡的时候并没有其他 Broker 的选择,因此在路由层面上,全局顺序消息是不存在问题的。

对于后者,RocketMQ 的 Pull/Push Consumer 中 Queue 是负载均衡的基本单位,原生的 Consumer 中其实是要感知与自己处于同一 ConsumerGroup 下消费同一 Topic 的 Consumer 数目的,每个 Consumer 根据自己的位置来选择相应的 Queue 来进行消费,这些 Queue 在一个 Topic-ConsumerGroup 映射下是被每个 Consumer 独占的,而这一点在现有的数据平面是很难实现的,而且,现有数据平面的负载均衡没法做到 Queue 粒度,这使得 RocketMQ 中的负载均衡策略已经不再适用于 Service Mesh 体系下。

此时我们将目光投向了 RocketMQ 为支持 HTTP 而开发的 Pop 消费接口,在 Pop 接口下,每个 Queue 可以不再是被当前 Topic-ConsumerGroup 的 Consumer 独占的,不同的消费者可以同时消费一个 Queue 里的数据,这为我们使用 Envoy 中原生的负载均衡策略提供了可能。



图 2

图 2 右侧即为 Service Mesh 中 Pop Consumer 的消费情况,在 Envoy 中我们会忽略掉 SDK 传来的 Queue 信息。

弹内海量的 Topic 路由信息

在集团内部,Nameserver 中保存着上 GB 的 Topic 路由信息,在 Mesh 中,我们将这部分抽象成 CDS,这使得对于无法预先知道应用所使用的 Topic 的情形而言,控制平面只能全量推送 CDS,这无疑会给控制平面带来巨大的稳定性压力。

在 Envoy 更早期,是完全的全量推送,在数据平面刚启动时,控制平面会下发全量的 xDS 信息,之后控制平面则可以主动控制数据的下发频率,但是无疑下发的数据依旧是全量的。后续 Envoy 支持了部分的 delta xDS API,即可以下发增量的 xDS 数据给数据平面,这当然使得对于已有的 sidecar,新下发的数据量大大降低,但是 sidecar 中拥有的 xDS 数据依然是全量的,对应到 RocketMQ ,即全量的 CDS 信息都放在内存中,这是我们不可接受的。于是我们希望能够有 on-demand CDS 的方式使得 sidecar 可以仅仅获取自己想要的 CDS 。而此时正好 Envoy 支持了 delta CDS,并仅支持了这一种 delta xDS。其实此时拥有 delta CDS 的 xDS 协议本身已经提供了 on-demand CDS 的能力,但是无论是控制平面还是数据平面并没有暴露这种能力,于是在这里对 Envoy 进行了修改并暴露了相关接口使得数据平面可以主动向控制平面发起对指定 CDS 的请求,并基于 delta gRPC 的方式实现了一个简单的控制平面。Envoy 会主动发起对指定 CDS 资源的请求,并提供了相应的回调接口供资源返回时进行调用。

对于 on-demand CDS 的叙述对应到 RocketMQ 的流程中是这样的,当 GetTopicRoute 或者 SendMessage 的请求到达 Envoy 时,Envoy 会 hang 住这个流程并发起向控制平面中相应 CDS 资源的请求并直到资源返回后重启这个流程。

关于 on-demand CDS 的修改,之前还向社区发起了 Pull Request ,现在看来当时的想法还是太不成熟了。原因是我们这样的做法完全忽略了 RDS 的存在,而将 CDS 和 Topic 实现了强绑定,甚至名称也一模一样,关于这一点,社区的 Senior Maintainer [@htuch ]() 对我们的想法进行了反驳,大意就是实际上的 CDS 资源名可能带上了负载均衡方式,inbound/outbound 等各种 prefix 和 suffix,不能直接等同于 Topic 名,更重要的是社区赋予 CDS 本身的定义是脱离于业务的,而我们这样的做法过于 tricky ,是与社区的初衷背道而驰的。

因此我们就需要加上 RDS 来进行抽象,RDS 通过 topic 和其他信息来定位到具体所需要的 CDS 名,由于作为数据平面,无法预先在代码层面就知道所需要找的 CDS 名,那么如此一来,通过 CDS 名来做 on-demand CDS 就更无从谈起了,因此从这一点出发只能接受全量方案,不过好在这并不会影响代码贡献给社区。

route_config:
  name: default_route
  routes:
  - match:
      topic:
        exact: mesh
      headers:
        - name: code
          exact_match: 105
    route:
      cluster: foo-v145-acme-tau-beta-lambda

上面可以看到对于 topic 名为 mesh 的请求会被 RDS 路由到 foo-v145-acme-tau-beta-lambda 这个 CDS 上,事先我们只知道 topic 名,无法知道被匹配到的 CDS 资源名。

如今站在更高的视角,发现这个错误很简单,但是其实这个问题我们直到后续 code review 时才及时纠正,确实可以更早就做得更好。

不过从目前社区的动态来看,on-demand xDS 或许已经是一个 roadmap,起码目前 xDS 已经全系支持 delta ,VHDS 更是首度支持了 on-demand 的特性。

Mesh 为 RocketMQ 带来了什么?

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java