调用外观背后的消息交换

远程过程调用并不是让处理器直接跳到另一台机器的函数地址,而是用通信协议实现调用抽象。服务接口约定过程名称或编号、参数类型与返回结果。客户端通常调用一个本地代理或存根,由它把参数编码成可传输的消息;服务端解码、找到过程、执行并编码结果,再由客户端还原返回值。

序列化解决的是参数怎样表示和传递,不能把本地指针或任意内存状态自动变成远端可用对象。服务发现、连接、认证及接口版本也需要相应机制。网络套接字提供通信能力,RPC则在其上组织方法、参数、结果与错误,二者处于不同抽象层次。

RPC不是某一个产品的专名。ONC RPC有自己的消息和程序编号体系,gRPC则常由接口定义生成客户端与服务端代码。同步请求应答接近普通过程调用的使用方式,但RPC实现也可以支持异步调用及流式消息,不必把所有调用都设计为单线程阻塞。

参考:[1] [2]

超时并不能证明没有执行

本地调用与远程调用的重要差别,是通信双方和网络可能各自发生故障。请求可能未到服务器,服务器可能处理中崩溃,也可能已经完成操作,只是回复丢失或迟到。客户端看到超时,通常无法仅凭这一现象区分这些情况,这就是部分失败带来的不确定性。

gRPC的状态码文档明确指出,改变状态的操作即使已经成功,也可能因回复延迟而让客户端得到DEADLINE_EXCEEDED。客户端与服务器可以各自判断调用是否成功,结论未必一致;取消调用也不会自动回滚取消之前已经产生的变化。超时是等待边界,不是业务事务的撤销证明。

参考:[2] [3]

重试和执行次数的边界

假设一个远程方法把计数器加一,服务器执行后回复丢失。客户端再次发送同样内容,若服务端把它视作新调用,计数器就可能再加一。重试能提高最终得到结果的机会,却也可能扩大副作用。即使底层使用TCP,连接中断或服务器故障后没有收到回复,也不能断定过程未执行。

至多一次语义意在避免同一调用被重复执行,但允许调用根本未执行,且客户端仍可能不知道结果。服务端可以记录请求标识并识别重复请求;保证到底覆盖多久、重启后是否保留记录、同一标识如何绑定同一操作,都需要具体协议说明。不能把“有请求编号”直接说成任何故障下都恰好执行一次。

至少一次方式着重通过重试争取执行,但可能重复;其最终成功仍依赖服务和通信能够恢复。幂等操作则是重复应用不会额外改变预期效果,例如把同一字段设为同一值,与“把数值加一”不同。幂等性描述效果,不等于过程只运行过一次。

参考:[1] [3] [4]

接口需要暴露真正的失败语义

可靠接口应区分参数错误、权限错误、临时不可用和执行结果不明,不能一见异常就无限重试。gRPC的重试机制按状态码、次数和退避策略决定是否再次尝试;未配置一般重试策略时,其透明重试只覆盖能够确定尚未进入服务端业务处理的特定情形。

因此,RPC的价值是统一调用接口和消息处理,而不是消除分布式系统的边界。设计者仍须明确哪些方法适合重试、怎样确认结果、如何限制等待和重试负载,并把这些约定交给调用方,而不能承诺远程调用与本地调用具有完全相同的语义。

参考:[3] [4]