本文基于dubbo 2.7.5版本代码
dubbo在传统服务发现功能的基础上,提供了服务自省模式的服务发现,下面简称为自省服务发现。
如果要使用自省服务发现功能,需要使用如下配置:
dubbo.registry.address=zookeeper://localhost:2181
dubbo.registry.parameters[registry-type]=service
dubbo在启动的时候,会将dubbo实例注册到注册中心,注册中心的ip地址端口以及注册中心的类型由上面的第一行配置指定,这里的注册中心使用的是zk,本文介绍也以zk为例。
文章目录
- 一、ServiceDiscoveryRegistry
- 二、ServiceDiscoveryRegistry之服务注册
- 三、ServiceDiscoveryRegistry之服务订阅
- 四、ServiceInstance注册
一、ServiceDiscoveryRegistry
在自省服务发现中,服务端注册发布的服务,客户端注册引用的服务,均使用ServiceDiscoveryRegistry完成注册。
下面我们分析一下这个类的构造方法。
public ServiceDiscoveryRegistry(URL registryURL) {
super(registryURL);
//创建ServiceDiscovery对象
//后文详细分析该对象的创建
this.serviceDiscovery = createServiceDiscovery(registryURL);
//subscribedServices保存提供对应服务的应用名,
//可以通过RegistryConfig或者ApplicationConfig的parameters属性设置参数“subscribed-services”值,一般无需设置该参数
this.subscribedServices = parseServices(registryURL.getParameter(SUBSCRIBED_SERVICE_NAMES_KEY));
//服务名匹配,实现类是DynamicConfigurationServiceNameMapping
//serviceNameMapping访问配置中心获取匹配的服务提供者
this.serviceNameMapping = ServiceNameMapping.getDefaultExtension();
String metadataStorageType = getMetadataStorageType(registryURL);
//获取元数据中心对象。默认是InMemoryWritableMetadataService,
//可以通过参数“dubbo.metadata.storage-type”修改,一共两个值:local和remote
this.writableMetadataService = WritableMetadataService.getExtension(metadataStorageType);
//rest协议服务使用,用于合成rest协议服务的URL
this.subscribedURLsSynthesizers = initSubscribedURLsSynthesizers();
}
ServiceDiscovery类是自省服务发现中非常重要的类,构造方法中通过createServiceDiscovery创建出ServiceDiscovery对象:
protected ServiceDiscovery createServiceDiscovery(URL registryURL) {
//根据入参registryURL中协议使用SPI加载ServiceDiscoveryFactory对象,
//通过ServiceDiscoveryFactory创建ServiceDiscovery对象
//ServiceDiscoveryFactory只有一个实现类DefaultServiceDiscoveryFactory,SPI的名字是default
//方法代码见[2]
ServiceDiscovery originalServiceDiscovery = getServiceDiscovery(registryURL);
//使用EventPublishingServiceDiscovery装饰ServiceDiscovery,
//EventPublishingServiceDiscovery可以在调用ServiceDiscovery的某些方法时,发布对应的事件,这些事件用于打印日志
//方法代码见[3]
ServiceDiscovery serviceDiscovery = enhanceEventPublishing(originalServiceDiscovery);
execute(() -> {
//初始化ServiceDiscovery,该方法解析后文。
serviceDiscovery.initialize(registryURL.addParameter(INTERFACE_KEY, ServiceDiscovery.class.getName())
.removeParameter(REGISTRY_TYPE_KEY)); [1]
});
return serviceDiscovery;
}
[2]
private ServiceDiscovery getServiceDiscovery(URL registryURL) {
//ServiceDiscoveryFactory只有一个实现类DefaultServiceDiscoveryFactory
ServiceDiscoveryFactory factory = getExtension(registryURL);
//使用SPI加载ServiceDiscovery实现类,ServiceDiscovery有哪些实现类可以参见文件org.apache.dubbo.registry.client.ServiceDiscovery,文件内容在代码下方。
//加载哪个实现类由dubbo.registry.address的协议指定,这里使用的协议是zookeeper
return factory.getServiceDiscovery(registryURL);
}
[3]
private ServiceDiscovery enhanceEventPublishing(ServiceDiscovery original) {
return new EventPublishingServiceDiscovery(original);
}
org.apache.dubbo.registry.client.ServiceDiscovery文件内容如下:
file=org.apache.dubbo.registry.client.FileSystemServiceDiscovery
zookeeper=org.apache.dubbo.registry.zookeeper.ZookeeperServiceDiscovery
consul=org.apache.dubbo.registry.consul.ConsulServiceDiscovery
etcd3=org.apache.dubbo.registry.etcd.EtcdServiceDiscovery
nacos=org.apache.dubbo.registry.nacos.NacosServiceDiscovery
上面代码创建ServiceDiscovery对象,在本文中以ZookeeperServiceDiscovery为例,创建出ZookeeperServiceDiscovery对象之后,需要执行[1]处代码对ZookeeperServiceDiscovery初始化。
public void initialize(URL registryURL) throws Exception {
//创建事件分发器
this.dispatcher = EventDispatcher.getDefaultExtension();
//ZookeeperServiceDiscovery实现EventListener接口,
//本对象监听事件ServiceInstancesChangedEvent,下面代码将本对象注册为监听器
this.dispatcher.addEventListener(this);
//使用CuratorFramework连接zk
this.curatorFramework = buildCuratorFramework(registryURL);
//获取zk的根路径,自省服务发现使用的数据存储在该目录下。
//默认根路径是/services,可以通过rootPath参数修改根路径。
this.rootPath = ROOT_PATH.getParameterValue(registryURL);
//创建ServiceDiscoveryImpl对象,ServiceDiscoveryImpl是Curator提供的
this.serviceDiscovery = buildServiceDiscovery(curatorFramework, rootPath);
//启动ServiceDiscoveryImpl
this.serviceDiscovery.start();
}
Curator框架提供服务发现功能,dubbo的自省服务发现基于Curator。Curator的服务发现功能可以参见文章:
ZookeeperServiceDiscovery的initialize方法主要作用是:建立与zk的连接,设置数据存储的根目录,设置本对象监听事件ServiceInstancesChangedEvent。
到此为止,ServiceDiscoveryRegistry对象完全创建完毕。该对象中有两个重要方法:subscribe和register,分别用于服务订阅和服务注册。
二、ServiceDiscoveryRegistry之服务注册
服务注册由ServiceDiscoveryRegistry的register方法完成,在以下两个场景调用该方法:
- 服务端暴露服务,将被暴露的服务协议、IP、参数、接口名等信息注册到元数据中心;
- 服务端发布MetadataService服务,该服务发布是在DubboBootstrap的start方法中调用的;
下面解析注册过程:
public final void register(URL url) {
if (!shouldRegister(url)) { //只有服务端才可以注册
return;
}
//调用父类FailbackRegistry的register方法,
//父类再调用ServiceDiscoveryRegistry的doRegister方法
//FailbackRegistry的作用是当调用子类的doRegister失败时,FailbackRegistry可以发起重调,默认是每5s调用一次,定时器使用时间轮算法。
super.register(url);
}
protected boolean shouldRegister(URL providerURL) {
String side = providerURL.getParameter(SIDE_KEY);
boolean should = PROVIDER_SIDE.equals(side); //只有服务端才可以注册
//代码删减
return should;
}
上面的register方法最终调用ServiceDiscoveryRegistry的doRegister方法:
public void doRegister(URL url) {
//writableMetadataService默认实现是InMemoryWritableMetadataService,
//下面的代码exportURL方法将入参url注册到元数据中心
if (writableMetadataService.exportURL(url)) {
if (logger.isInfoEnabled()) {
logger.info(format("The URL[%s] registered successfully.", url.toString()));
}
}
} else {
if (logger.isWarnEnabled()) {
logger.info(format("The URL[%s] has been registered.", url.toString()));
}
}
}
doRegister调用WritableMetadataService的exportURL方法将url注册到元数据中心,前面文章介绍WritableMetadataService时,WritableMetadataService有三个实现类:InMemoryWritableMetadataService、RemoteWritableMetadataServiceDelegate、RemoteWritableMetadataService。
RemoteWritableMetadataService的exportURL方法如下:
public boolean exportURL(URL url) {
return true;
}
InMemoryWritableMetadataService的exportURL方法如下:
public boolean exportURL(URL url) {
return addURL(exportedServiceURLs, url);
}
//下面的代码将被注册的url添加到SortedSet集合中
boolean addURL(Map<String, SortedSet<URL>> serviceURLs, URL url) {
return executeMutually(() -> {
SortedSet<URL> urls = serviceURLs.computeIfAbsent(url.getServiceKey(), this::newSortedURLs);
// make sure the parameters of tmpUrl is variable
return urls.add(url);
});
}
RemoteWritableMetadataServiceDelegate的exportURL则是分别调用上述两个类的exportURL方法,当两个类都返回true时,RemoteWritableMetadataServiceDelegate才返回true。
从上面代码分析可以看出,服务注册是将服务端暴露的服务元数据注册到元数据中心,但是仅仅注册到本地内存的元数据中心,这些数据供消费端做服务订阅使用。
三、ServiceDiscoveryRegistry之服务订阅
服务订阅调用subscribe方法,服务订阅在以下场景中调用:
- 消费端创建远程服务代理过程中调用subscribe方法建立与远程服务的连接;
- 消费端发布MetadataService服务时调用subscribe方法建立与远程服务的连接。
subscribe方法代码如下:
public final void subscribe(URL url, NotifyListener listener) {
if (!shouldSubscribe(url)) { //判断是否是消费端,只有消费端才可以调用subscribe方法
return;
}
//调用父类FailbackRegistry的subscribe方法
super.subscribe(url, listener);
}
super.subscribe调用父类FailbackRegistry的subscribe,然后在父类中调用ServiceDiscoveryRegistry的doSubscribe方法:
public void doSubscribe(URL url, NotifyListener listener) {
subscribeURLs(url, listener);
}
protected void subscribeURLs(URL url, NotifyListener listener) {
//subscribeURL方法与前文介绍的exportURL方法类似。
writableMetadataService.subscribeURL(url);
//查找提供服务的应用名,方法代码见[1]处
Set<String> serviceNames = getServices(url);
if (CollectionUtils.isEmpty(serviceNames)) {
throw new IllegalStateException("Should has at least one way to know which services this interface belongs to, subscription url: " + url);
}
//遍历上面找到的应用名,方法代码看下面[2]处
//subscribeURLs作用是根据应用名找到dubbo实例信息,并根据实例信息建立与服务端的连接
serviceNames.forEach(serviceName -> subscribeURLs(url, listener, serviceName));
}
[1]
//getServices方法用于查找提供所需服务的应用名,
//一共有三种查找方式:1、参数“provided-by”指定,2、从配置中心获取,3、参数“subscribed-services”指定
protected Set<String> getServices(URL subscribedURL) {
Set<String> subscribedServices = new LinkedHashSet<>();
//可以通过参数“provided-by”指定应用名,如果有多个应用,应用名之间使用“,”分隔
String serviceNames = subscribedURL.getParameter(PROVIDED_BY);
if (StringUtils.isNotEmpty(serviceNames)) {
subscribedServices = parseServices(serviceNames);
}
if (isEmpty(subscribedServices)) {
//访问配置中心获取应用名,该应用名是由ServiceNameMappingListener发布到配置中心的
subscribedServices = findMappedServices(subscribedURL);
if (isEmpty(subscribedServices)) {
//如果上述两种方式都找不到合适的应用,
//那么使用参数“subscribed-services”指定的应用,如果有多个应用,之间使用“,”分隔
subscribedServices = getSubscribedServices();
}
}
return subscribedServices;
}
[2]
protected void subscribeURLs(URL url, NotifyListener listener, String serviceName) {
//根据应用名从注册中心找到服务实例ServiceInstance,
//ServiceInstance的注册过程在后文介绍。ServiceInstance里面记录有ip地址,端口等信息
List<ServiceInstance> serviceInstances = serviceDiscovery.getInstances(serviceName);
//根据ServiceInstance里面的ip地址等信息,访问服务端的MetadataService服务的getExportedURLs方法,
//从该方法中得到服务端发布的所有的服务元数据信息,并保存到ServiceDiscoveryRegistry的serviceRevisionExportedURLsCache属性中。
//将服务元数据信息保存到serviceRevisionExportedURLsCache中的作用是:
//后续其他的需要引用同一个dubbo实例服务的,便可以直接从
//serviceRevisionExportedURLsCache获取可用服务元数据信息,而不用访问远程服务。
//当获取了元数据服务信息后,dubbo根据这些信息建立与服务端的连接,
//也就是创建连接远程服务的Invoker对象,消费端访问服务时,就是通过这些Invoker对象完成的。
subscribeURLs(url, listener, serviceName, serviceInstances);
//设置监听器监听目录/services/应用名,当该目录下数据发生变化时,通过调用subscribeURLs方法
//重新建立对远程服务的引用
registerServiceInstancesChangedListener(url, new ServiceInstancesChangedListener(serviceName) {
@Override
public void onEvent(ServiceInstancesChangedEvent event) {
subscribeURLs(url, listener, event.getServiceName(), new ArrayList<>(event.getServiceInstances()));
}
});
}
服务订阅功能总体来说,完成了如下几件事:
- 获取提供远程服务的实例信息;
- 建立与远程服务的连接;
- 监听指定目录,当目录数据发生变化时,重新建立与远程的连接。
四、ServiceInstance注册
消费端做服务订阅时,根据应用名从注册中心查找对应的服务实例ServiceInstance对象。下面介绍一下ServiceInstance对象如何注册到注册中心的。
ServiceInstance对象是服务端做DubboBootstrap启动时,调用registerServiceInstance方法注册的,代码如下:
private void registerServiceInstance() {
//判断是否使用自省服务发现
if (CollectionUtils.isEmpty(getServiceDiscoveries())) {
return;
}
ApplicationConfig application = getApplication();
String serviceName = application.getName();
//获取服务端发布的任意一个服务URL
URL exportedURL = selectMetadataServiceExportedURL();
String host = exportedURL.getHost();
int port = exportedURL.getPort();
//创建DefaultServiceInstance对象,该对象持有IP、端口、应用名。
ServiceInstance serviceInstance = createServiceInstance(serviceName, host, port);
//遍历ServiceDiscovery对象,每个ServiceDiscovery实现类都由EventPublishingServiceDiscovery封装,
//所以这里调用的都是EventPublishingServiceDiscovery的register方法
getServiceDiscoveries().forEach(serviceDiscovery -> serviceDiscovery.register(serviceInstance));
}
private ServiceInstance createServiceInstance(String serviceName, String host, int port) {
this.serviceInstance = new DefaultServiceInstance(serviceName, host, port);
setMetadataStorageType(serviceInstance, getMetadataType());
return this.serviceInstance;
}
registerServiceInstance通过EventPublishingServiceDiscovery调用到ZookeeperServiceDiscovery的register方法:
public void register(ServiceInstance serviceInstance) throws RuntimeException {
doInServiceRegistry(serviceDiscovery -> {
//使用curator框架的ServiceDiscoveryImpl完成dubbo实例的注册,下面[1]处分析build方法
serviceDiscovery.registerService(build(serviceInstance));
});
}
[1]
public static org.apache.curator.x.discovery.ServiceInstance<ZookeeperInstance> build(ServiceInstance serviceInstance) {
ServiceInstanceBuilder builder = null;
String serviceName = serviceInstance.getServiceName();//应用名
String host = serviceInstance.getHost();
int port = serviceInstance.getPort();
//这里对metadata做一下说明:调用EventPublishingServiceDiscovery的register方法时,发布事件ServiceInstancePreRegisteredEvent,
//监听器监听到事件后会向serviceInstance的metadata属性中添加一些参数信息,详细参见监听器CustomizableServiceInstanceListener。
Map<String, String> metadata = serviceInstance.getMetadata();
String id = generateId(host, port);//id是ip:端口
ZookeeperInstance zookeeperInstance = new ZookeeperInstance(null, serviceName, metadata);
try {
//创建curator框架的ServiceInstance对象,
//ServiceInstance对象存储有:ip、端口、应用名、元数据。
builder = builder()
.id(id)
.name(serviceName)
.address(host)
.port(port)
.payload(zookeeperInstance);
} catch (Exception e) {
throw new RuntimeException(e);
}
return builder.build();
}
curator框架的ServiceDiscoveryImpl对象将dubbo实例信息发布到 “/services/应用名/ServiceInstance的id属性值” 路径下,比如/services/accountService/192.168.0.102:20880,节点的数据是JSON格式的ServiceInstance对象。
不知道大家有没有注意到:ServiceInstance注册的时候,通过selectMetadataServiceExportedURL方法获取任意一个服务URL,并将该URL的端口注册到注册中心,如果服务以多个协议或者多个端口发布,那么为什么只注册一个端口?
在ServiceDiscoveryRegistry的服务订阅中,dubbo根据应用名从注册中心获取数据并创建ServiceInstance对象,ServiceInstance保存了dubbo实例信息,之后根据ServiceInstance中的IP地址和metadata属性生成连接服务端的URL,代码如下:
//该方法用于创建连接服务端的URL
public List<URL> build(ServiceInstance serviceInstance) {
//getMetadataServiceURLsParams方法从入参的metadata属性中查找
//key=dubbo.metadata-service.url-params的value值,
//根据value生成了paramsMap,value里面记录的是服务端发布的MetadataService服务信息
//其中包括了MetadataService服务发布的端口。
//key=dubbo.metadata-service.url-params的value值是在
//CustomizableServiceInstanceListener监听到事件ServiceInstancePreRegisteredEvent之后,调用MetadataServiceURLParamsMetadataCustomizer添加到metadata中的。
Map<String, Map<String, String>> paramsMap = getMetadataServiceURLsParams(serviceInstance);
List<URL> urls = new ArrayList<>(paramsMap.size());
String serviceName = serviceInstance.getServiceName();
String host = serviceInstance.getHost();
for (Map.Entry<String, Map<String, String>> entry : paramsMap.entrySet()) {
String protocol = entry.getKey();
Map<String, String> params = entry.getValue();
int port = Integer.parseInt(params.get(PORT_KEY));
URLBuilder urlBuilder = new URLBuilder()
.setHost(host)
.setPort(port)//设置MetadataService服务的端口
.setProtocol(protocol)
.setPath(MetadataService.class.getName());
params.forEach((name, value) -> urlBuilder.addParameter(name, valueOf(value)));
// add the default parameters
urlBuilder.addParameter(GROUP_KEY, serviceName);
urls.add(urlBuilder.build());
}
return urls;
}
创建出链接服务端的URL后,便建立与服务端的连接,访问MetadataService服务,以获取服务端发布的的所有服务。
从这里可以看出上面问题中提到的端口其实没有使用。