天天看點

SpringCloud 自定義ribbon路由實作灰階釋出

整合nacos(Euraka 類似)實作灰階釋出

在一般情況下,更新伺服器端應用,需要将應用源碼或程式包上傳到伺服器,然後停止掉老版本服務,再啟動新版本。但是這種簡單的釋出方式存在兩個問題,一方面,在新版本更新過程中,服務是暫時中斷的,另一方面,如果新版本有BUG,更新失敗,復原起來也非常麻煩,容易造成更長時間的服務不可用。

什麼是灰階釋出呢?要想了解這個問題就要先明白什麼是灰階。灰階從字面意思了解就是存在于黑與白之間的一個平滑過渡的區域,是以說對于網際網路産品來說,上線和未上線就是黑與白之分,而實作未上線功能平穩過渡的一種方式就叫做灰階釋出。

網際網路産品的幾個特點:使用者規模大、版本更新頻繁。新版本的每次上線,産品都要承受極大的壓力,而灰階釋出很好的規避了這種風險。

在了解了什麼是灰階釋出的定義以後,就可以來了解一下灰階釋出的具體操作方法了。可以通過很多種形式來抽取一部分使用者,比如說選擇自己的VIP使用者,或者選擇一些活躍使用者,把這些使用者分成兩批,其中一批投放A版本,另外一批投放B版本,在投放之前就要對各種可能存在的資料做到收集記錄工作,這樣才能在投放以後檢視兩個版本的使用者資料回報,通過大量的資料分析以及調查來确定最後使用哪一個版本來進行投放。

那麼,在springcloud的分布式環境中,我們如何來區分使用者(版本),如何來指定這部分使用者使用不同版本的微服務?這篇文章将會通過實際的例子來說明這個過程。

假設使用者發起一個通路,服務的調用路徑為:使用者--> ZUUL -->app-consumer-->app-provider,那麼我們在ZUUL和SERVICE1都需要實作自定義的通路路由。

下面這個設計的重點主要在:

1. 利用threadlocal+feign實作http head中實作版本資訊的傳遞

2. 使用nacos的中繼資料,定義我們需要的灰階服務

3. 自定義ribbon的路由規則,根據nacos的中繼資料選擇服務節點

公共配置:

1. ThreadLocal

package com.start.commom.threadLocal;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;

public class PassParameters {

        private static final Logger log = LoggerFactory.getLogger(PassParameters.class);

        private static final ThreadLocal localParameters = new ThreadLocal();

        public static <T> T get(){
            T t = (T) localParameters.get();
            log.info("ThreadID:{}, threadLocal {}", Thread.currentThread().getId(), JSON.toJSONString(t));
            return t;
        }

        public static <T> void set(T t){
            log.info("ThreadID:{}, threadLocal set {}", Thread.currentThread().getId(), JSON.toJSONString(t));
            localParameters.set(t);
        }
    }

           

2 .AOP攔截請求頭 package com.start.commom.aop;

import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.start.commom.threadLocal.PassParameters;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
 
/**
 * @author hsn
 */
@Aspect
@Order(85)
@Component
public class ApiRequestAspect {
    private static Logger logger = LoggerFactory.getLogger(ApiRequestAspect.class);


    @Pointcut("execution(* com.start.app..controller..*Controller*.*(..))")
    private void anyMethod() {
    }
 
    /**
     * 方法調用之前調用
     */
    @Before(value= "anyMethod()")
    public void doBefore(JoinPoint jp){
        logger.info("開始處理請求資訊!");
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest();
 
        Map<String,String> map = new HashMap<>();
        String username =  request.getHeader("username");
        String token = request.getHeader("token");
        String version = request.getHeader("version");
        
        
        if(version == null) {
            version = request.getParameter("v");
        }
        
        
        map.put("username", username);
        map.put("token", token);
        map.put("version", version);
        
        //将map放到threadLocal中
        PassParameters.set(map);
    }
 
    /**
     * 方法之後調用
     */
    @AfterReturning(pointcut = "anyMethod()")
    public void  doAfterReturning(){
        
    }
    
}

           

3. 實作自己的GrayMetadataRule

GrayMetadataRule 将會從nacos中擷取元伺服器的資訊,并根據這個資訊選擇伺服器

package com.start.commom.core;

import com.alibaba.nacos.api.naming.pojo.Instance;
import com.google.common.base.Optional;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
//import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
import com.start.commom.threadLocal.PassParameters;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryClient;
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.*;

public class GrayMetadataRule extends ZoneAvoidanceRule {
    public static final String META_DATA_KEY_VERSION = "version";

    private static final Logger logger = LoggerFactory.getLogger(GrayMetadataRule.class);

    @Override
    public Server choose(Object key) {

        List<Server> servers = this.getLoadBalancer().getReachableServers();

        if (CollectionUtils.isEmpty(servers)) {
            return null;
        }

        // 需要從head取灰階辨別
        //String version = "mx";
        Map<String,String> map = PassParameters.get();  
        
        String  version = null;
        
        
        
        if(map != null && map.containsKey("version")) {
            version = map.get("version");
        }
        
        logger.info("GrayMetadataRule:"+version);
        
        /*if(StringUtils.isEmpty(version)){
           
        }*/
        


        List<Server> noMetaServerList = new ArrayList<>();
        for (Server server : servers) {
            if (!(server instanceof NacosServer)) {
                logger.error("參數非法,server = {}", server);
                throw new IllegalArgumentException("參數非法,不是NacosServer執行個體!");
            }

            NacosServer nacosServer = (NacosServer) server;
            Instance instance = nacosServer.getInstance();

            Map<String, String> metadata = instance.getMetadata();
            
            if(version !=null) {
                // version政策
                String metaVersion = metadata.get(META_DATA_KEY_VERSION);
                if (!StringUtils.isEmpty(metaVersion)) {
                    if (metaVersion.equals(version)) {
                        return server;
                    }
                } else {
                    noMetaServerList.add(server);
                }
            }else {
                noMetaServerList.add(server);
            }
            
        }

        if (StringUtils.isEmpty(version) && !noMetaServerList.isEmpty()) {
            logger.info("====> 無請求header...");
            return originChoose(noMetaServerList, key);
        }

        return null;

    }

