前言
记录一次 Dubbo 线上故障排查和原因分析。
线上 Dubbo 消费者启动有错误日志如下,但是不影响服务启动。
java.lang.TypeNotPresentException: Type org.example.model.ThirdParam not present
...
Caused by: java.lang.ClassNotFoundException: org.example.model.ThirdParam
...
紧接着,消费者发起 RPC 调用,偶发性报错如下:
Caused by: org.apache.dubbo.remoting.RemotingException:
Failed to send message Request [id=-7672337589162309142, version=2.0.2, twoWay=true, event=false, broken=false, mPayload=0, data=null] to /192.168.98.92:20880,
cause: org.apache.dubbo.common.serialize.SerializationException:
java.lang.IllegalArgumentException: [Serialization Security]
Serialized class org.example.api.InnerParam is not in allow list.
Current mode is `STRICT`, will disallow to deserialize it by default.
Please add it into security/serialize.allowlist or follow FAQ to configure it.
消费者尝试重启,RPC 调用偶尔成功,偶尔失败,没有规律。
故障重现
为了重现故障,我写了个示例工程,有四个模块:
- third-sdk
模拟依赖的三方 SDK,有两个版本:V1.0 V2.0,区别是 ThirdParam 类只在 V2.0 才提供。
- dubbo-api
Dubbo API 模块,包含服务接口和参数类,依赖third-sdk V2.0
。
- dubbo-provider
Dubbo 服务提供者,直接依赖dubbo-api
,间接依赖third-sdk V2.0
。
- dubbo-consumer
Dubbo 服务消费者,直接依赖dubbo-api
,直接依赖third-sdk V1.0
。
重点:dubbo-consumer 模块自身依赖了低版本的**<font style="color:#DF2A3F;">third-sdk</font>**
,ThirdParam 类是不存在的。
dubbo-api
模块,IService 接口如下。InnerParam 类存在于当前模块,ThirdParam 来自三方库。
由于dubbo-consumer
模块依赖的是低版本的third-sdk
,所以 ThirdParam 类不存在,但只要不调用 M2 就没事。
public interface IService {String M1(InnerParam innerParam);String M2(Optional<ThirdParam> optional);
}
ServiceImpl.java
和服务提供者的启动和消费者的调用均不是重点,这里不贴代码。
说明:为啥 M2 参数类型是Optional<ThirdParam>
?
因为如果参数类型直接是 ThirdParam,消费者启动时,解析 Service Class 这一步就会因为 Class Not Found 直接报错而退出进程。ThirdParam 必须是泛型,才不至于 Service Class 无法解析。
接着,启动 Provider,启动 Consumer,就能看到错误日志,但是不影响消费者启动。
再接着,Consumer 发起 RPC 调用,就会报错:
最快的修复方式,重构 IService.java ,方法名 M1 改为 a1
public interface IService {String a1(InnerParam innerParam);String M2(Optional<ThirdParam> optional);
}
接着,重启 Provider,Consumer。Consumer 启动依然有错误日志,但是不影响启动。
Consumer 发起 a1 的 RPC 调用,成功,不再报错。
call a1 start...
call a1 result: OK
为什么仅仅修改个方法名,RPC 调用就不再报错了呢?
故障分析
已知,Dubbo 从 3.1.6 版本开始,为了避免序列化引起的 RCE 攻击,引入了“序列化类检查机制”。只有在信任白名单里的类,才允许被序列化和反序列化。
同时,为了避免开发者手动添加白名单带来的额外负担,Dubbo 默认开启“自动信任机制”。即 Dubbo 会在 Service 暴露和引用的同时,自动信任 Service Class 依赖的相关类,这些类包括:Service Class 本身、父类和接口类型、属性类型、方法的所有入参/出参类型、异常类型等,将它们全部加入到白名单里。
根据消费者报错的信息来看,很明显提示org.example.api.InnerParam
类不在白名单里面,所以序列化失败。
cause: org.apache.dubbo.common.serialize.SerializationException:
java.lang.IllegalArgumentException: [Serialization Security]
Serialized class org.example.api.InnerParam is not in allow list.
由此我们推测,Dubbo 的“自动信任机制”出现了问题。
通过源码我们发现,Service 在暴露和引用的时候,默认会注册 Service Class,方法是SerializeSecurityConfigurator#registerInterface
。
注册接口就是将 Service Class 自身、以及超类、属性类、方法的入参/出参、返回类型、异常类型等通通加入到白名单。
public synchronized void registerInterface(Class<?> clazz) {/*** 是否自动信任序列化类?默认是true* 默认会将 Service Class 涉及到的类加入白名单,全部信任*/if (!autoTrustSerializeClass) {return;}Set<Type> markedClass = new HashSet<>();/*** 1. 信任 Service Class 自身* 2. 根据 TrustSerializeClassLevel 信任所在包的层级* 3. 信任 Service Class 的接口、父类、属性类型、*/checkClass(markedClass, clazz);addToAllow(clazz.getName());Method[] methodsToExport = clazz.getMethods();// 信任 Service Class 方法的入参、出参类型、抛出的异常类型for (Method method : methodsToExport) {Class<?>[] parameterTypes = method.getParameterTypes();for (Class<?> parameterType : parameterTypes) {checkClass(markedClass, parameterType);}Type[] genericParameterTypes = method.getGenericParameterTypes();for (Type genericParameterType : genericParameterTypes) {checkType(markedClass, genericParameterType);}Class<?> returnType = method.getReturnType();checkClass(markedClass, returnType);Type genericReturnType = method.getGenericReturnType();checkType(markedClass, genericReturnType);Class<?>[] exceptionTypes = method.getExceptionTypes();for (Class<?> exceptionType : exceptionTypes) {checkClass(markedClass, exceptionType);}Type[] genericExceptionTypes = method.getGenericExceptionTypes();for (Type genericExceptionType : genericExceptionTypes) {checkType(markedClass, genericExceptionType);}}
}
Dubbo 会遍历 Service Class 所有方法,依次注册方法的入参、出参到白名单。
问题就出在这个遍历上,因为dubbo-consumer
模块直接依赖了third-sdk V1.0
,对于方法IService#M2(Optional<ThirdParam>)
的入参,ThirdParam 类是不存在的,导致整个注册过程中断跳出,后续方法的参数都没有注册到白名单,进而导致 Consumer 发起 RPC 调用时,参数序列化报错。
另一个问题,为什么方法**IService#M1**
重构为**IService#a1**
,Consumer 就正常了呢?
这是因为方法签名修改后,导致Class#getMethods
返回的 Method 顺序发生了改变,如果a1
方法先于M2
方法返回,让中断发生在a1
方法注册之后,虽然整个注册过程还是会异常,但是org.example.api.InnerParam
类已经添加到白名单了,对后续的 RPC 调用当然没有影响。
注意:虽然示例中通过修改方法名来改变**Class#getMethods**
返回的 Method 顺序,但是强烈不建议这么做,因为 Java Doc 已经写的非常清楚了,返回的方法数组没有特定顺序,取决于JVM实现。
The elements in the returned array are not sorted and are not in any particular order.
推荐的修复方式,Service Class 所有的方法入参和出参,都不应该直接用三方 SDK 的类,这本身就不规范。在 API 模块新建 DTO 类,把三方类转换成自己的 DTO 类。
尾巴
因为消费者模块和公共 API 模块依赖的三方库版本不同,导致消费者模块缺少一部分类,进而导致消费者在注册 Service Class 方法参数到序列化白名单时,发生异常中断跳出,没有被信任的参数类,一旦序列化就会抛出异常。
又因为Class#getMethods
返回的方法顺序并不固定,就会导致方法参数偶尔被信任,偶尔不被信任,所以会出现服务重启后可能又恢复正常的错觉。