操作系统:RPC 中可能遇到的问题(Issues in RPC)

发布于:2025-08-05 ⋅ 阅读:(18) ⋅ 点赞:(0)

目录

常见的 RPC 问题及其解决方案

问题 1:客户端和服务器对数据的表示方式不同

问题 2:远程调用可能失败或重复执行

问题 3:RPC 中的“绑定”(Binding)是怎么实现的?

RPC的执行全过程(Execution of a Remote Procedure Call)

一、客户端发起调用

二、客户端收到端口号,重新发送调用


现在我们继续来讲解:RPC 中可能遇到的问题(Issues in RPC)以及它们是如何被解决的。

常见的 RPC 问题及其解决方案

当客户端和服务器不在同一台机器时,会遇到很多实际问题,比如机器结构不同、数据表达不同、连接失败等等。我们一一来看。

问题 1:客户端和服务器对数据的表示方式不同

举个例子:

假设客户端和服务器运行在两种不同架构的计算机上:

系统类型 存储顺序说明
Big-endian 高位字节存在低地址(最重要的字节排在前面)
Little-endian 低位字节存在低地址(最不重要的字节排在前面)

比如数字 0x12345678,在两种机器中会被以不同顺序存储。

❗问题:如果客户端把数字按自己的方式发给服务器,而服务器直接按自己方式解读,数据就会错乱!
想象一下你给别人发了“1234”,对方看到的却是“3412”。

解决方案:使用统一的外部数据表示格式(XDR)

🔑 XDR(External Data Representation)

是一种与平台无关的中立数据格式,双方都用它来交换信息。

RPC 系统的做法是:

操作
 客户端 在发送前,把数据 转换成 XDR 格式(这一过程叫 marshalling)
 网络上传输 全部用 XDR
 服务器端 收到后,把 XDR 数据 转换回本地格式(这一过程叫 unmarshalling)

这样,不管你是 big-endian 还是 little-endian,大家都说“共同的语言”。

一个形象的类比:

你可以把 XDR 理解成“英语”:

  • 客户端说“中文”,但翻译成“英语”发给对方;

  • 服务器是“西班牙人”,但他也会“英语”,所以能理解;

  • 最终双方沟通无障碍。


问题 2:远程调用可能失败或重复执行

本地函数调用 vs 远程调用

当我们在一台电脑上调用本地函数时,比如:

result = add(3, 5)

这个调用几乎永远不会失败,除非:

  • 程序崩溃;

  • 内存出错;

  • 系统关机。

通常这种失败是“极端情况”。

❗ 但在 RPC 中,不确定因素太多了:

远程调用时,函数在另一台机器上,调用过程可能因为以下“常见网络问题”而失败:

网络问题 可能的影响
网络延迟 客户端等待超时,以为失败
网络中断 请求根本没有到达
服务器繁忙 请求被延迟处理,结果返回慢
响应丢失 函数执行了,但客户端没有收到返回值

导致的两种危险情况:

情况 举例 后果
函数根本没被执行 请求丢失 用户点击按钮没有反应
函数被执行多次 客户端没收到回应,以为失败,于是重发请求 用户可能“被重复扣款”或“重复提交表单”

 操作系统应尽量实现:Exactly Once 语义

所谓 Exactly Once(刚好一次),指的是:

不管网络出了什么问题,RPC 的目标函数要么执行一次,要么根本不执行,但绝不会执行多次

这是最理想的状态。

但问题是:

本地调用天然就是 "exactly once",因为它运行在同一台机器上;
而在 RPC 中,要达到 exactly once 很难,因为:

  • 网络不稳定;

  • 客户端可能重发请求;

  • 服务器不知道这个请求是不是“新来的”还是“重复的”;

  • 网络响应可能丢失但函数其实已经执行了。

为了尽量实现 “exactly once”,RPC 系统可以用这些技术:

方法 说明
唯一请求编号(Request ID) 每次调用附带一个唯一编号,服务器记录已处理的请求 ID,如果重复收到,就不再执行,只返回结果。
幂等操作设计 让函数重复执行不会有坏结果(如多次提交都得到相同结果),比如“查询余额”而不是“扣款”
超时重试机制 客户端设定超时,如果过了时间没收到响应,可以重发请求,但要搭配请求 ID 使用
事务日志 服务器记录处理历史,保证请求即使中断也可以恢复状态

问题 3:RPC 中的“绑定”(Binding)是怎么实现的?

什么是“绑定”?

在我们调用本地函数时,比如:

int result = add(3, 5);

背后会发生“绑定”:

编译器会在编译时或运行时,把 add() 这个名字替换成它在内存中的具体地址。

所以程序运行时就知道“去哪儿找这个函数”。

❗问题来了:RPC 中的函数不在本地,是在远程的另一台机器上!

RPC 函数调用时,需要发消息到远程服务器,但该发到哪个端口呢?

