服務實例
- eureka-server
- zuul -server
- Apollo-config
- provider-test
啓動兩個服務實例,一個帶有版本號信息,一個沒有port:7770 version:無
port: 7771 version:v1
- consumer-test
啓動兩個服務實例,一個帶有版本號信息,一個沒有任何信息.分別爲:port:8880 version:無
port:8881 version: v1
場景分析
公司採用的是SpringCloud微服務體系,註冊中心爲eureka。我們知道,對於eureka-server
而言,其他服務都是eureka-client
的存在,我們在業務服務中只需要引入@EnableDiscoveryClient
即可實現註冊.
比如我們想要調用order-service
的服務,只需要通過resttemplate
或者fegin
的方式指定該服務的服務名即可完成調用。這是爲什麼呢?這一切還得從Ribbon
這個背後的男人說起,因爲ribbon會根據order-service
的服務實例名獲取該服務實例的列表,這些列表中包含了每個服務實例的IP和端口號,Ribbon
會從中根據定義的負載均衡算法選取一個作爲返回.
看到這裏一切都有點撥雲見霧了,那麼意味着是不是只要能夠讓Ribbon用我們自定義的負載均衡算法就可以爲所欲爲了呢?顯然,是的!
簡單分析下我們在調用過程中的集中情況:
外部調用:
- 請求==>zuul==>服務
zuul在轉發請求的時候,也會根據Ribbon
從服務實例列表中選擇一個對應的服務,然後選擇轉發.
內部調用:
- 服務==>Resttemplate==>服務
- 服務==>Fegin==>服務
無論是通過Resttemplate
還是Fegin
的方式進行服務間的調用,他們都會從Ribbon
選擇一個服務實例返回.
上面幾種調用方式應該涵蓋了我們平時調用中的場景,無論是通過哪種方式調用(排除直接ip:port調用),最後都會通過Ribbon
,然後返回服務實例.
設計思路
eureka預備知識
eureka元數據:
- 標準元數據:主機名,IP地址,端口號,狀態頁健康檢查等信息
- 自定義元數據:通過
eureka.instance.metadata-map
配置
更改元數據:
- 源碼地址:
com.netflix.eureka.resources.InstanceResource.updateMetadata()
- 接口地址:
/eureka/apps/appID/instanceID/metadata?key=value
- 調用方式:
PUTE
流程解析:
1.在需要灰度發佈的服務實例配置中添加eureka.instance.metadata-map.version=v1
,註冊成功後該信息後保存在eureka
中.配置如下:eureka.instance.metadata-map.version=v1
2.自定義zuul
攔截器GrayFilter
。當請求帶着token
經過zuul
時,根據token得到userId,然後從分佈式配置中心Apollo中獲取灰度用戶列表,並判斷該用戶是否在該列表中(Apollo非必要配置,由於管理端比較完善所以筆者這裏選擇採用).
若在列表中,則把version
信息存在ThreadLocal
中,從而使Ribbon
中我們自定義的Rule
能夠拿到該信息;若不在,則不做任何處理。 但是我們知道hystrix
是用線程池做隔離的,線程池中是無法拿到ThreadLocal
中的信息的! 所以這個時候我們可以參考Sleuth
做分佈式鏈路追蹤的思路或者使用阿里開源的TransmittableThreadLocal
.
爲了方便繼承,這裏採用Sleuth
的方式做處理。Sleuth
能夠完整記錄一條跨服務調用請求的每個服務請求耗時和總的耗時,它有一個全局traceId和對每個服務的SpanId.利用Sleuth全局traceId的思路解決我們的跨線程調用,所以這裏可以使用HystrixRequestVariableDefault
實現跨線程池的線程變量傳遞效果.
3.zuul在轉發之前會先到我們自定義的Rule中,默認Ribbon
的Rule爲ZoneAvoidanceRule
.自定義編寫的Rule只需要繼承ZoneAvoidanceRule
並且覆蓋其父類的PredicateBasedRule#choose()
方法即可.該方法是返回具體server,源碼如下
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
public abstract AbstractServerPredicate getPredicate();
@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;
}
}
}
這裏就完成了zuul轉發到具體服務的灰度流程了,流程圖如下
4.上面完成了zuul-->服務的灰度過程,接下來就是解決服務與服務間的調用.服務與服務間的調用依然可以用Sleuth的方式解決,只需要把信息添加到header裏面即可.在使用RestTemplate
調用的時候,可以添加它的攔截器ClientHttpRequestInterceptor
,這樣可以在調用之前就把header信息加入到請求中.
restTemplate.getInterceptors().add(YourHttpRequestInterceptor());
5.到這裏基本上整個請求流程就比較完整了,但是我們怎麼使用自定義的Ribbon的Rule了?這裏其實非常簡單,只需要在properties
文件中配置一下代碼即可.
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工作機制的理解
6.這裏就完成了灰度服務的正確路由,若灰度服務已經發布測試完畢想要把它變成正常服務,則只需要通過eureka
的RestFul接口把它在eureka
的metadata-map
存儲的version
信息置空即可.
7.到這裏基本上整個請求流程就比較完整了,上述例子中是以用戶ID作爲灰度的維度,當然這裏可以實現更多的灰度策略,比如IP等,基本上都可以基於此方式做擴展