需求
目标:将所有微服务中的Dubbox2.8.4 升级到 Dubbo3.0,为Service Mesh做准备
方式:平滑升级、微服务按需逐个升级、Dubbo多版本共存、Dubbo共享组件多版本共存
现状:20多个微服务使用Dubbox版本相互RPC,共享Dubbo SPI扩展组件,多个开发团队维护不同的微服务
一、Dubbo多版本通信问题的解决
原生Apache Dubbo的版本升级一直保持着向下兼容,而Dubbox由于微调了通信编解码协议导致与原生Apache Dubbo无法正常通信。所以要想实现平滑升级,第一个要解决的问题就是多版本通信问题。
通过分析Dubbox的代码变更得知,通信协议的变更是为了支持更多特性,而新增的那些特性在我的项目里压根没有用到,所以我决定采用通过修改源代码将Dubbox恢复到原生Dubbo通信协议的方案来解决通信问题。
Dubbox编码协议的修改:
dubbo-rpc/dubbo-rpc-default/src/main/java/com/alibaba/dubbo/rpc/protocol/dubbo/DubboCodec.java
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data) throws IOException {
RpcInvocation inv = (RpcInvocation) data;
out.writeUTF(inv.getAttachment(Constants.DUBBO_VERSION_KEY, DUBBO_VERSION));
out.writeUTF(inv.getAttachment(Constants.PATH_KEY));
out.writeUTF(inv.getAttachment(Constants.VERSION_KEY));
out.writeUTF(inv.getMethodName());
// NOTICE modified by lishen
// TODO
// /* if (getSerialization(channel) instanceof OptimizedSerialization && !containComplexArguments(inv)) {
// out.writeInt(inv.getParameterTypes().length);
// } else {
// out.writeInt(-1);
// out.writeUTF(ReflectUtils.getDesc(inv.getParameterTypes()));
// }*/
//使用 dubbo 协议规范 过渡,add by lval 20210628
out.writeUTF(ReflectUtils.getDesc(inv.getParameterTypes()));
Object[] args = inv.getArguments();
if (args != null)
for (int i = 0; i < args.length; i++){
out.writeObject(encodeInvocationArgument(channel, inv, i));
}
out.writeObject(inv.getAttachments());
}
Dubbox解码协议的修改:
dubbo-rpc/dubbo-rpc-default/src/main/java/com/alibaba/dubbo/rpc/protocol/dubbo/DecodeableRpcInvocation.java
public Object decode(Channel channel, InputStream input) throws IOException {
ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
.deserialize(channel.getUrl(), input);
try {
setAttachment(Constants.DUBBO_VERSION_KEY, in.readUTF());
setAttachment(Constants.PATH_KEY, in.readUTF());
setAttachment(Constants.VERSION_KEY, in.readUTF());
setMethodName(in.readUTF());
try {
Object[] args;
Class<?>[] pts;
// NOTICE modified by lishen
// /* int argNum = in.readInt();
// if (argNum >= 0) {
// if (argNum == 0) {
// pts = DubboCodec.EMPTY_CLASS_ARRAY;
// args = DubboCodec.EMPTY_OBJECT_ARRAY;
// } else {
// args = new Object[argNum];
// pts = new Class[argNum];
// for (int i = 0; i < args.length; i++) {
// try {
// args[i] = in.readObject();
// pts[i] = args[i].getClass();
// } catch (Exception e) {
// if (log.isWarnEnabled()) {
// log.warn("Decode argument failed: " + e.getMessage(), e);
// }
// }
// }
// }
// } else {
// String desc = in.readUTF();
// if (desc.length() == 0) {
// pts = DubboCodec.EMPTY_CLASS_ARRAY;
// args = DubboCodec.EMPTY_OBJECT_ARRAY;
// } else {
// pts = ReflectUtils.desc2classArray(desc);
// args = new Object[pts.length];
// for (int i = 0; i < args.length; i++) {
// try {
// args[i] = in.readObject(pts[i]);
// } catch (Exception e) {
// if (log.isWarnEnabled()) {
// log.warn("Decode argument failed: " + e.getMessage(), e);
// }
// }
// }
// }
// }*/
//使用 dubbo 协议规范 过渡,add by lval 20210628
String desc = in.readUTF();
if (desc.length() == 0) {
pts = DubboCodec.EMPTY_CLASS_ARRAY;
args = DubboCodec.EMPTY_OBJECT_ARRAY;
} else {
pts = ReflectUtils.desc2classArray(desc);
args = new Object[pts.length];
for (int i = 0; i < args.length; i++) {
try {
args[i] = in.readObject(pts[i]);
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("Decode argument failed: " + e.getMessage(), e);
}
}
}
}
setParameterTypes(pts);
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 {
// modified by lishen
if (in instanceof Cleanable) {
((Cleanable) in).cleanup();
}
}
return this;
}
通过以上修改我们就可以按照下面的路线来推进了
二、共享组件的多版本共存来支撑平滑过渡升级
此次Dubbo升级为较大跨度升级,包名发生变化导致需要修改业务代码类上的import语句,还有个别的dubbo类位置发生了变化,需要修改业务类引用。所以共享组件需要有多个版本共存过渡来达到独立平滑升级的目的。
三、微服务依赖独立化
牵一发而动全身可能是很多项目的现状,因为共享了父工程的依赖管理以及微服务接口之间的网状依赖传递,导致无法一眼看出它的依赖来自于哪里,此时jar包冲突到处可见。有些jar包是从很远的微服务依赖传递过来的,你改了A服务的依赖无意间导致B服务不可用,牵一发而动全身由此诞生,导致无法独立升级。正确的姿势从微服务依赖高度自治性开始。
正确的实施方案:
1、如果是较大的项目,多个团队或者部门在维护不同的微服务,最好去除 微服务的Maven父子工程依赖共享(微服务内部的聚合工程可继续保留),这样可以使 微服务的自治性更好,依赖来源更清楚。如果是小型项目,团队很小,也可以继续使用Maven父子工程共享依赖管理。
2、试运行每一个微服务,依据测试情况,补充POM文件,确保依赖的jar都加入到微服务自身的pom中,如果出现类找不到等错误,就说明发生了版本冲突或者缺包,则应将正确版本的包坐标信息放到pom文件的最上方,依靠maven的依赖就近原则(依赖的深度越浅、声明的定义越靠前,优先级越高)自动覆盖 间接依赖的错误的版本;或者如果你知道了错误版本来自于哪个jar,可以通过<exclusions></exclusions>来排除错误的版本,但是极有可能还是解决不了问题,最简单的就是采用前面的方式。
四、Spring Boot平滑升级
在步骤二、三的基础上各个微服务可独立升级spring互不影响
五、Dubbo平滑升级
在步骤一、二、三的基础上各个微服务可独立升级dubbo互不影响
1、升级微服务A的dubbo版本到 3.0 版本,依赖的共享组件版本指向Dubbo3版本的共享组件
2、测试其它服务与微服务A的通信是否正常
3、循环步骤1、2,按需滚动升级其它微服务3.0版本
4、测试
5、升级完毕
滚动升级过程示意图
六、总结
通过架构的平滑滚动升级可以判断出你目前的项目是否真的符合微服务架构的理念,不是说你把单体应用拆分成了多个小应用上一个RPC框架就是微服务架构了。架构师要彻底理解微服务架构的优势在哪里,并向它靠拢,否则注定你在引入微服务劣势的同时不能发挥出它的优势,这是得不偿失的。
老吕认为在微服务架构中解耦大于共享是指导方针,当你想在微服务之间共享一些东西的时候,不妨思考一下:
1、如果微服务使用的不同的语言开发会怎样
2、微服务的自治性是否受到了破坏,还能否独立开发,独立测试,独立部署,独立上线,独立升级
3、微服务的灵魂是否还健在
升级架构不难,难的是兼容已有业务代码、平滑、可控、按需、滚动升级。
今天就到这里,感谢大家关注老吕架构。