SpringCloudカスタムribbonルーティングによる階調パブリケーション


統合nacos(Euraka類似)による階調公開
一般的に、サーバ側アプリケーションをアップグレードするには、アプリケーションのソースコードまたはパッケージをサーバにアップロードし、古いバージョンのサービスを停止してから新しいバージョンを起動する必要があります.しかし、このような簡単なリリース方式には2つの問題があり、一方で、新しいバージョンのアップグレード中にサービスが一時的に中断される一方、新しいバージョンにBUGがあれば、アップグレードに失敗し、ロールバックするのも面倒で、より長い時間のサービスが利用できない可能性があります.
グレースケールパブリケーションとは?この問題を理解するには、まず階調とは何かを理解しなければならない.階調は文字通り黒と白の間に存在する平滑な遷移の領域であるため、インターネット製品にとって、オンラインと未オンラインは黒と白の区別であり、未オンライン機能の安定した遷移を実現する一つの方法は階調パブリケーションと呼ばれる.
インターネット製品のいくつかの特徴:ユーザーの規模が大きく、バージョンの更新が頻繁である.新しいバージョンがオンラインになるたびに、製品は大きな圧力に耐えなければならないが、階調の発表はこのようなリスクを回避している.
階調パブリケーションの定義を理解したら、階調パブリケーションの具体的な操作方法を理解することができます.多くの形式で一部のユーザーを抽出することができます.例えば、自分のVIPユーザーを選択するか、アクティブなユーザーを選択して、これらのユーザーを2つのロットに分けて、そのうちの1つはAバージョンを投入して、もう1つはBバージョンを投入して、投入する前に各種の存在する可能性のあるデータに対して記録を収集しなければなりません.これにより、リリース後に2つのバージョンのユーザーデータフィードバックを表示し、大量のデータ分析と調査を通じて、最後にどのバージョンを使用してリリースするかを決定できます.
ではspringcloudの分散環境では、ユーザー(バージョン)をどのように区別し、このユーザーが異なるバージョンのマイクロサービスを使用することをどのように指定しますか?この文章は実際の例を通じてこの過程を説明する.
ユーザがアクセスを開始すると、サービスの呼び出し経路は、ユーザ-->ZUUL-->app-consumer-->app-providerであると仮定すると、ZUULおよびSERVICE 1の両方でカスタムアクセスルーティングを実現する必要がある.
次の設計のポイントは主に次のとおりです.
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 get(){
            T t = (T) localParameters.get();
            log.info("ThreadID:{}, threadLocal {}", Thread.currentThread().getId(), JSON.toJSONString(t));
            return t;
        }

        public static  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 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 servers = this.getLoadBalancer().getReachableServers();

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

        //    head     
        //String version = "mx";
        Map 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 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 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 noMetaServerList, Object key) {
        Optional server = getPredicate().chooseRoundRobinAfterFiltering(noMetaServerList, key);
        if (server.isPresent()) {
            return server.get();
        } else {
            return null;
        }
    }
}

4.feignブロッキング
threadadadlocalのコンテンツをリクエストヘッダに読み出して書き込み、このようにバージョン情報を伝達する
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 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. 環境変数の設定
カスタムルーティングルールは、 アプリケーション.propertiesで構成して使用するには、(service 1.ribbon.NFLoadBalancerRuleClassName=com.start.commom.core.GrayMetadataRule   サービス1はこのルールを使用する具体的なサービスです)、この構成の実際の役割は環境変数を設定することです.サービスが多い場合は、配列を作成し、コードで作成します.次の構成は、プロファイルを通じてこのルーティングルールを利用する必要があるサービスリストを読み取り、環境変数を作成することです.
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 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 map = new HashMap();
			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