Dubbo默认序列化的原理与服务接口迭代规则
前言
目前项目底层服务采用Dubbo,由于项目上线了,必然会发生类似接口变更的重启次序问题、兼容性问题等。 在此进行分析,并假定我们采用的都是Dubbo的dubbo协议及其默认的序列化方式Hessian(目前我们项目的服务也的确是都采用这些设置)。
Hessian序列化原理
由Dubbo文档可知,目前dubbo协议默认采用hessian序列化方式。 Dubbo并不是引入Hessian的依赖,而是对Hessian进行了一些改动并内嵌到了Dubbo自己的代码中。btw,Dubbo最近对他们改写过Hessian做了一个抽离版本[3]。
一次Dubbo调用的Hessian字节码分析
Hessian is organized as a bytecode protocol. A Hessian reader is essentially a switch statement on the initial octet.
如上,由文档可知,Hessian是一个字节码协议。Hessian解析器实质上是一个对于头字节做选择的语句。
所以,我这里进行一次Dubbo调用的字节码分析:
GetFollowingListRequest request = new GetFollowingListRequest();
request.setUid(123L);
request.setLimit(10);
request.setName("hetl");
request.setStartId(123L);
GetFollowingListResponse response = demoService.objectCompatibleTest(request);
@Data
public class GetFollowingListRequest implements java.io.Serializable {
private static final long serialVersionUID = 92219829045331279L;
private long uid; // required
private int limit; // required
private Long startId; // optional
private Long checkFollowUid;
}
@Data
public class GetFollowingListResponse implements java.io.Serializable {
private static final long serialVersionUID = -880066094852559296L;
public List<FollowUserInfo> userList; // required
public Long nextUserId; // optional
}
下面wireshark抓包并分析下它序列化后的字节码:
......(Dubbo协议头 定义协议类型、请求类型、数据size等等)
05 //标记超短字符串and长度, utf-8字符串,长度范围 0-32
32 2e 30 2e 32 //2.0.2
30 //保留值,dubbo定义为短字符串
31 //标记字符串长度
6e 65 74 2e 74 65 61 68
6f 2e 64 65 6d 6f 2e 64
75 62 62 6f 68 65 61 72
74 62 65 61 74 74 65 73
74 2e 73 64 6b 2e 44 65
6d 6f 53 65 72 76 69 63
65 //net.teaho.demo.dubboheartbeattest.sdk.DemoService
05 //标记超短字符串and长度
31 2e 30 2e 32 //1.0.2
14 //标记字符串长度
6f 62 6a 65 63 74 43 6f
6d 70 61 74 69 62 6c 65
54 65 73 74 //objectCompatibleTest
30 //保留值
3f //标记字符串长度
4c 6e 65 74 2f 74 65 61
68 6f 2f 64 65 6d 6f 2f
64 75 62 62 6f 68 65 61
72 74 62 65 61 74 74 65
73 74 2f 73 64 6b 2f 47
65 74 46 6f 6c 6c 6f 77
69 6e 67 4c 69 73 74 52
65 71 75 65 73 74 3b //Lnet/teaho/demo/dubboheartbeattest/sdk/GetFollowingListRequest;
43 // C 类标记
30 // 短字符串
3d // 标记字符串长度
6e 65 74 2e 74 65 61 68
6f 2e 64 65 6d 6f 2e 64
75 62 62 6f 68 65 61 72
74 62 65 61 74 74 65 73
74 2e 73 64 6b 2e 47 65
74 46 6f 6c 6c 6f 77 69
6e 67 4c 69 73 74 52 65
71 75 65 73 74 //net.teaho.demo.dubboheartbeattest.sdk.GetFollowingListRequest
94 //标记为四个属性
04 //标记字符串长度
6e 61 6d 65 // name
07 //标记字符串长度
73 74 61 72 74 49 64 //startId
05 //标记字符串长度
6c 69 6d 69 74 //limit
03 //标记字符串长度
75 69 64 //uid
60 //保留值
04 //标记字符串长度
68 65 74 6c //hetl
f8 7b //131 (f8标记了某情况下的long,请看hessian doc)
9a //10
f8 7b //131 (f8标记了long)
48 //untyped map ('H')
04 //标记字符串长度
70 61 74 68 //path
30 //保留值
31 //标记字符串长度
6e 65 74 2e 74 65 61 68
6f 2e 64 65 6d 6f 2e 64
75 62 62 6f 68 65 61 72
74 62 65 61 74 74 65 73
74 2e 73 64 6b 2e 44 65
6d 6f 53 65 72 76 69 63
65 //net.teaho.demo.dubboheartbeattest.sdk.DemoService
09 //标记字符串长度
69 6e 74 65 72 66 61 63 65 //interface
30 //保留值
31 //标记字符串长度
6e 65 74 2e 74 65 61 68
6f 2e 64 65 6d 6f 2e 64
75 62 62 6f 68 65 61 72
74 62 65 61 74 74 65 73
74 2e 73 64 6b 2e 44 65
6d 6f 53 65 72 76 69 63
65 //net.teaho.demo.dubboheartbeattest.sdk.DemoService
07 //标记字符串长度
76 65 72 73 69 6f 6e //version
05 //标记字符串长度
31 2e 30 2e 32 //1.0.2
07 //标记字符串长度
74 69 6d 65 6f 75 74 //timeout
04 //标记字符串长度
35 30 30 30 //5000
05 //标记字符串长度
67 72 6f 75 70 //group
03 //标记字符串长度
64 65 76 //dev
5a //Z(map/list结束标记)
......(Dubbo协议头 定义协议类型、请求类型、数据size等等)
43 //C 类标记
30 //utf-8 string length 0-1023
3e //62
6e 65 74 2e 74 65 61 68
6f 2e 64 65 6d 6f 2e 64
75 62 62 6f 68 65 61 72
74 62 65 61 74 74 65 73
74 2e 73 64 6b 2e 47 65
74 46 6f 6c 6c 6f 77 69
6e 67 4c 69 73 74 52 65
73 70 6f 6e 73 65 //net.teaho.demo.dubboheartbeattest.sdk.GetFollowingListResponse
92 //两个属性
08 //长度8的字符串
75 73 65 72 4c 69 73 74 //userList
0a //10
6e 65 78 74 55 73 65 72 49 64 //nextUserId
60 //对象
71 //fixed list with direct length
30 //utf-8 string length 0-1023
23 //长度
6a 61 76 61 2e 75 74 69
6c 2e 43 6f 6c 6c 65 63
74 69 6f 6e 73 24 53 69
6e 67 6c 65 74 6f 6e 4c
69 73 74 //java.util.Collections$SingletonList
43 //C 类标记
30
34 //长度
6e 65 74 2e 74 65 61 68
6f 2e 64 65 6d 6f 2e 64
75 62 62 6f 68 65 61 72
74 62 65 61 74 74 65 73
74 2e 73 64 6b 2e 46 6f
6c 6c 6f 77 55 73 65 72
49 6e 66 6f //net.teaho.demo.dubboheartbeattest.sdk.FollowUserInfo
96 //6个属性
0a //长度10字符串
63 72 65 61 74 65 54 69 6d 65 //createTime
09
66 6f 6c 6c 6f 77 69 6e 67 //following
09
73 69 67 6e 61 74 75 72 65 //signature
07
69 63 6f 6e 49 6d 67 //iconImg
08
6e 69 63 6b 6e 61 6d 65 //nickname
03
75 69 64 //uid
61 //对象引用
e0 //0
46 //boolean
4e //'N'空
4e //'N'空
4e //'N'空
e0 //0
f8 7b //123
48 //untyped map ('H')
05 //
64 75 62 62 6f //dubbo
05 //
32 2e 30 2e 32 //2.0.2
5a //Z(map/list结束标记)
补充说明,如果调用是返回异常的话,在协议头中有一位是标记返回的 response类型的,具体可看DubboCodec中的0-5 response type。
对上的分析有疑问的,可看Hessian的序列化定义[2]和Dubbo的com.alibaba.com.caucho.hessian.io
包下的代码。不作赘叙。
服务接口兼容性分析及建议
调用转换分析
围绕服务接口兼容,这里细化为关注调用中的两个点,一是Hessian的相关Serializer的转换代码,二是包裹实际执行对象的Wrapper的条件限制。
拆分第一点:
- 请求时在provider的转码分析(有兴趣的同学,调试时可多注意JavaDeserializer和Hessian2Input这两个类)。
- 虽然Hessian在官方的文档里并没声明不同类型的字节码之间的转换,但是,Hessian的Java实现的核心转换类Hessian2Input,里面的不同类型的read方法一定程度上支持了类型转换。如readString支持读取boolean数据。
- 对于consumer传来的对象中存在provider不存在的属性,Dubbo的Serializer会readObject读取出来然后丢掉。对于,consumer传来的对象,缺少provider中对应对象的属性,此provider将对象的此类属性值,置为属性的缺省值。
- 请求返回时在consumer的转码分析。
- 和上面(1.)的规则是基本一致的。
第二点:
经arthas反编译出来的代理类(见附录1)中会对参数个数和类型作适当转换。
建议
假设有如下接口:
long getXXX(long a);
改动分析:
- 入参或返回值Long迭代为long没问题
- 入参和返回值long转其他基本数据类型(int、byte等),如上所述,虽然Hessian在官方的文档里并没声明不同类型的字节码之间的转换,但是,Hessian的Java实现的核心转换类Hessian2Input里面的不同类型的read方法一定程度上支持了类型转换。
- 入参的参数个数增加或减少,是服务迭代不友好的。
- 入参或返参迭代为一般对象也是服务迭代不友好的。
建议的用法:
XXXResponse getXXX(XXXRequest request);
由上一节分析,该种定义,当入参和返回对象发生属性个数变化时,是比较友好的。当然,对象中修改字段类型或直接修改对象的类路径也是服务迭代不友好的。
上面没提及‘服务迭代不友好的‘,都是可以相对平滑的重启的。
而上面所说的迭代不友好的(不兼容),可以用Dubbo多版本中介绍这种方法去迭代:
当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。
可以按照以下的步骤进行版本迁移:
- 在低压力时间段,先升级一半提供者为新版本
- 再将所有消费者升级为新版本
- 然后将剩下的一半提供者升级为新版本
这种做法,乍看可能觉得没什么不妥,但其实,这种迭代方式所带来的成本比较大。因为,要兼顾两个服务,至少需要两个人(当项目有一定规模甚至需要两个小组)在同一时间去重启,而按部就班地执行完这三步(不算上可能的回滚)起码也要半天。所以接口的定义尽量往可兼容的方向做。
最后,Dubbo文档中,Dubbo协议的约束一节,有如下官方的定义,比较清晰,可对照我的分析和如下描述来定义接口:
- 参数及返回值需实现 Serializable 接口
- 参数及返回值不能自定义实现 List, Map, Number, Date, Calendar 等接口,只能用 JDK 自带的实现,因为 hessian 会做特殊处理,自定义实现类中的属性值都会丢失。
- Hessian 序列化,只传成员属性值和值的类型,不传方法或静态变量,兼容情况 [1][2]:
数据通讯 情况 结果 A->B 类A多一种 属性(或者说类B少一种 属性) 不抛异常,A多的那 个属性的值,B没有, 其他正常 A->B 枚举A多一种 枚举(或者说B少一种 枚举),A使用多 出来的枚举进行传输 抛异常 A->B 枚举A多一种 枚举(或者说B少一种 枚举),A不使用 多出来的枚举进行传输 不抛异常,B正常接 收数据 A->B A和B的属性 名相同,但类型不相同 抛异常 A->B serialId 不相同 正常传输 接口增加方法,对客户端无影响,如果该方法不是客户端需要的,客户端不需要重新部署。输入参数和结果集中增加属性,对客户端无影响,如果客户端并不需要新属性,不用重新部署。
输入参数和结果集属性名变化,对客户端序列化无影响,但是如果客户端不重新部署,不管输入还是输出,属性名变化的属性值是获取不到的。
总结:服务器端和客户端对领域对象并不需要完全一致,而是按照最大匹配原则。
补充下,用到枚举的服务迭代要求:
如果入参的枚举字段加了值,需先重启provider,再重启consumer。如果返回值的枚举字段加了值,反之。
Reference
附录
附录一、经arthas反编译出来的,javassist生成的DemoService的代理类
$ jad org.apache.dubbo.common.bytecode.Wrapper0
ClassLoader:
+-sun.misc.Launcher$AppClassLoader@18b4aac2
+-sun.misc.Launcher$ExtClassLoader@aec6354
Location:
/C:/.m2/repository/org/apache/dubbo/dubbo/2.7.2/dubbo-2.7.2.jar
/*
* Decompiled with CFR 0_132.
*
* Could not load the following classes:
* net.teaho.demo.dubboheartbeattest.sdk.DemoService
* net.teaho.demo.dubboheartbeattest.sdk.GetFollowingListRequest
* net.teaho.demo.dubboheartbeattest.sdk.GetFollowingListResponse
* net.teaho.demo.dubboheartbeattest.sdk.TestRequest
* net.teaho.demo.dubboheartbeattest.sdk.TestResponse
*/
package org.apache.dubbo.common.bytecode;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import net.teaho.demo.dubboheartbeattest.sdk.DemoService;
import net.teaho.demo.dubboheartbeattest.sdk.GetFollowingListRequest;
import net.teaho.demo.dubboheartbeattest.sdk.GetFollowingListResponse;
import net.teaho.demo.dubboheartbeattest.sdk.TestRequest;
import net.teaho.demo.dubboheartbeattest.sdk.TestResponse;
import org.apache.dubbo.common.bytecode.ClassGenerator;
import org.apache.dubbo.common.bytecode.NoSuchMethodException;
import org.apache.dubbo.common.bytecode.NoSuchPropertyException;
import org.apache.dubbo.common.bytecode.Wrapper;
public class Wrapper0
extends Wrapper
implements ClassGenerator.DC {
public static String[] pns;
public static Map pts;
public static String[] mns;
public static String[] dmns;
public static Class[] mts0;
public static Class[] mts1;
public static Class[] mts2;
public static Class[] mts3;
@Override
public boolean hasProperty(String string) {
return pts.containsKey(string);
}
public Object invokeMethod(Object object, String string, Class[] arrclass, Object[] arrobject) throws InvocationTargetException {
DemoService demoService;
try {
demoService = (DemoService)object;
}
catch (Throwable throwable) {
throw new IllegalArgumentException(throwable);
}
try {
if ("test".equals(string) && arrclass.length == 1) {
return demoService.test((TestRequest)arrobject[0]);
}
if ("objectCompatibleTest".equals(string) && arrclass.length == 1) {
return demoService.objectCompatibleTest((GetFollowingListRequest)arrobject[0]);
}
if ("compatibleTest".equals(string) && arrclass.length == 1) {
return demoService.compatibleTest((Long)arrobject[0]);
}
if ("ping".equals(string) && arrclass.length == 1) {
return demoService.ping((String)arrobject[0]);
}
}
catch (Throwable throwable) {
throw new InvocationTargetException(throwable);
}
throw new NoSuchMethodException(new StringBuffer().append("Not found method \"").append(string).append("\" in class net.teaho.demo.dubboheartbeattest.sdk.DemoService.").toString());
}
public Class getPropertyType(String string) {
return (Class)pts.get(string);
}
@Override
public String[] getPropertyNames() {
return pns;
}
@Override
public Object getPropertyValue(Object object, String string) {
try {
DemoService demoService = (DemoService)object;
}
catch (Throwable throwable) {
throw new IllegalArgumentException(throwable);
}
throw new NoSuchPropertyException(new StringBuffer().append("Not found property \"").append(string).append("\" field or setter method in class net.teaho.demo.dubboheartbeattest.sdk.DemoService.").toString());
}
@Override
public void setPropertyValue(Object object, String string, Object object2) {
try {
DemoService demoService = (DemoService)object;
}
catch (Throwable throwable) {
throw new IllegalArgumentException(throwable);
}
throw new NoSuchPropertyException(new StringBuffer().append("Not found property \"").append(string).append("\" field or setter method in class net.teaho.demo.dubboheartbeattest.sdk.DemoService.").toString());
}
@Override
public String[] getMethodNames() {
return mns;
}
@Override
public String[] getDeclaredMethodNames() {
return dmns;
}
}
Affect(row-cnt:1) cost in 185 ms.