Dubbo服务暴露流程
1. 前言
DubboBootstrap啟動時,首先會通過initialize()方法完成初始化,裝配各種Config對象,為后續(xù)的服務(wù)暴露和引用準(zhǔn)備好環(huán)境。
?
ServiceConfig對象就是Dubbo對服務(wù)的描述對象,服務(wù)暴露的邏輯都在ServiceConfig#export()里面,Dubbo暴露服務(wù)也就是遍歷所有的ServiceConfig,挨個進(jìn)行暴露。
上圖是官方文檔給的圖,在Dubbo的架構(gòu)體系中,像集群容錯、負(fù)載均衡等邏輯都是客戶端實現(xiàn)的,所以服務(wù)暴露的過程相對會簡單很多。ServiceConfig描述了對外提供的服務(wù),ref屬性引用了具體的服務(wù)實現(xiàn)對象,當(dāng)Provider接收到Consumer發(fā)起的RPC調(diào)用時,會交給ref執(zhí)行,但是Dubbo不會直接使用ref,因為不管是Provider還是Consumer,Dubbo都在向Invoker靠攏。
Invoker在Dubbo體系中是一個非常重要的概念,它代表一個調(diào)用者,可能是本地調(diào)用、遠(yuǎn)程調(diào)用、甚至是集群調(diào)用。對于Provider而言就是本地調(diào)用了,生成Invoker非常簡單,字節(jié)碼技術(shù)動態(tài)生成Wrapper對象,底層調(diào)用的還是ref對象。
?
將ref包裝成Invoker后,接下來就是根據(jù)協(xié)議進(jìn)行服務(wù)暴露了,對應(yīng)的方法是Protocol#export(),會得到一個Exporter。Provider在接收到Consumer的RPC請求時,會根據(jù)Invocation參數(shù)映射到Exporter,然后獲取它關(guān)聯(lián)的Invoker,執(zhí)行本地調(diào)用,最后響應(yīng)結(jié)果。
2. 源碼分析
Dubbo服務(wù)暴露的入口在ServiceConfig#export()方法,主要做了三件事:
代碼精簡后,如下:
public synchronized void export() {// 檢查和更新配置checkAndUpdateSubConfigs();if (shouldDelay()) {// 延遲暴露DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);} else {// 暴露服務(wù)doExport();}// 分發(fā)暴露事件exported(); }2.1 checkAndUpdateSubConfigs()
該方法主要是對ServiceConfig對象做一些配置的校驗和自動更新。例如使用ProviderConfig的全局默認(rèn)配置、將protocolIds轉(zhuǎn)換成ProtocolConfig對象、自身的屬性按照優(yōu)先級進(jìn)行刷新等等。配置更新完了,接下來就是做服務(wù)暴露的前置Check,例如注冊中心是否有效、ref對象是否符合要求等等。
private void checkAndUpdateSubConfigs() {// 使用ProviderConfig默認(rèn)配置completeCompoundConfigs();// ProviderConfig不存在則自動創(chuàng)建checkDefault();// protocolIds轉(zhuǎn)換checkProtocol();if (!isOnlyInJvm()) {// 服務(wù)注冊,還要檢查配置中心checkRegistry();}// 自身屬性根據(jù)優(yōu)先級刷新this.refresh();checkStubAndLocal(interfaceClass);ConfigValidationUtils.checkMock(interfaceClass, this);ConfigValidationUtils.validateServiceConfig(this);postProcessConfig();代碼有精簡... }2.2 doExport()
最終會調(diào)用doExportUrls()方法多注冊中心多協(xié)議暴露服務(wù),Dubbo暴露服務(wù)除了會向注冊中心注冊一份,本地也會注冊到ServiceRepository。
ServiceRepository保存了當(dāng)前應(yīng)用提供了哪些服務(wù)、引用了哪些服務(wù),后續(xù)Consumer服務(wù)引用時,如果自身已經(jīng)提供了該服務(wù),就會通過ServiceRepository直接引用本地提供的服務(wù),跳過網(wǎng)絡(luò)傳輸。
接下來,獲取當(dāng)前服務(wù)需要注冊到哪些注冊中心,加載對應(yīng)的URL。
// 加載配置中心URL List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);在Check那一步,就已經(jīng)解析了服務(wù)需要通過哪些協(xié)議進(jìn)行暴露,所以接下來會遍歷protocols,進(jìn)行單協(xié)議多注冊中心暴露。
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);// 單協(xié)議多注冊中心暴露doExportUrlsFor1Protocol(protocolConfig, registryURLs); }服務(wù)暴露需要用到各種參數(shù),用于構(gòu)建后續(xù)的服務(wù)暴露URL,這里會使用HashMap存儲。Dubbo的配置粒度是到方法級別的,對應(yīng)的類是MethodConfig,如果有方法級別的配置,也需要解析到Map中。
// 服務(wù)暴露的各種參數(shù),用于組裝服務(wù)暴露URL Map<String, String> map = new HashMap<String, String>(); map.put(SIDE_KEY, PROVIDER_SIDE); // 運行時參數(shù) ServiceConfig.appendRuntimeParameters(map); AbstractConfig.appendParameters(map, getMetrics()); AbstractConfig.appendParameters(map, getApplication()); AbstractConfig.appendParameters(map, getModule()); ......參數(shù)組裝完畢,解析出服務(wù)暴露的host和port,然后構(gòu)建URL。
// 查找服務(wù)暴露的host和port String host = findConfigedHosts(protocolConfig, registryURLs, map); Integer port = findConfigedPorts(protocolConfig, name, map); // 構(gòu)建URL URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);服務(wù)暴露的范圍可以通過scope屬性配置,none代表不暴露、local僅暴露到本地JVM、remote會暴露到遠(yuǎn)程。Dubbo在服務(wù)暴露前會進(jìn)行判斷,默認(rèn)情況下會同時暴露到本地JVM和遠(yuǎn)程。
2.2.1 服務(wù)本地暴露
exportLocal()方法用來本地暴露,本地暴露非常的簡單,就是injvm協(xié)議暴露,創(chuàng)建InjvmExporter存儲到Map。不監(jiān)聽本地端口,不走網(wǎng)絡(luò)傳輸,但是會走Filter和Listener。
private void exportLocal(URL url) {// 改寫協(xié)議為injvm,port為-1,不開啟端口監(jiān)聽,不走網(wǎng)絡(luò)傳輸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 服務(wù)遠(yuǎn)程暴露
暴露完本地,接下來就是遠(yuǎn)程暴露了。遠(yuǎn)程暴露前會先將ref包裝成Invoker,對應(yīng)的方法是ProxyFactory#getInvoker(),Invoker會根據(jù)methodName調(diào)用ref的方法。
有兩種方式,一種是利用Java自帶的反射,另一種是利用字節(jié)碼技術(shù)動態(tài)生成代理對象。Dubbo默認(rèn)會選擇第二種方式,利用javassist動態(tài)創(chuàng)建Class對應(yīng)的Wrapper對象,動態(tài)生成的Wrapper類會根據(jù)方法名和參數(shù)直接調(diào)用ref對應(yīng)的方法,避免Java反射帶來的性能問題。
ref包裝成了Invoker,然后就可以開始根據(jù)協(xié)議來暴露服務(wù)了。如果服務(wù)要注冊到注冊中心,解析出來的URL協(xié)議部分會被改寫為registry,這樣SPI觸發(fā)的就是RegistryProtocol#export()方法。registry本身是個偽協(xié)議,它只是在原有協(xié)議暴露的基礎(chǔ)上,增加了服務(wù)注冊到注冊中心的功能。
?
首先從URL中分別提取出注冊中心URL和服務(wù)暴露的真實URL。
// 注冊中心URL,前面改寫過協(xié)議,真實協(xié)議放到參數(shù)里去了,這里會還原 URL registryUrl = getRegistryUrl(originInvoker); // 服務(wù)提供者URL,這里會將協(xié)議改為dubbo URL providerUrl = getProviderUrl(originInvoker); // 獲取訂閱的URL,URL變更服務(wù)會重新發(fā)布 final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl); // 創(chuàng)建URL監(jiān)聽器 final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker); overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener); // Configurator配置URL providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);providerUrl才是服務(wù)暴露的真實協(xié)議地址,然后通過doLocalExport()方法開始根據(jù)指定的協(xié)議來暴露服務(wù)。
要區(qū)分服務(wù)的暴露和注冊,暴露一般是指開始監(jiān)聽本地端口,對外提供服務(wù)。注冊是指將服務(wù)注冊到注冊中心,讓Consumer可以感知到。
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {// 協(xié)議://host:port/interfaceName?參數(shù)String key = getCacheKey(originInvoker);return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> {Invoker<?> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl);// 這里才是真實的 根據(jù)URL協(xié)議加載Protocol服務(wù)暴露return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker);}); }在進(jìn)行具體的協(xié)議暴露服務(wù)前,需要先經(jīng)過Protocol的兩個Wrapper類,這個是由SPI的自動包裝特性支持的。首先是ProtocolFilterWrapper,它的目的就是對Invoker封裝一層過濾器鏈FilterChain,在執(zhí)行目標(biāo)方法前先執(zhí)行Filter。然后是ProtocolListenerWrapper,它的目的是在服務(wù)unexport時觸發(fā)事件。
?
經(jīng)過上面兩個包裝類后,SPI的自適應(yīng)調(diào)用,根據(jù)URL的協(xié)議加載對應(yīng)的Protocol實現(xiàn),以dubbo協(xié)議為例,對應(yīng)的就是DubboProtocol#export方法。Dubbo協(xié)議暴露服務(wù),首先自然還是創(chuàng)建DubboExporter,但Dubbo服務(wù)是要供Consumer調(diào)用的,不開啟網(wǎng)絡(luò)服務(wù),Consumer如何調(diào)用呢?所以openServer()方法會開啟服務(wù)。
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {URL url = invoker.getUrl();// 生成服務(wù)唯一標(biāo)識String key = serviceKey(url);DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);exporterMap.put(key, exporter);// 開啟服務(wù)openServer(url);// 優(yōu)化序列化效率optimizeSerialization(url);return exporter; }同一個address,不管暴露多少服務(wù),都只會也只能開啟一個服務(wù),所以會用address作為Key,將服務(wù)端緩存到Map容器,address不存在時,才會調(diào)用createServer()創(chuàng)建ProtocolServer。
創(chuàng)建服務(wù)需要綁定本地端口,最終調(diào)用的是Exchanger#bind()。Exchanger實現(xiàn)類會通過SPI自適應(yīng)加載,目前只有一種實現(xiàn)類HeaderExchanger。
ExchangerServer依賴Transporter,Transporter是Dubbo對網(wǎng)絡(luò)傳輸層的抽象接口,默認(rèn)使用Netty,其他還有如Mina、Grizzly等,Transporter實現(xiàn)也是通過SPI自適應(yīng)加載的,可以通過參數(shù)server或transporter指定,這里我們只看Netty。
NettyTransporter開啟服務(wù)很簡單,就是創(chuàng)建了NettyServer實例,在它的構(gòu)造函數(shù)中,會開啟Netty服務(wù)端。
在NettyServer的構(gòu)造函數(shù)中,最終會調(diào)用doOpen()開啟服務(wù),熟悉Netty的同學(xué)應(yīng)該很眼熟下面的代碼。創(chuàng)建ServerBootstrap,設(shè)置EventLoopGroup,編配ChannelHandlerPipeline,最終調(diào)用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就開始監(jiān)聽網(wǎng)絡(luò)請求了,服務(wù)的暴露就完成了。
2.2.3 服務(wù)注冊
服務(wù)暴露完了,接下來就是注冊,讓Consumer可以感知到。先根據(jù)注冊中心URL加載對應(yīng)的注冊中心實現(xiàn)類。
protected Registry getRegistry(final Invoker<?> originInvoker) {// 注冊中心URLURL registryUrl = getRegistryUrl(originInvoker);// SPI加載Registry實現(xiàn)return registryFactory.getRegistry(registryUrl); }解析出需要注冊到注冊中心的URL,然后調(diào)用RegistryService#register()完成服務(wù)注冊。
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);// 注冊服務(wù),最終調(diào)用 NamingService#registerInstance()execute(namingService -> namingService.registerInstance(serviceName,getUrl().getParameter(GROUP_KEY, Constants.DEFAULT_GROUP), instance)); }3. 總結(jié)
Dubbo服務(wù)暴露,先將ref封裝成Invoker,Invoker會根據(jù)Consumer的Invocation參數(shù)對ref發(fā)起調(diào)用,Dubbo默認(rèn)使用javassist字節(jié)碼技術(shù)動態(tài)生成Wrapper類,避免了Java反射帶來的性能問題。
有了Invoker就可以通過Protocol根據(jù)協(xié)議進(jìn)行服務(wù)暴露,如果服務(wù)需要注冊,Dubbo會改寫URL協(xié)議為registry,這是個偽協(xié)議,只是在原服務(wù)暴露的基礎(chǔ)上,增加了服務(wù)注冊的功能。
在根據(jù)協(xié)議暴露服務(wù)前,還需要關(guān)注兩個包裝類:ProtocolFilterWrapper和ProtocolListenerWrapper,前者用于構(gòu)建Filter鏈,后者用于服務(wù)取消暴露時觸發(fā)事件。
以dubbo協(xié)議為例,除了創(chuàng)建DubboExporter,還會根據(jù)服務(wù)暴露的address創(chuàng)建ProtocolServer。Transporter是dubbo對網(wǎng)絡(luò)傳輸層的抽象接口,以Netty為例,底層其實就是創(chuàng)建了ServerBootstrap,然后bind本地接口監(jiān)聽網(wǎng)絡(luò)請求。
總結(jié)
以上是生活随笔為你收集整理的Dubbo服务暴露流程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c# picturebox 刷新_c#
- 下一篇: 3D建模软件有哪些?怎么才能掌握3D建模