learn-tech/专栏/周志明的架构课/08_远程服务调用(下):如何选择适合自己的RPC框架?.md
2024-10-16 06:37:41 +08:00

199 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
08 _ 远程服务调用如何选择适合自己的RPC框架
你好,我是周志明。
上一讲我们主要是从学术的角度出发一起学习了RPC概念的形成过程。今天这一讲我会带你从技术的角度出发去看看工业界在RPC这个领域曾经出现过的各种协议以及时至今日还在层出不穷的各种框架。你会从中了解到RPC要解决什么问题以及如何选择适合自己的RPC框架。
RPC框架要解决的三个基本问题
在第1讲“原始分布式时代”中我曾提到过在80年代中后期惠普和Apollo提出了网络运算架构Network Computing ArchitectureNCA的设想并随后在DCE项目中发展成了在Unix系统下的远程服务调用框架DCE/RPC。
这是历史上第一次对分布式有组织的探索尝试。因为DCE本身是基于Unix操作系统的所以DCE/RPC也仅面向于Unix系统程序之间的通用。
补充这句话其实不全对微软COM/DCOM的前身MS RPC就是DCE的一种变体版本而它就可以在Windows系统中使用。
在1988年Sun Microsystems起草并向互联网工程任务组Internet Engineering Task ForceIETF提交了RFC 1050规范此规范中设计了一套面向广域网或混合网络环境的、基于TCP/IP网络的、支持C语言的RPC协议后来也被称为是ONC RPCOpen Network Computing RPC/Sun RPC
这两个RPC协议就可以算是如今各种RPC协议的鼻祖了。从它们开始一直到接下来的这几十年所有流行过的RPC协议都不外乎通过各种手段来解决三个基本问题
如何表示数据?
如何传递数据?
如何表示方法?
接下来,我们分别看看是如何解决的吧。
如何表示数据?
这里的数据包括了传递给方法的参数,以及方法的返回值。无论是将参数传递给另外一个进程,还是从另外一个进程中取回执行结果,都会涉及应该如何表示的问题。
针对进程内的方法调用,我们使用程序语言内置的和程序员自定义的数据类型,就很容易解决数据表示的问题了;而远程方法调用,则可能面临交互双方分属不同程序语言的情况。
所以即使是只支持同一种语言的RPC协议在不同硬件指令集、不同操作系统下也完全可能有不一样的表现细节比如数据宽度、字节序的差异等。
行之有效的做法,是将交互双方涉及的数据,转换为某种事先约定好的中立数据流格式来传输,将数据流转换回不同语言中对应的数据类型来使用。这个过程说起来比较拗口,但相信你一定很熟悉它,这其实就是序列化与反序列化。
每种RPC协议都应该有对应的序列化协议比如
ONC RPC的External Data Representation XDR
CORBA的Common Data RepresentationCDR
Java RMI的Java Object Serialization Stream Protocol
gRPC的Protocol Buffers
Web Service的XML Serialization
众多轻量级RPC支持的JSON Serialization
……
如何传递数据?
准确地说如何传递数据是指如何通过网络在两个服务Endpoint之间相互操作、交换数据。这里“传递数据”通常指的是应用层协议实际传输一般是基于标准的TCP、UDP等传输层协议来完成的。
两个服务交互不是只扔个序列化数据流来表示参数和结果就行了,诸如异常、超时、安全、认证、授权、事务等信息,都可能存在双方交换信息的需求。
在计算机科学中专门有一个“Wire Protocol”用来表示两个Endpoint之间交换这类数据的行为。常见的Wire Protocol有以下几种
Java RMI的Java Remote Message ProtocolJRMP也支持RMI-IIOP
CORBA的Internet Inter ORB ProtocolIIOP是GIOP协议在IP协议上的实现版本
DDS的Real Time Publish Subscribe ProtocolRTPS
Web Service的Simple Object Access ProtocolSOAP
如果要求足够简单双方都是HTTP Endpoint直接使用HTTP也可以如JSON-RPC
……
如何表示方法?
“如何表示方法”,这在本地方法调用中其实也不成问题,因为编译器或者解释器会根据语言规范,把调用的方法转换为进程地址空间中方法入口位置的指针。
不过一旦考虑到不同语言,这件事儿又麻烦起来了,因为每门语言的方法签名都可能有所差别,所以,针对“如何表示一个方法”和“如何找到这些方法”这两个问题,我们还是得有个统一的标准。
这个标准做起来其实可以很简单只要给程序中的每个方法都规定一个通用的又绝对不会重复的编号在调用的时候直接传这个编号就可以找到对应的方法。这种听起来无比寒碜的办法还真的就是DCE/RPC最初准备的解决方案。
虽然最后DCE还是弄出了一套跟语言无关的接口描述语言Interface Description LanguageIDL成为了此后许多RPC参考或依赖的基础如CORBA的OMG IDL但那个唯一的“绝不重复”的编码方案UUID却意外地流行了起来已经被广泛应用到了程序开发的方方面面。
这类用于表示方法的协议还有:
Android的Android Interface Definition LanguageAIDL
CORBA的OMG Interface Definition LanguageOMG IDL
Web Service的Web Service Description LanguageWSDL
JSON-RPC的JSON Web Service ProtocolJSON-WSP
……
你看如何表示数据、如何传递数据、如何表示方法这三个RPC中的基本问题都可以在本地方法调用中找到对应的操作。RPC的思想始于本地方法调用尽管它早就不再追求要跟本地方法调用的实现完全一样了但RPC的发展仍然带有本地方法调用的深刻烙印。因此我们在理解PRC的本质时比较轻松的方式是以它和本地调用的联系来对比着理解。
理解了RPC要解决的三个基本问题以后我们接着来看一下现代的RPC框架都为我们提供了哪些可选的解决方案以及为什么今天会有这么多的RPC框架在并行发展。
统一的RPC
DCE/RPC与ONC RPC都有很浓厚的Unix痕迹所以它们其实并没有真正地在Unix系统以外大规模流行过而且它们还有一个“大问题”只支持传递值而不支持传递对象ONC RPC的XDR的序列化器能用于序列化结构体但结构体毕竟不是对象。这两个RPC协议都是面向C语言设计的根本就没有对象的概念。
而90年代正好又是面向对象编程Object-Oriented ProgrammingOOP风头正盛的年代所以在1991年对象管理组织Object Management GroupOMG便发布了跨进程、面向异构语言的、支持面向对象的服务调用协议CORBA 1.0Common Object Request Broker Architecture
CORBA 1.0和1.1版本只提供了对C和C++的支持而到了末代的CORBA 3.0版本不仅支持了C、C++、Java、Object Pascal、Python、Ruby等多种主流编程语言还支持了Smalltalk、Lisp、Ada、COBOL等已经“半截入土”的非主流语言阵营不可谓不强大。
可以这么说CORBA是一套由国际标准组织牵头、由多个软件提供商共同参与的分布式规范。在当时只有微软私有的DCOM的影响力可以稍微跟CORBA抗衡一下。但是与DCE一样DCOM也受限于操作系统不过比DCE厉害的是DCOM能跨语言哟。所以能够同时支持跨系统、跨语言的CORBA其实原本是最有机会统一RPC这个细分领域的竞争者。
但很无奈的是CORBA并没有抓住这个机会。一方面CORBA本身的设计实在是太过于啰嗦和繁琐了甚至有些规定简直到了荒谬的程度。比如说我们要写一个对象请求代理ORB这是CORBA中的关键概念大概要200行代码其中大概有170行是纯粹无用的废话这句带有鞭尸性质的得罪人的评价不是我说的是CORBA的首席科学家Michi Henning在文章《The Rise and Fall of CORBA》中自己说的
另一方面为CORBA制定规范的专家逐渐脱离实际了所以做出的CORBA规范非常晦涩难懂导致各家语言的厂商都有自己的解读结果弄出来的CORBA实现互不兼容实在是对CORBA号称支持众多异构语言的莫大讽刺。这也间接造就了后来W3C Web Service的出现。
所以Web Service一出现CORBA就在这场竞争中犹如十八路诸侯讨董卓互乱阵脚、一触即溃局面可以说是惨败无比。最终下场就是CORBA和DCOM一起被扫进了计算机历史的博物馆中而Web Service获得了一统RPC的大好机会。
1998年XML 1.0发布并成为了万维网联盟World Wide Web ConsortiumW3C的推荐标准。1999年末以XML为基础的SOAP 1.0Simple Object Access Protocol规范的发布代表着一种被称为“Web Service”的全新RPC协议的诞生。
Web Service是由微软和DevelopMentor公司共同起草的远程服务协议随后被提交给W3C并通过投票成为了国际标准。所以Web Service也被称为是W3C Web Service。
Web Service采用了XML作为远程过程调用的序列化、接口描述、服务发现等所有编码的载体当时XML是计算机工业最新的银弹只要是定义为XML的东西几乎就都被认为是好的风头一时无两连微软自己都主动宣布放弃DCOM迅速转投Web Service的怀抱。
交给W3C管理后Web Service再没有天生属于哪家公司的烙印商业运作非常成功很受市场欢迎大量的厂商都想分一杯羹。但从技术角度来看它设计得也并不优秀甚至同样可以说是有显著缺陷。
对于开发者而言Web Service的一大缺点就是过于严格的数据和接口定义所带来的性能问题。
虽然Web Service吸取了CORBA的教训不再需要程序员手工去编写对象的描述和服务代理了但是XML作为一门描述性语言本身的信息密度就很低都不用与二进制协议比与今天的JSON或YAML比一下就知道了。同时Web Service是一个跨语言的RPC协议这使得一个简单的字段为了在不同语言中不会产生歧义要以XML描述去清楚的话往往比原本存储这个字段值的空间多出十几倍、几十倍乃至上百倍。
这个特点就导致了要想使用Web Service就必须要有专门的客户端去调用和解析SOAP内容也需要专门的服务去部署如Java中的Apache Axis/CXF更关键的是这导致了每一次数据交互都包含大量的冗余信息性能非常差。
如果只是需要客户端、传输性能差也就算了又不是不能用。既然选择了XML来获得自描述能力也就代表着没打算把性能放到第一位。但是Web Service还有另外一点原罪贪婪。
“贪婪”是指它希望在一套协议上一揽子解决分布式计算中可能遇到的所有问题。这导致Web Service生出了一整个家族的协议出来。
Web Service协议家族中除它本身包括了的SOAP、WSDL、UDDI协议之外还有一堆以WS-*命名的子功能协议,来解决事务、一致性、事件、通知、业务描述、安全、防重放等问题。这些几乎数不清个数的家族协议,对开发者来说学习负担极其沉重。结果就是,得罪惨了开发者,谁爱用谁用去。
当程序员们对Web Service的热情迅速燃起又逐渐冷却之后也不禁开始反思那些面向透明的、简单的RPC协议如DCE/RPC、DCOM、Java RMI要么依赖于操作系统要么依赖于特定语言总有一些先天约束那些面向通用的、普适的RPC协议如CORBA就无法逃过使用复杂性的困扰而那些意图通过技术手段来屏蔽复杂性的RPC协议如Web Service又不免受到性能问题的束缚。
简单、普适和高性能,似乎真的难以同时满足。
分裂的RPC
由于一直没有一个能同时满足以上简单、普适和高性能的“完美RPC协议”因此远程服务器调用这个小小的领域就逐渐进入了群雄混战、百家争鸣的“战国时代”距离“统一”越来越远并一直延续至今。
我们看看相继出现过的RPC协议/框架就能明白了RMISun/Oracle、ThriftFacebook/Apache、Dubbo阿里巴巴/Apache、gRPCGoogle、Motan2新浪、FinagleTwitter、brpc百度、.NET Remoting微软、ArvoHadoop、JSON-RPC 2.0公开规范JSON-RPC工作组……
这些RPC的功能、特点都不太一样有的是某种语言私有有的能支持跨越多门语言有的运行在HTTP协议之上有的能直接运行于TCP/UDP之上但没有哪一款是“最完美的RPC”。据此我们也可以发现一个规律任何一款具有生命力的RPC框架都不再去追求大而全的“完美”而是会找到一个独特的点作为主要的发展方向。
我们看几个典型的发展方向:
朝着面向对象发展。这条线的缘由在于在分布式系统中开发者们不再满足于RPC带来的面向过程的编码方式而是希望能够进行跨进程的面向对象编程。因此这条线还有一个别名叫作分布式对象Distributed Object它的代表有RMI、.NET Remoting。当然了之前的CORBA和DCOM也可以归入这一类。
朝着性能发展代表为gRPC和Thrift。决定RPC性能主要就两个因素序列化效率和信息密度。序列化效率很好理解序列化输出结果的容量越小速度越快效率自然越高信息密度则取决于协议中有效荷载Payload所占总传输数据的比例大小使用传输协议的层次越高信息密度就越低SOAP使用XML拙劣的性能表现就是前车之鉴。gRPC和Thrift都有自己优秀的专有序列化器而在传输协议方面gRPC是基于HTTP/2的支持多路复用和Header压缩Thrift则直接基于传输层的TCP协议来实现省去了额外的应用层协议的开销。
朝着简化发展代表为JSON-RPC。要是说选出功能最强、速度最快的RPC可能会有争议但要选出哪个功能弱的、速度慢的JSON-RPC肯定会是候选人之一。它牺牲了功能和效率换来的是协议的简单。也就是说JSON-RPC的接口与格式的通用性很好尤其适合用在Web浏览器这类一般不会有额外协议、客户端支持的应用场合。
……
经历了RPC框架的“战国时代”开发者们终于认可了不同的RPC框架所提供的不同特性或多或少是互相矛盾的很难有某一种框架说“我全部都要”。
要把面向对象那套全搬过来就注定不会太简单比如建Stub、Skeleton就很烦了即使由IDL生成也很麻烦功能多起来协议就要弄得复杂效率一般就会受影响要简单易用那很多事情就必须遵循约定而不是配置才行要重视效率那就需要采用二进制的序列化器和较底层的传输协议支持的语言范围容易受限。
也正是因为每一种RPC框架都有不完美的地方才会有新的RPC轮子不断出现。
而到了最近几年RPC框架有明显朝着更高层次不仅仅负责调用远程服务还管理远程服务与插件化方向发展的趋势不再选择自己去解决表示数据、传递数据和表示方法这三个问题而是将全部或者一部分问题设计为扩展点实现核心能力的可配置再辅以外围功能如负载均衡、服务注册、可观察性等方面的支持。这一类框架的代表有Facebook的Thrift和阿里的Dubbo现在两者都是Apache的
尤其是断更多年后重启的Dubbo表现得更为明显它默认有自己的传输协议Dubbo协议同时也支持其他协议它默认采用Hessian 2作为序列化器如果你有JSON的需求可以替换为Fastjson如果你对性能有更高的需求可以替换为Kryo、FST、Protocol Buffers等如果你不想依赖其他包直接使用JDK自带的序列化器也可以。这种设计就在一定程度上缓解了RPC框架必须取舍难以完美的缺憾。
小结
今天我们一起学习了RPC协议在工业界的发展包括它要解决的三个基本问题以及层出不穷的RPC协议/框架。
表示数据、传递数据和表示方法是RPC必须解决的三大基本问题。要解决这些问题可以有很多方案这也是RPC协议/框架出现群雄混战局面的一个原因。
出现这种分裂局面的另一个原因,是简单的框架很难能达到功能强大的要求。
功能强大的框架往往要在传输中加入额外的负载和控制措施,导致传输性能降低,而如果既想要高性能,又想要强功能,这就必然要依赖大量的技巧去实现,进而也就导致了框架会变得过于复杂,这就决定了不可能有一个“完美”的框架同时满足简单、普适和高性能这三个要求。
认识到这一点后一个RPC框架要想取得成功就要选择一个发展方向能够非常好地满足某一方面的需求。因此我们也就有了朝着面向对象发展、朝着性能发展和朝着简化发展这三条线。
以上就是这一讲我要和你分享的RPC在工业界的发展成果了。这也是你在日后工作中选择RPC实现方案的一个参考。
最后我再和你分享一点我的心得。我在讲到DCOM、CORBA、Web Service的失败的时候虽然说我的口吻多少有一些戏谑但我们得明确一点这些框架即使没有成功但作为早期的探索先驱并没有什么应该被讽刺的地方。而且其后续的发展都称得上是知耻后勇反而值得我们的掌声赞赏。
比如说到CORBA的消亡OMG痛定思痛之后提出了基于RTPS协议栈的“数据分发服务”商业标准Data Distribution ServiceDDS“商业”就是要付费使用的意思。这个标准现在主要用在物联网领域能够做到微秒级延时还能支持大规模并发通讯。
再比如说到DCOM的失败和Web Service的衰落微软在它们的基础上推出了.NET WCFWindows Communication FoundationWindows通信基础
.NET WCF的优势主要有两点一是把REST、TCP、SOAP等不同形式的调用自动封装为了完全一致的、如同本地方法调用一般的程序接口二是依靠自家的“地表最强IDE”Visual Studio把工作量减少到只需要指定一个远程服务地址就可以获取服务描述、绑定各种特性如安全传输、自动生成客户端调用代码甚至还能选择同步还是异步之类细节的程度。
虽然.NET WCF只支持.NET平台而且也是采用XML语言描述但使用体验真的是非常畅快足够挽回Web Service得罪开发者丢掉的全部印象分。
一课一思
我们通过两讲学习了RPC在学术界和工业界的发展后再回过头来思考一个问题开发一个分布式系统是不是就一定要用RPC呢
我提供给你一个分析思路吧。RPC的三大问题源自对本地方法调用的类比模拟如果我们把思维从“方法调用”的约束中挣脱那参数与结果如何表示、方法如何表示、数据如何传递这些问题都会海阔天空拥有焕然一新的视角。但是我们写程序真的可能不面向方法来编程吗
这就是我在下一讲准备跟你探讨的话题了。现在你可以先自己思考一下,欢迎在留言区分享你的看法。另外,如果觉得有收获,也非常欢迎你把今天的内容分享给更多的朋友。
好,感谢你的阅读,我们下一讲再见。