当前位置: 首页 > 后端技术 > Node.js

NetworkProtocol22-RPCProtocol(Part2)-BinaryRPCProtocol

时间:2023-04-03 16:34:32 Node.js

我们已经知道了两种常用的基于文本的RPC协议。对于陌生人之间的交流,使用NBA、CBA等缩写词会使协议约定很不方便。在讲CDN和DNS的时候,我们讲到了接入层的设计,可以用来缓存静态资源或者动态资源的静态部分。但对于下单、支付等交易场景,仍然需要调用API。对于微服务架构,API需要API网关统一管理。API网关的实现方式有很多种。使用Nginx或OpenResty结合Lua脚本是一种常见的方式。在上一节提到的SpringCloud系统中,有一个组件Zuul也是做这件事的。数据中心如何相互调用?API网关是用来管理API的,但是API的实现一般是在一个叫做Controller层的地方。该层向外界提供API。因为是陌生人访问,所以我们可以看到目前业界的主流基本都是RESTfulAPI,面向大型互联网应用。在Controller中,就是我们互联网应用的业务逻辑实现。上一节讲RESTful的时候说过,业务逻辑的实现最好是无状态的,这样可以横向扩展,但是资源的状态还是需要服务端来维护。资源的状态不应该在业务逻辑层维护,而是在最底层的持久层,一般使用分布式数据库和ElasticSearch。这些服务器的状态,比如订单、库存、商品等,是重中之重,需要持久化到硬盘上。数据不能丢失。但是由于硬盘的读写性能较差,导致持久层的吞吐量往往达不到互联网。应用程序需要的吞吐量,需要在它前面加一层缓存。使用Redis或者memcached拦截请求,让所有的请求都无法进入数据库“中国军营”。在缓存和持久层之上一般是基础服务层,提供一些原子接口。比如用户、商品、订单、库存的增删改查,缓存和数据库对上层业务逻辑进行屏蔽。有了这一层,上层业务逻辑看到的都是接口,不需要调用数据库和缓存。所以对于缓存层的扩展,数据库的分库分表,所有的改动都终止在这一层,有利于缓存和数据库以后的运维。再往上是复合层。因为基础服务层只提供简单的接口来实现简单的业务逻辑,而复杂的业务逻辑,比如下单、扣券、扣库存等,必须在复合服务层实现。这样Controller层、复合服务层、基础服务层就会互相调用。这个调用是在数据中心内部,量会比较大。仍然是使用RPC机制实现的。由于服务较多,需要单独的注册中心进行服务发现。服务提供者会向注册中心注册自己提供了哪些服务,服务消费者会订阅这个服务,这样才能调用这个服务。调用的时候有问题,这里的RPC调用应该是二进制的还是文本的?事实上,文本最大的问题是它占用了很多字节。比如数字123,其实二进制8位就够了,但是如果转换成文本,就变成了字符串123。如果是UTF-8编码,就是三个字节;如果是UTF-16,就是六个字节。同样的信息,占用空间大,传输时占用带宽大,时延高。因此,对于数据中心内部的相互调用,很多企业在选型时还是希望采用节省空间和带宽的二进制方案。这里有一个著名的例子是Dubbo服务框架的二进制RPC方法。Dubbo会在客户端本地启动一个Proxy,其实就是客户端的Stub,所有的远程调用都是通过这个Stub来封装的。接下来,Dubbo会从注册中心获取服务器列表,根据路由规则和负载均衡规则,在多个服务器中选择最合适的服务器进行调用。在调用服务端时,首先要进行编码和序列化,形成Dubbo头和序列化后的方法和参数。将编码后的数据发送给网络客户端发送,网络服务器收到报文后对报文进行解码。然后将任务分发到某个线程进行处理,在线程中会调用服务器的代码逻辑,然后返回结果。这个过程类似于经典的RPC模式!如何解决协议协议问题?接下来我们来看一下RPC的三大问题,其中注册发现问题已经通过注册中心解决了。现在让我们谈谈协议问题。Dubbo中默认的RPC协议是Hessian2。为了保证传输效率,Hessian2将远程调用序列化为二进制进行传输,并且可以进行一定的压缩。这时候你可能会疑惑,Hessian2和之前的二进制RPC有什么区别,都是二进制序列化协议?这不是绕了一圈又回来了吗?Hessian2解决了一些问题。比如,本来需要定义一个协议文件,然后通过这个文件为客户端和服务端生成存根,让它们可以互相调用,修改起来很不方便。Hessian2不需要定义这个协议文件,它是自描述的。什么是自我描述?所谓自描述,就是调用哪个函数,参数是什么。对方不需要拿到某个协议文件或二进制文件,根据Hessian2的规则自行解析即可。有约定文档的场景有点像两个人事先约定好,0表示方法add,后面传两个数。服务器把两个数相加,这样当一方发送012时,对方知道是1和2相加,但不知道协议文件。当它收到012时,它不知道这意味着什么。自述的场景就像两个人说的每一句话的因果关系。例如,传递“函数:add,第一个参数1,第二个参数2”。所以无论谁得到这个表达都知道它的意思。但是它们都是以二进制形式编码的。这实际上相当于一个结合了XML和二进制共同优点的协议。Hessian2是如何做到这一点的?这就需要看Hessian2的序列化文法描述文件了。看起来很复杂,编译原理里面就有这样的语法规则。我们从Top开始,下一层是value,直到形成一棵树。这里有一个想法。为了防止歧义,每个类型的起始编号设置为唯一的。这样,在解析的时候,看到这个数字,就知道后面跟着的是什么了。这里我们还是以加法为例。“add(2,3)”序列化后是什么样子的?Hx02x00#Hessian2.0C#RPCcallx03add#method"add"x92#两个参数x92#2-argument1x93#3-argument2H以Hession开头,H的二进制以0x48C开头,意思就是这个一个RPC调用0x03,表示方法名是三个字符0x92,表示有两个参数。其实这里存放的应该是2,之所以加上0x90是为了防止歧义,也就是说这里必须是int。自我描述。另外,Hessian2是面向对象的,可以传递一个对象。汽车类{字符串颜色;Stringmodel;}out.writeObject(newCar("red","corvette"));out.writeObject(newCar("green","civic"));---C#对象定义(#0)x0bexample.Car#typeisexample.Carx92#twofieldsx05color#colorfieldnamex05model#modelfieldnameO#objectdef(longform)x90#objectdefinition#0x03red#colorfieldvaluex08corvette#model字段值x60#objectdef#0(shortform)x05green#colorfieldvaluex05civic#modelfieldvalue首先定义这个类。类型的定义也是传递过来的,所以也是自描述的。类名为example.Car,字符长度为11个字符,所以前面的长度为0x0b。有两个成员变量,一个是color,一个是model,字符长度是5位,所以前面的长度是0x05。然后,传输的对象引用这个类。由于类定义在位置0,所以对象会指向这个位置0,编码为0x90。下面的red和corvette是两个成员变量的值,字符长度分别为3和8。然后转移一个属于同一个类的对象。此时类的引用并没有保存,只保存了一个0x60,意思同上。可以看出Hessian2真的是能压缩到什么程度,没有再传输更多的字节。如何解决RPC传输问题?接下来我们看一下Dubbo的RPC传输问题。前面我们也说过,基于Socket实现一个高性能的服务器是非常复杂的。Dubbo中使用了Netty的网络传输框架。Netty是一个非阻塞的基于事件的网络传输框架。当服务器启动时,它会监听一个端口并注册以下事件。连接事件:当接收到客户端的连接事件时,将调用voidconnected(Channelchannel)方法。当触发可写事件时,将调用voidsent(Channelchannel,Objectmessage),服务端将响应数据返回给客户端。当read事件触发时,会调用voidreceived(Channelchannel,Objectmessage),当服务端收到客户端的请求数据,发生异常时,会调用voidcaught(Channelchannel,Throwableexception)时事件触发后,服务端会将这些函数中的逻辑选择直接在这个函数中进行操作,或者将请求分发到线程池中进行处理。一般异步数据读写需要另外一个线程池的参与,真正的服务端业务代码逻辑会在线程池中调用返回结果。Hessian2是Dubbo默认的RPC序列化方式,当然还有其他选择。例如,Dubbox从Spark借用Kryo来实现高性能序列化。在这里,我们谈到了数据中心的相互调用。为了高性能,大家都愿意用二进制,但是为什么后来SpringCloud又兴起了呢?这是因为并发量越来越大,已经到了微服务的阶段。与原来的SOA不同,微服务的粒度更细,模块之间的关系也更复杂。在上述架构中,如果使用二进制方式进行序列化,虽然没有使用协议文件生成存根,但是接口的定义和传递的对象DTO仍然需要共享JAR。因为只有客户端和服务端有这个JAR,序列化和反序列化才能成功。但当关系复杂时,JAR依赖变得极其复杂,难以维护。而且,如果在DTO中加入了一个字段,双方的JAR没有匹配好,也会导致序列化不成功,可能会出现循环依赖。这时候,一般有两种选择。一是建立严格的项目管理流程。不允许循环调用,不允许跨层调用,只允许上层调用下层,不允许下层调用上层接口。必须保持兼容性。不兼容的接口是新添加的,而不是原来的接口。当通过监控发现接口未被使用时,在下载升级时,先升级服务提供者,再升级服务消费者。二是改用RESTful方式。使用SpringCloud,consumer和provider不需要共享JAR,各自声明自己的,只要能转成JSON即可,而JSON也是RESTful的一种更灵活的使用方式,性能会有所降低,所以需要通过横向扩展来抵消单机的性能损失总结RESTfulAPI已经基本形成了访问层和Controller层外部调用的事实标准,但是随着内部服务之间的调用越来越多,性能也越来越差越来越重要,所以Dubbo的RPC框架有Dubbo通过注册中心解决服务发现问题,通过Hessian2序列化解决协议约定问题,通过Netty解决网络传输问题。在更复杂的微服务场景下,内部调用也会考虑SpringCloud的RESTful方式,主要是JAR包的依赖和管理