password
icon
AI summary
type
status
date
slug
summary
tags
category
写 RPC 框架的时候,为了追求极致性能,我果断抛弃了 JDK 自带的序列化,选了 Protostuff。大家都说它快、体积小,是 RPC 界的宠儿。
然而,高性能的背后往往藏着一些不起眼的“坑”。最近在调试客户端调用时,遇到一个极其诡异的超时问题。这篇文记录一下从一脸懵逼到真相大白的全过程,希望能帮大家避避雷。
一、 现场还原:换个姿势居然就行了?
事情是这样的,我的 RPC 客户端在调用服务端接口时,配置一切正常,ZooKeeper 也能连上,但就是死活拿不到结果,最后只能抛出
TimeoutException。代码长这样:
我也试过打断点,客户端请求确实发出去了。这就很僵硬了,到底是网络问题,还是服务端挂了?
为了排除法,我随手改了一个配置:把序列化方式从
protostuff 换回了最原始的 jdk。结果……竟然通了! 调用成功,丝般顺滑。
这一刻我大概心里有底了:网络没锅,ZK 没锅,代理逻辑也没锅。这绝对是 Protostuff 搞的事情。
二、 案情推理:服务端“沉默”的真相
既然换 JDK 序列化能通,说明业务逻辑没问题。那么问题大概率发生在这个环节:
- 客户端用 Protostuff 把
RpcRequest变成了字节数组,发出去了。
- 服务端收到了字节数组。
- 关键点来了: 服务端试图用 Protostuff 把这堆字节还原成
RpcRequest对象时,炸了。
但是,为什么客户端收到的是超时(Timeout)而不是报错?
因为反序列化通常发生在 IO 线程或者解码阶段,如果这里抛出了异常(比如空指针、类型转换错误)且没有被很好的捕获并封装成 RPC 响应返回给客户端,服务端处理线程就会直接中断。
这就导致服务端“沉默”了,既不回成功,也不回失败。客户端在那傻傻地等,直到耐心耗尽,抛出超时异常。
三、 深挖根源:Protostuff 的“泛型失忆症”
顺着这个思路,我去扒了一下 Protostuff 的文档和相关 Issue,发现这货对数组和集合的泛型处理由于性能优化的原因,其实是有缺陷的。
看看我的
RpcRequest 对象定义:这种“模糊类型”时,会患上“失忆症”:
- 序列化时: 它是看着具体的对象操作的(比如它知道这个
Object其实是个String),能写成字节。
- 反序列化时: 它对着一堆字节,只知道这就该是个
Object。但它不知道这堆字节原本是String还是Integer。元数据丢了,无法还原,直接报错。
而 JDK 序列化之所以慢,就是因为它把所有的类元信息都塞进去了,所以它没事。
四、 解决方案:给它套个壳(Wrapper 模式)
既然 Protostuff 记不住泛型里的具体类型,那我们就手动帮它记。最简单的办法就是Wrapper(包装)模式。
我们不直接序列化
RpcRequest,而是把它包在一个有明确泛型定义的类里面。1. 造个壳子 ProtostuffWrapper
弄个简单的泛型类,相当于给数据穿个马甲:
2. 改造序列化逻辑
在
ProtostuffSerialization 类里,稍微做个拦截。如果是 RpcRequest 或 RpcResponse,就先套上壳子再序列化;反序列化时,先解开壳子。五、 最后的翻车:由于没执行 mvn install 引发的惨案
代码改完,我觉得稳了,兴冲冲地重启服务端,运行客户端。
结果……还是超时?!
那一瞬间我都要怀疑人生了。逻辑天衣无缝,为什么不行?难道我对于 Protostuff 的理解全是错的?
排查了半天,最后发现了一个极其低级、但也是大家最容易犯的错误:
我只改了
serialization 模块的代码,但是没重新安装到本地仓库!服务端项目依赖的是本地 Maven 仓库里的 jar 包,而我刚才改的代码还在 IDEA 的源码里,根本没更新到 jar 包里。服务端跑的还是旧代码!
执行完这句,重启服务端,再次调用。控制台终于打印出了久违的 Result。搞定!
碎碎念
这次排坑虽然花了一下午,但教训挺深刻:
- 控制变量法永远的神: 遇到疑难杂症,先把复杂的组件换成简单的(比如 Protostuff 换 JDK),能瞬间定位问题域。
- 高性能的代价: Protostuff 确实快,但它对 Java 语言特性的支持不如 JDK 完整,用的时候得清楚它的脾气(比如怕
Object数组)。
- Maven 的锅: 只要涉及多模块开发,改了底层代码觉得没生效,先别急着改代码,先
clean install一下,能省一半的头发。