Dubbo之Kryo序列化在分布式處理情景下的問題

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里。

源碼分析——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)師必備

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末柿冲,一起剝皮案震驚了整個濱河市茬高,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌假抄,老刑警劉巖怎栽,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宿饱,居然都是意外死亡熏瞄,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進店門刑棵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來巴刻,“玉大人,你說我怎么就攤上這事蛉签『悖” “怎么了?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵碍舍,是天一觀的道長柠座。 經(jīng)常有香客問我,道長片橡,這世上最難降的妖魔是什么妈经? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮捧书,結(jié)果婚禮上吹泡,老公的妹妹穿的比我還像新娘。我一直安慰自己经瓷,他們只是感情好爆哑,可當我...
    茶點故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著舆吮,像睡著了一般揭朝。 火紅的嫁衣襯著肌膚如雪队贱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天潭袱,我揣著相機與錄音柱嫌,去河邊找鬼。 笑死屯换,一個胖子當著我的面吹牛编丘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播趟径,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼瘪吏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蜗巧?” 一聲冷哼從身側(cè)響起掌眠,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎幕屹,沒想到半個月后蓝丙,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡望拖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年渺尘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片说敏。...
    茶點故事閱讀 40,438評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡鸥跟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出盔沫,到底是詐尸還是另有隱情医咨,我是刑警寧澤,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布架诞,位于F島的核電站拟淮,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏谴忧。R本人自食惡果不足惜很泊,卻給世界環(huán)境...
    茶點故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沾谓。 院中可真熱鬧委造,春花似錦、人聲如沸均驶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辣恋。三九已至亮垫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間伟骨,已是汗流浹背饮潦。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留携狭,地道東北人继蜡。 一個月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像逛腿,于是被迫代替她去往敵國和親稀并。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,446評論 2 359

推薦閱讀更多精彩內(nèi)容