天天看點

微服務之-----灰階釋出(金絲雀釋出)

灰階釋出(又名金絲雀釋出)

是指在黑與白之間,能夠平滑過渡的一種釋出方式。在其上可以進行A/B 測試(AB測試即為Web或App界面或流程制作兩個(A/B)或多個(A/B/n)版本,在同一時間次元,分别讓組成成分相同(相似)的訪客群組(目标人群)随機的通路這些版本,收集各群組的使用者體驗資料和業務資料,最後分析、評估出最好版本,正式采用。),即讓一部分使用者繼續用産品特性A,一部分使用者開始用産品特性B,如果使用者對B沒有什麼反對意見,那麼逐漸擴大範圍,把所有使用者都遷移到B上面來。灰階釋出可以保證整體系統的穩定,在初始灰階的時候就可以發現、調整問題,以保證其影響度。

今天來說說微服務環境下怎麼實作金絲雀釋出,包括網關按指定規則轉發到對應服務和微服務之間按指定規則請求下遊服務。

1.網關層按指定規則路由服務,本例以zuul網關為例。

首先準備需要的微服務環境cloud-eureka、cloud-zuul、sms-service,其中eureka是注冊中心,cloud-zuul、gray-service作為服務注冊到cloud-eureka。其中 sms-service 我們會注冊兩個執行個體到注冊中心(友善我們後期轉發服務請求到指定服務案例),可以通過以下配置實作idea同一服務啟動多個執行個體:

1.修改application.yml檔案

spring:
  application:
    #應用名稱
    name: service-sms
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7900/eureka/
    registry-fetch-interval-seconds: 30
    enabled: true
  instance:
    lease-renewal-interval-in-seconds: 30

---

spring:
  profiles: 8003
server:
  port: 8003

---
spring:
  profiles: 8004
server:
  port: 8004
           

2.修改idea配置

微服務之-----灰階釋出(金絲雀釋出)

至此選中需要啟動的配置即可完成多執行個體啟動,然後通路http://localhost:7900/即可看到注冊中心所注冊的服務執行個體:

微服務之-----灰階釋出(金絲雀釋出)

可以看到注冊中心共注冊了兩個服務,其中service-sms有兩個執行個體。

修改服務,在sms-servie服務下建立controller:

@RestController
@Slf4j
public class RibbonLoadBalanceController {

    @Value("${server.port}")
    private Integer port;

    @Value("${spring.application.name}")
    private String applicationName;

    @GetMapping("/load-balance")
    public String loadBalance() {
        return port + ":" + applicationName;
    }
}
           

重新開機sms-servie兩個服務執行個體,等服務注冊到注冊中心開始測試,通過網關請求sms服務:http://localhost:9100/service-sms/load-balance可以發現 8003:service-sms和 8004:service-sms交替出現,現在我們來實作讓網關按照指定規則路由請求到指定服務。

首先需要區分不同版本的服務,可以通過配置服務的meta-data來實作,如修改sms-service的application.yml配置檔案如下:

spring:
  profiles: 8004
eureka:
  instance:
    metadata-map:
      # key value均為自定義
      version: v2
server:
  port: 8004

---

spring:
  profiles: 8003
eureka:
  instance:
    metadata-map:
      # key value均為自定義
      version: v1
server:
  port: 8003
           

修改完成後重新開機,通過通路 http://localhost:7900/eureka/apps 即可看到注冊到eureka的執行個體資訊:

微服務之-----灰階釋出(金絲雀釋出)

當然如果不想重新開機服務,也可以通過請求接口來完成metadata資料的更新:

PUT /eureka/v2/apps/appID/instanceID/metadata?key=value,相關接口資訊見官網 https://github.com/Netflix/eureka/wiki/Eureka-REST-operations。

至此已經可以區分同一服務不同版本的執行個體資訊了(因為執行個體的metadata不同),怎麼通過網關來路由轉發請求呢?

我們知道網關可以做很多事情,例如鑒權、限流等等都可以通過繼承ZuulFilter來實作,其實通過網關按指定規則轉發請求也是通過類似方式來實作。

這裡用到一個三方元件,pom.xml中添加

<dependency>
      <groupId>io.jmnarloch</groupId>
      <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
      <version>2.1.0</version>
</dependency>
           

建立GrayFilter

@Component
public class GrayFilter extends ZuulFilter {

    @Override
    public String filterType() {
        // 指定過濾器類型  這裡選擇路由
        return FilterConstants.ROUTE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 多個過濾器執行順序,值越小,執行順序越靠前
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        // 指定需要過濾的請(可以按自己業務需求實作,這裡預設全部過濾)
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();

        String userId = request.getHeader("userId");
        if (StringUtils.isBlank(userId)) {
            return null;
        }
        // 查詢規則(這裡的規則可以儲存到db等地方)
        if (Integer.valueOf(userId) == 1) {
            // 滿足規則的請求路由到指定服務
            RibbonFilterContextHolder.getCurrentContext().add("version", "v1");
        }
        return null;
    }
}

           

在其中我們指定userId為1的使用者規定路由到metadata中version值為v1的服務。

重新開機cloud-zuul以及sms-service服務兩個執行個體開始測試:

打開postman,get請求請求 http://localhost:9100/service-sms/load-balance,同時在header中添加userId=1

微服務之-----灰階釋出(金絲雀釋出)

通過測試我們現在當userId=1時,是以的請求都被轉發到了metadata為version=v1的8003服務上,當請求頭不添加userId參數或者userId不等于1時,請求依然會被轉發到8003和8004兩個服務中。

微服務之-----灰階釋出(金絲雀釋出)

這樣就實作了我們的需求。

2.服務間按指定規則路由服務

