SOFAMesh中的多协议通用解决方案x-protocol介绍系列(2):快速解码转发

2018年上半年,蚂蚁金服决定基于 Istio 订制本身的 ServiceMesh 解决方案,并在6月底正式对外公布了 SOFAMesh。
git

在 SOFAMesh 的开发过程当中,针对遇到的实际问题,咱们给出了一套名为 x-protocol 的解决方案,本文将会对这个解决方案进行详细的讲解,后面会有更多内容,欢迎持续关注本系列文章。github

上一篇:SOFAMesh中的多协议通用解决方案x-protocol介绍系列(1) : DNS通用寻址方案服务器

前言

在 Istio 和 Envoy 中,对通信协议的支持,主要体如今 HTTP/1.1和 HTTP/2上,而咱们 SOFAMesh,则须要支持如下几个 RPC 协议:网络

  • SOFARPC:这是蚂蚁金服大量使用的RPC协议(已开源)架构

  • HSF RPC:这是阿里集团内部大量使用的RPC协议(未开源)并发

  • Dubbo RPC: 这是社区普遍使用的RPC协议(已开源)负载均衡


更适合的平衡点:性能和功能

对于服务间通信解决方案,性能永远是一个值得关注的点。而 SOFAMesh 在项目启动时就明确要求在性能上要有更高的追求,为此,咱们不得不在 Istio 标准实现以外寻求能够获取更高性能的方式,好比支持各类 RPC 协议。框架

期间有两个发现:分布式

  1. Istio 在处理全部的请求转发如 REST/gRPC 时,会解码整个请求的 header 信息,拿到各类数据,提取为 Attribute,而后以此为基础,提供各类丰富的功能,典型如 Content Based Routing。ide

  2. 而在测试中,咱们发现:解码请求协议的 header 部分,对 CPU 消耗较大,直接影响性能。

所以,咱们有了一个很简单的想法:是否是能够在转发时,不开启部分功能,以此换取转发过程当中的更少更快的解码消耗?毕竟,不是每一个服务都须要用到 Content Based Routing 这样的高级特性,大部分服务只使用 Version Based Routing,尤为是使用 RPC 通信协议的服务,没有HTTP那么表现力丰富的 header,对 Content Based Routing 的需求要低不少。

此外,对于部分对性能有极高追求的服务,不开启高级特性而换取更高的性能,也是一种知足性能要求的折中方案。考虑到系统中总存在个别服务对性能很是敏感,咱们以为 Service Mesh 提供一种性能能够接近直连的方案会是一个有益的补充。为了知足这些特例而不至于所以总体否决 Service Mesh 方案,咱们须要在 Service Mesh 的大框架下提供一个折中方案。


请求转发

在咱们进一步深刻前,咱们先来探讨一下实现请求转发的技术细节。

有一个关键问题:当 Envoy/SOFA MOSN 这样的代理程序,接收到来自客户端的TCP 请求时,须要得到哪些信息,才能够正确的转发请求到上游的服务器端?


最关键的信息:destination

首先,毫无疑问的,必须拿到 destination 目的地,也就是客户端请求必须经过某种方式明确的告之代理该请求的 destination,这样代理程序才能根据这个 destionation去找到正确的目标服务器,而后才有后续的链接目标服务器和转发请求等操做。

Destination 信息的表述形式可能有:

1. IP地址

多是服务器端实例实际工做的 IP 地址和端口,也多是某种转发机制,如Nginx/HAProxy 等反向代理的地址或者 Kubernetes 中的 ClusterIP。

举例:“192.168.1.1:8080”是实际IP地址和端口,“10.2.0.100:80”是 ngxin 反向代理地址,“172.168.1.105:80”是Kubernetes的ClusterIP。

2. 目标服务的标识符

可用于名字查找,如服务名,可能带有各类前缀后缀。而后经过名字查找/服务发现等方式,获得地址列表(一般是IP地址+端口形式)。

举例:“userservice”是标准服务名, “com.alipay/userservice”是加了域名前缀的服务名, “service.default.svc.cluster.local”是k8s下完整的全限定名。

Destination信息在请求报文中的携带方式有:

1. 经过通信协议传递

这是最多见的形式,标准作法是经过header头,典型如HTTP/1.1下通常使用 host header,举例如“Host: userservice”。HTTP/2下,相似的使用“:authority” header。

