上線前一個小時,dubbo這個問題可把我折騰慘了

以下文章來源於猿天地 ,作者尹吉歡

前因

那是一個月黑風高的夜晚,不管有沒有圓圓的月亮,都無法解救要加班的我。這就是苦澀的人生啊!

那天正好是春節回家的日子,定了晚上的票,然後還是上線的日子。

測試在做迴歸測試的時候,發現一個老功能報錯了,什麼鬼,都沒改過那塊程式碼怎麼會出問題?案件疑點重重呀。。。

為了能夠早點上線,早點回家,所以這個 Bug 就顯得十萬火急了,因為就這一個問題,其他都沒問題,解決好了就可以上線了,於是開啟了破案之路。

第一步:找到錯誤資訊

機智的我在第一時間打開了 Cat 檢視具體的錯誤,由於當時並沒有想到去寫一篇文章出來,錯誤資訊也就沒有截圖,後面透過模擬的操作,得到了類似的一樣的錯誤資訊如下:

上線前一個小時,dubbo這個問題可把我折騰慘了

Cat錯誤資訊

居然是類轉換錯誤,點進去檢視詳細的錯誤資訊,如下圖:

上線前一個小時,dubbo這個問題可把我折騰慘了

Cat錯誤詳情

真正有價值的錯誤資訊如下:

dubbo version: 2。7。3, current host: 192。168。8。224 java。lang。ClassCastException:

第二步:排查報錯的程式碼

公司程式碼不方便透露,下面都是模擬的程式碼:

public ResponseData login(UserLoginRequest loginRequest) { loginRequest。getAddress()。stream()。map(a -> a。getStatus())。collect(Collectors。toList());return Response。ok(“xxxxxxxxx”);}

問題就出在了 map 這裡,從 loginRequest 引數中獲取 address 是一個 List

,Address 中有 status 欄位,如果是正常的物件沒有問題,錯誤告訴我們是 HashMap 不能轉換成 Address 類,也就是說引數中的 Address 變成了 HashMap 導致的錯誤。

引數程式碼:

@Datapublic class UserLoginRequest implements Serializable { private String username; private String pass; private List

address;}@Data@AllArgsConstructorpublic class Address implements Serializable { private int status;}第三步:本地復現錯誤

找到錯誤後,馬上本地啟動相關的兩個服務,我們分別叫 A 和 B 吧,現象是 A 呼叫 B 的某個 RPC 介面報錯。

本地啟動後馬上覆現了錯誤,在報錯的地方打斷點看引數是否變成了 HashMap,果不其然,如下圖:

上線前一個小時,dubbo這個問題可把我折騰慘了

引數資訊

到這裡感覺有點懵,引數中明明是具體的物件型別,怎麼突然就變成了 HashMap,匪夷所思。

然後想著是不是在上層什麼地方出問題了,繼續檢視報錯的上層程式碼,沒有發現異常。然後決定在 PRC 的入口處打個斷點看看是不是引數一過來就出問題了,最後經過驗證確實如此,也就排除了 B 服務中對引數做了轉換。

接著再看下 Dubbo 內部的引數解碼,

org。apache。dubbo。rpc。protocol。dubbo。DecodeableRpcInvocation#decode(org。apache。dubbo。remoting。Channel, java。io。InputStream)。也就是請求到達 B 之後解碼出來的已經是 HashMap 了,那麼問題肯定是呼叫方傳輸的引數有問題。

上線前一個小時,dubbo這個問題可把我折騰慘了

Dubbo內部引數檢視

第四步:排查呼叫方程式碼

在呼叫方這邊發起請求前,查看了引數物件,發現這個時候引數已經出問題了,欄位型別發生了變化,所以問題就出在這裡,都是老程式碼,應該都沒改過,而是事實卻被改了,透過 Idea 的 Annotate 快速的查看了當前方法中有被修改的記錄,找到了修改的程式碼,下面透過模擬的方式貼出有問題的程式碼,如下:

@Reference(version = DubboConstant。VERSION_V100, group = DubboConstant。DEFAULT_GROUP)private UserRemoteService userRemoteService;public void test() { UserLoginRequest request = new UserLoginRequest(); request。setUsername(“yjh”); request。setPass(“123456”); List

address = new ArrayList<>(); address。add(new Address(1)); request。setAddress(address); UserLoginRequest2 request2 = new UserLoginRequest2(); request2。setUsername(“yjh2”); request2。setPass(“1234562”); List address2 = new ArrayList<>(); address2。add(new Address2(StatusEnum。INVALID)); request2。setAddress(address2); BeanUtils。copyProperties(request2, request); userRemoteService。login(request);}

出問題的就是 BeanUtils。copyProperties(request2, request); 這行程式碼,將一個物件複製到另一個物件,兩個物件的屬性都一樣,唯一不一樣的是 Address 中的 status 是 int 型別,Address2 中的 status 是 Enum,複製過去就出問題了。

上線前一個小時,dubbo這個問題可把我折騰慘了

屬性複製

這種情況也只在 Dubbo 的 RPC 請求出問題,如果是 Http 請求,基本型別變成了列舉,直接就報錯了,無法轉換。

上線前一個小時,dubbo這個問題可把我折騰慘了

Http請求錯誤

第五步:BeanUtils 問題排查

歸根到底還是 copy 的問題,我做了個小實驗,如果是 Address2 copy 到 Address 是不會出問題的,只有巢狀的物件才會出問題。

特意看了下 copy 的程式碼,如果是 Address2 copy 到 Address,那麼就是 status 到 status,在 copy 之前會進行判斷 Address 的 setStatus 的第一個引數型別和 Address2 的 getStatus 的返回值是否相同,如果相同才會進行賦值操作,不同就不會,如果是單個物件在這裡就會直接過濾掉了,一個是 int 一個是 Enum。

上線前一個小時,dubbo這個問題可把我折騰慘了

BeanUtils原始碼

巢狀物件之所以可以那是因為 address 的引數和返回型別都是 List,沒有去判斷巢狀類裡面的,是整個集合直接複製賦值的,下圖是目標方法:

上線前一個小時,dubbo這個問題可把我折騰慘了

BeanUtils原始碼

value 是新的集合物件,invoke 後整個 address 就變了。

上線前一個小時,dubbo這個問題可把我折騰慘了

巢狀物件複製後

第六步:Dubbo 解碼問題排查

前面分析中,呼叫之前透過 BeanUtils 複製,只是將列舉賦值給了基本型別,如果 Dubbo 在接收到引數進行解碼時能夠識別出型別不一致,這樣就直接會報錯了,然而並沒有,特意除錯了下 Dubbo 解碼的程式碼,預設是 Hessian 的解碼,懷疑跟 Hessian 有關,於是我把序列化改成了 FastJson,在解碼引數的時候就直接報錯了,不能轉換成 int 型別。而 Hessian 在對映不了的時候就直接變成 HashMap 了,這才有了我們前面的錯誤。

上線前一個小時,dubbo這個問題可把我折騰慘了

FastJson解碼失敗

結局

找到原因後解決就是分分鐘的事了,透過這個問題還是說明了加任何的程式碼都有風險。剩下的就是開發的鍋了,加了程式碼沒有自測,好在有測試把關,否則就涼涼了。