你总得知道“对方在哪儿”,才能把请求发出去 —— 这就是 RPC 中需要解决的“绑定问题”。

❓为什么麻烦?

因为客户端和服务器:

  • 不共享内存;

  • 不知道对方具体位置;

  • 网络连接的“地址”和“端口”得事先知道,或者在运行时动态获得。

 两种绑定方式:静态绑定 vs 动态绑定

方式 1:静态绑定(Static Binding)

💡 也叫编译时绑定

  • 客户端和服务器事先约定好端口号。

  • 比如:Web 服务器使用端口 80,FTP 使用 21,Telnet 使用 23。

  • 在写程序的时候就已经写死了这个端口号。

send("192.168.1.5", port=8080, message)

一旦编译好程序,服务端口就不能改了。

✅ 优点:

  • 实现简单;

  • 没有额外的“查找开销”。

❌ 缺点:

  • 不灵活:端口固定;

  • 如果服务迁移到其他端口或机器,客户端就无法找到;

  • 多个服务可能冲突。

 方式 2:动态绑定(Dynamic Binding)

 使用“协调者”(Rendezvous / Matchmaker)

  • 操作系统运行一个叫 rendezvous 守护进程(协调者)的服务,监听在一个固定的端口。

  • 客户端并不直接访问目标服务,而是:

调用流程:

  1. 客户端向 rendezvous 守护进程 发请求,说明自己需要调用哪个远程过程(函数);

  2. 守护进程查找这个服务是否已经运行,并返回其端口号;

  3. 客户端随后用这个端口号继续发起实际的 RPC 调用;

  4. 一旦连接建立,客户端可以一直用这个端口,直到:

    • 调用结束;

    • 连接断开;

    • 服务崩溃。

✅ 优点:

  • 灵活:服务端口可以动态分配;

  • 可扩展:多个服务共享基础设施;

  • 更适合分布式和微服务系统。

❌ 缺点:

  • 实现更复杂;

  • 有轻微性能开销(多一次请求);

  • 依赖 rendezvous 守护进程的可靠性。

假设你打电话找“张三”:

静态绑定

你手上已经有张三的电话号码(写在程序里了),直接拨过去

动态绑定 你先打给“114 电话查询台”(rendezvous),问张三的号码,然后再打过去

RPC的执行全过程(Execution of a Remote Procedure Call)

当你写下一个 RPC 函数调用,比如:

result = remote_add(3, 5);

背后其实系统自动帮你完成了非常多的事情。我们接下来一步一步拆解这个过程。

操作系统:远程过程调用( Remote Procedure Call,RPC)-CSDN博客

一、客户端发起调用

  1. 用户程序调用函数

    • 你调用的是一个看似本地的函数 remote_add(),实际你调用的是一个 Stub 函数。

    • Stub 会准备 RPC 请求消息,其中包括:

      • 要调用的函数名(比如 remote_addX);

      • 参数;

      • 唯一编号;

      • 此时还不知道目标端口 P(目标服务端口)!

  2. 内核把请求转发给“协调者”

    • 协调者又称 matchmaker,在一个固定端口上监听所有“服务查找”请求。

    • 于是:

      • 内核将该 RPC 消息发送到 matchmaker 进程;

      • matchmaker 的作用是查找哪个服务提供了函数 X,并返回它的真实端口号 P

消息往返流程如下:

发出 接收方 内容
客户端 Matchmaker 请求端口号:请问函数 X 对应的服务端口是多少?
Matchmaker 客户端 回复端口号 P:该服务正在端口 P 上监听

 

二、客户端收到端口号,重新发送调用

  1. 客户端拿到目标端口 P 之后:

    • 把 RPC 消息(调用函数 X + 参数)重新发往端口 P。

    • 这时,消息就送到了 提供服务的远程服务器 上。

  2. 服务器收到消息后:

    • 服务器上的 Stub 解包消息;

    • 执行目标函数 X(3, 5)

    • 获取结果,比如 8

    • 把结果打包发回客户端。

最终结果返回:

发出 接收方 内容
服务器 Stub 客户端 Stub 返回值(比如结果 8)

客户端 stub 收到响应后,把结果交还给原始程序,你的变量就得到了返回值:

result = 8; // 完成
  • 使用这个端口 P 发送调用请求;

  • 服务器收到请求,执行函数,生成输出;

  • 输出结果返回客户端,整个调用结束。

全流程图示

客户端程序(调用)
     │
     ▼
客户端 stub 打包参数
     │
     ▼
查找端口号:
    └─> Matchmaker:请求函数 X 的端口?
    ◄── 回复:端口号 P

     │
     ▼
发送调用消息到端口 P
     │
     ▼
服务器 stub 解包 + 执行函数
     │
     ▼
打包结果,发回客户端
     │
     ▼
客户端 stub 解包,返回结果

网站公告

今日签到

点亮在社区的每一天
去签到