Kryo序列化在分布式處理情景下的問題
最近在dubbo的issue列表里看到了這樣一個issue撵渡,主要描述的是在某種情況下使用kryo作為序列化方式會報錯,這個issue下面,另外一位來自阿里的Committer也給了問題出現(xiàn)的原因撞蚕,這個問題還是非常有意思的戈毒,這里順便看一下kryo的序列化方式,復現(xiàn)一下這個問題嘿悬。
issue下面其實已經(jīng)貼出了一個demo可以用來復現(xiàn)上述的問題。但是在我的機器上一直不work水泉,我就沒有用他的demo了善涨。這里我準備一個更簡單的demo讓大家更清楚的了解這個問題,先看看我們的類:
Server:啟動的main函數(shù)代碼可以從dubbo-demo下面找一個provider抄過來即可草则,主要看配置文件:
<!-- 這里使用kryo作為序列化方式 -->
<!-- 關(guān)于PhoneOptimizer這個類我們后面看 -->
<!-- 如果你要運行代碼記得把zk地址改掉钢拧,或者用直連等方式 -->
<!-- 暴露一個服務 -->
Client:代碼就不看了,直接看配置:
<!-- 一會再看這個optimizer炕横,注意這里用的是UserOptimizer和上面使用的PhoneOptimizer不一樣 -->
<!-- 同樣記得改改zk地址或者改成直連 -->
<!-- 引用一個服務 -->
PhoneService:這里只貼PhoneService的實現(xiàn)源内、Phone類還有User類,實在是太簡單了:
publicclassPhoneServiceImplimplementsPhoneService{
publicPhonegetPhone(){
returnnewPhone();
? ? }
}
publicclassPhoneimplementsSerializable{
privatestaticfinallongserialVersionUID = -8578553481899481234L;
privateString color ="black";
publicStringgetColor(){
returncolor;
? ? }
publicvoidsetColor(String color){
this.color = color;
? ? }
}
publicclassUserimplementsSerializable{
privatestaticfinallongserialVersionUID =2345729265494289413L;
privateString name ="xyh";
publicStringgetName(){
returnname;
? ? }
publicvoidsetName(String name){
this.name = name;
? ? }
}
Optimizer:這里把兩個Optimizer都貼出來:
publicclassUserOptimizerimplementsSerializationOptimizer{
publicCollectiongetSerializableClasses(){
returnCollections.singletonList(User.class);
? ? }
}
publicclassPhoneOptimizerimplementsSerializationOptimizer{
publicCollectiongetSerializableClasses(){
returnCollections.singletonList(Phone.class);
? ? }
}
代碼特別簡單份殿,就是簡單的引用和暴露膜钓,唯一特別的地方就是我們設(shè)置了我們的optimizer,服務端用PhoneOptimizer而客戶端使用UserOptimizer卿嘲,并且我們使用Kryo作為序列化方式颂斜。
啟動一下程序,啟動Server沒什么特別的拾枣,正常啟動沃疮。啟動Client的時候報錯:
PhoneService ps = (PhoneService) context.getBean("phoneService");
Phone p = ps.getPhone();
錯誤:
Exception in thread"main"java.lang.ClassCastException: dubbo.action.domain.User cannot be cast to dubbo.action.domain.Phone
at com.alibaba.dubbo.common.bytecode.proxy0.getPhone(proxy0.java)
at dubbo.action.ServerAndConsumer.main(ServerAndConsumer.java:20)
我們明明在PhoneService里返回的是Phone類,怎么就cast exception了呢梅肤。帶著這個問題我們看下源碼司蔬。
要解決這個問題,我們要先看看Optimizer是怎么起作用的姨蝴,這個部分的代碼是dubbo重啟維護之后俊啼,從dubbox中合并過來的,代碼在DubboProtocol#optimizeSerialization:
// 加載我們配置中的 optimizer="xxx.PhoneOptimezer" 這個類
Class clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
if(!SerializationOptimizer.class.isAssignableFrom(clazz)) {
// throw exception...
}
SerializationOptimizer optimizer = (SerializationOptimizer) clazz.newInstance();
if(optimizer.getSerializableClasses() ==null) {
return;
}
// 把我們PhoneOptimezer中的類注冊到SerializableClassRegistry中
for(Class c : optimizer.getSerializableClasses()) {
? ? SerializableClassRegistry.registerClass(c);
}
optimizers.add(className);
SerializableClassRegistry的的內(nèi)容非常簡單左医,不多說了授帕,看下SerializableClassRegistry的getRegisteredClasses調(diào)用方,在AbstractKryoFactory里(注意Fst序列化方式中也用到了類似的地方炒辉,我們先不看豪墅,只看kryo相關(guān)):
for(Class clazz : SerializableClassRegistry.getRegisteredClasses()) {
? ? kryo.register(clazz);
}
其實就是把我們的Phone和User兩個domain類注冊到了kryo里。
現(xiàn)在我們知道了這個Optimizer的作用之后黔寇,就要看看為什么會報錯了偶器。這里我們要從客戶端和服務端兩個端看起,先看看服務端寫出去了什么東西,再看看客戶端收到了什么東西屏轰。
這里我需要跟大家說明一下kryo的兩種寫class信息的序列化方式:
寫入的是class name颊郎,默認情況下就是這種寫class的方式,性能不好霎苗,因為寫入的數(shù)據(jù)比較大姆吭,無論是傳輸還是寫入都慢。
寫入的是id唁盏,對于我們注冊過的類(調(diào)用過kryo.register(Class type))内狸,我們寫入的class信息是一個id,性能好厘擂,寫入的數(shù)據(jù)量小昆淡,寫入和傳輸都快。
我們下面看看我們?nèi)绾纬龅腻e刽严。
我會帶著大家一起跟蹤整個過程昂灵,先以debug的方式運行服務端,再以普通方式運行客戶端舞萄。我們看看我們到底寫出去個什么東西眨补,我們既然要看寫出去個什么東西,當然要看我們encode相關(guān)的部分倒脓,這里我們encode的是response撑螺,代碼入口在DubboCountCodec#encode,我們跳過一些信息把还,看kryo相關(guān)的內(nèi)容实蓬,入口在KryoObjectOutput#writeObject,追進去:
可以看到我們寫出去的確實是一個Phone對象吊履,我們看看writeClass方法,這個主要是把類的信息寫到流里调鬓,最終走到DefaultClassResolver#writeClass里:
這里要注意艇炎,我們發(fā)現(xiàn)我們走到了registration.getId() != NAME這個分支里,結(jié)果我們實際上并沒有向流里寫什么信息腾窝,只是寫了一個id(output.writeVarInt)缀踪,這是為什么,我們要追溯一下Kryo#getRegistration(java.lang.Class)這個方法虹脯,這個方法底層是從classResolver的classToRegistration這個map中獲取一個Registration驴娃,那我們就看看這個classToRegistration中的值是從哪來的,看下使用這個字段的地方循集,我們找到了線索:
DefaultClassResolver#register方法中使用了put唇敞。那我們可以很輕松的聯(lián)想到我們之前在啟動dubbo的時候,注冊了我們的Phone.class到kryo中〗幔看下這個register方法:
if(registration.getId() != NAME) {
// 如果id不是-1咒精,就把我們的registration放到一個map里,鍵就是id
? ? idToRegistration.put(registration.getId(), registration);
}elseif(TRACE) {
// sth...
}
classToRegistration.put(registration.getType(), registration);
if(registration.getType().isPrimitive())
? ? classToRegistration.put(getWrapperClass(registration.getType()), registration);
returnregistration;
我們在使用kryo.register(Class type)這個方法時旷档,其實默認調(diào)用的是這樣的:return classResolver.register(new Registration(type, serializer, getNextRegistrationId()));模叙,注意這里getNextRegistrationId,實際上就是從idToRegistration這個字段里取最大的id鞋屈,然后加1后返回范咨。kryo默認會注冊五十多個類,都是java原生的類厂庇,比如Boolean和Integer渠啊。
我們寫完了class信息之后,就是實際寫入我們的對象的具體信息了宋列,比如我們的color字段昭抒。這里不說了,跟我們的問題關(guān)系不大炼杖。
其實說到這里灭返,基本上很多朋友也能猜到問題了,我們在服務端注冊的Phone.class坤邪,但是在客戶端熙含,我們注冊的User.class,而二者對應的id是同一個艇纺,這種情況下就會出現(xiàn)問題怎静。為什么id是同一個,這里我說一下黔衡,之前我們說過蚓聘,kryo會默認注冊五十多個jdk原生的類,那么客戶端和服務端的kryo絕對是兩個對象盟劫,在這兩個kryo都沒有注冊其他類的情況下夜牡,Phone和User這兩個類在兩個kryo中的位置是一樣的。
我們猜完了侣签,就要實際看看效果了塘装。這里我們用普通方式運行服務端,debug方式運行客戶端影所,看看我們客戶端收到數(shù)據(jù)以后蹦肴,為什么報錯。
上面是encode這里就是decode了猴娩,decode的就是響應信息阴幌,最終和我們kryo相關(guān)的地方在KryoObjectInput#readObject()勺阐,調(diào)用的是kryo的readClassAndObject方法:
我們看看讀取出來的是什么:
果然是一個User對象。至此我們就知道為什么會class cast exception了裂七。
這里可能有人會說皆看,你并沒有復現(xiàn)issue中提到的場景啊,issue里說報出來的是另外一個錯誤背零。這里其實本質(zhì)是一樣的腰吟,如果你看得懂這篇文章,應該會明白徙瓶。說白了毛雇,kryo中有個注冊表,標記id - class的關(guān)系侦镇。
但是在分布式系統(tǒng)中灵疮,兩端的這個id - class表關(guān)系一旦錯誤,就會出現(xiàn)各種問題壳繁,這是kryo在分布式系統(tǒng)中的局限性震捣,下面借用一下解決這個issue的這位大神的圖,一目了然:
這個部分與我們的問題無關(guān)闹炉,有興趣的可以看下蒿赢,沒興趣的直接跳過即可。
kryo初始化對象很特別渣触,使用的是ConstructorAccess這個類羡棵。我們看下ConstructorAccess#get方法,先看頭部:
Class enclosingType = type.getEnclosingClass();
booleanisNonStaticMemberClass = enclosingType !=null&& type.isMemberClass() && !Modifier.isStatic(type.getModifiers());
String className = type.getName();
// 拼裝類名嗅钻,我們的場景中就是 xxx.PhoneConstructorAccess
String accessClassName = className +"ConstructorAccess";
if(accessClassName.startsWith("java."))
accessClassName ="reflectasm."+ accessClassName;
Class accessClass;
AccessClassLoader loader = AccessClassLoader.get(type);
try{
// 嘗試加載xxx.PhoneConstructorAccess
accessClass = loader.loadClass(accessClassName);
catch(xxx) {
xxx
}
很奇怪皂冰,居然嘗試去加載一個不存在的類,我們每個類進來养篓,都會嘗試加載ClassNameConstructorAccess這個類秃流。我們的例子中,當然不存在這個PhoneConstructorAccess類柳弄,繼續(xù)看如果捕捉異常會怎樣:
在捕捉到異常之后剔应,使用ASM重新創(chuàng)建了一個ConstructorAccess子類,并且這個新類的newInstance方法语御,返回的就是我們的User(或者Phone)對象的實例。
這里我們可以看到席怪,實際上kryo是通過一個額外的应闯、通過ASM生成的類,來進行對象的初始化的挂捻。為什么用這種方式碉纺,不甚了解,有了解的朋友可以告知我一下。
在此我向大家推薦一個架構(gòu)學習交流群骨田。交流學習群號:938837867 暗號:555 里面會分享一些資深架構(gòu)師錄制的視頻錄像:有Spring耿导,MyBatis,Netty源碼分析态贤,高并發(fā)舱呻、高性能、分布式悠汽、微服務架構(gòu)的原理箱吕,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備