1. 请求-应答协议
书中的协议不是基于TCP流(大多数C/S交换实现),而是基于UDP数据包,可避免以下开销:
- 多余的ACK确认
- 连接建立多出2对额外信息(除了一对请求和应答之外)
- 多余的流控制
1.1. 协议原语
书中原语包括:
doOperation(remoteRef, opId, args)
: 客户调用它进行远程操作,将发送的参数编码,并从返回的字节数组中解码getRequest()
: 服务端获取请求消息sendReply(reply, clientHost, clientPort)
: 服务端发送应答给客户端,同步情况下客户端得到应答后doOperation
会解除阻塞
发送的消息解构如下:
messageType
: Request or responserequestId
remoteRef
operationId
: Identifier of the methodargs
: Byte array
1.2. 消息标识符
由2部分组成:requestId
(对发送者唯一), 发送者进程标识符(网络地址+端口号, 对整个分布式系统唯一)
1.3. 故障模型
基于UDP的请求-应答协议故障模型包括:
- 遗漏故障
- 不保证消息按发送顺序进行传输
- 进程故障(由于是同步的,可以用应答调用来检测故障,所以不会有拜占庭行为)
1.4. 超时
doOperation
超时时,可有多种选择:
- 返回给客户一个故障指示(但不难避免消息丢失的可能性)
doOperation
重复发送请求消息直到收到应答,或已有理由相信服务器没做应答
最后返回时,会以未收到结果的异常高速客户
1.5. 丢弃重复请求
服务端根据协议,识别相同请求标识符的连续消息,并过滤掉重发的消息
1.6. 丢失的应答
幂等操作:客户端重复执行某一操作的结果和操作一次的结果效果一样。
HTTP的
GET
,PUT
,DELETE
都是幂等的,而POST
不是
服务器的应答会丢失,当客户端重复执行不幂等的操作时,服务端需要保存原先的结果,采取特殊措施避免重复操作。(而幂等操作就不需要采取特殊措施)
1.7. 历史
历史:包含已发送的应答消息记录结构,历史内容包含请求标识符、消息以及应答的客户标识符。
目的是实现服务端重新传输应答消息的机制。
*1.8. 协议交互类型
3种协议类型,能够在出现通信故障时产生不同行为
a) 请求协议(R)
客户端向服务端单独发一个请求消息,不需要操作返回值和应答消息。
基于UDP实现,会遇到相同的通信故障
b) 请求-应答协议(RR)
对绝大多数C/S交互有用,其基于请求-应答协议,不要求特殊的确认消息(服务端应答可看成客户端请求的确认,而客户端的调用可作为服务器应答的确认)
而UDP的数据丢失的故障可通过重复请求(带有过滤的)和重新传输历史应答的方式屏蔽
c) 请求-应答-确认协议(RRA)
最后的确认是客户端发送的,用于告诉服务端自己已经收到了应答消息(包含了来自需要被确认的应答消息requestId
,应答也可以看成对客户端的请求),使得服务端可从“历史”中删除相应条目
而A的丢失不会造成造成损失,发送时不用阻塞
1.9. 请求-应答协议在TCP中的使用
基于UDP的请求应答协议,由于缓冲区大小固定,所以可能会出现“多包”,数据长度的限定不适应于透明的RMI/RPC系统使用
TCP流可传输任意长度的数据,期望避免实现“多包”协议,使得可靠传输任意长度的对象集合成为可能。
由于TCP能保证可靠传输请求和应答,因此不必要处理重传、重复过滤、历史等问题。
若持续发送消息,TCP只需建立一次连接即可,减少连接开销。
发送请求后不久就收到应答,TCP确认消息引起的开销也将减少。
2. 远程过程调用
远程计算机的进程中的过程能被调用,好像它们在本地地址空间的过程一样。
实现高级的分布透明性,让分布式编程和传统编程相似。
2.1. RPC设计问题
三个重要问题:
- 接口编程
- RPC调用语义
- 透明性的关键问题和它如何与远程过程调用相关联
a) 接口编程
接口与实现分离,保证模块接口相同,模块实现就可以随意改变,不会影响到接口调用者。
分布式系统接口
在分布式编程中使用接口有很多好处,源于接口和实现的分离。
服务接口定义也受分布式底层影响:
- 服务接口不能指定到变量的直接访问(某个进程客户模块不可能访问另一个进程模块的变量)
- 本地的参数传递机制(值传递和引用传递,尤其是引用传递)不适用于调用者和过程在不同进程中的情况。因此需要规定
input
/output
描述参数,必须将值内容传送进去 - 一个过程的地址对于另一个远程过程往往是无效的,地址不能作为参数/结果
接口定义语言(IDL)
RPC机制可集成到某些编程语言中,只要语言有定义接口的表示方法,并允许将输入和输出参数映射到该语言中正常使用的参数(如Java)。
IDL允许相互调用以不同语言实现的过程。它提供一种定义接口的表示法,接口中操作的每个参数可以附加输入/输出类型说明
IDL也可在RMI和Web服务中使用
b) RPC调用语义
上文中doOperation
可选择实现以下3个功能以容错:
- 重发请求
- 过滤重复请求
- 重传响应
这些选择组合使用导致调用者所见到的远程调用可靠性的各种可能语义,包括:
- 或许调用:远程过程可能执行1次也可能不执行。(在可接收偶尔调用失败的应用中是有用的)
- 至少一次:远程过程至少被执行1次,调用者可能收到返回结果,也可能是一个异常(通知调用者没接收到执行结果)(不保证非幂等操作正确性,接口中所有方法是幂等的就有用)
- 至多一次:远程过程要么恰好执行了1次,要么没执行,调用者可能收到返回结果,也可能是一个异常(通知调用者没接收到执行结果,过程可能执行1次也可能没执行)(应用所有容错措施)
重发请求 | 过滤重复请求 | 重新执行过程/重传应答结果 | 调用语义 |
---|---|---|---|
X | X | X | 或许 |
√ | X | 重新执行过程 | 至少一次 |
√ | √ | 重传应答结果 | 至多一次 |
c) 透明性
透明性,让RPC和本地过程调用尽可能相似,将编码和消息传递细节对程序员隐藏,保证位置和访问的透明性。
保证透明性,可能还需要做到:
- 可能收不到结果,故障情况下,不能准确定位故障位置。这要求发出RPC的对象能在这个情况下恢复
- 延迟更高,这要求调用者应该要及时终止时间很长且对服务器毫无用处的RPC,而服务端要恢复到被调用前的状态
- 要求另外的参数传递类型,因为本地参数传递机制可能不适用(尤其是引用)
2.2. RPC实现
客户端
- 包含一个存根(stub)过程
- 行为对客户端类似于本地过程,但不执行调用
- 它把过程标识符和参数,编码成一个请求消息(请求通过客户端通信模块发给服务器)
- 接收到应答后,对结果进行解码,返回给客户程序
服务端
包含以下几个重要组件:
- 分发器程序
- 根据请求消息的过程标识符选择一个服务器存根过程
- 服务器存根过程
- 对从分发器获得的请求进行解码,然后调用对应的服务过程
- 将返回值编码成应答消息返回
- 服务过程
- 是服务接口定义过程的实现
而RPC通常基于请求-应答协议实现,消息格式也和上述类似,并且选择一种调用语义(一般为至少一次或至多一次)以满足应用需求
3. 远程方法调用
RMI与RPC关系紧密,只是RMI被扩展到分布式对象范畴。
两者共性如下:
- 支持接口编程
- 基于请求-应答协议,提供如至少一次、至多一次的调用语义
- 提供相似的透明性
RMI还提供:
- 分布式系统开发种提供所有OO编程功能
- 基于OO系统种对象标识的概念,在RMI系统中,所有对象都有唯一的对象引用(不论本地还是远程),对象引用可作为参数传递(因此RMI比RPC提供更丰富的参数传递语义)
3.1. RMI设计问题
a) 对象模型
对象由一组数据和一组方法组成。
在本地环境,有时允许直接访问一个对象的数据,但分布式对象系统中,对象数据仅能通过它的方法访问。
对象引用
即通过引用访问对象(Java就是这么做的),调用方法需要由对象引用、方法名和参数。
对象引用是first-class value,即可被赋给一个变量,也可作为方法参数/返回结果。
接口
提供一系列方法的定义(即方法名、参数、返回值、异常),无需实现
动作
方法调用会产生3个结果
- 被调用者状态会改变
- 新的对象可以被实例化
- 其它对象中的方法可能会被调用
异常
类似于Java,为了在方法执行期间处理不同的问题,且增加代码清晰度,可定义一块代码,在不期望的条件下抛出(throw)异常,而另一端需要捕捉(catch)它并进行对应处理。
垃圾回收
有的语言支持自动GC,如Java;有些不支持,如C++,内存的分配和释放会是一个出错根源
b) 分布式对象
分布式系统中,对象可被物理分布在不同进程和计算机中
这提高了封装性,意味着分布式对象的状态只能通过对象方法访问
C/S体系下:对象由服务器管理,客户端通过RMI发送消息以调用它们的方法(服务端也可能成为客户端,因为可能要调用其它对象的方法)
也可采用其它体系,如复制、迁移对象提高容错性和性能
对于异构系统,对象通过RMI访问,可屏蔽异构性
c) 分布式对象模型
远程对象
能接收远程调用的对象,其暴露引用给调用者(这样才能调用其方法),且都有远程接口(指定哪些方法可被远程调用)
远程对象引用
可用于整个分布式系统的标识符,指向某个唯一的对象,与本地引用不同
调用者通过远程对象引用调用远程对象的方法
该引用可作为参数/返回值
远程接口
远程对象类实现了远程接口的方法,其它进程的对象只能远程调用远程接口定义的方法
动作
- 调用其它对象的方法,当跨越进程的时候就要使用RMI
- 实例化新对象,其生命周期常是实例化该对象所在进程的生命周期,新对象也可以成为远程对象
垃圾收集
在本地GC基础上,还需要一个附加的分布式GC模块协作(一般用引用计数法)
若语言不支持GC,需要手动释放内存空间
异常
远程方法调用必须要能引起异常,因为任何远程调用都有可能失败
3.2. RMI实现
a) 通信模块
C和S都有通信模块,执行请求-应答协议,传递请求和应答,指定调用语义(如至多一次)。
消息中,消息类型、requestId
和被调用对象的远程对象引用是必须有的。
S端的通信模块还为被调用对象类选择分发器,传输其本地引用(取自远程引用模块,用于替换请求消息的远程对象引用)
b) 远程引用模块
负责在本地对象引用和远程对象引用之间进行翻译,并负责创建远程对象引用
每个进程的远程引用模块维护一张表,描述本地引用和远程对象引用的映射,包括:
- 该进程拥有的所有远程对象
- 每个本地代理
其动作有:
- 远程对象第一次作为参数/结果传递时,创建远程对象引用,并将其加入表中
- 远程对象引用到来时,要转译成本地引用(可能给出一个代理,也可能指向一个远程对象),若不在表中,RMI软件要创建一个新代理并要求远程引用模块将它添加到表中
c) 伺服器
即一个远程对象的实例,请求(由相应骨架传来的)最终由其处理。
其存活在服务端进程中,当远程对象被实例化时,就会生成一个伺服器
d) RMI软件
有如下几个角色:
代理
- 位于C端,其对调用者而言表现像调用本地对象一样,使RMI对客户透明。
对外开放远程接口
- 不执行调用,而是将调用放到消息中传递给远程对象(真正要调用的实例)
消息编码解码,发送请求,接收应答都是代理做的
- 对于有远程对象引用的进程,每个远程对象都有一个代理。
若p1有远程对象引用
a
,b
,它们位于其它进程中,那么p1就有a
,b
两个对象的代理
分发器
- 位于S端,对表示远程对象的每个类都有一个分发器和骨架
- 接收来自通信模块的请求消息,通过
methodId
选择骨架中的恰当方法,任何传递消息给骨架
骨架
- 远程对象类对应的远程接口方法的实现(不是实例)
- 将分发器的消息解码,并调用伺服器的方法
- 调用完成后编码应答消息,返回给客户端发送方的代理的方法
上述的组件一般而言是静态的,但若接口和系统在编译器不能确定,那么可变为动态的,如动态代理、动态骨架
动态代理如JDK代理,CGLib代理,在Spring AOP中广泛应用
e) 绑定程序
一个单独的服务,维护一张表,记录了文本名字到远程对象引用的映射。
服务器可用该表来按照名字注册远程对象,客户端可查该表获得远程对象引用。
f) 远程对象的激活
主动与被动:
- 一远程对象在一个运行的进程中可被调用是,则是主动的
- 一远程对象不是主动的,但可被激活成主动的,则它是被动的
激活:根据相应被动对象,创建一个主动对象。方法是:创建被动对象所在类的实例,并根据存储的状态初始化该实例。
激活器:用于启动用于驻留远程对象的进程,负责
- 注册可被激活的被动对象(涉及服务器名字,而非被动对象的URL/文件名)
- 启动已命名的服务器进程,激活进程中的远程对象
- 跟踪已激活的远程对象所在服务器的位置
Java: 编码状态(被动) -> 激活状态(主动)
g) 持久对象
一般由持久对象存储来管理,在磁盘上以特定的编码格式存储持久状态的状态。
持久状态一般先存在磁盘中,等被调用时,会被激活。这对于调用者而言是透明的。
钝化:保存持久对象到磁盘中,对象变为被动的,即进入编码状态。钝化策略是很重要的。
持久对象存储一般运行相关持久对象集有可读的名字(如URL)
可通过持久对象存储是否维护对应对象的持久根,来判断该对象是否是持久的
h) 对象定位
帮助客户根据远程对象引用定位远程对象
使用数据库实现,将远程对象引用映射到它们的大概位置
3.3. 分布式GC
其要保证一个对象,没有本地引用且没有远程的引用时,可被收集,其它情况下都不能被收集
Java分布式GC算法
基于引用计数法,并与本地GC协作
- 每个服务进程为它的每个远程对象维护拥有该远程对象引用的一组进程名
若进程p1调用进程p2的对象
a
, 那么a.holders
就包含了p1,此时p1会有a
的代理
- 客户p1第一次接收到远程对象
a
的远程引用时,向服务端p2发送addRef(a)
,创建a
的代理,服务端p2将p1加入到a.holders
中 - 客户p1发现自己的
a
的代理不可达时,向服务端p2发送removeRef(a)
,代理被GC,服务器中a.holders
移除p1 - 若服务端p2的
a
本地不可达,且a.holders
为空,则a
被GC
其在至多一次的语义下实现,且只在代理创建和删除的时候进行,addRef
和removeRef
是幂等的
租借:允许一端在一段时间内使用资源的一种授权
在分布式GC中,租借给客户的租借期从客户端向服务端发出
addRef
开始,到超过过期时间或客户端向服务端发出removeRef
结束。服务端负责管理各个客户的标识符和租期,而客户负责在租期过期前续租。