服务自省,Dubbo面向了应用级
微信公众号:九点半的马拉
路途虽遥远,将来更美好
学海无涯,大家一起加油!
Dubbo是一款很优秀的RPC框架,目前Github的Star数已经达到34.6k,有效的反映出它的受欢迎程度。Dubbo提供高性能的基于代理的远程调用能力,服务以接口
为粒度,为开发者屏蔽远程调用底层细节。Dubbo设计的稳定架构为数万服务的稳定运行提供了坚实的基础。
Dubbo的传统架构
对于传统架构,Dubbo主要可以分为3个组件:Consumer
、Provider
和Registry
,Monitor
组件不是较为重要的组件,主要负责服务相关数据的统计与查询,可以忽略掉。
Provider为服务提供者,在启动时会根据设置的协议(如:Dubbo协议),将服务进行服务暴露,生成对应的Invoker(AbstractProxyInvoker),并存储在HashMap中,key为端口、接口名、接口版本和接口分组组成的字符串。最后将服务元数据信息注册到注册中心。
Consumer为服务消费者,从注册中心订阅要引用的服务元数据信息,封装成DubboInvoker,最后通过动态代理转换成一个代理对象。
在服务消费时,它会通过上述的代理对象进行调用,在调用之前会通过Directory获取所有可以调用的远程服务Invoker列表,然后通过负载均衡策略选择出一个进行调用,在具体的调用之前,它会经过一系列的Filter调用链,可以进行处理上下文,限流,回声检测,超时日志打印等。
传统结构的不足
从上面的分析中我们可以看出,传统的Dubbo架构过多依赖注册中心,当注册的服务元数据信息发生变化时,Consumer通过订阅感知信息变化后,会从注册中心重新拉取信息,如果变化频繁,对其网络也造成了一定的压力。同时,注册中心存储的是接口级别的服务信息,容易造成数据存储容量骤增,较多的信息存在冗余。
现在云原生技术兴起,Spring Cloud等提供了面向应用的服务注册与发现。从Dubbo2.7.5
开始,Dubbo开始提出一种服务自省
的概念,开始提供面向应用级别的服务注册与发现。此时,服务的维度从接口级别降到了应用级别,注册中心存储应用级别的信息,数据容量大幅度减少。
假设定义了两个服务DemoService和RoadService,传统架构下的节点信息和服务自省下的节点信息如下所示(选用Zookeeper作为注册中心)
传统节点.png上图是传统架构下的注册中心节点。它主要是一种树形结构,该结构分为四层。
/dubbo
作为服务信息的根节点
org.apache.dubbo.demo.DemoService
和org.apache.dubbo.demo.RoadService
作为第二级节点,表示服务的全限定接口名,
comsumers
、configcurators
、providers
和routors
为第三级节点,其中:
consumers下的子节点表示多个消费者URL元数据信息;
configcurators下的子节点包含多个用于提供者动态配置URL元数据信息;
providers下的子节点包含多个服务提供者URL元数据信息
routors下的子节点包含用于多个消费者路由策略URL元数据信息。
服务自省节点.png上图是服务自省机制下的注册中心节点信息。(注意,为了方便,我们将Zookeeper也作为配置中心使用)
在最外层节点中,有两个重要的节点dubbo
和service
对于service节点
它主要存储的不同应用的元数据信息。比如,dubbo-zookeeper-service-introspection-provider-sample
表示一个应用名,它的子节点名是该应用的ip地址,节点值存储的是有关元数据信息。
对于dubbo节点
最重要的是config
节点,该节点是配置中心的节点,下面的子节点org.apache.dubbo.spring.boot.sample.consumer.DemoService
和org.apache.dubbo.spring.boot.sample.consumer.RoadServce
表示Dubbo服务接口名,下面的子节点表示对应的提供对应服务的应用名。这样就做到了Dubbo服务接口与应用的映射。
服务自省在服务端和消费端都有体现,出于篇幅的考虑,将服务自省拆分为服务端和消费端,各自用部分篇幅介绍。
服务端的服务自省部分
DubboBootstrap主要处理dubbo所有的配置信息,当调用start方法时,表示dubbo进行启动,服务暴露和服务自省等均在该方法中执行。
执行时机
我们可以使用注解@EnableDubbo
开启Dubbo,其中包含了@DubboComponentScan
注解。
通过@DubboComponentScan
引入类DubboComponentScanRegistrar,后续创建了ServiceAnnotationBeanPostProcessor
后置处理器,它实现了BeanDefinitionRegistryPostProcessor
接口,Spring容器中的所有Bean注册之后会回调postProcessBeanDefinitionRegistry
方法,将@Service
注解的服务BeanDefinition注册到spring容器中,Spring容器启动完成后会发布ContextRefreshEvent
事件,DubboBootstrapApplicationListener
类会监听到该事件,之后调用dubboBootstrap.start
方法,此时开启了Dubbo相关逻辑。
我们对其中的几个重要方法进行解释。
1. exportServices方法
在exportServices方法中进行服务暴露,这是一个很重要的额工作。
我们对定义的一个ServiceBean变量进行解释,即上面提到的DemoService。比如:
beanName : ServiceBean:org.apache.dubbo.spring.boot.sample.consumer.DemoService:1.0.0
interfaceName : org.apache.dubbo.spring.boot.sample.consumer.DemoService
ref : 实现该接口的具体类
id: ServiceBean:org.apache.dubbo.spring.boot.sample.consumer.DemoService:1.0.0
code5.png code3.png获取到具体的ServiceConfig
对象后,开始执行它的export方法,进行服务暴露。
在具体的服务暴露之前,会更新一些相关的serviceMetadata,然后调用doExportUrls方法进行暴露。
code6.png2. doExportUrls方法
2.1)获取ServiceRepository对象,(该对象记录发布的服务信息,客户端需要访问的服务),从中获取 ServiceDescriptor对象存储暴露的服务
public class ApplicationModel {
private static final ExtensionLoader<FrameworkExt> LOADER = ExtensionLoader.getExtensionLoader(FrameworkExt.class);
public static ServiceRepository getServiceRepository() {
return (ServiceRepository)LOADER.getExtension("repository");
}
}
public ServiceDescriptor registerService(Class<?> interfaceClazz) {
return (ServiceDescriptor)this.services.computeIfAbsent(interfaceClazz.getName(), (_k) -> {
return new ServiceDescriptor(interfaceClazz);
});
}
code7.png
2.2)在ServiceRepository注册服务提供者的详细信息
code8.png在ServiceRepository中有一个ConcurrentHashMap,key为服务接口名+":"+group+":"+":"+version组成的,value为ProviderModel,主要包含接口的实现实例,上述的serviceDescriptor,serviceConfig和seviceMetadata
2.3)获取当前服务对应的注册中心实例,本案例中只设置了Zookeeper,其中最重要的一点是会判断是否开启了服务自省,最终的URL的协议头是不一样的,在开启时,添加一个变量service-discovery-registry=service
private static String extractRegistryType(URL url) {
return UrlUtils.isServiceDiscoveryRegistryType(url) ? "service-discovery-registry" : "registry";
}
public static boolean isServiceDiscoveryRegistryType(Map<String, String> parameters) {
return parameters != null && !parameters.isEmpty() ? "service".equals(parameters.get("registry-type")) : false;
}
开启了服务自省后,对应的URL样例:
service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-zookeeper-service-introspection-provider-sample&dubbo=2.0.2&metadata-type=composite&pid=14508&qos.enable=false®istry=zookeeper®istry-type=service&release=2.7.8×tamp=1612096358401
如果是原先普通的话,开头 的协议应该为registry
2.4)获取ProtocolConfig,调用doExportUrlsFor1Protocol方法进行暴露。
code10.png2.5)获取元数据注册中心
2.6)封装URL进行本地服务暴露,通过动态代理转换成Invoker,这几步和之前的相同
2.7)调用protocol.export
方法进行服务暴露,开启了服务自省,协议为service-discovery-registry
具体调用RegistryProtocol.export方法,里面有一步要将服务信息注册到注册中心,这里有一些区别。
服务自省是获取ServiceDiscoveryRegistry类,而普通的是ZookeeperRegistry
在这里主要简单讲下服务自省下的服务信息注册。
注册.png2.8)根据serviceKey找到对应的ProviderModel,将url添加进去
2.9)获取WritableMetadataService,默认是(InMemoryWritableMetadataService),调用publishServiceDefinition方法,将url中的pid,timestamp,bind.ip,bind.port等参数字段移除掉,读取side字段,判断是provider端还是consumer端,根据url生成ServiceDefinition,并生成json字符串,并存放到InMemoryWritableMetadataService的serviceDefinitions(ConcurrentSkipListMap)中。
2.10)发布ServiceConfigExportedEvent事件和ServiceBeanExportedEvent事件
在上面介绍服务自省下的Zookeeper树型图时存在一个/dubbo/mapping/接口名/应用名
的路径配置,进行接口名和应用名的映射,那它是在什么时候产生的呢?
这时就要借助上述的ServiceConfigExportedEvent事件。
ServiceNameMappingListener监听该事件,调用onEvent方法
code12.png然后调用DynamicConfigurationServiceNameMapping的map方法。
其中,MetadataService服务信息不发布到配置中心,该类在服务自省中发挥着重要的作用,具体的解释在以后的篇幅解释。
private static final List<String> IGNORED_SERVICE_INTERFACES = Arrays.asList(MetadataService.class.getName());
public void map(URL exportedURL) {
String serviceInterface = exportedURL.getServiceInterface();
// MetadataService服务信息不发布到配置中心
if (!IGNORED_SERVICE_INTERFACES.contains(serviceInterface)) {
String group = exportedURL.getParameter("group");
String version = exportedURL.getParameter("version");
String protocol = exportedURL.getProtocol();
String key = ApplicationModel.getName();
String content = String.valueOf(System.currentTimeMillis());
this.execute(() -> {
// 将服务信息发布到配置中心,可能有多个配置中心 DynamicConfiguration.getDynamicConfiguration().publishConfig(key, buildGroup(serviceInterface, group, version, protocol), content);
}
});
}
}
上述基本为服务暴露的过程,继续讲解Bootstrap.start()方法的接下来的步骤。
4.registerServiceInstance方法
code13.png当开启服务自省后,MetadataService中会存储暴露的url信息,
此时,会调用内部的exportMetadataService()和registerServiceInstance()方法。
在exportMetadataService方法中,会暴露相关的metadataServiceExporter,主要有两个,即ConfigurableMetadataServiceExporter和RemoteMetadataServiceExporter。
在样例的配置信息设置了dubbo.application.metadata-type=composite
信息,默认值为local,
当为local时,暴露ConfigurableMetadataServiceExporter类,具体的暴露过程与上述的暴露过程一致,具体的暴露方法如下所示:
code15.png
当为composite时,除了暴露上面的exporter,另外也暴露RemoteMetadataServiceExporter类,
在暴露该类时,不会将信息注册到配置中心中,这是与普通的服务的重要区别之一。
之后,执行registerServiceInstance方法。
在上述的Zookeeper树形图中存在一个类似/service/应用名/host:port
的路径,通过该路径,我们可以把应用相关的信息记录下来,进而可以面向应用调用,而不是往常的面向接口调用,存储容量大大降低。
在该方法中主要是获取服务端发布的任一一个服务URL,从中提取出host和port,然后与应用名等信息创建一个ServiceInstance,具体结构如下所示:
code14.png然后遍历具体的ServiceDiscovery.register方法,样例配置的是ZookeeperServiceDiscovery,将ServiceInstace信息注册到上述路径中。
至此,在服务端的启动部分就介绍结束了,在另一篇中,将会介绍在消费端的服务自省部分。