本文基于刘超老师的网络文章的总结
自定义标题
1.RPC介绍和需要解决的问题
RPC协议致力于实现将调用远程函数就像调用本地函数一样方便。本质就是客户端将需要调用远程服务器的程序版本,调用的函数,函数中所用的参数等数据利用socket发送给服务端,服务端拿到客户端发送过来的数据,然后调用本地的服务函数,然后再将结果返回给客户端的过程。
主要需要解决的问题有三个:
一、协议约定问题,具体我们需要将调用的函数名称,函数需要用到的参数,调用程序是哪一个版本的信息如果整理发送给服务器端,服务器才能看得懂客户端发送过来的参数。
二、网络问题,如果我们在传递参数过程中网络发生了错误,例如丢包,重传或者性能问题,我们应该如何保证网络能够确认有效的发送到服务器上。
三、服务发现问题,我们需要去哪里获取某个服务函数在哪一个电脑上,从哪一个端口可以访问到这一台电脑上的这一个服务。
因为RPC现在官方只是给了一个调用标准,并没有一个官网的协议,所以现在涌现出了许多实现了RPC协议的解决方案。但是所有的解决方案的底层都是基于解决上面三个问题的。
2、ONC RPC
RPC的调用标准是客户端本地访问本地调用放的Stub,它负责将调用的接口、方法和参数,通过约定的协议规范进行编码,并通过本地的 RPCRuntime 进行传输,将调用网络包发送到服务器。服务器端的 RPCRuntime 收到请求后,交给提供方 Stub 进行解码,然后调用服务端的方法,服务端执行方法,返回结果,提供方 Stub 将返回结果编码后,发送给客户端,客户端的 RPCRuntime 收到结果,发给调用方 Stub 解码得到结果,返回给客户端。就是客户端和服务器端都有三层,分别是本地调用放–>Stub–>RPCRuntime这三层。
ONC RPC是最早的RPC的一种实现方式。ONC RPC 将客户端要发送的参数,以及服务端要发送的回复,都压缩为一个二进制串。
2.1、协议约定问题
为了可以成功调用 RPC,在客户端和服务端实现 RPC 的时候,首先要定义一个双方都认可的程序、版本、方法、参数等。将这些约定为一个协议定义文件。同时为了增加传输效率,利用了XDR来压缩数据进行传输。XDR(External Data Representation,外部数据表示法)是一个标准的数据压缩格式,可以表示基本的数据类型,也可以表示结构体。在 RPC 的调用过程中,所有的数据类型都要封装成类似的格式。而且 RPC 的调用和结果返回,也有严格的格式。这样解析出数据才不会出错。有了协议定义文件,ONC RPC 会提供一个工具,根据这个文件生成客户端和服务器端的 Stub 程序。从而解决了协议约定问题。
2.2、网络传输问题
这个问题已经在 ONC RPC 的类库中解决了。简单叙述是对于每一个客户端,都会创建一个传输管理层,而每一次 RPC 调用,都会是一个任务,在传输管理层,你可以看到熟悉的队列机制、拥塞窗口机制等。因为在网络中同步的方式往往效率比较低,所以这里的解决方案是利用socket的异步模型,为了更好的治理,往往是通过状态机来实现的。只有当满足某个状态的时候,才进行下一步,如果不满足状态,不是在那里等,而是将资源留出来,用来处理其他的 RPC 调用。
2.3、服务发现问题
在 ONC RPC 中,服务发现是通过 portmapper 实现的。portmapper 会启动在一个众所周知的端口上,RPC 程序由于是用户自己写的,会监听在一个随机端口上,但是 RPC 程序启动的时候,会向 portmapper 注册。客户端要访问 RPC 服务端这个程序的时候,首先查询 portmapper,获取 RPC 服务端程序的随机端口,然后向这个随机端口建立连接,开始 RPC 调用。
3.基于XML的SOAP协议
ONC RPC协议传输数据都是通过二进制形式的,但是这样会有一点问题。一、需要双方的压缩格式完全一致,不然会解析出错。二、协议修改不灵活,客户端或者服务端添加或者删除了某一个字段,如果对方没有感知到,会造成协议解析出错。三、版本问题,在程序更新后,例如参数变化了,用这个形式,需要将这个通知给所有的客户端,这样工程比较大。这一切的根源就在于压缩,用二进制来实现造成。
3.1、协议约定问题改进
可以使用文本类的方式进行传输,一种常见的文本类格式是 XML,我们通过解析xml文件可以获取数据,添加参数或者删除参数我们只需要在解析的时候做一步处理就行了,同样,获取的数据更加不容易出错。
基于 XML 的最著名的通信协议就是 SOAP 了,全称简单对象访问协议(Simple Object Access Protocol)。它使用 XML 编写简单的请求和回复消息,并用 HTTP 协议进行传输。SOAP 将请求和回复放在一个信封里面,就像传递一个邮件一样。信封里面的信分抬头和正文。
对于客户端来说,它如何知道应该拼装成上面的xml格式呢?这时候就需要用到一种相对比较严谨的 Web 服务描述语言,WSDL(Web Service Description Languages)。它也是一个 XML 文件。对于某个服务,哪怕是一个陌生人,都可以通过在服务地址后面加上“?wsdl”来获取到这个文件
3.2、服务发现问题改进
有一个 UDDI(Universal Description, Discovery, and Integration),也即统一描述、发现和集成协议。它其实是一个注册中心,服务提供方可以将上面的 WSDL 描述文件,发布到这个注册中心,注册完毕后,服务使用方可以查找到服务的描述,封装为本地的客户端进行调用。
4.基于JSON的RESTful接口协议
对于 SOAP 来讲,无论 XML 中调用的是什么函数,多是通过 HTTP 的 POST 方法发送的,HTTP 除了 POST,还有 PUT、DELETE、GET 等方法。我们其实创建订单完全可以使用 POST 动作,然后在 XML 里面放一个订单的信息就可以了,而删除用 DELETE 动作,然后在 XML 里面放一个订单的 ID 就可以了。
4.1、协议约定问题改进支持横向扩充
我们在上面RPC协议中文档使用的 XML 格式可以改成另外一种简单的文本化的对象表示格式 JSON。这就是 RESTful 格式的 API 的样子。RESTful 可不仅仅是指 API,而是一种架构风格。
我们用远程跨网络调用,不仅仅有网络性能问题,还有更重要的问题是客户端和服务端谁来维护状态。所谓的状态就是对某个数据当前处理到什么程度了。你可以想象“双十一”,多少人同时来购物,作为服务端,如果一台服务器上需要记住每一个客户端的浏览记录,那显然是不可能的,所以需要多个服务端同时提供服务,大家分担一下。但是这就存在一个问题,服务端怎么把自己记住的客户端状态告诉另一个服务端呢?所以最好的解决方案是服务端就只记录资源的状态,例如文件的状态,报表的状态,库存的状态,而客户端自己维护自己的状态。比如,你访问到哪个目录了啊,报表的哪一页了等等。这就是服务端的无状态化。这样服务端就可以横向扩展了,一百个人一起服务,不用交接,每个人都能处理。所谓的无状态,其实是服务端维护资源的状态,客户端维护会话的状态。而且只要服务端的资源状态不变,就给了我们缓存的可能。例如可以将状态缓存到接入层,甚至缓存到 CDN 的边缘节点,这都是资源状态不变的好处。
但是这时候需要注意这种 API 的设计需要实现幂等,因为网络不稳定,就会经常出错,因而需要重试,但是一旦重试,就会存在幂等的问题,也就是同一个调用,多次调用的结果应该一样,不能一次支付调用,因为调用三次变成了支付三次。
按照这种设计模式,无论 RESTful API可以将架构实现成无状态的,面向资源的、幂等的、横向扩展的、可缓存的。
4.2、服务发现问题解决
对于 RESTful API 来讲,我们已经解决了传输协议的问题——基于 HTTP,协议约定问题——基于 JSON,最后要解决的是服务发现问题。有个著名的基于 RESTful API 的跨系统调用框架叫 Spring Cloud。其中 Eureka 是用来实现注册中心的,负责维护注册的服务列表。
服务分服务提供方,它向 Eureka 做服务注册、续约和下线等操作,注册的主要数据包括服务名、机器 IP、端口号、域名等等。另外一方是服务消费方,向 Eureka 获取服务提供方的注册信息。为了实现负载均衡和容错,服务提供方可以注册多个。当消费方要调用服务的时候,会从注册中心读出多个服务来,那怎么调用呢?当然是 RESTful 方式了。Spring Cloud 提供一个 RestTemplate 工具,用于将请求对象转换为 JSON,并发起 Rest调用,RestTemplate 的调用也是分 POST、PUT、GET、 DELETE 的,当结果返回的时候,根据返回的 JSON 解析成对象。通过这样封装,调用起来也很方便。
5.二进制类RPC协议
Dubbo 服务化框架二进制的 RPC 方式就是其中一种著名实现。Dubbo的二进制传输协议效率很高,但是其不能取代Spring cloude的基于JSON的RESTful,原因是随着我们工作中程序的并发量越来越大,已经到了微服务的阶段。同原来的 SOA 不同,微服务粒度更细,模块之间的关系更加复杂。JAR 的依赖也变得异常复杂,Dubbo需要明确双方使用的jar。而Spring cloud在这方面有着非常大的优势,消费端和提供端不用共享 JAR,各声明各的,只要能变成 JSON 就行,而且 JSON 也是比较灵活的。
5.1、协议约定问题改进
Dubbo 中默认的 RPC 协议是 Hessian2。为了保证传输的效率,Hessian2 将远程调用序列化为二进制进行传输,并且可以进行一定的压缩。Hessian2 和ONC RPC的二进制相比优势是原来要定义一个协议文件,然后通过这个文件生成客户端和服务端的 Stub,才能进行相互调用,这样使得修改就会不方便。Hessian2 不需要定义这个协议文件,而是自描述的。所谓自描述就是,关于调用哪个函数,参数是什么,另一方不需要拿到某个协议文件、拿到二进制,靠它本身根据 Hessian2 的规则,就能解析出来。
5.2、网络传输问题改进
基于 Socket 实现一个高性能的服务端,是很复杂的一件事情,在 Dubbo 里面,使用了 Netty 的网络传输框架。Netty 是一个非阻塞的基于事件的网络传输框架,在服务端启动的时候,会监听一个端口,并注册以下的事件。
6.集大成者-GRPC
先对之前的RCP实现协议总结下:ONC RPC和Hessian2是二进制的,传输性能较好,难于跨语言。基于 XML 的 SOAP、基于 JSON 的 RESTful是基于文本的,输性能差一些,可以跨语言。ONC RPC和基于 XML 的 SOAP要写协议文件的严谨一些,基于 JSON 的 RESTful和Hessian2不写协议文件的灵活一些。
GRPC 是一种二进制,性能好,跨语言,还灵活,同时可以进行服务治理的多快好省的 RPC 框架,唯一不足就是还是要写协议文件。
6.1、协议约定问题解决方案改进
GRPC使用了Protocol Buffers来封装参数格式。Protocol Buffers 是一款压缩效率极高的序列化协议,有很多设计精巧的序列化方法。
在灵活性方面,这种基于协议文件的二进制压缩协议往往存在更新不方便的问题。例如,客户端和服务器因为需求的改变需要添加或者删除字段。这一点上,Protocol Buffers 考虑了兼容性。Protocol Buffers 协议中包含了optional:可选字段,可以设置,也可以不设置,如果不设置,则使用默认值;这样就可以方便增删改参数。
6.2、网络传输问题解决方案改进
如果是 Java 技术栈,GRPC 的客户端和服务器之间通过 Netty Channel 作为数据通道,每个请求都被封装成 HTTP 2.0 的 Stream。HTTP 2.0 协议将一个 TCP 的连接,切分成多个流,每个流都有自己的 ID,而且流是有优先级的。流可以是客户端发往服务端,也可以是服务端发往客户端。它其实只是一个虚拟的通道。HTTP 2.0 还将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码。将网络传输效率大大提升。
由于基于 HTTP 2.0,GRPC 和其他的 RPC 不同,可以定义四种服务方法:第一种,也是最常用的方式是单向 RPC。第二种方式是服务端流式 RPC,即服务端返回的不是一个结果,而是一批。第三种方式为客户端流式 RPC,也即客户端的请求不是一个,而是一批。服务端返回一个一个的应答。第四种方式为双向流式 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者读写相结合的其他方式。每个数据流里消息的顺序会被保持。
6.3、服务发现与治理问题解决方案改进
GRPC 本身没有提供服务发现的机制,需要借助其他的组件,发现要访问的服务端,在多个服务端之间进行容错和负载均衡。对于 GRPC 支持比较好的负载均衡器 Envoy。其实 Envoy 不仅仅是负载均衡器,它还是一个高性能的 C++ 写的 Proxy 转发器,可以配置非常灵活的转发规则。
Envoy需要配置四个东西,分别是第一个是 listener,标识监听的端口。第二个是 endpoint,是目标的 IP 地址和端口。第三个是 cluster。一个 cluster 是具有完全相同行为的多个 endpoint,也即如果有三个服务端在运行,就会有三个 IP 和端口,但是部署的是完全相同的三个服务,它们组成一个 cluster,从 cluster 到 endpoint 的过程称为负载均衡,可以轮询。第四个是 route。有时候多个 cluster 具有类似的功能,但是是不同的版本号,可以通过 route 规则,选择将请求路由到某一个版本号,也即某一个 cluster。
如果是静态的,则将后端的服务端的 IP 地址拿到,然后放在配置文件里面就可以了。如果是动态的,就需要配置一个服务发现中心,这个服务发现中心要实现 Envoy 的 API,Envoy 可以主动去服务发现中心拉取转发策略。
Envoy可以实现配置路由策略和负载均衡策略,其中负载均衡总所有这些节点的变化都会上传到注册中心,所有这些策略都可以通过注册中心进行下发,所以,更严格的意义上讲,注册中心可以称为注册治理中心。
Envoy的优势是服务也不用像 Dubbo,或者 Spring Cloud 一样,自己感知到注册中心,自己注册,自己治理,对应用干预比较大。这就是未来服务治理的趋势 Serivce Mesh,也即应用之间的相互调用全部由 Envoy 进行代理,服务之间的治理也被 Envoy 进行代理,完全将服务治理抽象出来,到平台层解决。