Dubbo服务暴露流程

1. 前言

DubboBootstrap启动时,首先会通过initialize()方法完成初始化,装配各种Config对象,为后续的服务暴露和引用准备好环境。 ​

ServiceConfig对象就是Dubbo对服务的描述对象,服务暴露的逻辑都在ServiceConfig#export()里面,Dubbo暴露服务也就是遍历所有的ServiceConfig,挨个进行暴露。 image.png 上图是官方文档给的图,在Dubbo的架构体系中,像集群容错、负载均衡等逻辑都是客户端实现的,所以服务暴露的过程相对会简单很多。ServiceConfig描述了对外提供的服务,ref属性引用了具体的服务实现对象,当Provider接收到Consumer发起的RPC调用时,会交给ref执行,但是Dubbo不会直接使用ref,因为不管是Provider还是Consumer,Dubbo都在向Invoker靠拢。 Invoker在Dubbo体系中是一个非常重要的概念,它代表一个调用者,可能是本地调用、远程调用、甚至是集群调用。对于Provider而言就是本地调用了,生成Invoker非常简单,字节码技术动态生成Wrapper对象,底层调用的还是ref对象。 ​

将ref包装成Invoker后,接下来就是根据协议进行服务暴露了,对应的方法是Protocol#export(),会得到一个Exporter。Provider在接收到Consumer的RPC请求时,会根据Invocation参数映射到Exporter,然后获取它关联的Invoker,执行本地调用,最后响应结果。

2. 源码分析

在这里插入图片描述

Dubbo服务暴露的入口在ServiceConfig#export()方法,主要做了三件事:

  1. 配置的校验和更新
  2. 暴露服务
  3. 分发服务暴露事件

代码精简后,如下:

public synchronized void export() {
    // 检查和更新配置
    checkAndUpdateSubConfigs();

    if (shouldDelay()) {
        // 延迟暴露
        DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
    } else {
        // 暴露服务
        doExport();
    }
    // 分发暴露事件
    exported();
}
复制代码

2.1 checkAndUpdateSubConfigs()

该方法主要是对ServiceConfig对象做一些配置的校验和自动更新。例如使用ProviderConfig的全局默认配置、将protocolIds转换成ProtocolConfig对象、自身的属性按照优先级进行刷新等等。配置更新完了,接下来就是做服务暴露的前置Check,例如注册中心是否有效、ref对象是否符合要求等等。

private void checkAndUpdateSubConfigs() {
    // 使用ProviderConfig默认配置
    completeCompoundConfigs();
    // ProviderConfig不存在则自动创建
    checkDefault();
    // protocolIds转换
    checkProtocol();
    
    if (!isOnlyInJvm()) {
        // 服务注册,还要检查配置中心
        checkRegistry();
    }
    // 自身属性根据优先级刷新
    this.refresh();
    checkStubAndLocal(interfaceClass);
    ConfigValidationUtils.checkMock(interfaceClass, this);
    ConfigValidationUtils.validateServiceConfig(this);
    postProcessConfig();
    代码有精简...
}
复制代码

2.2 doExport()

最终会调用doExportUrls()方法多注册中心多协议暴露服务,Dubbo暴露服务除了会向注册中心注册一份,本地也会注册到ServiceRepository。 ServiceRepository保存了当前应用提供了哪些服务、引用了哪些服务,后续Consumer服务引用时,如果自身已经提供了该服务,就会通过ServiceRepository直接引用本地提供的服务,跳过网络传输。

ServiceRepository repository = ApplicationModel.getServiceRepository();
// 注册Service
ServiceDescriptor serviceDescriptor = repository.registerService(getInterfaceClass());
// 注册Provider
repository.registerProvider(getUniqueServiceName(),ref,serviceDescriptor,this,serviceMetadata);
复制代码

接下来,获取当前服务需要注册到哪些注册中心,加载对应的URL。

// 加载配置中心URL
List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);
复制代码

在Check那一步,就已经解析了服务需要通过哪些协议进行暴露,所以接下来会遍历protocols,进行单协议多注册中心暴露。

for (ProtocolConfig protocolConfig : protocols) {
    String pathKey = URL.buildKey(getContextPath(protocolConfig)
                                  .map(p -> p + "/" + path)
                                  .orElse(path), group, version);
    repository.registerService(pathKey, interfaceClass);
    serviceMetadata.setServiceKey(pathKey);
    // 单协议多注册中心暴露
    doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
复制代码

服务暴露需要用到各种参数,用于构建后续的服务暴露URL,这里会使用HashMap存储。Dubbo的配置粒度是到方法级别的,对应的类是MethodConfig,如果有方法级别的配置,也需要解析到Map中。

// 服务暴露的各种参数,用于组装服务暴露URL
Map<String, String> map = new HashMap<String, String>();
map.put(SIDE_KEY, PROVIDER_SIDE);
// 运行时参数
ServiceConfig.appendRuntimeParameters(map);
AbstractConfig.appendParameters(map, getMetrics());
AbstractConfig.appendParameters(map, getApplication());
AbstractConfig.appendParameters(map, getModule());
......
复制代码

参数组装完毕,解析出服务暴露的host和port,然后构建URL。

// 查找服务暴露的host和port
String host = findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = findConfigedPorts(protocolConfig, name, map);
// 构建URL
URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);
复制代码

