丰富的线上&线下活动,深入探索云世界
做任务,得社区积分和周边
最真实的开发者用云体验
让每位学生受益于普惠算力
让创作激发创新
资深技术专家手把手带教
遇见技术追梦人
技术交流,直击现场
海量开发者使用工具、手册,免费下载
极速、全面、稳定、安全的开源镜像
开发手册、白皮书、案例集等实战精华
为开发者定制的Chrome浏览器插件
RPC(RemoteProcedureCall)即远程过程调用,我简单解释下字面的意思,远程肯定是指要跨机器而不是本机,所以需要用网络编程才能实现,但是不是说只通过网络通信访问到另外的一台主机的应用程序,就可以称为RPC调用了,显然还远远不够。
我的理解的RPC是帮助我们屏蔽网络编程的细节,实现调用远程方法就跟调用本地(同一个项目中的方法)是一样的,不需要因为这个方法是远程调用就需要编写很多业务无关的代码。
这就好比在修在小河的桥一样,它连着河的两岸,如果没有桥了,你只能通过划船,绕道等方式才能到达河对面。现在是有桥的,这跟路上走到河对面有啥区别呢?没得区别。所以总结我认为,RPC的作用就是两个方面:
屏蔽远程调用和本地调用的区别,让我们感觉调项目内的方法一样的
隐藏底层网络的复杂性,让我们更专注于业务逻辑
我们从定义提炼三个关键词
简单高效通用
但是想想互联网应用的量级越来越大,单台计算机的能力有限,那么需要借助可扩展的集群来完成,具体看下如何RPC在不同机器之间的完成调用。
咱们就开始了
典型的RPC架构可划成三部分:
1)服务提供者(RPCServer):运行在服务端,提供服务接口定义和服务实现类。
2)服务消费者(RPCClient):运行在客户端的,通过远程代理对象调用远程服务。
3)注册中心(Registry):也是运行在服务端,负责把本地服务发布成远程的服务,它也要去管理,提供给服务消费者来使用的。
通过上面的图可以看出,一次简单的RPC调用可以分为以下几个步骤:
(1)服务提供者启动后主动向注册中心注册机器ip、端口以及提供的服务列表;
(2)服务消费者在启动时到注册中心获取服务提供方地址列表,在本地缓存一份;
(3)服务消费者通过本地调用的方式调用服务,调用模块收到请求后通过负载均衡策略选取合适的远程服务地址;
(4)协议模块负责把方法、入参等信息进行序列化(编码)成能够进行网络传输的消息体,并将消息通过网络发送给服务端;
(5)服务端收到消息后进行解码(反序列化操作)。
(7)服务端将处理返回的结果进行序列化(编码),并将结果通过网络发送至服务消费者;
(8)服务消费者收到消息后进行解码最终得到结果;
敲黑板:在不同的RPC框架实现中步骤1、2、3的顺序可能有些不同。
RPC调用分以下两种:
同步调用客户方等待调用执行完成并返回结果。异步调用客户方调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果。若客户方不关心调用返回结果,则变成单向异步调用,单向调用不用返回结果。异步和同步的区分在于是否等待服务端执行完成并返回结果。
RPC结构拆解如下图所示。
RPC组件职责上面我们进一步拆解了RPC实现结构的各个组件组成部分,下面我们详细说明下每个组件的职责划分。
RPC服务方通过RpcServer去导出(export)远程接口方法,而客户方通过RpcClient去引入(import)远程接口方法。客户方像调用本地方法一样去调用远程接口方法,RPC框架提供接口的代理实现,实际的调用将委托给代理RpcProxy。代理封装调用信息并将调用转交给RpcInvoker去实际执行。在客户端的RpcInvoker通过连接器RpcConnector去维持与服务端的通道RpcChannel,并使用RpcProtocol执行协议编码(encode)并将编码后的请求消息通过通道发送给服务方。
RPC服务端接收器RpcAcceptor接收客户端的调用请求,同样使用RpcProtocol执行协议解码(decode)。解码后的调用信息传递给RpcProcessor去控制处理调用过程,最后再委托调用给RpcInvoker去实际执行并返回调用结果。
一个完整的商用RPC框架有很多功能,最最核心的基本功能就是三个:服务寻址、数据编解码、网络传输。
如果是本地调用,被调用的方法在同一个进程内,操作系统或者是虚拟机可以去地址空间去找;但是在远程调用中,这是行不通的,因为两个进程的地址空间是完全不一样的,肯定也无法知道远端的进程在那。
如果要想实现远程调用,我们需要对服务消费者和服务提供者两者进行约束:
在远程过程调用中所有的函数都必须有一个ID,这个ID在整套系统中是唯一存在确定的。
服务消费者在做远程过程调用时,发送的消息体中必须要携带这个ID。
服务消费者和服务提供者分别维护一个函数和ID的对应表。
当服务消费者需要进行远程调用时,它就查一下这个表,找出对应的ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码就行。
上面说的可能比较抽象,通俗一点就是服务消费者如何寻找服务提供者,这就是服务寻址。
图片
服务寻址的实现方式有很多种,常见的是:服务注册中心。要调用服务,首先你需要一个服务注册中心去查询对方服务都有哪些实例,然后根据负载均衡策略择优选一。
像Dubbo框架的服务注册中心是可以配置的,官方推荐使用Zookeeper。
我私下用的nacos也行。
对计算机网络稍微有一点熟悉的同学都知道,数据在网络中传输都是二进制的:01010101010101010,类似这种,只有二进制数据才能在网络间传。
那一个客户端调用远程服务的一个方法,像方法入参这些必然需要转换成二进制才能进行传输,这种将对象转换成二进制流的过程就叫做序列化(编码),学过JavaIO流那部分你应该熟悉。
服务端接收到二进制流不能识别,势必要将二进制流转换成对象,这个逆过程就叫做反序列化,也可以叫解码。
常见的RPC序列化协议
XML(ExtensibleMarkupLanguage)是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点。狭义webservice就是基于SOAP消息传递协议(一个基于XML的可扩展消息信封格式)来进行数据交换的。
Hessian是一个动态类型,简洁的,可以移植到各个语言的二进制序列化对象协议。采用简单的结构化标记、采用定长的字节记录值、采用引用取代重复遇到的对象。
JSON(JavascriptObjectNotation)起源于弱类型语言Javascript,是采用"Attribute-value"的方式来描述对象协议。与XML相比,其协议比较简单,解析速度比较快。
ProtocolBuffers是google提供的一个开源序列化框架,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或RPC数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。同XML相比,Protobuf的主要优点在于性能高。它以高效的二进制方式存储,比XML小3到10倍,快20到100倍。
Thrift既是rpc框架,同时也具有自己内部定义的传输协议规范(TProtocol)和传输数据标准(TTransports),通过IDL脚本对传输数据的数据结构(struct)和传输数据的业务逻辑(service)根据不同的运行环境快速的构建相应的代码,并且通过自己内部的序列化机制对传输的数据进行简化和压缩提高高并发、大型系统中数据交互的成本。
提起网络传输大家脑海里肯定马上就能想到TCP/IP四层模型、OSI七层模型,那通常RPC会选择那一层作为传输协议呢?
在回答这个问题前
先来看下RPC需要网络传输实现什么样的功能。
客户端的数据经过序列化+编码后,就需要通过网络传输到服务端。网络传输层需要把前面说的函数ID和序列化后的参数字节流传给服务端,服务端处理完然后再把序列化后的调用结果传回客户端。
原则上只要能实现上面这个功能的都可以作为传输层来使用,具体协议没有限制。
我们先来看下TCP协议,TCP连接可以是按需连接,需要调用的时候就先建立连接,调用结束后就立马断掉,也可以是长连接,客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效。
由此可见TCP的性能确实很好,因此市面上大部分RPC框架都使用TCP协议,但也有少部分框架使用其他协议,比如gRPC用的是HTTP2来实现。
敲黑板:
数据编解码和网络传输可以有多种组合方式,比如常见的有:HTTP+JSON,Dubbo协议+TCP等。
最近刚升为apache顶级项目的dubbo可以说是java语言中RPC架构最流行的框架。同时Dubbo的文档也是开源软件中写的最详细的文档之一,细看dubbo官方文档。下图是dubbo的整体设计:
上面的dubbo展示了一个完整的带服务治理功能的RPC框架的分层架构。下面我们写一个简易的RPC框架,我把dubbo中的一些非核心层省略掉。具体细节见demo。
省略掉config层,直接通过单例模式获取服务。保留proxy服务代理层,客户端采用简单的反射机制实现服务接口的动态代理,demo中对应ServiceProxyClient类;服务端初始化的时候,按一定规则写进Map映射中,这样直接获取服务实例对象即可,类似RMI的skeleton模块,demo中对应ServiceProcessor类。省略registry注册中心层,demo服务只有一个实例机器提供,故直接写死ip和端口,在ClientRemoter类中getDataRemote方法中直接写死。省略cluster路由层,只有当服务实例有多个时才需要通过算法决定哪个服务实例“接待”请求。省略monitor监控层,这个是服务治理需要的,不影响核心流程调用。保留protocol远程调用层以下四层,统称为remote层。负责请求双方调用协议的约定,序列化、传输。client端的remote层对应demo中ClientRemoter类,将请求服务接口转化成二进制通过socket发送给服务端;服务端的remote层对应demo中的ServerRemoter类,负责客户端的二进制按照协议转化成本地的方法调用,然后又将返回结果通过按照协议翻译成二进制通过socket送给客户端。
敲黑板:首先强调一下轮子的目的不是为了放在生产上面去用,肯定会有很多缺陷是不得行,而是造轮子以实战经验来促进你高效来学习,让你掌握零散的知识点连成一条线。
这次我会深入研究带大家从零开始撸一个RPC框架,如果在编写过程中如果呈现问题,欢迎小伙伴们到github(id:Datalong)提交你的issue,会及时修正。
[过程]这个词估计你学C的时候听说过,也不知道最开始谁翻译的,个人觉得程序,服务,方法,这几个都比[过程]要好。
为了后面能够好理解,我们统一一下术语,干脆叫,远程服务调用,服务包括是指程序接口,方法等一类资源。
RPC是一种进程间通信的模式,程序分布在不同的地址空间里。如果在同一主机里,RPC可以通过不同的虚拟地址空间(即便使用相同的物理地址)进行通讯,而在不同的主机间,则通过不同的物理地址进行交互。许多技术(通常是不兼容)都是基于这种概念而实现的。
听懂了些,好像又没完全懂,那说人话吧。比如说两台服务器:A和B,一个应用部署在A服务器上,想要调用B服务器上某个应用提供的函数/方法。
由于跨应用服务器,你不能直接调用,需要通过网络来表达调用的语意和传达调用的数据,这种调用的方式就是RPC,如下图所示。
RPC服务方通过RpcServer去导出远程接口方法,而客户端通过RpcClient去引入远程接口方法。那客户端像调用本地方法一样去调用远程接口方法,RPC框架提供接口的代理实现,实际的调用将委托给代理RpcProxy。代理封装调用信息并将调用转交给RpcInvoker去实际执行。在客户端的RpcInvoker通过连接器RpcConnector去维持与服务端的通道RpcChannel,并使用RpcProtocol执行协议编码(encode)并将编码后的请求消息通过通道发送给服务方。
既然存在RPC这种远程过程调用,必然会有与之对应的本地过程调用了。本地过程调用在不同的操作系统中,叫法不同,使用方式也不太一样。在Windows编程中,称为LPC;在linux编程中,更习惯称之为IPC,即进程间通信,这不就绕回来了。
但是,不管如何,其本质上就是本地机器上的不同进程之间通信协作的调用方式。
RPC家族中,RMI是Java制定的远程通信协议。而后,基本上RPC框架都或多或少有RMI的影子(当然,其实主要是RPC本身的实现方式就是这样子的)。RMI既然是Java的标准RPC组件,那必然其他编程语言就无法使用了但是,作为服务化的组件,如果没有服务治理来完成大规模应用集群中服务调用管理工作,则运维工作则是非常繁重的,因此类似dubbo这种包含服务治理的RPC组件出现。
也就是RPC本身的实现方式。在JDK1.2的时候,引入到Java体系的。当应用比较小,性能要求不高的情况下,使用RMI还是挺方便快捷的。下面先看看RMI的调用流程。
目前大规模使用的RPC框架有:
作为Java自带的官方RPC组件,单独介绍;然后我们来看看通用RPC实现结构。
全称是RemoteMethodInvocation,也就是远程方法调用。在JDK1.2的时候,引入到Java体系的。当应用比较小,性能要求不高的情况下,使用RMI还是比较快捷的。
从零开始,手写一个RPC,跟随这篇前提知识做铺垫以及数个迭代版本的代码,由简陋到逐渐完备,目的让所有人都能看懂并且写出一个RPC框架。
本文档与代码都是本人第一次手写RPC的心路历程,会有理解的偏差与代码上的不完善,但更是由于这样,有着与新手对同样问题的疑惑,也许会使新手更容易理解这样做的缘故是啥。
另外期待与你的合作:代码,帮助文档甚至rpc框架功能的完备
一定要实际上手敲代码
每一版本都有着对应独立的代码与文档,结合来看
每一个版本都有着要解决的问题与此版本的最大痛点,带着问题去写代码,并且与上个版本的代码进行比较差异
如果你认真去学下去,可以掌握下面的技术:
basic
hard
还有就是底层的网络是基于netty,学完后netty入门没有问题;
有没有一点心动呢?!
上面简单解答了为啥咱们要学rpc轮子,那铁子们就问为什么需要RPC?一项新技术肯定是为了改善技术工具或者重新设计解决具体的业务及架构方面的问题。
不得不说单体架构,这种就是把应用程序的所有功能都打成一个部署包。从上面的架构图总结一下单体架构的特点:
如果网站流量很小时,只需一个应用,把所有功能部署在一起,来减少部署结点和运维成本。如果流量稍微大一点可以通过部署应用集群和数据库集群来提高系统的性能。
这种肯定逐渐淘汰,业务逐渐复杂,应用的外部流量压力骤增,团队成员会越来越多,这时的抗压能力会逐渐暴露。
具体聊下不足
缺点一:高耦合
某个模块出现死循环,导致内存溢出,上下应用都会挂掉。
缺点二:扩展性很差
系统的扩容只能针对应用进行扩容,不能做到对某个功能模块进行扩容,扩容后必然带来资源浪费的问题
缺点三:持续交付周期拉长
网站应用的规模不断扩大,传统的垂直应用架构早已经没法满足了,分布式服务架构以及流动计算架构也在行业应用很广泛,但是在这背景就需要个治理系统确保架构有条不紊的继续演进。
当网站流量很小,它只需一个应用,将所有功能如下单,支付等都部署在一起,减少了部署节点和成本。
垂直应用架构解决了单一应用架构所面临的扩容问题,流量能够分散到各个子系统当中,且系统的体积可控,一定程度上降低了开发人员之间协同以及维护的成本,提升了开发效率。
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心
它也叫服务治理结构,随着服务化的进一步发展,服务越来越多,服务之间的调用和依赖关系也越来越复杂,诞生了面向服务的架构体系(SOA),也因此衍生出了一系列相应的技术,如对服务提供、服务调用、连接处理、通信协议、序列化方式、服务发现、服务路由、日志输出等行为进行封装的服务框架
在RPC框架中主要有三个角色:Provider、Consumer和Registry。如下图所示:节点角色说明:
此RPC的最大痛点:只能调用服务端Service唯一确定的方法,如果有两个方法需要调用呢(Reuest需要抽象)返回值只支持User对象,如果需要传一个字符串或者一个Dog,String对象呢(Response需要抽象)客户端不够通用,host,port,与调用的方法都特定(需要抽象)
是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接的,调用结束后就断掉了呗,也可以是长连接,多个远程过程调用共享同一个连接。
A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么,这样才能完成调用。比如基于Web服务协议栈的RPC,就要提供一个endpointURI,或者是从UDDI服务上查找。如果是RMI调用的话,还需要一个RMIRegistry来注册服务的地址。
当A服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshal),通过寻址和传输将序列化的二进制发送给B服务器。同理,B服务器接收参数要将参数反序列化。B服务器应用调用自己的方法处理后返回的结果也要序列化给A服务器,A服务器接收也要经过反序列化的过程。