1. 程式人生 > >上線前一個小時,dubbo這個問題可把我折騰慘了

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

前因

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

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

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

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

第一步:找到錯誤資訊

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

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

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

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

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

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

public ResponseData<String> 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導致的錯誤。

引數程式碼:

@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;
}

第三步:本地復現錯誤

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

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

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

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

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

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

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

在呼叫方這邊發起請求前,查看了引數物件,發現這個時候引數已經出問題了,欄位型別發生了變化,所以問題就出在這裡,都是老程式碼,應該都沒改過,而是事實卻被改了,通過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> 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);
}

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

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

第五步:BeanUtils問題排查

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

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

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

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

第六步:Dubbo解碼問題排查

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

結局

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