RPC函数
RPC 想解决的问题很简单:让调用远程服务像调用本地函数一样容易。这个想法听起来很美好,但它建立在一个微妙的谎言之上。
💡 Click the maximize icon to view in fullscreen
距离依然存在
当你写下 result = getUserInfo(userId) 这样的代码,你可能没有意识到这个函数调用穿越了多少层网络。RPC 框架帮你隐藏了所有细节:参数的序列化、网络传输、服务端的查找、结果的返回。
但隐藏不等于消失。网络依然会延迟,服务端依然会宕机,数据包依然会丢失。RPC 只是让你在写代码时可以暂时忘记这些,但在运行时,这些问题全都会回来。
这就是 RPC 的第一个真相:它降低了心智负担,但不能消除真实的复杂度。
同步的幻觉
传统的函数调用是同步的。你调用一个函数,程序停下来等它返回,然后继续执行。这种确定性让代码容易理解。
RPC 尝试保持这种同步的感觉。当你调用远程函数,线程会阻塞等待结果,就像调用本地函数一样。但这个等待和本地函数的等待完全不同。
本地函数可能执行几微秒,RPC 调用可能需要几十甚至几百毫秒。在这个等待过程中,服务端可能重启了,网络可能断了,或者只是单纯地慢。你的线程在那里空等,什么也做不了。
正因为同步等待的代价太高,现代 RPC 框架都支持异步调用。你可以发起请求后继续做其他事,等结果准备好再处理。
但异步意味着你要管理更多状态:请求可能乱序返回,你需要处理超时,需要考虑取消。代码变得更复杂,但这才是面对分布式系统的正确姿态。
gRPC 的流式调用、Promise/Future 模式、async/await 语法,都是在试图让异步变得更容易管理。
失败是常态
本地函数调用很少失败。如果函数存在,参数类型正确,它大概率会返回结果或抛出业务异常。你不需要担心函数突然消失,或者执行到一半失去响应。
但 RPC 调用会失败,而且会经常失败。网络不可靠是分布式系统的基本事实。你需要处理超时、重试、降级。每次调用都可能失败,而且可能以各种意想不到的方式失败。
这意味着你不能像对待本地函数那样对待 RPC。你需要时刻准备着处理失败,需要设计降级方案,需要思考重试会不会导致副作用。
RPC 让调用变简单了,但让错误处理变复杂了。
接口即契约
本地函数可以随时修改签名。改个参数名、加个默认值、调整返回类型,只要调用方同步更新就行。
但 RPC 的接口是契约。服务端和客户端可能由不同团队维护,可能部署在不同时间,可能运行着不同版本的代码。一旦接口发布,就很难改变。
你需要考虑向后兼容:新版本能不能兼容旧客户端?你需要版本控制:如何让新老接口共存?你需要灰度发布:如何确保变更不会影响线上服务?
这就是为什么 Protocol Buffers、Thrift 这些 IDL (Interface Definition Language) 会存在。它们强制你显式定义接口,强制你思考兼容性,强制你认真对待每一个字段的增删。
选择决定架构
当你决定用 RPC,你就在做架构选择。你在说:这两个服务之间的关系足够紧密,值得用同步调用。
但紧密也意味着耦合。A 服务直接调用 B 服务,B 的性能和可用性会直接影响 A。如果 B 变慢, A 也会变慢。如果 B 挂了, A 可能也没法正常工作。
这就是为什么很多系统后来会改用消息队列。消息队列让服务之间解耦,让失败可以优雅降级,让流量可以削峰填谷。但代价是丧失了 RPC 的简单和直观。
没有完美的选择,只有适合的选择。RPC 适合那些需要实时响应、逻辑紧密、延迟敏感的场景。对于可以异步处理、能容忍延迟、需要解耦的场景,消息队列可能更好。
REST API 也是远程调用,但它和 RPC 有本质区别。
REST 是资源导向的。你操作的是资源(用户、订单、文章),动作是标准的 HTTP 方法(GET、POST、PUT、DELETE)。REST 强调无状态、可缓存、统一接口。
RPC 是行为导向的。你调用的是函数(getUserInfo、createOrder、updateProfile),可以有任意复杂的输入输出。RPC 更灵活,但也更随意。
如果你在设计公开 API,REST 更合适。它有更好的兼容性、更容易被理解、更容易缓存。
如果你在设计内部服务,RPC 更高效。它可以用二进制协议减少序列化开销,可以用代码生成简化开发,可以用流式调用支持复杂交互。
但核心问题不在于选 REST 还是 RPC,而在于你是否理解分布式调用的本质。
性能的代价
RPC 框架会宣传自己有多快:高效的序列化、优化的传输、智能的负载均衡。但再快的 RPC 也无法改变物理定律。
网络延迟通常是毫秒级的。这意味着一次 RPC 调用比本地函数调用慢几千倍甚至几万倍。如果你在循环里调用 RPC,性能会灾难性地下降。
这就是为什么批量接口这么重要。与其调用一百次 getUser(id),不如调用一次 getUsers([id1, id2, ...])。与其在每个元素上调用远程函数,不如把整个数据传过去一次处理。
但批量意味着复杂:你需要处理部分失败、需要拆分大请求、需要平衡批次大小。简单的代码又变复杂了。
抽象的意义
RPC 不是完美的抽象。它隐藏了一些东西,但让另一些东西变得更难处理。
但这不意味着 RPC 是个坏主意。恰恰相反,RPC 是分布式系统中最常用的通信方式之一。因为它在复杂度和易用性之间找到了一个平衡点。
关键在于知道你在用什么。当你写 RPC 调用时,要记得这不是普通的函数调用。当你设计 RPC 接口时,要记得这是一个长期契约。当你调试 RPC 问题时,要记得网络和服务端都可能出错。
理解 RPC 的局限,才能更好地使用 RPC。
最后
远程和本地从来就不一样。RPC 让它们看起来一样,但本质的差异不会因为抽象而消失。
你可以用 RPC 写出优雅的代码,但前提是你理解这层抽象下隐藏的复杂性。你需要处理失败、管理超时、设计降级、考虑性能。
这些都是分布式系统必须面对的问题。RPC 只是让你可以先专注于业务逻辑,然后再回来处理这些问题。它不是魔法,只是一个工具。
理解工具的边界,才知道何时使用它,何时寻找其他方案。
Related Posts
Articles you might also find interesting
RPC函数的原子化处理
当一个远程函数做太多事情,失败就变得难以理解
监听 Redis 连接事件 - 让不可见的脆弱变得可见
连接看起来应该是透明的。但当它断开时,你才意识到透明不等于可靠。监听不是多余,而是对脆弱性的承认。
资源不会消失,只会泄露
在积分系统中,用户的钱不会凭空消失,但会因为两个时间窗口而泄露:并发请求之间的竞争,和回调永不到达的沉默。
幂等性检查
在不确定的系统中,幂等性检查是对重复的容忍,是对稳定性的追求,也是对失败的预期与接纳
管理后台需要两次设计
第一次设计回答"发生了什么",第二次设计回答"我能做什么"。在第一次就试图解决所有问题,结果是功能很多但都不够深入。
告警分级与响应时间
不是所有问题都需要立即响应。RPC失败会在凌晨3点叫醒人。安全事件每15分钟检查一次。支付成功只记录,不告警。系统的响应时间应该匹配问题的紧急程度。
文档标准是成本计算的前提
API文档不只是写给开发者看的。它定义了系统的边界、成本结构和可维护性。统一的文档标准让隐性成本变得可见。
BullMQ 队列
队列不是技术选型,而是对时间的承认,对顺序的尊重,对不确定性的应对
BullMQ Worker
Worker 的本质是对时间的重新分配,是对主线的解放,也是对专注的追求
配置不会自动同步
视频生成任务永远pending,代码完美部署,队列正确配置。问题不在代码,在于配置的独立性被低估。静默失败比错误更危险。
CRUD 操作
四个字母背后,是数据的生命周期,是权限的边界,也是系统设计的基础逻辑
数据库参数国际化:从 13 个迁移学到的设计原则
数据不该懂语言。当数据库参数嵌入中文标签时,系统的边界就被语言限制了。这篇文章从 13 个参数对齐迁移中提炼出设计原则——国际化不是功能,是系统设计的底层约束。
Stripe Webhook中的防御性编程
三个Bug揭示的真相:假设是代码中最危险的东西。API返回类型、环境配置、变量作用域——每个看似合理的假设都可能导致客户损失。
双重验证:Stripe生产模式的防御性切换
从测试到生产不是更换API keys,而是建立一套双重验证系统。每一步都在两个环境中验证,确保真实支付不会因假设而失败。
端到端 Postback 模拟测试
真实的测试不是模拟完美的流程,而是重现真实世界的混乱。Postback 测试的价值在于发现系统在不确定性中的表现。
错误隔离
失败是必然的。真正的问题不是失败本身,而是失败如何蔓延。错误隔离不是为了消除失败,而是为了控制失败的范围。
在运行的系统上生长新功能
扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。
实现幂等性处理,忽略已处理的任务
在代码层面识别和忽略已处理的任务,不是简单的布尔检查,而是对时序、并发和状态的深刻理解
单例模式管理 Redis 连接
连接不是技术细节,而是系统与外部世界的第一次握手,是可靠性的起点
缺失值的级联效应
一个NULL值如何在调用链中传播,最终导致错误的错误消息。理解防御层的设计,在失败传播前拦截。
监控观察期法
部署不是结束,而是验证的开始。修复代码只是假设,监控数据才是证明。48小时观察期:让错误主动暴露,让数据证明修复。
Props Drilling
数据在组件树中层层传递,每一层都不需要它,却必须经手。这不是技术债,是架构对真实需求的误解。
队列生产者实例的工厂函数
工厂函数不是设计模式的炫技,而是对重复的拒绝,对集中管理的追求,对变化的准备
队列、可靠性与系统边界
探讨消息队列系统如何通过时间换空间,用异步换解耦,以及可靠性背后的权衡
使用Secret Token验证回调请求的合法性
在开放的网络中,信任不能被假设。Secret Token 是对身份的确认,对伪装的识别,也是对安全边界的坚守