RPC
RPC 把远程请求封装成方法调用接口。本文负责调用链、序列化和 Netty 4.2 的执行模型。
1. 面试核心
先记住这几条,再读正文- 请求与响应关联。 RPC 用请求标识将远端响应关联到本地 Future 或回调。
- 字段兼容。 Protobuf 字段编号用于识别字段,删除后应通过 reserved 保留编号。卡 · 序列化协议
- 消息拆帧。 LengthFieldBasedFrameDecoder 根据长度字段拆分消息帧。卡 · 半包与粘包
- 事件循环。 NIO EventLoop 可管理多个 Channel,阻塞处理会拖延其他连接。
- 缓冲区生命周期。 ByteBuf 通过 retain 与 release 管理引用生命周期。卡 · ByteBuf 引用计数
2. RPC 调用链
一次调用包含以下阶段:
- 客户端代理根据服务名选择实例。
- 编码方法、参数、请求 ID 和元数据。
- 取得连接并发送消息。
- 服务端解码、鉴权并分派业务。
- 服务端编码并返回执行结果。
- 客户端按请求 ID 完成原调用。
多个请求可共享一条连接。响应完成顺序由服务端执行决定。客户端用请求 ID 查找等待者,而非按接收顺序配对。
超时和取消时要移除等待记录。迟到响应按已结束调用处理,避免泄漏。只统计业务方法耗时会漏掉队列和网络等待。
3. 序列化协议
序列化将对象转换成协议字节。协议需要约定字段类型、缺省值和大小限制。接收方还要限制嵌套深度与集合长度。
Protobuf 的二进制字段通过编号识别。proto3 可保留未知字段,再序列化时继续输出。转成 JSON 或逐字段复制时,未知字段会丢失。
删除字段后应将编号列入 reserved。复用编号会让旧数据被解释成新含义。即使 wire type 相同,业务语义也要保持兼容。
下例使用 Protobuf 3.15+,其 proto3 optional 已默认启用。
syntax = "proto3";
message Query {
reserved 2;
string request_id = 1;
optional int32 limit = 3;
}
这里编号 2 已退役。optional 允许区分 limit 未提供与显式为 0。协议演进还应验证旧客户端与新服务端的组合。
4. 半包与粘包
TCP 读取可得到半条消息,也可得到多条消息。应用协议需要定义边界。
| 方案 | 处理条件 |
|---|---|
| 固定长度 | 每条消息占固定字节数。 |
| 分隔符 | 需要处理转义并限制长度。 |
| 长度字段 | 先解析头部,再累计所需消息体。 |
Netty 的 LengthFieldBasedFrameDecoder 支持长度字段。配置需明确字段偏移、宽度和长度调整。maxFrameLength 限制最大帧,避免异常长度耗尽内存。
5. EventLoop
以 Netty 4.2 的 NIO 服务端为例。接收组处理监听 Channel,工作组处理已建立连接。一个 Channel 注册到一个 EventLoop。一个 EventLoop 可服务多个 Channel。
同一 EventLoop 上的 I/O 事件和任务按序执行。handler 若阻塞,会拖延该线程上的其他 Channel。慢数据库调用应转交有界业务线程池。
业务结果跨线程返回时,还需维护请求顺序和共享状态。Channel 的有序 I/O 不会自动串行所有业务任务。
6. ChannelPipeline
入站事件按添加顺序经过 inbound handler。出站操作沿相反方向经过 outbound handler。处理链可以包含 TLS、拆帧、解码、鉴权和业务分派。
channel.write 从管线尾端发起出站传播。ctx.write 从当前上下文向前寻找出站处理器。选择错误入口会跳过预期的编码器。
handler 的共享需要满足线程安全要求。@Sharable 表示实例可安全复用,注解本身不会为状态加锁。
7. ByteBuf 引用计数
ByteBuf 使用引用计数管理生命周期。最后的使用者 release 后,缓冲区可被回收。向后传播消息时,应把释放责任交给下一个处理器。
SimpleChannelInboundHandler 默认自动释放接收消息。把 ByteBuf 交给异步任务时,需要先 retain。异步任务结束后在 finally 中 release。任务提交失败的路径也要释放新增引用。
slice 和 duplicate 共享底层数据,且不增加引用计数。跨异步边界需要独立引用时,可用 retainedSlice 或 retainedDuplicate。保留引用只延长生命周期,并未复制数据。
8. deadline 传递
RPC 应传递剩余调用预算,避免每层重新计时。gRPC 会将 deadline 转换为剩余超时再传递。服务端处理器需响应取消,停止过期任务。
取消请求后,已经提交的外部业务仍可生效。结果不明的写操作需要幂等键和结果查询。具体重试约束见 HTTP。
9. 背压
服务端应限制在途请求、业务队列和待发送字节。Netty 写缓冲超过高水位后,isWritable 返回 false。降到低水位以下后,它重新返回 true。
水位是反馈信号,应用仍可继续调用 write。处理器要主动暂停生产,并监听可写状态变化。入站压力过高时,可临时关闭 autoRead。容量恢复后再开启读取。
背压延缓上游发送,容量不足还需限流或拒绝。无限队列只会积累内存和等待时间。
10. 常见问题
10.1 连接与线程数量
大量连接处于等待状态。EventLoop 用就绪事件驱动 I/O,减少所需线程。短小 handler 适合事件循环,耗时业务使用独立执行资源。
10.2 RPC 重试与业务副作用
服务端提交后响应丢失,客户端会看到超时。重试会再次发起同一业务。稳定幂等键和持久化结果用于约束重复效果。
11 · 面试题卡片
11.1 Netty 如何处理连接、粘包和耗时业务
出现公司:美团、字节跳动、阿里巴巴、Zoom
考察重点
- EventLoop 与 Channel 的线程关系。
- TCP 字节流和应用消息边界。
- ByteBuf 所有权、业务线程池与背压。
参考回答
Netty 将 Channel 注册到 EventLoop,用少量线程处理多个连接。
长度字段解码器负责消息拆帧。耗时业务转交有界线程池,防止阻塞事件循环。
ByteBuf 的异步使用要配对 retain 和 release。压力过高时限制队列和在途请求。写缓冲水位通知应用暂停生产,应用需要主动响应。
11.2 Protobuf 删除字段后为什么要保留编号?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:字段编号、历史数据与缺省值。
相关内容:序列化协议。
参考回答
编号决定二进制字段的身份。
复用旧编号会把历史数据解释成新字段。应使用 reserved 保留退役编号。需要区分未填写和默认值时,可使用 proto3 optional。
11.3 把 ByteBuf 交给异步线程时,怎样避免提前释放?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:自动释放、引用计数与共享内存。
相关内容:ByteBuf 引用计数。
参考回答
在当前引用释放前,为异步任务 retain 一次。
任务结束后在 finally 中 release。提交失败时也要归还这次引用。普通 slice 共享内存且不增加计数。retainedSlice 可保留引用,但数据仍然共享。