    private Server originChoose(List<Server> noMetaServerList, Object key) {
        Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(noMetaServerList, key);
        if (server.isPresent()) {
            return server.get();
        } else {
            return null;
        }
    }
}
           

4. feign 攔截器

将threadadlocal中的内容讀出并寫入請求頭,通過這種方式傳遞版本資訊

package com.start.commom.core;


import feign.Feign;
import feign.RequestInterceptor;
import feign.RequestTemplate;

import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;

import com.start.commom.threadLocal.PassParameters;

 
@Configuration
@ConditionalOnClass(Feign.class)
public class DefaultFeignConfig implements RequestInterceptor {
 
    @Value("${spring.application.name}")
    private String appName;
 
    @Override
    public void apply(RequestTemplate requestTemplate)
    {
    	Map<String,String> map = PassParameters.get();
    	
        String username = map.get("username");
        if(StringUtils.isNotEmpty(username)){
            requestTemplate.header("username", username);
        }
        String token = map.get("token");
        if(StringUtils.isNotEmpty(token)){
            requestTemplate.header("token", token);
        }
        //這裡是灰階的版本資訊
        String  version = map.get("version");
        if(StringUtils.isNotEmpty(version)){
            requestTemplate.header("version", version);
        }
        
        
        
    }
 
}
           

5. 設定環境變量

自定義的路由規則,需要在 application.properties 中配置才能使用,(service1.ribbon.NFLoadBalancerRuleClassName=com.start.commom.core.GrayMetadataRule   service1就是要用這個規則的具體服務),這個配置的實際作用就是設定了一個環境變量,如果服務很多,我們建立一個數組,用代碼建立 ,下面這個配置就是通過配置檔案讀取需要利用這個路由規則的服務清單,建立環境變量

package com.start.commom.core;

import java.util.List;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnClass(com.netflix.loadbalancer.ZoneAvoidanceRule.class)
public class MyRibbonConfiguration implements InitializingBean {




    @Value("#{'${loadbalanced.services}'.split(',')}")
    private List<String> loadbalancedServices;

     

    /**

     * 預設使用切流量的負載均衡政策

     */

    @Value("${ribbon.NFLoadBalancerRuleClassName}")
    private String ribbonLoadBancerRule;



    @Override

    public void afterPropertiesSet() throws Exception {

        if (null != loadbalancedServices){

            for (String service : loadbalancedServices){

                String key = service + ".ribbon.NFLoadBalancerRuleClassName";

                System.setProperty(key, ribbonLoadBancerRule);

            }

        }

    }



}

           

6. ZUUL配置

spring.application.name=zuul-gateway
server.port=8899
spring.cloud.nacos.discovery.server-addr = 127.0.0.1:8848
swagger.enabled=true
swagger.title=zuul-gateway
#自定義的負載均衡類
ribbon.NFLoadBalancerRuleClassName=com.start.commom.core.GrayMetadataRule
#需要使用自定義負載均衡的服務(spring.application.name 對應的值)
loadbalanced.services=app-consumer
           

增加自定義ZUUL過濾器,攔截後将版本資訊放到threadlocal中,在路由的時候,判斷目前的版本資訊

package com.start.zuul.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import com.start.commom.threadLocal.PassParameters;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
public class GrayFilter extends ZuulFilter {

	private static final String HEADER_TOKEN = "token";
	private static final Logger logger = LoggerFactory.getLogger(GrayFilter.class);

	@Override
	public String filterType() {
		return FilterConstants.PRE_TYPE;
	}

	@Override
	public int filterOrder() {
		return 1000;
	}

	@Override
	public boolean shouldFilter() {
		return true;
	}

	@Override
	public Object run() {
		RequestContext ctx = RequestContext.getCurrentContext();
		String token = ctx.getRequest().getHeader(HEADER_TOKEN);

		String userId = token;
		log.info("======>userId:{}", userId);

		// 傳遞給後續微服務,這裡可以根據使用者來判定是否應有灰階,我這裡直接在請求中加了個v來判斷
		String v = ctx.getRequest().getParameter("v");
		String version = v;
		if (v != null) {
			ctx.addZuulRequestHeader("version", version);
			Map<String, String> map = new HashMap<String, String>();
			map.put("version", version);
			PassParameters.set(map);
		}

		return null;
	}
}

           

7. APP-CONSUMER配置

app-consumer,通過切面擷取到版本資訊,并将版本資訊放入threadlocal中,通過feign再封裝到http頭,傳遞到下一層

spring.application.name=app-consumer
server.port=8900

spring.cloud.nacos.discovery.server-addr = 127.0.0.1:8848
spring.cloud.nacos.config.file-extension = properties
swagger.enabled=true
swagger.title=app-consumer

ribbon.NFLoadBalancerRuleClassName=com.start.commom.core.GrayMetadataRule
loadbalanced.services=app-provider
           

更多代碼請參考:https://github.com/hsn999/SpringCloud-grayRelease