服务暴露的范围可以通过scope属性配置,none代表不暴露、local仅暴露到本地JVM、remote会暴露到远程。Dubbo在服务暴露前会进行判断,默认情况下会同时暴露到本地JVM和远程。

2.2.1 服务本地暴露

exportLocal()方法用来本地暴露,本地暴露非常的简单,就是injvm协议暴露,创建InjvmExporter存储到Map。不监听本地端口,不走网络传输,但是会走Filter和Listener。

private void exportLocal(URL url) {
    // 改写协议为injvm,port为-1,不开启端口监听,不走网络传输
    URL local = URLBuilder.from(url)
        .setProtocol(LOCAL_PROTOCOL)
        .setHost(LOCALHOST_VALUE)
        .setPort(0)
        .build();
    Exporter<?> exporter = PROTOCOL.export(
        PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, local));
    exporters.add(exporter);
}
复制代码

2.2.2 服务远程暴露

暴露完本地,接下来就是远程暴露了。远程暴露前会先将ref包装成Invoker,对应的方法是ProxyFactory#getInvoker(),Invoker会根据methodName调用ref的方法。 有两种方式,一种是利用Java自带的反射,另一种是利用字节码技术动态生成代理对象。Dubbo默认会选择第二种方式,利用javassist动态创建Class对应的Wrapper对象,动态生成的Wrapper类会根据方法名和参数直接调用ref对应的方法,避免Java反射带来的性能问题。

public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    // 提升反射效率
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
    return new AbstractProxyInvoker<T>(proxy, type, url) {
        @Override
        protected Object doInvoke(T proxy, String methodName,
                                  Class<?>[] parameterTypes,
                                  Object[] arguments) throws Throwable {
            return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
        }
    };
}
复制代码

ref包装成了Invoker,然后就可以开始根据协议来暴露服务了。如果服务要注册到注册中心,解析出来的URL协议部分会被改写为registry,这样SPI触发的就是RegistryProtocol#export()方法。registry本身是个伪协议,它只是在原有协议暴露的基础上,增加了服务注册到注册中心的功能。 ​

首先从URL中分别提取出注册中心URL和服务暴露的真实URL。

// 注册中心URL,前面改写过协议,真实协议放到参数里去了,这里会还原
URL registryUrl = getRegistryUrl(originInvoker);
// 服务提供者URL,这里会将协议改为dubbo
URL providerUrl = getProviderUrl(originInvoker);
// 获取订阅的URL,URL变更服务会重新发布
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
// 创建URL监听器
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
// Configurator配置URL
providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
复制代码

providerUrl才是服务暴露的真实协议地址,然后通过doLocalExport()方法开始根据指定的协议来暴露服务。

要区分服务的暴露和注册,暴露一般是指开始监听本地端口,对外提供服务。注册是指将服务注册到注册中心,让Consumer可以感知到。

private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
    // 协议://host:port/interfaceName?参数
    String key = getCacheKey(originInvoker);
    return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> {
        Invoker<?> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl);
        // 这里才是真实的 根据URL协议加载Protocol服务暴露
        return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker);
    });
}
复制代码

在进行具体的协议暴露服务前,需要先经过Protocol的两个Wrapper类,这个是由SPI的自动包装特性支持的。首先是ProtocolFilterWrapper,它的目的就是对Invoker封装一层过滤器链FilterChain,在执行目标方法前先执行Filter。然后是ProtocolListenerWrapper,它的目的是在服务unexport时触发事件。 ​

经过上面两个包装类后,SPI的自适应调用,根据URL的协议加载对应的Protocol实现,以dubbo协议为例,对应的就是DubboProtocol#export方法。Dubbo协议暴露服务,首先自然还是创建DubboExporter,但Dubbo服务是要供Consumer调用的,不开启网络服务,Consumer如何调用呢?所以openServer()方法会开启服务。

public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    URL url = invoker.getUrl();
    // 生成服务唯一标识
    String key = serviceKey(url);
    DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
    exporterMap.put(key, exporter);
    // 开启服务
    openServer(url);
    // 优化序列化效率
    optimizeSerialization(url);
    return exporter;
}
复制代码

