代碼GITjava
在平時的業務開發過程當中,後端服務與服務之間的調用每每經過fegin
或者resttemplate
兩種方式。可是咱們在調用服務的時候每每只須要寫服務名就能夠作到路由到具體的服務,這其中的原理相比你們都知道是SpringCloud
的ribbon
組件幫咱們作了負載均衡的功能。 git
灰度的核心就是路由,若是咱們可以重寫ribbon默認的負載均衡算法是否是就意味着咱們可以控制服務的轉發呢?是的!github
zuul在轉發請求的時候,也會根據
Ribbon
從服務實例列表中選擇一個對應的服務,而後選擇轉發.
不管是經過Resttemplate
仍是Fegin
的方式進行服務間的調用,他們都會從Ribbon
選擇一個服務實例返回.
上面幾種調用方式應該涵蓋了咱們平時調用中的場景,不管是經過哪一種方式調用(排除直接ip:port調用),最後都會經過Ribbon
,而後返回服務實例.算法
Eureka的元數據有兩種,分別爲標準元數據和自定義元數據。spring
標準元數據:主機名、IP地址、端口號、狀態頁和健康檢查等信息,這些信息都會被髮布在服務註冊表中,用於服務之間的調用。自定義元數據:自定義元數據可使用
eureka.instance.metadata-map
配置,這些元數據能夠在遠程客戶端中訪問,可是通常不會改變客戶端的行爲,除非客戶端知道該元數據的含義segmentfault
請求名稱 | 請求方式 | HTTP地址 | 請求描述 |
---|---|---|---|
註冊新服務 | POST | /eureka/apps/{appID} |
傳遞JSON或者XML格式參數內容,HTTP code爲204時表示成功 |
取消註冊服務 | DELETE | /eureka/apps/{appID} /{instanceID} |
HTTP code爲200時表示成功 |
發送服務心跳 | PUT | /eureka/apps/{appID} /{instanceID} |
HTTP code爲200時表示成功 |
查詢全部服務 | GET | /eureka/apps | HTTP code爲200時表示成功,返回XML/JSON數據內容 |
查詢指定appID的服務列表 | GET | /eureka/apps/{appID} |
HTTP code爲200時表示成功,返回XML/JSON數據內容 |
查詢指定appID&instanceID | GET | /eureka/apps/{appID} /{instanceID} |
獲取指定appID以及InstanceId的服務信息,HTTP code爲200時表示成功,返回XML/JSON數據內容 |
查詢指定instanceID服務列表 | GET | /eureka/apps/instances/{instanceID} |
獲取指定instanceID的服務列表,HTTP code爲200時表示成功,返回XML/JSON數據內容 |
變動服務狀態 | PUT | /eureka/apps/{appID} /{instanceID} /status?value=DOWN |
服務上線、服務下線等狀態變更,HTTP code爲200時表示成功 |
變動元數據 | PUT | /eureka/apps/{appID} /{instanceID} /metadata?key=value |
HTTP code爲200時表示成功 |
配置文件方式:後端
eureka.instance.metadata-map.version = v1
接口請求:緩存
PUT /eureka/apps/{appID}/{instanceID}/metadata?key=value
原圖連接app
zuul
,此時zuul
攔截器會根據用戶攜帶請求token
解析出對應的userId
version=xxx
;若不是,則不作任何處理放行zuul
攔截器執行完畢後,zuul
在進行轉發請求時會經過負載均衡器Ribbon。zuul
存入線程變量值version
。於此同時,Ribbon還會取出全部緩存的服務列表(按期從eureka刷新獲取最新列表)及其該服務的metadata-map
信息。而後取出服務metadata-map
的version
信息與線程變量version
進行判斷對比,若值一直則選擇該服務做爲返回。若全部服務列表的version信息與之不匹配,則返回null,此時Ribbon選取不到對應的服務則會報錯!當服務爲非灰度服務,即沒有version信息時,此時Ribbon會收集全部非灰度服務列表,而後利用Ribbon默認的規則從這些非灰度服務列表中返回一個服務。負載均衡
zuul
經過Ribbon將請求轉發到consumer服務後,可能還會經過fegin
或resttemplate
調用其餘服務,如provider服務。可是不管是經過fegin
仍是resttemplate
,他們最後在選取服務轉發的時候都會經過Ribbon
。fegin
或resttemplate
調用另一個服務的時候須要設置一個攔截器,將請求頭version=xxx
給帶上,而後存入線程變量。fegin
或resttemplate
的攔截器後最後會到Ribbon,Ribbon會從線程變量裏面取出version
信息。而後重複步驟(4)和(5)
首先,咱們經過更改服務在eureka的元數據標識該服務爲灰度服務,筆者這邊用的元數據字段爲version
。
1.首先更改服務元數據信息,標記其灰度版本。經過eureka RestFul接口或者配置文件添加以下信息eureka.instance.metadata-map.version=v1
2.自定義zuul
攔截器GrayFilter
。此處筆者獲取的請求頭爲token,而後將根據JWT的思想獲取userId,而後獲取灰度用戶列表及其灰度版本信息,判斷該用戶是否爲灰度用戶。
若爲灰度用戶,則將灰度版本信息version
存放在線程變量裏面。此處不能用Threadlocal
存儲線程變量,由於SpringCloud用hystrix作線程池隔離,而線程池是沒法獲取到ThreadLocal中的信息的! 因此這個時候咱們能夠參考Sleuth
作分佈式鏈路追蹤的思路或者使用阿里開源的TransmittableThreadLocal
方案。此處使用HystrixRequestVariableDefault
實現跨線程池傳遞線程變量。
3.zuul攔截器處理完畢後,會通過ribbon組件從服務實例列表中獲取一個實例選擇轉發。Ribbon默認的Rule爲
ZoneAvoidanceRule`。而此處咱們繼承該類,重寫了其父類選擇服務實例的方法。
如下爲Ribbon源碼:
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule { // 略.... @Override public Server choose(Object key) { ILoadBalancer lb = getLoadBalancer(); Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key); if (server.isPresent()) { return server.get(); } else { return null; } } }
如下爲自定義實現的僞代碼:
public class GrayMetadataRule extends ZoneAvoidanceRule { // 略.... @Override public Server choose(Object key) { //1.從線程變量獲取version信息 String version = HystrixRequestVariableDefault.get(); //2.獲取服務實例列表 List<Server> serverList = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key); //3.循環serverList,選擇version匹配的服務並返回 for (Server server : serverList) { Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata(); String metaVersion = metadata.get("version); if (!StringUtils.isEmpty(metaVersion)) { if (metaVersion.equals(hystrixVer)) { return server; } } } } }
4.此時,只是已經完成了 請求==》zuul==》zuul攔截器==》自定義ribbon負載均衡算法==》灰度服務這個流程,並無涉及到 服務==》服務的調用。
服務到服務的調用不管是經過resttemplate仍是fegin,最後也會走ribbon的負載均衡算法,即服務==》Ribbon 自定義Rule==》服務。由於此時自定義的GrayMetadataRule
並不能從線程變量中取到version,由於已經到了另一個服務裏面了。
5.此時依然能夠參考Sleuth
的源碼org.springframework.cloud.sleuth.Span
,這裏不作贅述只是大體講一下該類的實現思想。 就是在請求裏面添加請求頭,以便下個服務可以從請求頭中獲取信息。
此處,咱們能夠經過在 步驟2中,讓zuul添加添加線程變量的時候也在請求頭中添加信息。而後,再自定義HandlerInterceptorAdapter
攔截器,使之在到達服務以前將請求頭中的信息存入到線程變量HystrixRequestVariableDefault中。而後服務再調用另一個服務以前,設置resttemplate和fegin的攔截器,添加頭信息。
resttemplate攔截器
public class CoreHttpRequestInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request); String hystrixVer = CoreHeaderInterceptor.version.get(); requestWrapper.getHeaders().add(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer); return execution.execute(requestWrapper, body); } }
fegin攔截器
public class CoreFeignRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { String hystrixVer = CoreHeaderInterceptor.version.get(); logger.debug("====>fegin version:{} ",hystrixVer); template.header(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer); } }
6.到這裏基本上整個請求流程就比較完整了,可是咱們怎麼讓Ribbon使用自定義的Rule?這裏其實很是簡單,只須要在服務的配置文件中配置一下代碼便可.
yourServiceId.ribbon.NFLoadBalancerRuleClassName=自定義的負載均衡策略類
可是這樣配置須要指定服務名,意味着須要在每一個服務的配置文件中這麼配置一次,因此須要對此作一下擴展.打開源碼org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration
類,該類是Ribbon的默認配置類.能夠清楚的發現該類注入了一個PropertiesFactory
類型的屬性,能夠看到PropertiesFactory
類的構造方法
public PropertiesFactory() { classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName"); classToProperty.put(IPing.class, "NFLoadBalancerPingClassName"); classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName"); classToProperty.put(ServerList.class, "NIWSServerListClassName"); classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName"); }
因此,咱們能夠繼承該類從而實現咱們的擴展,這樣一來就不用配置具體的服務名了.至於Ribbon是如何工做的,這裏有一篇方誌明的文章(傳送門)能夠增強對Ribbon工做機制的理解
7.到這裏基本上整個請求流程就比較完整了,上述例子中是以用戶ID做爲灰度的維度,固然這裏能夠實現更多的灰度策略,好比IP等,基本上均可以基於此方式作擴展
配置文件示例
spring.application.name = provide-test server.port = 7770 eureka.client.service-url.defaultZone = http://localhost:1111/eureka/ #啓動後直接將該元數據信息註冊到eureka #eureka.instance.metadata-map.version = v1
分別啓動四個測試實例,有version表明灰度服務,無version則爲普通服務。當灰度服務測試沒問題的時候,經過PUT請求eureka接口將version信息去除,使其變成普通服務.
實例列表:
port:7770 version:無
port: 7771 version:v1
port:8880 version:無
port: 8881 version:v1
修改服務信息
服務在eureka的元數據信息可經過接口http://localhost:1111/eureka/apps訪問到。
服務信息實例:
訪問接口查看信息http://localhost:1111/eureka/apps/PROVIDE-TEST
注意事項
經過此種方法更改server的元數據後,因爲ribbon會緩存實力列表,因此在測試改變服務信息時,ribbon並不會立馬從eureka拉去最新信息m,這個拉取信息的時間可自行配置。同時,當服務重啓時服務會從新將配置文件的version信息註冊上去。
zuul==>provider服務
用戶andy爲灰度用戶。
1.測試灰度用戶andy,是否路由到灰度服務provider-test:7771
2.測試非灰度用戶andyaaa(任意用戶)是否能被路由到普通服務provider-test:7770
zuul==>consumer服務>provider服務
以一樣的方式再啓動兩個consumer-test服務,這裏再也不截圖演示。請求從zuul==>consumer-test==>provider-test,經過
fegin
和resttemplate
兩種請求方式測試
Resttemplate請求方式
fegin請求方式
與Apollo實現整合,避免手動調用接口。實現配置監聽,完成灰度。詳情見下篇文章