diff --git a/docs/content/zh-cn/docs/contribution-guidelines/runtime/dubbo2.7.md b/docs/content/zh-cn/docs/contribution-guidelines/runtime/dubbo2.7.md new file mode 100644 index 000000000..f9cd83238 --- /dev/null +++ b/docs/content/zh-cn/docs/contribution-guidelines/runtime/dubbo2.7.md @@ -0,0 +1,194 @@ +--- +title: dubbo2.7 的多模块化适配 +date: 2024-1-19T19:55:35+08:00 +weight: 1 +--- + +## 为什么需要做适配 +原生 dubbo2.7 在多模块场景下,无法支持模块发布自己的dubbo服务,调用时存在序列化、类加载异常等一系列问题。 + +## 多模块适配方案 + +dubbo2.7多模块适配SDK +```xml + + com.alipay.sofa.serverless + sofa-serverless-adapter-dubbo2.7 + 0.5.7-SNAPSHOT + +``` + +主要从类加载、服务发布、服务卸载、服务隔离、模块维度服务管理、配置管理、序列化等方面进行适配。 + +### 1. AnnotatedBeanDefinitionRegistryUtils使用基座classloader无法加载模块类 +com.alibaba.spring.util.AnnotatedBeanDefinitionRegistryUtils#isPresentBean + +```java +public static boolean isPresentBean(BeanDefinitionRegistry registry, Class annotatedClass) { + ... + + // ClassLoader classLoader = annotatedClass.getClassLoader(); // 原生逻辑 + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // 改为使用tccl加载类 + + for (String beanName : beanNames) { + BeanDefinition beanDefinition = registry.getBeanDefinition(beanName); + if (beanDefinition instanceof AnnotatedBeanDefinition) { + ... + String className = annotationMetadata.getClassName(); + Class targetClass = resolveClassName(className, classLoader); + ... + } + } + + return present; +} +``` + +### 2. 模块维度的服务、配置资源管理 +1. com.alipay.sofa.serverless.support.dubbo.ServerlessServiceRepository 替代原生 org.apache.dubbo.rpc.model.ServiceRepository + +原生service采用interfaceName作为缓存,在基座、模块发布同样interface,不同group服务时,无法区分,替代原生service缓存模型,采用Interface Class类型作为key,同时采用包含有group的path作为key,支持基座、模块发布同interface不同group的场景 +```java +private static ConcurrentMap, ServiceDescriptor> globalClassServices = new ConcurrentHashMap<>(); + +private static ConcurrentMap globalPathServices = new ConcurrentHashMap<>(); +``` + +2. com.alipay.sofa.serverless.support.dubbo.ServerlessConfigManager 替代原生 org.apache.dubbo.config.context.ConfigManager + + 为原生config添加classloader维度的key,不同模块根据classloader隔离不同的配置 + +```java +final Map>> globalConfigsCache = new HashMap<>(); + +public void addConfig(AbstractConfig config, boolean unique) { + ... + write(() -> { + Map configsMap = getCurrentConfigsCache().computeIfAbsent(getTagName(config.getClass()), type -> newMap()); + addIfAbsent(config, configsMap, unique); + }); +} +private Map> getCurrentConfigsCache() { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); // 根据当前线程classloader隔离不同配置缓存 + globalConfigsCache.computeIfAbsent(contextClassLoader, k -> new HashMap<>()); + return globalConfigsCache.get(contextClassLoader); +} +``` + +ServerlessServiceRepository 和 ServerlessConfigManager 都依赖 dubbo ExtensionLoader 的扩展机制,从而替代原生逻辑,具体原理可参考 org.apache.dubbo.common.extension.ExtensionLoader.createExtension + +### 3. 模块维度服务发布、服务卸载 +override DubboBootstrapApplicationListener 禁止原生dubbo模块启动、卸载时发布、卸载服务 + +- com.alipay.sofa.serverless.support.dubbo.BizDubboBootstrapListener + +原生dubbo2.7只在基座启动完成后发布dubbo服务,在多模块时,无法支持模块的服务发布,Ark采用监听器监听模块启动事件,并手动调用dubbo进行模块维度的服务发布 + +```java +private void onContextRefreshedEvent(ContextRefreshedEvent event) { + try { + ReflectionUtils.getMethod(DubboBootstrap.class, "exportServices") + .invoke(dubboBootstrap); + ReflectionUtils.getMethod(DubboBootstrap.class, "referServices").invoke(dubboBootstrap); + } catch (Exception e) { + + } +} +``` + +原生dubbo2.7在模块卸载时会调用DubboShutdownHook,将JVM中所有dubbo service unexport,导致模块卸载后基座、其余模块服务均被卸载,Ark采用监听器监听模块spring上下文关闭事件,手动卸载当前模块的dubbo服务,保留基座、其余模块的dubbo服务 + +```java +private void onContextClosedEvent(ContextClosedEvent event) { + // DubboBootstrap.unexportServices 会 unexport 所有服务,只需要 unexport 当前 biz 的服务即可 + Map> exportedServices = ReflectionUtils.getField(dubboBootstrap, DubboBootstrap.class, "exportedServices"); + + Set bizUnexportServices = new HashSet<>(); + for (Map.Entry> entry : exportedServices.entrySet()) { + String serviceKey = entry.getKey(); + ServiceConfigBase sc = entry.getValue(); + if (sc.getRef().getClass().getClassLoader() == Thread.currentThread().getContextClassLoader()) { // 根据ref服务实现的类加载器区分模块服务 + bizUnexportServices.add(serviceKey); + configManager.removeConfig(sc); // 从configManager配置管理中移除服务配置 + sc.unexport(); // 进行服务unexport + serviceRepository.unregisterService(sc.getUniqueServiceName()); // 从serviceRepository服务管理中移除配置 + } + } + for (String service : bizUnexportServices) { + exportedServices.remove(service); // 从DubboBootstrap中移除该service + } + } +``` + +### 4. 服务路由 +- com.alipay.sofa.serverless.support.dubbo.ConsumerRedefinePathFilter + +dubbo服务调用时通过path从ServiceRepository中获取正确的服务端服务模型(包括interface、param、return类型等)进行服务调用、参数、返回值的序列化,原生dubbo2.7采用interfaceName作为path查找service model,无法支持多模块下基座模块发布同interface的场景,Ark自定义consumer端filter添加group信息到path中,以便provider端进行正确的服务路由 + +```java +public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { + if (invocation instanceof RpcInvocation) { + RpcInvocation rpcInvocation = (RpcInvocation) invocation; + // 原生path为interfaceName,如com.alipay.sofa.rpc.dubbo27.model.DemoService + // 修改后path为serviceUniqueName,如masterBiz/com.alipay.sofa.rpc.dubbo27.model.DemoService + rpcInvocation.setAttachment("interface", rpcInvocation.getTargetServiceUniqueName()); // 原生path为interfaceName,如 + } + return invoker.invoke(invocation); +} +``` + +### 5. 序列化 +- org.apache.dubbo.common.serialize.java.JavaSerialization +- org.apache.dubbo.common.serialize.java.ClassLoaderJavaObjectInput +- org.apache.dubbo.common.serialize.java.ClassLoaderObjectInputStream + +在获取序列化工具JavaSerialization时,使用ClassLoaderJavaObjectInput替代原生JavaObjectInput,传递provider端service classloader信息 + +```java +// org.apache.dubbo.common.serialize.java.JavaSerialization +public ObjectInput deserialize(URL url, InputStream is) throws IOException { + return new ClassLoaderJavaObjectInput(new ClassLoaderObjectInputStream(null, is)); // 使用ClassLoaderJavaObjectInput替代原生JavaObjectInput,传递provider端service classloader信息 +} + +// org.apache.dubbo.common.serialize.java.ClassLoaderObjectInputStream +private ClassLoader classLoader; + +public ClassLoaderObjectInputStream(final ClassLoader classLoader, final InputStream inputStream) { + super(inputStream); + this.classLoader = classLoader; +} +``` + +- org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation 服务端反序列化参数 + +```java +// patch begin +if (in instanceof ClassLoaderJavaObjectInput) { + InputStream is = ((ClassLoaderJavaObjectInput) in).getInputStream(); + if (is instanceof ClassLoaderObjectInputStream) { + ClassLoader cl = serviceDescriptor.getServiceInterfaceClass().getClassLoader(); // 设置provider端service classloader信息到ClassLoaderObjectInputStream中 + ((ClassLoaderObjectInputStream) is).setClassLoader(cl); + } +} +// patch end +``` +- org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcResult 客户端反序列化返回值 + +```java +// patch begin +if (in instanceof ClassLoaderJavaObjectInput) { + InputStream is = ((ClassLoaderJavaObjectInput) in).getInputStream(); + if (is instanceof ClassLoaderObjectInputStream) { + ClassLoader cl = invocation.getInvoker().getInterface().getClassLoader(); // 设置consumer端service classloader信息到ClassLoaderObjectInputStream中 + ((ClassLoaderObjectInputStream) is).setClassLoader(cl); + } +} +// patch end +``` + +## 多模块 dubbo2.7 使用样例 + +[多模块 dubbo2.7 使用样例](https://github.com/sofastack/sofa-serverless/tree/master/samples/dubbo-samples/rpc/dubbo27/README.md) + +[dubbo2.7多模块适配sdk源码](https://github.com/sofastack/sofa-serverless/tree/master/sofa-serverless-runtime/sofa-serverless-adapter-ext/sofa-serverless-adapter-dubbo2.7) +