同一个address,不管暴露多少服务,都只会也只能开启一个服务,所以会用address作为Key,将服务端缓存到Map容器,address不存在时,才会调用createServer()创建ProtocolServer。 创建服务需要绑定本地端口,最终调用的是Exchanger#bind()。Exchanger实现类会通过SPI自适应加载,目前只有一种实现类HeaderExchanger。

public class HeaderExchanger implements Exchanger {
    public static final String NAME = "header";

    @Override
    public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
        return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))));
    }
}

复制代码

ExchangerServer依赖Transporter,Transporter是Dubbo对网络传输层的抽象接口,默认使用Netty,其他还有如Mina、Grizzly等,Transporter实现也是通过SPI自适应加载的,可以通过参数servertransporter指定,这里我们只看Netty。 NettyTransporter开启服务很简单,就是创建了NettyServer实例,在它的构造函数中,会开启Netty服务端。

public class NettyTransporter implements Transporter {
    public static final String NAME = "netty";

    @Override
    public RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException {
        return new NettyServer(url, handler);
    }
}
复制代码

在NettyServer的构造函数中,最终会调用doOpen()开启服务,熟悉Netty的同学应该很眼熟下面的代码。创建ServerBootstrap,设置EventLoopGroup,编配ChannelHandlerPipeline,最终调用bind()绑定本地端口。

protected void doOpen() throws Throwable {
    bootstrap = new ServerBootstrap();
    bossGroup = NettyEventLoopFactory.eventLoopGroup(1, "NettyServerBoss");
    workerGroup = NettyEventLoopFactory.eventLoopGroup(
        getUrl().getPositiveParameter(IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
        "NettyServerWorker");
    final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
    channels = nettyServerHandler.getChannels();
    bootstrap.group(bossGroup, workerGroup)
        .channel(NettyEventLoopFactory.serverSocketChannelClass())
        .option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
        .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
        .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                // FIXME: should we use getTimeout()?
                int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
                NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
                if (getUrl().getParameter(SSL_ENABLED_KEY, false)) {
                    ch.pipeline().addLast("negotiation",SslHandlerInitializer.sslServerHandler(getUrl(), nettyServerHandler));}
                ch.pipeline()
                    .addLast("decoder", adapter.getDecoder())
                    .addLast("encoder", adapter.getEncoder())
                    .addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
                    .addLast("handler", nettyServerHandler);
            }
        });
    ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
    channelFuture.syncUninterruptibly();
    channel = channelFuture.channel();

}
复制代码

至此,Provider就开始监听网络请求了,服务的暴露就完成了。

2.2.3 服务注册

服务暴露完了,接下来就是注册,让Consumer可以感知到。先根据注册中心URL加载对应的注册中心实现类。

protected Registry getRegistry(final Invoker<?> originInvoker) {
    // 注册中心URL
    URL registryUrl = getRegistryUrl(originInvoker);
    // SPI加载Registry实现
    return registryFactory.getRegistry(registryUrl);
}
复制代码

解析出需要注册到注册中心的URL,然后调用RegistryService#register()完成服务注册。

final Registry registry = getRegistry(originInvoker);
// 注册到注册中心的URL
final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);
// 是否立即注册
boolean register = providerUrl.getParameter(REGISTER_KEY, true);
if (register) {
    register(registryUrl, registeredProviderUrl);
}
复制代码

这里以Nacos为例,最终操作如下:

public void doRegister(URL url) {
    final String serviceName = getServiceName(url);
    final Instance instance = createInstance(url);
    // 注册服务,最终调用 NamingService#registerInstance()
    execute(namingService -> namingService.registerInstance(serviceName,getUrl().getParameter(GROUP_KEY, Constants.DEFAULT_GROUP), instance));
}
复制代码

3. 总结

Dubbo服务暴露,先将ref封装成Invoker,Invoker会根据Consumer的Invocation参数对ref发起调用,Dubbo默认使用javassist字节码技术动态生成Wrapper类,避免了Java反射带来的性能问题。 有了Invoker就可以通过Protocol根据协议进行服务暴露,如果服务需要注册,Dubbo会改写URL协议为registry,这是个伪协议,只是在原服务暴露的基础上,增加了服务注册的功能。 在根据协议暴露服务前,还需要关注两个包装类:ProtocolFilterWrapper和ProtocolListenerWrapper,前者用于构建Filter链,后者用于服务取消暴露时触发事件。 以dubbo协议为例,除了创建DubboExporter,还会根据服务暴露的address创建ProtocolServer。Transporter是dubbo对网络传输层的抽象接口,以Netty为例,底层其实就是创建了ServerBootstrap,然后bind本地接口监听网络请求。

猜你喜欢

转载自juejin.im/post/7040443246006403109
今日推荐