日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Dubbo服务暴露流程

發布時間:2023/12/20 编程问答 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Dubbo服务暴露流程 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

1. 前言

DubboBootstrap啟動時,首先會通過initialize()方法完成初始化,裝配各種Config對象,為后續的服務暴露和引用準備好環境。
?

ServiceConfig對象就是Dubbo對服務的描述對象,服務暴露的邏輯都在ServiceConfig#export()里面,Dubbo暴露服務也就是遍歷所有的ServiceConfig,挨個進行暴露。

上圖是官方文檔給的圖,在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()方法,主要做了三件事:

  • 配置的校驗和更新
  • 暴露服務
  • 分發服務暴露事件
  • 代碼精簡后,如下:

    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) {@Overrideprotected 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";@Overridepublic 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自適應加載的,可以通過參數server或transporter指定,這里我們只看Netty。
    NettyTransporter開啟服務很簡單,就是創建了NettyServer實例,在它的構造函數中,會開啟Netty服務端。

    public class NettyTransporter implements Transporter {public static final String NAME = "netty";@Overridepublic 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>() {@Overrideprotected 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) {// 注冊中心URLURL 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本地接口監聽網絡請求。

    總結

    以上是生活随笔為你收集整理的Dubbo服务暴露流程的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。