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

前因

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

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

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

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

第一步:找到錯誤信息

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

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


居然是類轉換錯誤,點進去查看詳細的錯誤信息,如下圖:

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


真正有價值的錯誤信息如下:

<code>dubbo version: 2.7.3, current host: 192.168.8.224 java.lang.ClassCastException: java.util.HashMap cannot be cast to com.cxytiandi.kittycloud.user.api.request.Address
/<code>

第二步:排查報錯的代碼

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

<code>public ResponseData<string> login(UserLoginRequest loginRequest) {
loginRequest.getAddress().stream().map(a -> a.getStatus()).collect(Collectors.toList());
return Response.ok("xxxxxxxxx");
}
/<string>/<code>

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

<address>,Address中有status字段,如果是正常的對象沒有問題,錯誤告訴我們是HashMap不能轉換成Address類,也就是說參數中的Address變成了HashMap導致的錯誤。

參數代碼:

<code>@Data
public class UserLoginRequest implements Serializable {
private String username;
private String pass;
private List<address> address;
}
@Data
@AllArgsConstructor
public class Address implements Serializable {
private int status;
}
/<address>/<code>

第三步:本地復現錯誤

找到錯誤後,馬上本地啟動相關的兩個服務,我們分別叫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這個問題可把我折騰慘了


第四步:排查調用方代碼

在調用方這邊發起請求前,查看了參數對象,發現這個時候參數已經出問題了,字段類型發生了變化,所以問題就出在這裡,都是老代碼,應該都沒改過,而是事實卻被改了,通過Idea的Annotate快速的查看了當前方法中有被修改的記錄,找到了修改的代碼,下面通過模擬的方式貼出有問題的代碼,如下:

<code>@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> address = new ArrayList<>();
address.add(new Address(1));
request.setAddress(address);
UserLoginRequest2 request2 = new UserLoginRequest2();
request2.setUsername("yjh2");
request2.setPass("1234562");
List<address2> address2 = new ArrayList<>();
address2.add(new Address2(StatusEnum.INVALID));
request2.setAddress(address2);

BeanUtils.copyProperties(request2, request);

userRemoteService.login(request);
}
/<address2>/<address>/<code>

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

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


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

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


第五步:BeanUtils問題排查

歸根到底還是copy的問題,我做了個小實驗,如果是Address2 copy到Address 是不會出問題的,只有嵌套的對象才會出問題。

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

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


嵌套對象之所以可以那是因為address的參數和返回類型都是List,沒有去判斷嵌套類裡面的,是整個集合直接複製賦值的,下圖是目標方法:

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


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

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


第六步:Dubbo解碼問題排查

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

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


結局

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

"/<address>


分享到:


相關文章: