跳到主要内容

RPC、序列化与 Netty 事件循环

RPC 把一次远程调用包装成接近本地方法的接口,但请求仍要经过序列化、连接、网络传输、服务端排队和业务执行。理解完整调用链,才能给超时、线程和协议边界分配责任。

1. 一次 RPC 调用经历多个阶段

客户端代理通常完成以下工作:

  1. 根据服务名选择目标实例。
  2. 把方法、参数、元数据和请求标识编码成消息。
  3. 从连接池取得连接并发送数据。
  4. 服务端解码消息,分派到业务处理器。
  5. 业务执行完成后编码响应并返回。
  6. 客户端把响应关联到原调用,完成 Future 或回调。

任何阶段都可能排队。只统计业务方法耗时,会遗漏客户端连接池、EventLoop 任务队列、业务线程池和响应写回的等待。

2. 序列化协议要支持演进

序列化决定对象如何变成网络字节。设计协议时需要明确字段编号或名称、数值范围、缺省值、未知字段处理和版本兼容规则。

兼容演进通常遵循以下约束:

  • 新增可选字段时,旧接收方能够忽略它。
  • 删除字段后不立即复用原字段编号。
  • 不随意改变字段类型或语义。
  • 枚举遇到未知值时有明确处理方式。
  • 限制消息大小、嵌套深度和集合长度。

Java 原生序列化把类实现细节直接带入协议,跨语言和版本演进都较困难,也扩大了反序列化攻击面。服务间协议通常使用 Protobuf 等显式 schema,或者经过约束的 JSON。

3. 字节流需要自己的消息边界

TCP 只提供有序字节流,一次 write 不对应接收方的一次 read。一条消息可能被拆成多段,也可能和后一条消息一起到达,这就是常说的半包和粘包。

应用协议必须自己确定边界,常见方式包括:

  • 固定长度。
  • 分隔符,并处理转义和最大长度。
  • 消息头携带正文长度。
  • 协议自身提供可解析的帧结构。

Netty 的 LengthFieldBasedFrameDecoder 可以根据长度字段拆帧,但仍要设置最大帧长度,防止异常长度占满内存。

4. EventLoop 顺序处理一个 Channel 的事件

Netty 的服务端通常由接收连接的 EventLoopGroup 和处理已建立连接的 EventLoopGroup 组成。一个 Channel 注册到某个 EventLoop 后,它的 I/O 事件和提交到该 EventLoop 的任务会按顺序执行。

这个模型让 Channel 内的状态修改更容易保持顺序,也让少量线程可以管理大量连接。代价是:一个处理器在 EventLoop 中执行阻塞 I/O 或长时间计算,会延迟同一 EventLoop 管理的其他 Channel。

数据库访问、远程调用和耗时计算应转交到有界业务线程池。线程池满时要有拒绝、降级或背压策略,不能用无限队列把延迟隐藏起来。

5. ChannelPipeline 分离协议处理步骤

入站数据沿 ChannelPipeline 中的 inbound handler 传播,出站操作则沿 outbound handler 传播。常见管线可以依次包含:

  1. TLS 处理。
  2. 帧解码。
  3. 消息反序列化。
  4. 鉴权和限流。
  5. RPC 请求分派。
  6. 响应序列化和帧编码。

每个 handler 只处理一个清晰阶段,有利于测试和复用。handler 是否可被多个 Channel 共享,要根据它有没有可变成员状态判断。

6. ByteBuf 的引用计数需要明确所有权

Netty 的 ByteBuf 使用引用计数管理池化或直接内存。最后一个消费消息的 handler 负责释放它;如果继续向后传播,就把处理责任交给下一个 handler,不能提前 release。

常见错误包括重复释放、异常路径漏释放,以及异步任务仍在使用已经释放的 ByteBuf。跨线程或延迟使用时,应先确认是否需要 retain(),并保证最终有对应的 release()

7. deadline 应从调用方贯穿服务端

调用方应从发起调用时开始计算总 deadline,并把剩余时间传给代理和下游。服务端发现 deadline 已经过期或客户端已经取消后,应停止不再有价值的排队和子任务。

如果每一层都重新给出固定的 3 秒超时,三级调用链可能远超最外层预算。若网关和 RPC 客户端同时自动重试,一次请求还可能展开成多次下游调用。

重试只适合幂等调用,并应使用退避、抖动和尝试上限。服务端已经产生外部副作用时,客户端超时不代表操作没有发生。

8. 背压保护接收端资源

当服务端处理速度低于接收速度时,应限制未完成请求数、业务线程池队列和单连接可写缓冲。Netty 可以根据 Channel 的可写状态暂停生产,必要时关闭自动读取,再在容量恢复后继续读取。

背压只能把压力推回上游,不能凭空产生吞吐。最终还需要超时、限流、拒绝和负载均衡共同决定哪些请求继续执行。

9. 常见问题

9.1 Netty 为什么不为每个连接创建一个线程

大量连接大部分时间都在等待网络。EventLoop 用事件通知处理就绪 I/O,减少线程数量和上下文切换。它适合短小、非阻塞的 handler;耗时业务仍需转交其他执行资源。

9.2 RPC 看起来成功,为什么业务可能执行两次

服务端可能已经完成写入,但响应在网络中丢失,客户端随后超时重试。RPC 框架只能知道响应是否收到,不能自动撤销已经发生的外部副作用。写操作需要业务幂等键、去重记录或可重复执行的状态转换。

10. 面试题

10.1 Netty 如何处理连接、粘包和耗时业务

出现公司:美团、字节跳动、阿里巴巴、Zoom

考察重点

  • EventLoop 与 Channel 的线程关系。
  • TCP 字节流和应用消息边界。
  • ByteBuf 所有权、业务线程池与背压。

相关内容:第 3 节“字节流需要自己的消息边界”至第 8 节“背压保护接收端资源”。

参考回答

Netty 把 Channel 注册到 EventLoop,一个 EventLoop 可以管理多个连接,并顺序执行各 Channel 的 I/O 事件。handler 不能在 EventLoop 中执行数据库调用或长时间计算,否则会连带阻塞该线程上的其他连接,耗时业务应进入有界业务线程池。

TCP 没有消息边界,所以协议要使用长度字段、分隔符或固定长度拆帧;Netty 可以用 LengthFieldBasedFrameDecoder 处理长度帧并限制最大消息。ByteBuf 采用引用计数,最后的消费者负责释放,跨异步边界时要明确 retain 和 release。处理速度不足时还要限制队列与未完成请求,通过背压、限流和超时保护系统。