RPC、序列化与 Netty 事件循环
RPC 把一次远程调用包装成接近本地方法的接口,但请求仍要经过序列化、连接、网络传输、服务端排队和业务执行。理解完整调用链,才能给超时、线程和协议边界分配责任。
1. 一次 RPC 调用经历多个阶段
客户端代理通常完成以下工作:
- 根据服务名选择目标实例。
- 把方法、参数、元数据和请求标识编码成消息。
- 从连接池取得连接并发送数据。
- 服务端解码消息,分派到业务处理器。
- 业务执行完成后编码响应并返回。
- 客户端把响应关联到原调用,完成 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 传播。常见管线可以依次包含:
- TLS 处理。
- 帧解码。
- 消息反序列化。
- 鉴权和限流。
- RPC 请求分派。
- 响应序列化和帧编码。
每个 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。处理速度不足时还要限制队列与未完成请求,通过背压、限流和超时保护系统。