REST、RPC、gRPC 与 OpenFeign:远程调用为什么永远不是本地方法
比较资源契约、IDL、序列化、连接复用、流式 RPC、客户端代理和失败语义,揭示远程调用抽象隐藏的成本。
gRPC 官方 Core Concepts 明确客户端与服务端可对同一次 RPC 得出不同完成结论,deadline 或取消也不会回滚已经发生的修改;OpenFeign 集成边界见 Spring Cloud OpenFeign。
协议选择从契约和交互形态开始
REST 适合围绕资源、HTTP 方法、状态码、缓存与中间设施建立公开或跨团队契约;传统 RPC 强调操作接口;gRPC 以 IDL 生成强类型 stub,并支持 unary、客户端流、服务端流和双向流。选择不能只比较 JSON 与 Protobuf 字节大小,还要考虑浏览器与代理支持、调试、版本演进、流控和多语言工具链。
字段兼容遵循各自 schema 规则,但语法兼容不保证业务语义兼容。金额单位、时区、空值和枚举含义必须写入契约测试。gRPC 字段号一旦使用不能重新分配给其他语义,删除字段应保留编号;REST 字段重命名也需要双读/双写窗口。
Feign 接口不是进程内接口
OpenFeign 根据注解和方法签名构建请求,经过编码器、契约、拦截器、服务发现、负载均衡、HTTP 客户端和解码器。返回类型看似普通 Java 对象,调用却会消耗连接、线程或事件循环、序列化内存与端到端预算。把 Feign client 注入后在循环里调用,会悄悄形成 N+1 远程调用。
每个 client 需要独立的连接池、超时、日志脱敏、错误解码和重试策略。fallback 不能把权限失败、参数错误和依赖超时都返回空对象。代理生成也不替代契约版本管理;服务端删除字段或改变状态码时,客户端仍会在运行时失败。
Deadline 和取消只控制等待责任
客户端在连接、写请求、服务端排队和执行之间消耗同一 deadline。下游应收到剩余预算而不是重新获得完整超时。客户端超时后,服务端可能已经提交数据库;取消信号也可能到达太晚。写操作重试需要幂等键或最终状态查询,不能根据异常类型猜测未执行。
gRPC stream 在单次 RPC 内保持各方向消息顺序,但双向流的读写彼此独立。流式调用必须处理背压、半关闭、慢消费者、最大消息尺寸和连接中断后的恢复位置。长 channel 复用提高效率,也会在连接故障时同时影响大量 RPC,因此要观察连接状态而不是每次都新建 channel。
把同步调用画成预算递减的状态机
微服务调用从入口排队开始,经路由、服务发现、负载选择、连接池、网络、服务端排队、业务事务和响应回程。每一跳都消耗同一个业务 Deadline,也都可能留下“服务端已提交、客户端却超时”的未知结果。代理、注解和自动配置只能插入这些环节,不能把远程调用恢复成本地方法语义。
设计时为每条边记录 owner、协议、连接模型、超时阶段、重试责任、幂等键、流量上限、降级语义和回滚方式。同步扇出越大,成功率乘积越低,尾延迟取最大值;能异步解耦的非关键副作用不要停留在主请求链。
失败控制必须按依赖隔离
注册中心不可用、无实例、连接池满、连接失败、读取超时、业务拒绝和服务端错误要分型。只有动作未开始或有幂等保障的暂态失败允许重试;结果未知先查询或使用稳定幂等键。限流、隔离和熔断的拒绝应成为可观测结果,不能统一包装成空数据。
错误契约必须保留可重试判断所需信息
HTTP 状态、gRPC status 和业务错误码分别表达协议与业务结果。服务端不能把所有异常变成 200 + success=false,也不能把所有业务拒绝变成 500;否则负载均衡、熔断与客户端无法区分永久失败和暂态失败。错误体应包含稳定 code、可安全展示的 message、correlation id,以及受控的 retryAfter 或当前状态查询入口。
客户端解码失败也可能发生在服务端成功之后,例如返回 schema 不兼容或响应过大。它不是安全重试证据。跨语言契约测试要覆盖未知字段、未知枚举、空值、最大尺寸、压缩和错误详情,并验证旧客户端不会因为新服务端增加可选信息而崩溃。
用两个 Java 状态模型固定决策边界
这两个 Java 17 模型不启动真实注册中心、Gateway 或 RPC Server,而是把本篇的状态、预算和不变量转成确定输出。真实集成测试继续验证客户端协议、自动配置、连接复用、推送延迟与故障时序。
javac --release 17 -Xlint:all -Werror examples/backend-development/microservice/rpc-rest-grpc-feign/RemoteCallOutcomeDemo.java examples/backend-development/microservice/rpc-rest-grpc-feign/GrpcDeadlineDemo.java
java -cp examples/backend-development/microservice/rpc-rest-grpc-feign RemoteCallOutcomeDemo
java -cp examples/backend-development/microservice/rpc-rest-grpc-feign GrpcDeadlineDemorequestWritten=true serverCommitted=true clientTimedOut=true outcome=UNKNOWN retryNeedsIdempotency=true
deadlineMillis=120 remainingAfterQueue=80 serverBudget=70 cancellationRollsBack=false状态模型输出变化时,要解释对应的服务边界、Deadline、版本或治理不变量为什么改变。集成测试必须主动制造连接已写出后超时、实例列表陈旧、配置刷新一半、半开探测和灰度标签断链。
契约、安全与配置共同决定可运营性
动态配置和发现元数据都可能陈旧。变更必须带版本、owner、范围、过期时间和回滚值,先灰度到少量实例并比较版本分布。会影响线程池、连接池、路由、限流和鉴权的配置要通过不变量校验,失败时保留上一完整快照。
容量与可观测证据要覆盖控制面和数据面
上线证据包括新旧契约互调、实例排空、Deadline 传播、幂等重试、治理状态机、灰度指标和回滚后的残留任务。完成标准不是“组件控制台显示健康”,而是业务不变量保持、未知结果可查询、故障被限制在预算内并最终收敛。
从开发环境到生产流量要跨过四层验证
依赖拓扑必须有复杂度上限
关键路径优先使用粗粒度接口、批量查询和本地读模型降低扇出。非关键通知、审计和派生索引使用事件异步处理,但异步不等于没有契约:仍需定义积压、幂等、顺序与最终状态。任何新增同步边都要说明为什么不能在本服务完成、为什么不能异步、Deadline 从哪里获得,以及下游不可用时用户会看到什么。
