前言
記錄一次 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
返回的方法順序并不固定,就會導致方法參數偶爾被信任,偶爾不被信任,所以會出現服務重啟后可能又恢復正常的錯覺。