对于非HTTP协议,一般也会有相似的设计,经过协议中某些字段来承载目标地址信息,只是不一样协议中这个字段的名字各有不一样。如SOFARPC,HSF等。

有些通信协议,可能会将这个信息存放在payload中,好比后面咱们会介绍到的dubbo协议,致使须要反序列化payload以后才能拿到这个重要信息。

2. 经过TCP协议传递

这是一种很是特殊的方式,经过在TCP option传递,上一节中咱们介绍Istio DNS寻址时已经详细介绍过了。

TCP拆包

如何从请求的通信协议中获取destination?这涉及到具体通信协议的解码,其中第一个要解决的问题就是如何在连续的TCP报文中将每一个请求内容拆分开,这里就涉及到经典的TCP沾包、拆包问题。

转发请求时,因为涉及到负载均衡,咱们须要将请求发送给多个服务器端实例。所以,有一个很是明确的要求:就是必须以单个请求为单位进行转发。即单个请求必须完整的转发给某台服务器端实例,负载均衡须要以请求为单位,不能将一个请求的多个报文包分别转发到不一样的服务器端实例。因此,拆包是请求转发的必备基础。

因为篇幅和主题限制,咱们不在这里展开TCP沾包、拆包的原理。后面针对每一个具体的通信协议进行分析时再具体看各个协议的解决方案。

多路复用的关键参数:RequestId

RequestId用来关联request和对应的response,请求报文中携带一个惟一的id值,应答报文中原值返回,以便在处理response时能够找到对应的request。固然在不一样协议中,这个参数的名字可能不一样(如streamid等)。

严格说,RequestId对于请求转发是可选的,也有不少通信协议不提供支持,好比经典的HTTP1.1就没有支持。可是若是有这个参数,则能够实现多路复用,从而能够大幅度提升TCP链接的使用效率,避免出现大量链接。稍微新一点的通信协议,基本都会原生支持这个特性,好比SOFARPC、Dubbo、HSF,还有HTTP/2就直接內建了多路复用的支持。

HTTP/1.1不支持多路复用(http1.1有提过支持幂等方法的pipeline机制可是未能普及),用的是经典的ping-pong模式:在请求发送以后,必须独占当前链接,等待服务器端给出这个请求的应答,而后才能释放链接。所以HTTP/1.1下,并发多个请求就必须采用多链接,为了提高性能一般会使用长链接+链接池的设计。而若是有了requestid和多路复用的支持,客户端和Mesh之间理论上就能够只用一条链接(实践中可能会选择创建多条)来支持并发请求:

而Mesh与服务器(也多是对端的Mesh)之间,也一样能够受益于多路复用技术,来自不一样客户端而去往同一个目的地的请求能够混杂在同一条链接上发送。经过RequestId的关联,Mesh能够正确将reponse发送到请求来自的客户端。

因为篇幅和主题限制,咱们不在这里展开多路复用的原理。后面针对每一个具体的通信协议进行分析时再具体看各个协议的支持状况。

请求转发参数总结

上面的分析中,咱们能够总结到,对于Sidecar,要正确转发请求:

  1. 必须获取到destination信息,获得转发的目的地,才能进行服务发现类的寻址

  2. 必需要可以正确的拆包,而后以请求为单位进行转发,这是负载均衡的基础

  3. 可选的RequestId,这是开启多路复用的基础

所以,这里咱们的第一个优化思路就出来了:尽可能只解码获取这三个信息,知足转发的基本要求。其余信息若是有性能开销则跳过解码,所谓“快速解码转发”。基本原理就是牺牲信息完整性追求性能最大化。

而结合上一节中咱们引入的DNS通用寻址方案,咱们是能够从请求的TCP options中获得ClusterIP,从而实现寻址。这个方式能够实现不解码请求报文,尤为是header部分解码destination信息开销大时。这是咱们的第二个优化思路:跳过解码destination信息,直接经过ClusterIP进行寻址。

具体的实现则须要结合特定通信协议的实际状况进行。


主流通信协议

如今咱们开始,以Proxy、Sidecar、Service Mesh的角度来看看目前主流的通信协议和咱们前面列举的须要在SOFAMesh中支持的几个协议。

