問題描述&模擬
線上登入介面,通過監控檢視,有型別轉換異常,具體報錯如下圖
此報錯資訊是dubbo consumer端顯示,且登入大部分是正常,有少量部分會報型別轉換異常,同事通過更換方法名+顯示指定序列化id解決此問題,但是產生這個問題的真正原因是什麼呢?沒有指定序列化id嗎?還是dubbo方法過載問題?為什麼服務端不顯示此錯誤資訊呢?,下面根據錯誤模擬下情況。
線上執行情況說明,報錯的這臺客戶端部署在容器內,jdk版本
服務方是混跑,有虛擬機器和容器,容器的jdk版本相同,虛擬機器jdk版本
一開始認為是由於沒有顯示指定序列化id導致容器呼叫虛擬機器的服務,由於jvm版本不一致導致的解碼問題,但是分析和試驗後,發現並非如此,模擬情況如下:
定義一個dubbo服務,方法過載且入參不顯示指定序列化id,程式碼如下
//定義dubbo服務
public interface ProductService {
Result<ProductVO> findProduct(String data);
Result<ProductVO> findProduct(ProductDTO product);
}
//入參
@Data
public class ProductDTO implements Serializable {
//不顯示指定序列化id
private Integer productId;
private String sn;
private String code;
}
//出參
@Data
public class ProductVO implements Serializable{
private static final long serialVersionUID = 4529782262922750326L;
private Integer productId;
private String productName;
}
dubbo客戶端呼叫ProductService.findProduct(ProductDTO product)
,並使用jdk1.8.0_202版本,服務方使用jdk1.8.0_73版本,經過試驗(jmeter壓測),發現並未出現型別轉換異常,現在通過程式碼分析來排除。
分析&dubbo provider處理請求流程
採用逆序方法,使用arthas進行反編譯dubbo生成的代理類,ProductService生成的代理類是Wrapper2,內容如下
public Object invokeMethod(Object object, String name, Class[] classArray, Object[] objectArray)
throws InvocationTargetException {
ProductService productService;
try {
productService = (ProductService) object;
} catch (Throwable throwable) {
throw new IllegalArgumentException(throwable);
}
try {
if ("findProduct".equals(name) && classArray.length == 1
&& classArray[0].getName().equals("java.lang.String")) {
return productService.findProduct((String) objectArray[0]);
}
if ("findProduct".equals(name) && classArray.length == 1
&& classArray[0].getName().equals("org.pangu.dto.ProductDTO")) {
return productService.findProduct((ProductDTO) objectArray[0]);
}
} catch (Throwable throwable) {
throw new InvocationTargetException(throwable);
}
throw new NoSuchMethodException(new StringBuffer().append("Not found method \"").append(name)
.append("\" in class org.pangu.api.ProductService.").toString());
}
}
通過檢視反編譯後的程式碼,得知dubbo方法過載,會根據方法型別和引數個數找到對應的目標方法執行。對於我這個線上問題,引數是ProductDTO,如果呼叫的是findProduct(String data),說明classArray[0]即引數型別是String型別,那麼引數型別是如何得來的呢?根據自己之前寫的dubbo流程分析,檢視原始碼,在com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker#invoke(Invocation invocation)
,程式碼內容如下
方法名稱+方法型別+方面引數都封裝在Invocation內,接著查詢Invocation的來源,在DubboProtocol的匿名內部類DubboProtocol$1內發現,具體是reply(ExchangeChannel channel, Object message)
方法內,引數message就是Invocation。
接著看哪裡呼叫DubboProtocol$1.reply(ExchangeChannel channel, Object message)
方法,在com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler#handleRequest(ExchangeChannel channel, Request req)
方法內,com.alibaba.dubbo.remoting.exchange.Request.getData()
獲取此Invocation,即DecodeableRpcInvocation,那麼接著看Request 以及Request.mData的來源;
接著向上找,在com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler#received(Channel channel, Object message)
的入參message就是Request ;
繼續向上找,com.alibaba.dubbo.remoting.transport.DecodeHandler#received(Channel channel, Object message)
的入參就是Request ,其中會對Request.mData即Invocation進行解碼(預設在IO執行緒已經解碼過,這裡實際並不會再執行解碼DecodeableRpcInvocation#hasDecoded=true)。
繼續向上找,com.alibaba.dubbo.remoting.transport.dispatcher.ChannelEventRunnable#run()
執行緒,message屬性就是Request,那麼接著只能找ChannelEventRunnable是如何建立並提交的
繼續向上找,在com.alibaba.dubbo.remoting.transport.dispatcher.all.AllChannelHandler#received(Channel channel, Object message)
方法內建立ChannelEventRunnable並提交到執行緒池執行。
繼續向上找,在com.alibaba.dubbo.remoting.exchange.support.header.HeartbeatHandler.received(Channel channel, Object message)
,入參message就是Request
繼續向上找,com.alibaba.dubbo.remoting.transport.MultiMessageHandler.received(Channel channel, Object message)
繼續向上找,com.alibaba.dubbo.remoting.transport.AbstractPeer.received(Channel ch, Object msg)
繼續向上找,com.alibaba.dubbo.remoting.transport.netty4.NettyServerHandler.channelRead(ChannelHandlerContext ctx, Object msg)
,看到這個就說明是netty的work執行緒,NettyServerHandler是個inbound & outbound事件
dubbo service netty啟動新增的inbound&outbound即pipeline chain[HeadContext InternalDecoder InternalEncoder NettyServerHandler TailContext]
,說明前面肯定有執行InternalDecoder 的channelRead事件。此時入參message就是Request。
下面著重分析InternalDecoder 的channelRead事件,執行堆疊依次為:
InternalDecoder(io.netty.handler.codec.ByteToMessageDecoder).channelRead(ChannelHandlerContext ctx, Object msg)
InternalDecoder(io.netty.handler.codec.ByteToMessageDecoder).callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
InternalDecoder.decode(ChannelHandlerContext ctx, ByteBuf input, List<Object> out)
DubboCountCodec.decode(Channel channel, ChannelBuffer buffer)
DubboCodec(ExchangeCodec).decode(Channel channel, ChannelBuffer buffer)
DubboCodec(ExchangeCodec).decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header)
DubboCodec.decodeBody(Channel channel, InputStream is, byte[] header)
DecodeableRpcInvocation.decode()
DecodeableRpcInvocation.decode(Channel channel, InputStream input)
InternalDecoder是netty pipeline的inboud事件,執行的是channelRead,具體邏輯在InternalDecoder.decode(ChannelHandlerContext ctx, ByteBuf input, List<Object> out)
內,程式碼如下
接著觸發下一個inbound的channelRead動作,傳入的就是Request了,程式碼說明如下
接著看DubboCountCodec.decode(Channel channel, ChannelBuffer buffer)
,這裡進行解碼
//com.alibaba.dubbo.rpc.protocol.dubbo.DubboCountCodec#decode(Channel channel, ChannelBuffer buffer)
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
int save = buffer.readerIndex();//獲取讀位置
MultiMessage result = MultiMessage.create();//MultiMessage是Request的集合
do {
Object obj = codec.decode(channel, buffer);//使用DubboCodec進行解碼,下面根據解碼結果進行不同處理
if (Codec2.DecodeResult.NEED_MORE_INPUT == obj) {//說明發生了tcp粘包,退出迴圈
buffer.readerIndex(save);
break;
} else {
result.addMessage(obj);//把obj即Request新增到集合MultiMessage
logMessageLength(obj, buffer.readerIndex() - save);
save = buffer.readerIndex();//設定新的buffer讀位置,繼續使用DubboCodec進行解碼
}
} while (true);
if (result.isEmpty()) {
return Codec2.DecodeResult.NEED_MORE_INPUT;
}
if (result.size() == 1) {//如果MultiMessage只有一個元素,則說明本次沒有發生粘包
return result.get(0);//返回Request
}
return result;//返回MultiMessage,在後續的MultiMessagehandler內獲取Request的集合遍歷處理
}
接著看DubboCodec(ExchangeCodec).decode(Channel channel, ChannelBuffer buffer)解碼過程,如何對dubbo協議解碼的,先看下dubbo協議的報文結構
接著看程式碼,對著報文結構進行解碼
//DubboCodec(ExchangeCodec).decode(Channel channel, ChannelBuffer buffer)
@Override
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
int readable = buffer.readableBytes();
byte[] header = new byte[Math.min(readable, HEADER_LENGTH)];
buffer.readBytes(header);//把緩衝區位元組存放到header
return decode(channel, buffer, readable, header);
}
//DubboCodec(ExchangeCodec).decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header)
@Override
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
// check magic number.
if (readable > 0 && header[0] != MAGIC_HIGH
|| readable > 1 && header[1] != MAGIC_LOW) {//非魔數,說明非dubbo報文的開頭,說明發生了tcp拆包/粘包
int length = header.length;
if (header.length < readable) {
header = Bytes.copyOf(header, readable);
buffer.readBytes(header, length, readable - length);
}
for (int i = 1; i < header.length - 1; i++) {
if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) {
buffer.readerIndex(buffer.readerIndex() - header.length + i);
header = Bytes.copyOf(header, i);
break;
}
}
return super.decode(channel, buffer, readable, header);
}
// check length.
if (readable < HEADER_LENGTH) {//為什麼是小於16呢?因為dubbo報文 magic(2)+falg(1)+status(1)+invokerId(8)+bodyLenght(4)就是16位元組了,小於16位元組,肯定發生了拆包,本次接收到的資料並沒有body
return DecodeResult.NEED_MORE_INPUT;
}
// get data length.
int len = Bytes.bytes2int(header, 12);//12的原因是dubbo報文 magic(2)+falg(1)+status(1)+invokerId(8)等於12,從12位後取4位,轉換為int,就是body的長度
checkPayload(channel, len);
int tt = len + HEADER_LENGTH;
if (readable < tt) {//可讀取數少於bodylen+16,說明tcp拆包,需要繼續進網路讀取
return DecodeResult.NEED_MORE_INPUT;
}
// limit input stream.
ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
try {
return decodeBody(channel, is, header);//解碼body內容
} finally {
if (is.available() > 0) {
try {
if (logger.isWarnEnabled()) {
logger.warn("Skip input stream " + is.available());
}
StreamUtils.skipUnusedStream(is);
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
}
}
接著看解碼dubbo body,在com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody
//com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody(Channel channel, InputStream is, byte[] header)
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
// get request id.
long id = Bytes.bytes2long(header, 4);
if ((flag & FLAG_REQUEST) == 0) {//是響應,編碼
//省略
} else {//請求,解碼
// decode request.
Request req = new Request(id);
req.setVersion(Version.getProtocolVersion());
req.setTwoWay((flag & FLAG_TWOWAY) != 0);
if ((flag & FLAG_EVENT) != 0) {
req.setEvent(Request.HEARTBEAT_EVENT);
}
try {
Object data;
if (req.isHeartbeat()) {//心跳
data = decodeHeartbeatData(channel, CodecSupport.deserialize(channel.getUrl(), is, proto));
} else if (req.isEvent()) {//事件
data = decodeEventData(channel, CodecSupport.deserialize(channel.getUrl(), is, proto));
} else {
DecodeableRpcInvocation inv;
if (channel.getUrl().getParameter(
Constants.DECODE_IN_IO_THREAD_KEY,
Constants.DEFAULT_DECODE_IN_IO_THREAD)) {//預設是在netty work執行緒進行解碼
inv = new DecodeableRpcInvocation(channel, req, is, proto);
inv.decode();//解碼dubbo body,解碼結果儲存在DecodeableRpcInvocation
} else {
inv = new DecodeableRpcInvocation(channel, req,
new UnsafeByteArrayInputStream(readMessageData(is)), proto);//否則在業務執行緒ChannelEventRunnable進行解碼
}
data = inv;
}
req.setData(data);//把Invocation儲存到Request.mData
} catch (Throwable t) {
if (log.isWarnEnabled()) {
log.warn("Decode request failed: " + t.getMessage(), t);
}
// bad request
req.setBroken(true);
req.setData(t);
}
return req;
}
}
接著看DecodeableRpcInvocation解碼dubbo body
//com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode()
@Override
public void decode() throws Exception {
if (!hasDecoded && channel != null && inputStream != null) {
try {
decode(channel, inputStream);//解碼
} catch (Throwable e) {
if (log.isWarnEnabled()) {
log.warn("Decode rpc invocation failed: " + e.getMessage(), e);
}
request.setBroken(true);
request.setData(e);
} finally {
hasDecoded = true;//解碼後置位已經解碼,這樣在ChannelEventRunnable執行緒內就不會再進行解碼
}
}
}
//com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(com.alibaba.dubbo.remoting.Channel, java.io.InputStream)
@Override
public Object decode(Channel channel, InputStream input) throws IOException {
ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
.deserialize(channel.getUrl(), input);//根據序列化標識獲取反序列物件,dubbo spi的自適應
String dubboVersion = in.readUTF();//從輸入流讀取dubbo version
request.setVersion(dubboVersion);
setAttachment(Constants.DUBBO_VERSION_KEY, dubboVersion);
setAttachment(Constants.PATH_KEY, in.readUTF());//從輸入流讀path
setAttachment(Constants.VERSION_KEY, in.readUTF());//從輸入流讀版本
setMethodName(in.readUTF());//從輸入流讀 呼叫的目標方法名
try {
Object[] args;
Class<?>[] pts;
String desc = in.readUTF();//從輸入流讀 引數描述符,即引數的型別 比如[Ljava/lang/String
if (desc.length() == 0) {//dubbo呼叫方法不存在入參
pts = DubboCodec.EMPTY_CLASS_ARRAY;
args = DubboCodec.EMPTY_OBJECT_ARRAY;
} else {//dubbo呼叫方法存在入參
pts = ReflectUtils.desc2classArray(desc);//型別描述符轉換為型別,比如[Ljava/lang/String => Ljava.lang.String
args = new Object[pts.length];//引數長度
for (int i = 0; i < args.length; i++) {
try {
args[i] = in.readObject(pts[i]);//從輸入流讀取引數,這裡是readObject,執行反序列化
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("Decode argument failed: " + e.getMessage(), e);
}
}
}
}
setParameterTypes(pts);//把引數型別儲存到Invocation物件,即parameterTypes屬性上
Map<String, String> map = (Map<String, String>) in.readObject(Map.class);//從輸入流讀取隱式引數並解碼
if (map != null && map.size() > 0) {
Map<String, String> attachment = getAttachments();
if (attachment == null) {
attachment = new HashMap<String, String>();
}
attachment.putAll(map);
setAttachments(attachment);
}
//decode argument ,may be callback
for (int i = 0; i < args.length; i++) {
args[i] = decodeInvocationArgument(channel, this, pts, i, args[i]);
}
setArguments(args);
} catch (ClassNotFoundException e) {
throw new IOException(StringUtils.toString("Read invocation data failed.", e));
} finally {
if (in instanceof Cleanable) {
((Cleanable) in).cleanup();
}
}
return this;
}
從解碼dubbo body看出,從輸入流解碼獲取呼叫的目標方法名稱、方法型別、方法入參、隱式引數都儲存到Invocation物件(即DecodeableRpcInvocation),其中讀取入參和隱式引數使用到了序列化解碼(需要使用到序列化id),而從輸入流獲取方法名稱+引數型別並沒有使用物件的反序列化。
dubbo provider處理接收總結
dubbo prodiver端從網路到dubbo業務執行緒池呼叫以及如何解碼流程分析完,現在總結下:
dubbo provider接收並處理consumer請求分兩步
1.網路通訊,在io執行緒上解碼,解碼結果儲存到Request。
2.IO執行緒調起dubbo業務執行緒,傳入解碼結果Request,通過Invoker呼叫目標方法,傳入要執行目標方法的物件、方法名、引數型別、引數進行呼叫目標方法。
該問題分析
解決2個問題
問題1:為什麼在服務端報錯ClassCastException,在服務端沒有任何error日誌呢?只有在客戶端才有error日誌
由於在dubbo代理類Wrapper2呼叫目標方法導致ClassCastException,異常被捕捉封裝為InvocationTargetException向上拋,接著在com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker#invoke
內異常被捕捉,封裝為RpcResult,繼而在ExceptionFilter內異常資訊被封裝為RuntimeException返回客戶端。這中間並沒有日誌列印,因此不產生error日誌,所以服務端看不到。
問題2:dubbo方法過載會導致問題嗎?
結論,基本不會,dubbo的動態代理類WrapperX會根據Invocation的methodName+引數型別+引數進行呼叫目標方法,因此不會。網上有個大佬說dubbo方法過載在某種情況會導致問題,但是他寫的語句有些不通順且凌亂,而且藍綠是流量隔離的,不會調錯,我認為他的舉例不合適,感興趣的可以參考dubbo同名方法的問題及思考。
問題3:是否是未顯式指定序列化id導致的呢?
經過前面分析,是由於判斷引數型別是String(本來應該是DTO型別),導致執行目標方法時候把引數轉換為String導致的異常,引數型別來源於Invocation物件(即RpcInvocation.parameterTypes),而Invocation來源於Request.mData,而Request是網路通訊解碼得來,其中在com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(com.alibaba.dubbo.remoting.Channel, java.io.InputStream)
內String desc = in.readUTF();
從輸入流讀取位元組流並解碼為引數型別描述符,這個地方並不涉及到物件的序列化和反序列化。
看客戶端編碼程式碼InternalEncoder,編碼引數型別程式碼如下圖
而客戶端傳送建立Request是在com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeChannel#request(java.lang.Object, int)
,而Invocation物件是在dubbo呼叫的入口InvokerInvocationHandler內(new RpcInvocation(method, args)
封裝方法名+引數建立Invocation物件,繼而引數型別就儲存在了Invocation物件。
這樣分析得來,不顯示指定序列化id並不會導致這個問題。
排除了jdk版本、不顯示指定序列化ID等原因,具體是什麼原因導致的dubbo方法過載導致呼叫ClassCastException呢?線上預發環境和生產網路是互通,是否是預發環境同事手工部署的應用只有入參String的方法呢(未和生產同步版本)?同事也記不清了,也無法查,這個問題暫時是無法知道答案了。
據我猜測,問題可能出現是預發環境部署的服務沒有和生產版本同步(缺少findProduct(ProductDTOdata)導致),我們預發和生成網路是互通的,應該是生產客戶端呼叫到了預發環境服務,而預發環境部署的此服務沒有findProduct(ProductDTOdata)。
為什麼需要顯示指定序列化id
rpc呼叫使用的tcp通訊,需要把物件轉換為二進位制流進行傳送(編碼)和接收(解碼),那麼就需要有套規則需要把記憶體中的java物件轉換為二進位制流,序列化就是做這個事情的。
在使用原生序列化的時候,serialVersionUID起到了一個類似版本號的作用,在反序列化的時候判斷serialVersionUID如果不相同,會丟擲InvalidClassException。
如果在使用原生序列化方式的時候官方是強烈建議指定一個serialVersionUID的,如果沒有指定,在序列化過程中,jvm會自動計算出一個值作為serialVersionUID,由於這種執行時計算serialVersionUID的方式依賴於jvm的實現方式,如果序列化和反序列化的jvm實現方式不一樣可能會導致丟擲異常InvalidClassException,所以強烈建議指定serialVersionUID。
不顯示指定序列化ID實際會導致問題嗎?
定義一個dubbo的入參,不顯示指定序列化id,客戶端執行不變更,服務端入參進行增加或刪除欄位(類結構發生變化),發現均能正常請求,並非像網上所說的不顯示指定序列化id情況下rpc引數類結構變化,並沒有導致什麼問題,當然我只是在jdk8版本下進行了此測試(當然現在都是jdk8),這樣情況下,實際使用過程中,不顯示指定序列化id好像也不會影響什麼呢。
網上有說法,不顯示指定序列化id會導致一種情況出現問題:舉個例子:比如該入參沒有顯示指定序列化id,後面有個需求需要在這個入參增加個欄位,而且看沒有顯示指定序列化id,順手就增加了個序列化id,這樣線上執行的客戶端應用由於引用的還是舊jar,新的服務部署上去,就會傳送序列化失敗(客戶端jvm生成的序列化id和服務端顯示指定的序列化id不同),好像這種情況是無法避免的。但是我經過測試,不顯示指定序列化id情況下 對dubbo引數進行增加欄位、刪除欄位、增加方法等都不會造成反序列化問題(jdk8, dubbo2.6.8下測試),請求均正常。驗證結果說明jvm生成序列化id和類的結構沒有關係。可以參考別人測試結果,和我測試結果相同。
那麼是否就可以大膽的不指定序列化id呢?還是建議不要,鬼知道jvm生成序列化id的實現方式呢,不指定萬一線上哪天出現么蛾子。
驗證了半天,得到一個不指定序列化id也沒關係的實際驗證結論,但是又不敢完全放心大膽不顯示指定序列化id,抓狂。。。
最終結論
根據實際驗證(jdk8, dubbo2.6.8下測試),不顯示指定序列化id時,dubbo的傳輸物件在增加欄位、刪除欄位、增加方法等都不會造成反序列化問題,但是還是強烈建議顯示指定序列化id,萬一jvm生成序列化id不相容了呢
結尾
分析了這麼長,最終也沒找到這個問題的產生原因,但是對dubbo的通訊層又加深了理解,下面一篇記錄下總結的dubbo通訊層