SpringCloud灰度發佈實踐

服務實例

  • 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轉發到具體服務的灰度流程了,流程圖如下
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接口把它在eurekametadata-map存儲的version信息置空即可.

7.到這裏基本上整個請求流程就比較完整了,上述例子中是以用戶ID作爲灰度的維度,當然這裏可以實現更多的灰度策略,比如IP等,基本上都可以基於此方式做擴展

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章