一、基礎篇
1.1 開篇說明
dubbo是一個分布式服務框架,致力於提供高性能透明化RPC遠程調用方案,提供SOA服務治理解決方案。本文旨在將對dubbo的使用和學習總結起來,深入源碼探究原理,以備今后可以作為借鑒用於工作之中。
由於dubbo各個分層都是很多擴展,比如注冊中心有redis、zookeeper選項,通信模塊有netty、mina,序列化有hession、hession2、java序列化等,本文不能面面俱到,重點闡述主線流程,注冊中心選擇zookeeper(client選擇curator),通信選擇netty,協議選擇dubbo,序列化選擇hession2,容器選擇Spring。本文不會照搬官網文檔,而是由淺入深闡述dubbo框架
。
1.2 為什么要服務化
為什么要做服務化
?
隨着業務發展,應用的功能和涵蓋的業務越來越大,造成復雜度越來越高,代碼量跟着加大,開發人員在發布環節會遇到前后端協調和代碼沖突導致發布失敗,在開發過程中由於代碼的臃腫而不得不背負較大的負擔降低開發效率,每個開發人員沒有具體分工不能夠做到業務模塊責任到人,單個應用包含了不同業務一方業務出現問題影響其他業務的正常服務,大量業務柔和在一起無法有效做到容量規划,造成數據庫連接和分布式緩存的浪費。
因此,將應用拆分,並抽取出核心服務來解決上述問題,還要考慮負載均衡、服務監控、高可用性、服務隔離與降級、路由策略、完善的容錯機制、序列化方案的選擇、通信框架的選擇、開發人員對底層細節無感知、服務升級兼容性等問題。Dubbo滿足了以上所有需求。
1.3 深入淺出dubbo
本節從零基礎服務調用到dubbo演進過程,如果對dubbo功能已經很熟悉,可以忽略。
- 1)初期一個簡單的服務調用方式
調用方與服務方約定請求參數字段和請求結果字段,服務方啟動一個tomcat+springmvc,監聽80端口,調用方通過httpclient發起http請求,服務方返回json或xml數據結果,調用方拿到http響應結果解析結果數據,一次服務調用結束。
那么問題來了,過了幾個月,核心服務越來越多,業務被拆的越來越精細,配置文件中由服務方提供的url地址越來越多,運維忙於架設負載均衡設備,部署新的軟負載服務nginx,lvs,haproxy,甚至每個服務的內部域名的變化或url路徑變化都要增加運維人員的人工成本。
- 2)通過注冊中心發現服務、client端完成負載均衡
服務方在啟動tomcat后,向注冊中心注冊自己的服務列表,包括服務器ip、port,以及代表服務的唯一標識,比如以格式/a_service/ip_port,/b_service/ip_port存儲在注冊中心。
調用方在啟動后,去注冊中心尋找a服務的地址列表,並且訂閱/a_service,當a服務列表變更就會將變更消息推給調用方。接下來地址列表得到了,調用方創建多個httpClient實例,每個實例對應一個服務器ip_port,每次發起調用,從httpclient實例列表中隨機選擇一個,發起調用請求。當服務方某台服務器出現宕機或者網絡故障,調用方會從收到由注冊中心推送過來的通知消息,進而將出現故障的ip_port對應的httpclient從列表中移出;當服務方新增加服務器時,調用方同樣會收到通知消息,進而新建httpclient實例,加入httpclient列表。由此我們增加了注冊中心集群,在服務方調用方加入注冊中心客戶端,解決了1)提出的問題。
那么問題來了?服務方提供的服務器硬件配置不一樣,性能也不一樣,希望通過設置服務器權重的方式,權重高的希望收到更多的請求,希望有輪詢的負載均衡方式,於是有了負載均衡策略的需求。
- 3) 負載均衡策略模塊
服務方將權重信息寫入注冊中心,調用方取到后根據自己或者服務方建議的負載均衡策略從httpclient列表中選擇一個實例,進而發起http請求。
那么問題來了?負載均衡雖然解決了均衡壓力的問題,但如果服務方與調用方之間網絡出現閃斷造成請求失敗怎么辦,如果能夠重試就好了,但又不能對所有的請求都開啟重試機制,有些寫請求比如賬戶充值,肯定不能重試多次,於是需要一個集群容錯模塊。
-
4)集群容錯模塊
對於不同服務選擇不同的容錯機制,比如非冪等的寫操作選擇failfast--失敗立即報錯,對於響應快速的讀操作選擇failover—重試其它服務器,如果調用方無法容忍因為服務調用的阻塞也可以選擇failfast,對於消息通知可以選擇failback---失敗定時重發,對於審計日志可以選擇failsafe—失敗直接忽略。
那么問題來了?如何解決不同機房調用的問題,讀寫分離的問題,當線上服務方某個網段服務器出現問題,需要立即隔離掉。於是需要一個路由策略模塊。 - 5)路由策略
通過web界面管理端可以直接操作注冊中心,管理員添加路由規則,調用方訂閱路由規則節點,當發生變更時調用方收到通知修改本地路由策略。
那么問題來了?添加這些路由規則需要一個管理端,這個管理端由於與注冊中心建立連接,還可以方便的進行權重修改、負載均衡策略變更、容錯機制變更等
- 6)管理端
具有良好web界面的管理端。
那么問題來了?只有管理端,但不能看到服務調用情況,無法做容量規划,比如調用次數,響應時間,QPS,服務依賴關系,服務方有幾台,調用方有幾台機器,同時提供哪些服務,我總不能每次都跳到注冊中心集群敲命令看吧,何況以上服務統計信息敲命令也看不到。那么因此需要一個監控中心集群,調用方和服務端定期上報監控中心服務的請求與返回數據,監控中心通過計算用曲線圖展示出來。
- 7)監控中心
調用方和服務方引入監控中心client,client定期將服務數據上報到監控中心集群,監控中心提供web界面,使應用負責人可以登錄查看。監控中心因為也是提供統計數據收集服務,所以同樣可以作為服務方接收來自普通服務方和普通調用方的統計上報請求。在代碼實現上,我們可以在發起調用和接收到結果之間做一層攔截比如monitorFilter,在發起調用前紀錄下時間戳,在收到響應結果紀錄下時間戳,然后算出時間差發送到監控中心。
那么問題來了?注冊中心、監控中心、負載均衡策略、容錯機制、路由規則都有了,但是現在每次調用服務都要寫一堆httpclient相關代碼,調用前要組織httpclient要求的請求對象request,結果返回后要解析httpclient封裝的response對象,除此之外還要負載均衡策略、容錯的代碼封裝在外面,這不僅沒有減少開發成本反而增加了,如果每次服務調用都像本地調用一樣,服務化對開發者無感知就好了。於是需要一個代理對象,來封裝底層細節,讓通信細節、路由、負載均衡對開發者不可見。
- 8)代理對象
上面說到了我們需要一個服務調用做一個代理,代碼看起來應該是醬紫的:
清單1.DemoService.javapackage Test; public interface DemoService{ String sayHello(String name); }
清單2.DemoService$Proxy.java
Public class DemoService$Proxy implements DemoService{ Private java.lang.reflect.InvocationHandler handler; Public static java.lang.reflect.Method[] methods; Public java.lang.String interfaceName; Public DemoService$Proxy(String interfaceName,java.lang.reflect.InvocationHandlerarg1){ this.handler=arg1; this.interfaceName=interfaceName; } Public java.lang.String sayHello(java.lang.String arg0){ Object[]args=new Object[1]; args[0]= arg0; Object ret =handler.invoke(interfaceName, methods[0],args); return(java.lang.String)ret; } }
DemoService$Proxy即為代理類,但需要傳遞一些必須字段比如類名、方法名、方法參數類型、參數值(這里的實現是與dubbo的代理類有出入的,比如handler里面invoker屬性已經有了interfaceName屬性)。其中handler隱藏了所有遠程調用細節,包括負載均衡、路由、容錯、通信。動態代理可以借助很多開源類庫都可以實現比如javassist,asm,cglib,jdk自帶的,那到底要選擇哪個呢,當然是哪個性能好易用性好選哪個,為了保證高性能就要避免反射,首先jdk自帶的方案排除了,dubbo推薦使用javassit,一方面是它性能好比cglib好,另一方面它可以拼接java源碼動態編譯生成字節碼而asm需要框架開發人員熟悉class字節碼結構開發成本較高,關於幾種動態代理的方案對比可以看dubbo作者的博客—動態代理方案性能對比(http://javatar.iteye.com/blog/814426).
問題又來了?現在調用方通過代理對象來調用遠程服務,不需要關注通信協議,已經可以作為一個RPC框架來使用了,但是傳輸參數是一個復雜對象而不是一個個基本類型參數該怎么辦?這就需要引入序列化,業界流行的序列化協議有java序列化,hession,hession2,json序列化,protobuf,thrift。除此之外,我們需要一個高性能的通信框架,比如netty、mina。
- 9)通信與序列化模塊
有了NIO通信框架,不再是httpclient和tomcat,性能得到了很大的提升;但同時帶來連接管理的工作量,第一,NIO沒有BIO那樣可以方便設置讀超時時間,超時管理是必不可少的(不然堆內存溢出);第二,client-server建立長連接,server端要定期掃描所有連接,關閉空閑連接;第三,為了維持長連接,client會定期發送心跳給server,發心跳也能及時檢測與server的連接狀態(當網絡斷開而FIN消息未能發出,client不知道連接關閉導致操作失敗;通過定期傳輸接收數據,在遇到IO異常比如ClosedChannelException時就可以判斷連接失效,發起關閉連接操作).
server端由tomcat改為netty,接收到調用方發過來的類名、方法名、參數等數據,一般情況下需要通過反射調用最終服務代碼,但是反射性能很差,我們需要對每個服務都動態生成一個Wrapper類(通過拼接源碼,借助javassist動態編譯),避免反射,代碼看起來是醬紫的:
清單 Wrapper.java
Public class Wrapper0{ public Object invokeMethod(Object object, String method, Class[]parameterTypes,Object[]parameterValues)throwsjava.lang.reflect.InvocationTargetException{ com.test.DemoServiceImpl w; try{ w =(com.test.DemoServiceImpl)object; }catch(Throwable e){ throw new IllegalArgumentException(e); } try{if("sayHello".equals( method )&¶meterTypes.length==1){ return w.sayHello((java.lang.String)parameterValues[0]);}} catch(Throwable e){ throw newjava.lang.reflect.InvocationTargetException(e); } }
以上代碼與dubbo真實代碼有些出入,這么寫為的是更能方便易懂。
那么問題又來了?之前用http協議傳輸的,client只需要在url中指定路徑,spring mvc通過url path找到方法反射調用。現在server使用netty,而調用方需要多種服務,服務方又暴露了多種服務不同服務還有不同方法,調用方應該怎么傳數據才能讓服務方知道它需要的服務呢?這就需要約定一種協議,協議規定了發出何種控制信息,接收方收到信息做出什么樣的動作做出什么響應。
- 10)協議
既然是遠程過程調用,肯定方法名、類名、參數類型、參數值這些少不了,通過這些信息,服務方就可以很容易映射到具體方法。除此之外,我通過傳遞某些頭信息,可以控制服務端不返回結果,比如消息通知。
總結:以上即是dubbo幾大核心組件:按照角色來划分分為
- provider(服務提供方,對應前文的服務方)
- consumer(服務消費方,對應前文的調用方)
- monitor center(監控中心)
- registry center(注冊中心,接下來我們以zookeeper為例子說明)
- admin web console(管理端,用於修改路由、修改配置,最終作用於注冊中心)
更細致的組件關系圖:按功能來划分
- directory (負責從zookeeper中心生成的provider列表)
- router (路由)
- fault-tolerantStrategy(容錯策略)
- loadBalance(負載均衡)
- monitorFilter(監控攔截)
- zookeeperClient(Zoookeeper客戶端,我們使用zookeeper做例子)
- proxy(代理對象)
- nettyClient(我們以netty作為通信框架)
- nettyServer(我們以netty作為通信框架)
- Hession2Serialization(我們選hession2作為序列化方案)
1.4dubbo SPI擴展框架
dubbo作為一個開源RPC框架,實現的功能也比較多,而不同的組件有各種不一樣的方案,
用戶會根據自己的情況來選擇合適的方案。比如序列化、通信框架、注冊中心都有不同方案可選,負載均衡,路由規則都有不同的策略,dubbo采用微內核+插件式擴展體系,獲得極佳的擴展性。
dubbo沒有借助spring,guice等IOC框架來管理,而是運用JDK SPI思路自己實現了一個IOC框架,減少了對外部框架的依賴,更多dubbo框架設計原則可以看dubbo作者的分享《框架設計原則》
- dubbo擴展框架特性
1). 內嵌在dubbo中
2). 支持通過SPI文件聲明擴展實現(interfce必須有@SPI注解),格式為extensionName=extensionClassName,extensionName類似於spring的beanName
3). 支持通過配置指定extensionName來從SPI文件中選出對應實現ExtensionLoader.getExtensionLoader(RegistryFactory.class).getExtension("Zookeeper")
類似於spring的beanFactory.getBean(“xxx”);
ExtensionLoader是擴展機制能夠實現的核心類,類似於spring的beanFactory,只不過ExtensionLoader里面只存放單一類型的所有單例實現,存放dubbo擴展的”bean”容器是有多個ExtensionFactory組成的。
5) 支持依賴注入,注入來源(ExtensionFactory)可以自己定義,比如可以來自於SPI,也可以來自於spring容器,ExtensionFactory也是一個擴展,可以自己擴展。查找方式是通過set${ExtName}方法名(ExtName可以替換為任意擴展名稱)來注入相關類型對應extName的擴展,找不到就不注入。
6) 可以指定或動態生成自適應擴展類,通過interface方法里@Adaptive注解指定的value值作為key,從配置中(com.alibaba.dubbo.common.URL)獲取key對應的extName值,找到對應擴展再調用具體方法實現方法調用自適應
6) 對於擁有構造方法參數為interface類型的擴展,按照順序依次包裝最終擴展實現類,比如ProtocolListenerWrapper-->ProtocolFilterWrapper—->DubboProtocol
7) 可以通過對同一類型不同擴展類名添加@Activate注解,基於@Activate屬性group和value獲取指定group、指定參數名的擴展
- dubbo擴展框架實現
1) 擴展bean類型划分
每個擴展類似於spring的bean概念,所以我們姑且將dubbo擴展稱為extension bean和spring一樣,spirng也有不同類型的bean,比如BeanFactoryPostProcessor,BeanPostProceesor,dubbo也不例外也有各種類型的beanExtensionFactory
:用做依賴注入bean的查找,默認實現有SPIExtensionFactory和SpringExtensionFactoryWrapper bean
:對普通bean做包裝,比如ProtocolFilterWrapper用於應用於請求攔截,如果有多個wrapper bean會依次包裝,比如ProtocolListenerWraaper包裝ProtocolFilterWrapper,ProtocolFilterWrapper包裝DubboProtocolActivate Bean
:可以通過給bean添加@Activate注解,達到通過group、value搜索bean的目的,比如希望Filter類型bean在consumer端和provider端使用不同的組合,provider只使用注解了@Activate(group = Constants.PROVIDER)的bean,consumer只使用注解了@Activate(group = Constants.CONSUMER)的bean。Adaptive Bean
:自適應bean,如果有bean在類上添加了Adaptive注解可以通過注解查找;如果找不到會通過動態代理生成一個,SPIExtensionFactory如果找到有通過SPI配置的bean,那么它就注入Adaptive Bean。下面是一個Adaptive Bean自動生成的例子
比如我們以CacheFactory的Adaptive Bean為例
清單1 CacheFactory.java@SPI("lru") Public interface CacheFactory{ @Adaptive("cache") Cache getCache(URL url); }
清單2 CacheFactory@Adaptive.java
Package com.alibaba.dubbo.cache; Import com.alibaba.dubbo.common.extension.ExtensionLoader; Public class CacheFactory$Adpativeimplementscom.alibaba.dubbo.cache.CacheFactory{ Public com.alibaba.dubbo.cache.CachegetCache(com.alibaba.dubbo.common.URL arg0){ if(arg0 ==null)thrownewIllegalArgumentException("url == null"); com.alibaba.dubbo.common.URLurl= arg0; String extName=url.getParameter("cache","lru");① if(extName==null)throw new IllegalStateException("Fail to get extension........"); com.alibaba.dubbo.cache.CacheFactory extension = (com.alibaba.dubbo.cache.CacheFactory)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.cache.CacheFactory.class).getExtension(extName); return extension.getCache(arg0);② } }
清單1中CacheFactory Interface方法@Adaptive注解value屬性是“cache”,清單2 CacheFactory@Adaptive即是生成Adaptive bean源碼,它首先從URL配置中獲取key為cache
的value值(①所示),比
如”dubbo://192.168.1.1:2080/xxxx?cache=jcache”如果沒有配置”cache”,那么就取extName等於lru的cache bean,取到bean之后再調用對應方法,比如②所示。
2) SPI 格式
dubbo使用的SPI文件格式和JDK SPI(spring、log4j都有用過)有些不同,JDK-SPI文件僅僅是class列表,而dubbo使用的SPI文件是key-value結構,extName=className格式,文件名規則一樣,都是尋求實現的interface類全名。還是以cacheFactory為例,cacheFactory的SPI文件是以com.alibaba.dubbo.cache.CacheFactory為文件名,放在目錄/META-INFO/dubbo/internal classpath之下的
文件內容如下
threadlocal=com.alibaba.dubbo.cache.support.threadlocal.ThreadLocalCacheFactory lru=com.alibaba.dubbo.cache.support.lru.LruCacheFactory jcache=com.alibaba.dubbo.cache.support.jcache.JCacheFactory
ExtensionLoader查找步驟:
- 找到classpath下文件名com.alibaba.dubbo.cache.CacheFactory
- 以extName為key,Class對象為value放入extensionClasses緩存
3) ExtensionLoader有幾個最常用方法,首先構造方法接收一個擁有@SPI注解的Class參數.
Private ExtensionLoader(Class<?> type){ this.type= type; objectFactory=(type ==ExtensionFactory.class?null:ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); }
構造方法是私有的,需要通過getExtensiooader(Class<T> type)來生成和得到ExtensionLoader實例。接下來就可以借助ExtensionLoader根據擴展名找到擴展實例。
public T getExtension(String name)//根據extName獲取擴展bean
第一次構造擴展實例成功后會放入ExtensionLoader實例變量中緩存起來,保證單一實例。
public T getAdaptiveExtension() //獲取或者生成自適應bean
獲得Activate bean可以通過如下方法
//獲取Activate bean public List<T> getActivateExtension(URL url, String key, String group)
- dubbo擴展框架思維導圖
1.5 擴展組件腦圖
二級主題是按功能分類,三級主題是接口名,四級主題是擴展名稱。
這里不會做一一組件介紹,后文會穿插着介紹。
1.6 功能示例說明
官網上已經很詳細,這里只作為補充;需要結合官網http://dubbo.io/來看
功能 | 場景 | 實現 | 哪端 |
---|---|---|---|
啟動時檢查 | Spring容器啟動時,需要完成初始化bean,有一個bean初始化失敗都會導致spring容器啟動失敗。每個ReferenceConfig在init的時候會從注冊中心尋找provider並建立連接,尋找不到可用provider(建立連接失敗,lazy連接除外)就將拋異常導致初始化失敗。 | Consumer | |
集群容錯&負載均衡 | 通過注冊中心發現和收到的通知得到invoker list,可以將一個invoker理解成封裝了與某個provider(比如192.168.100.1:20880)建立的一組連接的抽象。從invoker列表中先篩選出符合路由規則(從注冊中心得到)的invoker list,通過負載均衡策略從invoker list得到一個invoker,這就是provider路由選擇過程。如果調用失敗,可以根據容錯策略,比如failover,可以再重新選擇新的invoker去遠程調用。注意:如果啟用了粘滯連接,除非發生失敗重選,否則負載均衡策略會失效 | Consumer | |
線程模型 | 以netty為例,netty會起固定的一些IO線程取做底層網絡接口讀、寫建立連接、關閉連接操作,當數據讀完需要處理的時候如果不將處理派發到線程池就會造成網絡事件(讀時間)長時間得不到處理。PS:netty使用OrderedMemoryAware ThreadPoolExecutor來解決這個問題 | Dubbo有兩個擴展專門為解決IO線程占用的問題,一個是Dispatcher定義什么事件派發到線程池,一個是threadpool定義線程池類型,建議fixed | Consumer & provider |
直連提供者 | 當配置了url,consumer不會從注冊中心獲取provider | Consumer | |
注冊不訂閱&訂閱不注冊 | 注冊:將自己的ip:port和所擁有的服務寫入注冊中心節點訂閱:當注冊中心某個節點變更時收到通知比如,provider列表變更會通知consumer | Registry client,不訂閱就不做subscribe操作不注冊就是不做register操作 | Consumer & Provider |
靜態服務 | 比如zookeeper注冊中心,設置dynamic=false,那么在注冊的時候,創建的節點就是永久節點,session失效時,節點不會釋放,因此consumer也收不到通知,不建議將dynamic設為false,只在需要人工管理和調試時才設置 | Consumer & provider | |
多協議 | 1不同服務不同協議:根據服務性能的實用性 2同一服務多種協議:滿足不同consumer的要求 | 對於provider每種協議都是單獨的sever,支持多個協議需要多個server,比如同時支持dubbo和rmi,需要起兩個端口,dubbo起netty server,rmi協議啟rmi server | |
多注冊中心 | 1、 以zookeeper為例,在多個zookeeper集群中創建節點,使得來自不同注冊中心的consumer都可以發現我的服務2、 不同的服務注冊到不同的zookeeper集群中 3、 在consumer端,兩個reference bean,同一接口引用,可以訂閱不同的注冊中心 | Consumer & provider | |
服務分組&多版本 | 同一實現,有不同組,比如場景灰度發布,abtest,推薦算法對比。版本升級時,為了兼容老版本,可以注冊多個服務但是版本不一樣,服務實現也不一樣 | Consumer在從注冊中心抓取provider信息時,會對信息進行匹配,group和version匹配才會引用 | Consumer & provider |
分組聚合 | 略 | Consumer | |
參數驗證 | 略 | Consumer | |
結果緩存 | 通過CacheFilter實現 | Consumer | |
泛化引用 | 框架集成,可以用於測試,甚至快速寫程序,驗證provider端是否正常 | 深入淺出10協議中說了,可以通過接口名、方法名、參數來進行遠程調用,既然如此,我們就可以使用萬能的GenericService替換任何服務引用達到迅速發起服務的目的。 | Consumer |
泛化實現 | 框架集成 | 略 | Provider |
上下文信息 | 略 | ||
隱式傳參 | 略 | ||
異步調用&事件通知 | Consumer遠程調用有兩種方式 1是發送數據同步等待結果返回 2發送完數據(甚至不等數據發出)直接返回,等需要結果時通過get方法等待響應結果,或者不通過get方法而是設置回調方法來處理影響 | 實現實際上是用戶線程將java對象序列化成bytebuffer塞入隊列,通信框架中IO線程從隊里取出bytebuffer發出。同樣接收到數據后,IO線程將bytebuffer decode成request對象,如果用戶線程有對結果數據的wait操作,就喚醒用戶線程。最高效的方式是不通過get操作而是通過onreturn真正達到無阻塞的目的 | Consumer |
本地調用 | 略 | ||
參數回調 | 略 | ||
本地存根 | 做緩存、預處理、參數驗證 | Stub類包裝service類 | Consumer 但是需要通過provider配置,來使consumer啟用存根的執行 |
本地偽裝 | Provider | ||
並發控制 | executes、actives | 通過executeLimitFilter和ActiveLimitFilter實現,在調用前計數器加1,調用后計算器減1 | Provider & consumer |
連接控制 | accpets,connections | 去收到連接請求時,判斷連接數是否大於配置閥值,大於則關閉對於connection,是consumer在refer provider時,對一個服務建立的連接數 | Provider &Consumers |
延遲連接 | 略 | ||
粘滯連接 | 使用粘滯連接,只有在調用失敗的情況下才會啟用負載均衡策略,否則每次都是同一個provider | Consumer | |
令牌驗證 | 通過tokenFilter實現 | Consumer provider | |
路由規則 | 通過管理端向注冊中心,/xxx/routers節點下添加路由規則,consumer收到通知更新本地路由規則 | Consumer | |
配置規則 | 同樣通過管理端向注冊中心 /xxx/configurators 添加配置信息,provider和consumer收到通知后重新export或者refer.Provider收到通知后不需要重啟server,只需要動態修改配置即可,包括acceptes,心跳間隔,派發線程池最大最小線程數量等等。而consumer沒有那么幸運,每次收到修改通知哪怕一個參數都需要重建連接。 | Consumer & provider | |
延遲暴露 | Provider | ||
服務容器 | Consumer & provider | ||
服務降級 | 略 | ||
優雅停機 | 略 | ||
主機綁定 | 略 | ||
日志適配 | 略 | ||
服務容器 | Consumer & provider | ||
訪問日志 | 通過accessFilter實現 | Provider |
本文轉載自: 格_魯
鏈接:https://www.imooc.com/article/22585