浅析 Dubbo 服务暴露机制:本地暴露和远程暴露
转载自 Dubbo – 源码解析 – 服务导出 笔者对其进行重新排版,并加入自己的理解
暴露方式
Dubbo 的服务暴露有两种方式:
-
本地暴露,JVM 本地调用
在没有注册中心,直接暴露提供者的情况下,ServiceConfig 解析出的 URL 格式为:
dubbo://service-host/com.foo.FooService?version=1.0.0
。基于拓展点自适应机制,通过 URL 的dubbo://
协议头识别,直接调用 DubboProtocol 的 export 方法,打开服务端口 -
远程暴露,网络远程调用
- 在有注册中心,通过注册中心发现提供者地址的情况下,ReferenceConfig 解析出的 URL 格式为:
registry://registry-host/org.apache.dubbo.registry.RegistryService?refer=URL.encode("consumer://consumer-host/com.foo.FooService?version=1.0.0")
。 - 基于拓展点自适应机制,通过 URL 的
registry://
协议头识别,就会调用 RegistryProtocol 的 refer 参数中的条件,查询提供者 URL,如dubbo://service-host/com.foo.FooService?version=1.0.0
。 - 基于拓展点自适应机制,通过提供者 URL 的
dubbo://
协议头识别,就会调用 DubboProtocol 的 refer 方法,得到提供者引用 - 然后 RegistryProtocol 将多个提供者引用,通过 Cluster 拓展点,伪装成单个提供者引用返回
- 在有注册中心,通过注册中心发现提供者地址的情况下,ReferenceConfig 解析出的 URL 格式为:
服务导出过程
Dubbo 的服务导出过程始于 Spring 容器发布刷新事件,Dubbo 在接收到事件后,会立即执行服务导出逻辑,整个逻辑流程为:
- 前置工作:用于检查参数,组装 URL
- 导出服务:本地服务暴露和远程服务暴露
- 注册服务:向注册中心注册服务,用于服务发现
入口方法
服务导出入口方法是 ServiceBean 的 onApplicaitonEvent,它是一个事件响应方法,该方法在收到 Spring 上下文刷新事件后执行服务导出操作
public void onApplicationEvent(ContextRefreshedEvent event) {
// 是否有延迟导出 && 是否已导出 && 是不是已被取消导出
if (isDelay() && !isExported() && !isUnexported()) {
// 导出服务
export();
}
}
前置工作
前置工作主要包括两个部分:校验 ServiceConfig 对象的配置项和组成 Dubbo URL 数组
校验配置
onappliacationEvent 在一系列判断后,开始调用 export 方法
public synchronized void export() {
if (provider != null) {
// 获取 export 和 delay 配置
if (export == null) {
export = provider.getExport();
}
if (delay == null) {
delay = provider.getDelay();
}
}
// 如果 export 为 false,则不导出服务
if (export != null && !export) {
return;
}
// delay 0,延时导出服务
if (delay != null && delay 0) {
delayExportExecutor.schedule(new Runnable() {
@Override
public void run() {
doExport();
}
}, delay, TimeUnit.MILLISECONDS);
// 立即导出服务
} else {
doExport();
}
}
这块的逻辑比较简单,就不做分析,代码继续跟进 doExport()
,这部分代码比较长,就不贴了,简单总结下逻辑:
- 检测
<udbbo:service
标签的 interface 属性合法性,不合法则抛出异常 - 检测 ProviderConfig 和 ApplicationConfig 等核心配置类对象是否为空,若为空,则尝试从其他配置类对象中获取响应的实例
- 检测并处理泛化服务和普通服务类
- 检测本地存根配置,并进行相应处理
- 对 Application 和 RegistryConfig 等配置类进行检测,为空则尝试创建,若无法创建则抛出异常
组装 URL
URL 是 Dubbo 配置的载体,通过 URL 可以让 Dubbo 的各种配置在各个模块之间传递。组装 URL 就是将一些信息,比如版本、时间戳、方法名以及各种配置对象的字段信息放入到 map 中,map 中的内容将作为 URL 的查询字符串。构建好 map 后,紧接着获取上下文路径、主机名以及端口号等信息,最后将 map 和主机名等数据传给 URL 构造方法创建 URL 对象。
导出 Dubbo 服务
首先从宏观上看下服务导出的逻辑
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL registryURLs) {
// 省略无关代码
if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.hasExtension(url.getProtocol())) {
// 加载 ConfiguratorFactory,并生成 Configurator 实例,然后通过实例配置 url
url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}
String scope = url.getParameter(Constants.SCOPE_KEY);
// 如果 scope = none,则什么都不做
if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {
// scope != remote,导出到本地
if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
exportLocal(url);
}
// scope != local,导出到远程
if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {
if (registryURLs != null && !registryURLs.isEmpty()) {
for (URL registryURL : registryURLs) {
url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
// 加载监视器链接
URL monitorUrl = loadMonitor(registryURL);
if (monitorUrl != null) {
// 将监视器链接作为参数添加到 url 中
url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
}
String proxy = url.getParameter(Constants.PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
}
// 为服务提供类(ref)生成 Invoker
Invoker<? invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
// DelegateProviderMetaDataInvoker 用于持有 Invoker 和 ServiceConfig
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// 导出服务,并生成 Exporter
Exporter<? exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
// 不存在注册中心,仅导出服务
} else {
Invoker<? invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
Exporter<? exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
}
}
this.urls.add(url);
}
不管是导出本地还是远程,进行服务导出之前,都需要先创建一个 invoker。关于 invoker 这块,笔者也没有理解清楚,就不做详细探讨,仅引用下官网文档的说明:
Invoker 是实体域,它是 Dubbo 的核心模型,其他模型都是向它靠拢,或者转换它,它代表一个可执行体,可向它发起 invoke 调用,它可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现
这块想深入了解可以点进引用原文查看。
导出服务到本地
本都暴露时序图
导出服务到本地的相关代码如下:
private void exportLocal(URL url) {
// 如果 URL 的协议头等于 injvm,说明已经导出到本地了,无需再次导出
if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
URL local = URL.valueOf(url.toFullString())
.setProtocol(Constants.LOCAL_PROTOCOL) // 设置协议头为 injvm
.setHost(LOCALHOST)
.setPort(0);
ServiceClassHolder.getInstance().pushServiceClass(getServiceClass(ref));
// 创建 Invoker,并导出服务,这里的 protocol 会在运行时调用 InjvmProtocol 的 export 方法
Exporter<? exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
exporters.add(exporter);
}
}
exportLocal 逻辑比较简单,首先根据 URL 协议头决定是否导出服务(URL 协议头等于 injvm),如果需要导出,则创建一个新的 URL 并将协议头、主机名以及端口设置为新的值,然后创建 Invoker,并调用 InjvmProtocol 的 export 方法导出服务,export 方法仅仅创建了一个 InjvmExport
导出服务到远程
远程暴露时序图
导出服务到远程主要包含服务导出和服务注册两个过程。
服务导出
服务导出的代码在 RegistryProtocol 的 export 方法上
public <T Exporter<T export(final Invoker<T originInvoker) throws RpcException {
// 导出服务
final ExporterChangeableWrapper<T exporter = doLocalExport(originInvoker);
// 获取注册中心 URL
URL registryUrl = getRegistryUrl(originInvoker);
// 根据 URL 加载 Registry 实现类,比如 ZookeeperRegistry
final Registry registry = getRegistry(originInvoker);
// 获取已注册的服务提供者 URL
final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker);
// 获取 register 参数
boolean register = registeredProviderUrl.getParameter("register", true);
// 向服务提供者与消费者注册表中注册服务提供者
ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl);
// 根据 register 的值决定是否注册服务
if (register) {
// 向注册中心注册服务
register(registryUrl, registeredProviderUrl);
ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
}
// 获取订阅 URL,比如:
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl);
// 创建监听器
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
// 向注册中心进行订阅 override 数据
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
// 创建并返回 DestroyableExporter
return new DestroyableExporter<T(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl);
}
简单总结下这块的业务逻辑:
- 调用 doLoacalExport 导出服务
- 向注册中心注册服务
- 向注册中心订阅 override 数据
- 创建并返回 DestroyableExporter
首先分析下 doLocalExport 方法的逻辑:
private <T ExporterChangeableWrapper<T doLocalExport(final Invoker<T originInvoker) {
String key = getCacheKey(originInvoker);
// 访问缓存
ExporterChangeableWrapper<T exporter = (ExporterChangeableWrapper<T) bounds.get(key);
if (exporter == null) {
synchronized (bounds) {
exporter = (ExporterChangeableWrapper<T) bounds.get(key);
if (exporter == null) {
// 创建 Invoker 为委托类对象
final Invoker<? invokerDelegete = new InvokerDelegete<T(originInvoker, getProviderUrl(originInvoker));
// 调用 protocol 的 export 方法导出服务
exporter = new ExporterChangeableWrapper<T((Exporter<T) protocol.export(invokerDelegete), originInvoker);
// 写缓存
bounds.put(key, exporter);
}
}
}
return exporter;
}
这段代码的逻辑比较简单,就是双重检查锁调用 DubboProtocol 的 export 方法,跟进去 export 方法,相关代码如下:
public <T Exporter<T export(Invoker<T invoker) throws RpcException {
URL url = invoker.getUrl();
// 获取服务标识:服务组名/服务名/服务版本号/端口
// demoGroup/com.alibaba.dubbo.demo.DemoService:1.0.1:20880
String key = serviceKey(url);
// 创建 DubboExporter
DubboExporter<T exporter = new DubboExporter<T(invoker, key, exporterMap);
// 将 <key, exporter 键值对放入缓存中
exporterMap.put(key, exporter);
// 本地存根相关代码
Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT);
Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
if (isStubSupportEvent && !isCallbackservice) {
String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
// 省略日志打印代码
} else {
stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
}
}
// 启动服务器
openServer(url);
// 优化序列化
optimizeSerialization(url);
return exporter;
}
至此就完成了服务导出的过程,后续启动服务器 Dubbo 使用的是 Netty 实现,笔者对此不熟悉,就没有继续跟进去。想要深入理解,可以点进引用原文查看分析。
服务注册
服务注册对于 Dubbo 来说不是必须的,通过服务直连的方式可以绕过注册中心,但是这样不利于服务治理。Dubbo 支持多种注册中心,一般使用的是 Zookeeper,下文的分析也是基于 Zookeeper。
服务注册的入口方法在 RegistryProtocol 的 export 方法,代码如下:
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
// ${导出服务}
// 省略其他代码
boolean register = registeredProviderUrl.getParameter("register", true);
if (register) {
// 注册服务
register(registryUrl, registeredProviderUrl);
ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
}
// ${订阅 override 数据}
// 省略部分代码
}
前面已经分析了导出服务,接下在主要分析服务注册逻辑,跟进 register 查看代码
public void register(URL registryUrl, URL registedProviderUrl) {
// 获取 Registry
Registry registry = registryFactory.getRegistry(registryUrl);
// 注册服务
registry.register(registedProviderUrl);
}
register 方法包含两步操作,获取注册中心实例和向注册中心注册服务
获取注册中心实例
getRegistry 方法跟进去代码如下:
public Registry getRegistry(URL url) {
url = url.setPath(RegistryService.class.getName())
.addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName())
.removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY);
String key = url.toServiceString();
LOCK.lock();
try {
// 访问缓存
Registry registry = REGISTRIES.get(key);
if (registry != null) {
return registry;
}
// 缓存未命中,创建 Registry 实例
registry = createRegistry(url);
if (registry == null) {
throw new IllegalStateException("Can not create registry...");
}
// 写入缓存
REGISTRIES.put(key, registry);
return registry;
} finally {
LOCK.unlock();
}
}
代码逻辑比较简单,先访问缓存,缓存未命中则调用 createRegistry 创建 Registry,然后写入缓存。createRegistry 是一个模板方法,由子类 ZookeeperRegistryFactory 实现的,主要就是利用 Curator 框架创建 Zookeeper 客户端,创建好 Zookeeper 客户端,注册中心的创建过程就结束了。
节点创建
服务注册,本质上就是将服务配置数据写入到 Zookeeper 的某个路径的节点下。
流程说明:
- 服务提供者启动时:向
/dubbo/com.foo.BarService/providers
目录下写入自己的 URL 地址 - 服务消费者启动时:订阅
/dubbo/com.foo.BarService/providers
目录下的提供者 URL 地址。并向/dubbo/com.foo.BarService/consumers
目录下写入自己的 URL 地址 - 监控执行启动时:订阅
/dubbo/com.foo.BarService
目录下的所有提供者和消费者的 URL 地址
服务注册的接口为 register(URL) 这个方法定义在 FailbackRegistry 抽象类中
public void register(URL url) {
// ${参数检验}
}
protected abstract void doRegister(URL url);
我们重点关注的是 doRegister 方法调用,它是一个模板方法,在子类 ZookeeperRegistry 中实现
protected void doRegister(URL url) {
try {
// 通过 Zookeeper 客户端创建节点,节点路径由 toUrlPath 方法生成
zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
} catch (Throwable e) {
throw new RpcException("Failed to register...");
}
}
这段代码的逻辑比较简单,主要是调用 Zookeeper 客户端创建服务节点。节点路径格式为:/${group}/${serviceInterface}/providers/${url}
,比如 /dubbo/org.apache.dubbo.DemoService/providers/dubbo%3A%2F%2F127.0.0.1......
,创建完节点,整个注册过程就分析完了。