SOFARPC/bolt协议

SOFARPC 是一款基于 Java 实现的 RPC 服务框架,详细资料能够查阅 官方文档。SOFARPC 支持 bolt,rest,dubbo 协议进行通讯。REST、dubbo后面单独展开,这里咱们关注bolt协议。

bolt 是蚂蚁金服集团开放的基于 Netty 开发的网络通讯框架,其协议格式是变长,即协议头+payload。具体格式定义以下,以request为例(response相似):


咱们只关注和请求转发直接相关的字段:

TCP拆包

bolt协议是定长+变长的复合结构,前面22个字节长度固定,每一个字节和协议字段的对应如图所示。其中classLen、headerLen和contentLen三个字段指出后面三个变长字段className、header、content的实际长度。和一般的变长方案相比只是变长字段有三个。拆包时思路简单明了:

  1. 先读取前22个字节,解出各个协议字段的实际值,包括classLen,headerLen和contentLen

  2. 按照classLen、headerLen和contentLen的大小,继续读取className、header、content

Destination

Bolt协议中的header字段是一个map,其中有一个key为“service”的字段,传递的是接口名/服务名。读取稍微麻烦一点点,须要先解码整个header字段,这里对性能有影响。

RequestId

Blot协议固定字段中的requestID字段,能够直接读取。

SOFARPC中的bolt协议,设计的比较符合请求转发的须要,TCP拆包,读取RequestID,都没有性能问题。只是Destination的获取须要解码整个header,性能开销稍大。

总结:适合配合DNS通用解码方案,跳过对整个header部分的解码,从而提高性能。固然因为这个header自己也不算大,优化的空间有限,具体提高须要等对比测试的结果出来。

HSF协议

HSF协议是通过精心设计工做在4层的私有协议,因为该协议没有开源,所以不便直接暴露具体格式和字段详细定义。

不过基本的设计和bolt很是相似:

  • 采用变长格式,即协议头+payload

  • 在协议头中能够直接拿到服务接口名和服务方法名做为Destination

  • 有RequestID字段

基本和bolt一致,考虑到Destination能够直接读取,比bolt还要方便一些,HSF协议能够说是对请求转发最完美的协议。

总结:目前的实现方案也只解码了这三个关键字段,速度足够快,不须要继续优化。

Dubbo协议

Dubbo协议也是相似的协议头+payload的变长结构,其协议格式以下:

其中long类型的id字段用来把请求request和返回的response对应上,即咱们所说的RequestId

这样TCP拆包和多路复用都轻松实现,稍微麻烦一点的是:Destination在哪里?Dubbo在这里的设计有点不够理想,在协议头中没有字段能够直接读取到Destination,须要去读取data字段,也就是payload,里面的path字段一般用来保存服务名或者接口名。method字段用来表示方法名。

从设计上看,path字段和method字段被存放在payload中有些美中不足。庆幸的是,读取这两个字段的时候不须要完整的解开整个payload,好险,否则,那性能会无法接受的。

以hession2为例,data字段的组合是:dubbo version + path + interface version + method + ParameterTypes + Arguments + Attachments。每一个字段都是一个byte的长度+字段值的UTF bytes。所以读取时并不复杂,速度也足够快。

基本和HSF一致,就是Destination的读取稍微麻烦一点,放在payload中的设计让人吓了一跳,好在有惊无险。总体说仍是很适合转发的。

总结:同HSF,不须要继续优化。

HTTP/1.1

HTTP/1.1的格式应该你们都熟悉,而在这里,不得不指出,HTTP/1.1协议对请求转发是很是不友好的(甚至能够说是恶劣!):

  1. HTTP请求在拆包时,须要先按照HTTP header的格式,一行一行读取,直到出现空行表示header结束

  2. 而后必须将整个header的内容所有解析出来,才能取出Content-Length header

  3. 经过Content-Length 值,才能完成对body内容的读取,实现正确拆包

  4. 若是是chunked方式,则更复杂一些

  5. Destination一般从Host header中获取

  6. 没有RequestId,彻底没法实现多路复用

这意味着,为了完成最基本的TCP拆包,必须完整的解析所有的HTTP header信息,没有任何能够优化的空间。对比上面几个RPC协议,轻松自如的快速获取几个关键信息,HTTP无疑要重不少。这也形成了在ServiceMesh下,HTTP/1.1和REST协议的性能老是和其余RPC方案存在巨大差别。