上面我們已經實作了通過網關路由滿足要求的請求到指定服務,那麼服務之間調用不經過網關怎麼實作按指定規則路由服務呢?

建立gray-serivce服務,并注冊到eureka,建立RequestSmsController對service-sms發送請求進行測試。

@RestController
@Slf4j
public class RequestSmsController {

    private final RestTemplate restTemplate;

    public RequestSmsController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping("/request-sms")
    public String requestSms() {
        log.info("request-sms....");
        String url = "http://service-sms/gray";
        return restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<Object>(null, null), String.class).getBody();
    }
}
           

使用RestTemplate我們需要申明一個bean并使用@LoadBalanced注解。

@Configuration
public class RestTemplateConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate(simpleClientHttpRequestFactory());
    }

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(180000);
        factory.setConnectTimeout(5000);
        return factory;
    }
}
           

通過請求 http://localhost:8080/request-sms 測試我們發現,service-sms:8003 和 service-sms:8004 服務均會被轉發到。

通過上面通過網關路由服務的案例我們發現,要實作按指定規則路由服務,在這一個流程中肯定要有一個參數辨別這個請求是誰的,然後我們才能拿到他去按指定規則進行路由,那什麼可以貫穿一個線程的聲明周期呢?ThreadLocal

1.建立RibbonParameters建立一個線程一對一的變量副本

@Component
public class RibbonParameters {

    private static final ThreadLocal local = new ThreadLocal();

    public static <T> T get() {
        return (T) local.get();
    }

    public static <T> void set(T data) {
        local.set(data);
    }
}
           

2.建立切面,在需要的地方注入參數,通過threadLocal程序傳遞。

@Aspect
@Component
public class RibbonParameterAspect {
    
    /**
     * 聲明切入點
     * @return {@link void}
     */
    @Pointcut("execution(* com.info.grayservice.controller..*Controller*.* (..))")
    private void ribbonParameterPoint() { };
    
    /**
     * 具體增強邏輯
     * @return {@link void}
     */
    @Before("ribbonParameterPoint()")
    public void before(JoinPoint joinPoint) {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String userId = request.getHeader("userId");
        if (StringUtils.isBlank(userId)) {
            return;
        }
        Map<String, String> map = new HashMap<>();
        if (Integer.valueOf(userId) == 1) {
            // 注入路由規則參數
            map.put("version", "v2");
            RibbonParameters.set(map);
        }
    }
}
           

3.建立路由規則GrayRule

/**
 * 自定義服務間路由規則
 */

@Slf4j
public class GrayRule extends AbstractLoadBalancerRule {


    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }

    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }

    private Server choose(ILoadBalancer lb, Object key) {
        Server server = null;
        Map<String, String> threadLocalMap = RibbonParameters.get();
        List<Server> reachableServers;
        log.info("lb = {}, key = {}, threadLocalMap = {}", lb, key, threadLocalMap);
        do {
            reachableServers = lb.getReachableServers();
            for (Server reachableServer : reachableServers) {
                server = reachableServer;
                InstanceInfo instanceInfo = ((DiscoveryEnabledServer) (server)).getInstanceInfo();
                Map<String, String> metadata = instanceInfo.getMetadata();
                String version = metadata.get("version");
                if (StringUtils.isBlank(version)) {
                    continue;
                }
                if (version.equals(threadLocalMap.get("version"))) {
                    return server;
                }
            }
        } while (server == null);
        // 比對不到則随機選取一個,避免調用出錯
        return reachableServers.get(new Random().nextInt(reachableServers.size()));
    }
}
           

4.當然,為了使我們的路由規則生效,我們需要聲明這個自定義的bean

import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;

public class GrayRibbonConfiguration {

    @Bean
    public IRule grayRule() {
        return new GrayRule();
    }
}
           

5.主類添加@RibbonClient注解,使我們新定義的grayRule生效

@SpringBootApplication
@RibbonClient(name = "service-sms", configuration = GrayRibbonConfiguration.class)
public class GrayServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(GrayServiceApplication.class, args);
    }
}
           

重新開機開始測試,在請求頭添加userId=1,請求 http://localhost:8080/request-sms,我們發現所有的服務都被轉發到了metadata中version為v2的服務8004。

微服務之-----灰階釋出(金絲雀釋出)

當然,通過我們之前提到過的ribbon-discovery-filter-spring-cloud-starter可以快速的實作這個效果,我們上面實作的很多細節都已經被封裝好了。

pom.xml添加maven坐标:

<dependency>
       <groupId>io.jmnarloch</groupId>
       <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
       <version>2.1.0</version>
</dependency>
           

修改RibbonParameterAspect.java如下

@Aspect
@Component
public class RibbonParameterAspect {

    /**
     * 聲明切入點
     */
    @Pointcut("execution(* com.info.apipassenger.controller..*Controller*.* (..))")
    private void ribbonParameterPoint() {
    }

    ;

    /**
     * 具體增強邏輯
     */
    @Before("ribbonParameterPoint()")
    public void before(JoinPoint joinPoint) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String userId = request.getHeader("userId");
        if (StringUtils.isBlank(userId)) {
            return;
        }
        if (Integer.valueOf(userId) == 1) {
            RibbonFilterContextHolder.getCurrentContext().add("version", "v2");
        }
    }
}
           

這時,GrayRule、GrayRibbonConfiguration、RibbonParameters都不需要了,通過測試我們發現同樣可以達到需要的效果。

微服務之-----灰階釋出(金絲雀釋出)

至此,我們實作了通過網關按指規則路由服務以及服務間按指定規則路由,全篇結束,謝謝您的觀看。本人能力有限,如有描述了解不到位的還請多多包涵,多多指教,感謝。