对于注定要解码整个header部分,彻底没有优化空间可言的HTTP/1.1协议来讲,Content Based Routing 的解码开销是必须付出的,不管是否使用 Content Based Routing 。所以,快速解码的构想,对HTTP/1.1无效。

总结:受HTTP/1.1协议格式限制,上述两个优化思路都没法操做。

HTTP/2和gRPC

做为HTTP/1.1的接班人,HTTP/2则表现的要好不少。

备注:固然HTTP/2的协议格式复杂多了,因为篇幅和主题的限制,这里不详细介绍HTTP/2的格式。

首先HTTP/2是以帧的方式组织报文的,全部的帧都是变长,固定的9个字节+可变的payload,Length字段指定payload的大小:

HTTP2的请求和应答,也被称为Message,是由多个帧构成,在去除控制帧以外,Message一般由Header帧开始,后面接CONTINUATION帧和Data帧(也可能没有,如GET请求)。每一个帧均可以经过头部的Flags字段来设置END_STREAM标志,表示请求或者应答的结束。即TCP拆包的问题在HTTP/2下是有很是标准而统一的方式完成,彻底和HTTP/2上承载的协议无关。

HTTP/2经过Stream內建多路复用,这里的Stream Identifier 扮演了相似前面的RequestId的角色。

而Destination信息则经过Header帧中的伪header :authority 来传递,相似HTTP/1.1中的Host header。不过HTTP/2下header会进行压缩,读取时稍微复杂一点,也存在须要解压缩整个header帧的性能开销。考虑到拆包和获取RequestId都不须要解包(只需读取协议头,即HTTP/2帧的固定字段),速度足够快,所以存在很大的优化空间:不解码header帧,直接经过DNS通用寻址方案,这样性能开销大为减小,有望得到极高的转发速度。

总结:HTTP/2的帧设计,在请求转发时表现的很是友好。惟独Destination信息放在header中,会形成必须解码header帧。好在DNS通用寻址方案能够弥补,实现快速解码和转发。


Service Mesh时代的RPC理想方案

在文章的最后,咱们总结并探讨一下,对于Service Mesh而言,什么样的RPC方案是最理想的?

  1. 必须能够方便作TCP拆包,最好在协议头中就简单搞定,标准方式如固定协议头+length字段+可变payload。HSF协议、 bolt协议和dubbo协议表现完美,HTTP/2采用帧的方式,配合END_STREAM标志,方式独特但有效。HTTP/1.1则是反面典型。

  2. 必须能够方便的获取destination字段,一样最好在协议头中就简单搞定。HSF协议表现完美,dubbo协议藏在payload中但终究仍是能够快速解码有惊无险的过关,bolt协议和HTTP/2协议就很遗憾必须解码header才能拿到,好在DNS通用寻址方案能够弥补,但终究丢失了服务名和方法名信息。HTTP/1.1依然是反面典型。

  3. 最好有RequestId字段,一样最好在协议头中就简单搞定。这方面HSF协议、dubbo协议、bolt协议表现完美,HTTP/2协议更是直接內建支持。HTTP/1.1继续反面典型。

所以,仅以方便用最佳性能进行转发,对 Service Mesh、sidecar 友好而言,最理想的 RPC 方案是:

传统的变长协议

固定协议头+length 字段+可变 payload,而后在固定协议头中直接提供 RequestId 和destination。

基于帧的协议

以 HTTP/2 为基础,除了请求结束的标志位和 RequestId 外,还须要经过帧的固定字段来提供 destination 信息。

或许,在将来,在 Service Mesh 普及以后,对 Service Mesh 友好成为 RPC 协议的特别优化方向,咱们会看到表现完美更适合Service Mesh时代的新型 RPC 方案。


相关连接:

SOFA 文档: http://www.sofastack.tech/

SOFA: https://github.com/alipay

SOFAMosn:https://github.com/alipay/sofa-mosn

SOFAMesh:https://github.com/alipay/sofa-mesh

长按关注,获取分布式架构干货

欢迎你们共同打造 SOFAStack https://github.com/alipay

相关文章
相关标签/搜索