spring cloud灰度环境和平滑重启

目录

1.前言

2.实现思路

3.流程图

4.关键代码

4.1 HystrixRequestVariableDefault

4.2 自定义网关过滤器ZuulFilter

4.3 网关自定义路由转发RoundRobinRule

4.4 微服务在程序处理前拦截并获取请求头信息

4.5 自定义feign拦截器

4.6 微服务自定义灰度转发规则同4.3网关一样

5.拓展

5.1 平滑重启

5.2 开放对外接口


1.前言

     灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

    公司搭建灰度环境,主要基于两点考虑:1.灰度测试 2.服务灰度上线后,再平滑过度到正式环境(这对小公司的开发来说太友好了,再也不用等到晚上11点上线服务了)。

2.实现思路

    1.对于从spring cloud zuul网关调用的请求,通过自定义过滤器,进行一些灰度规则的逻辑处理(判断当前调用者是否为灰度用户,已经可用的服务列表,并把信息存储到HystrixRequestVariableDefault),同时把灰度信息添加到ZuulRequestHeader,在ZuulRequestHeader中的信息会在调用其它微服务时带过去(必须传递给被调用的微服务,被调用的微服务可能会再调用其它服务,也需要进行灰度规则判断);然后实现IRule进行自定义的路由选择,根据灰度规则选择可用的服务进行转发。

    2.对于单个微服务,通过实现HandlerInterceptor,自定义处理程序执行链的工作流(在程序处理前获取请求头中的灰度信息,并存储到HystrixRequestVariableDefault);然后通过实现IRule接口根据灰度规则进行自定义的路由转发。

3.流程图

4.关键代码

4.1 HystrixRequestVariableDefault

        首先必须了解HystrixRequestVariableDefault,这是为Hystrix 跨线程传递数据而设计的,详细了解请转Spring Cloud 之 Hystrix 跨线程传递数据_码代码的陈同学-CSDN博客_hystrix线程隔离参数传递

        定义了一个类GrayVersionHolder,用于跨线程存取灰度信息。

import com.alibaba.fastjson.JSONObject;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * author: zhanggw
 * 创建时间:  2021/1/21
 */
public class GrayVersionHolder {

    private static final Logger logger = LoggerFactory.getLogger(GrayVersionHolder.class);
    private static final HystrixRequestVariableDefault<JSONObject> grayVersionHRV = new HystrixRequestVariableDefault<>();

    public static void setGrayJson(JSONObject grayJson){
        if (!HystrixRequestContext.isCurrentThreadInitialized()) {
            HystrixRequestContext.initializeContext();
        }
        grayVersionHRV.set(grayJson);
    }

    public static JSONObject getGrayJson(){
        if (!HystrixRequestContext.isCurrentThreadInitialized()) {
            logger.trace("getGrayJson null! !HystrixRequestContext.isCurrentThreadInitialized()");
            return null;
        }
        return grayVersionHRV.get();
    }

    public static void shutdownHystrixRequestContext() {
        if (HystrixRequestContext.isCurrentThreadInitialized()) {
            HystrixRequestContext.getContextForCurrentThread().shutdown();
        }
    }

}

4.2 自定义网关过滤器ZuulFilter

import com.alibaba.fastjson.JSONObject;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.ybjdw.base.constant.ConstantData;
import com.ybjdw.base.constant.SysStaticConstData;
import com.ybjdw.base.generatorid.TokenCreate;
import com.ybjdw.base.tool.http.HttpTool;
import com.ybjdw.base.utils.redis.RedisServiceImpl;
import com.ybjdw.gateway.rule.GrayVersionHolder;
import com.ybjdw.gateway.util.ConstantDataUtil;
import com.ybjdw.gateway.util.WebUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;

/**
 * <一句话功能简述> 第一个过滤器
 * <功能详细描述> pre 20
 * author: zhanggw
 * 创建时间:  2021/1/21
 */
@Configuration
public class FirstZuulFilter extends ZuulFilter {

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

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    private ConstantDataUtil constantDataUtil;

    @Override
    public String filterType() {
        return "pre";
    }

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

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

    @Override
    public Object run() throws ZuulException {
        try{
            // 获取参数data
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();

            RedisServiceImpl<JSONObject> redis = new RedisServiceImpl<>(redisTemplate);
            String requestIp = HttpTool.getIpAddr(request);
            ctx.set("requestIp", requestIp);
            String userId = "";
            String phone = "";
            String accessToken = "";

            boolean grayAccessFlag = false;
            JSONObject dataJson = WebUtil.getDataFromParam(request);
            if(dataJson != null){
                String token = dataJson.getString("token");
                accessToken = dataJson.getString(SysStaticConstData.ACCESS_TOKEN);

                if(StringUtils.isNotBlank(accessToken)){ // 第三方调用
                    phone = TokenCreate.getPhoneFromThirdToken(accessToken);
                }

                if(StringUtils.isBlank(accessToken) && StringUtils.isNotBlank(token)){ // 非第三方调用才判断用户是否为灰色服务用户
                    userId = redis.get(token);
                    userId = userId == null ? "": userId;
                    ctx.set("userId", userId);

                    phone = TokenCreate.getPhoneFromToken(token);
                    ctx.set("phone", phone);

                    List<String> grayUserPhoneList = constantDataUtil.getCacheDataListQuick("grayUserPhoneList"); // 灰色用户
                    if(StringUtils.isNotBlank(phone) && grayUserPhoneList != null && grayUserPhoneList.contains(phone)){
                        grayAccessFlag = true; // 灰色访问
                    }
                }
            }

            // 获取相关数据
            List<String> downServiceList = redis.get("downServiceList", new ArrayList<>()); // 已下线服务列表
            String ipHashSwitchRedis = constantDataUtil.getCacheDataQuick("ipHashSwitch"); // true:同一ip连续访问同一台服务器
            String ipHashSwitch = StringUtils.isBlank(ipHashSwitchRedis) ? "false" : ipHashSwitchRedis;

            // zuul本身调用微服务
            JSONObject gatewayInfo = new JSONObject();
            gatewayInfo.put("grayAccessFlag", grayAccessFlag); // 必需
            gatewayInfo.put("downServiceList", downServiceList);
            gatewayInfo.put("grayVersion", constantDataUtil.getCacheDataQuick("grayVersion")); // 灰色服务版本
            gatewayInfo.put("ipHashSwitch", ipHashSwitch);
            gatewayInfo.put("userId", userId);
            gatewayInfo.put("phone", phone);
            gatewayInfo.put("uri", request.getRequestURI());
            gatewayInfo.put("accessToken", accessToken);
            GrayVersionHolder.setGrayJson(gatewayInfo);

            logger.debug("FirstZuulFilter.run,data:{},gatewayInfo:{}", dataJson, gatewayInfo);
            // 传递给后续微服务
            ctx.addZuulRequestHeader(ConstantData.gatewayHeader, gatewayInfo.toJSONString());
        }catch (Exception e){
            logger.error("FirstZuulFilter.run异常!", e);
        }
        return null;
    }

}

4.3 网关自定义路由转发RoundRobinRule

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.loadbalancer.RoundRobinRule;
import com.netflix.loadbalancer.Server;
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;

/**
 * author: zhanggw
 * 创建时间:  2021/1/20
 */
public class GrayRule extends RoundRobinRule {

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

    @Override
    public Server choose(Object key) {
        Server chooseServer = null;
        try{
            JSONObject grayJson = GrayVersionHolder.getGrayJson();
            logger.trace("进入灰度规则grayJson:{}", grayJson);
            grayJson = grayJson == null ? new JSONObject() : grayJson;
            String grayVersion = grayJson.getString("grayVersion");
            boolean grayAccessFlag = grayJson.getBooleanValue("grayAccessFlag");
            JSONArray downServiceArray = grayJson.getJSONArray("downServiceList");
            String ipHashSwitch = grayJson.getString("ipHashSwitch");

            List<Server> serverList = this.getLoadBalancer().getReachableServers();
            if(serverList == null){
                return null;
            }

            // 只有一个 直接返回
            int serverSize = serverList.size();
            if(serverSize == 1){
                chooseServer = serverList.get(0);
                logger.debug("只有一个服务:{}", getInstanceInfoFromServer(chooseServer));
                return chooseServer;
            }

            // 同一个IP访问指定的服务ip_hash,后续再判断灰色
            if(StringUtils.isNotBlank(ipHashSwitch) && "true".equalsIgnoreCase(ipHashSwitch)){
                RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
                if(requestAttributes != null){
                    HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
                    String remoteAddr = request.getRemoteAddr();
                    if(StringUtils.isNotBlank(remoteAddr)){
                        int hashCode = Math.abs(remoteAddr.hashCode()); //据说hashcode会有负数导致索引越界
                        int ipHashIndex = hashCode%serverSize;

                        chooseServer = serverList.get(ipHashIndex);
                        logger.debug("根据请求地址:{}进行hash后选择服务:{}", remoteAddr, getInstanceInfoFromServer(chooseServer));
                    }
                }
            }

            if(grayAccessFlag){ // 灰色访问
                if(chooseServer == null || !isGrayServer(chooseServer, grayVersion) || isDownServer(chooseServer, downServiceArray)){ // 非灰色服务或已下线,重新选择
                    chooseServer = null;
                    for(Server server:serverList){
                        if(isGrayServer(server, grayVersion) && !isDownServer(server, downServiceArray)){ // 灰色服务并且未下线
                            chooseServer = server;
                            logger.debug("灰色访问选择灰色服务:{}", getInstanceInfoFromServer(chooseServer));
                            break;
                        }
                    }
                    if(chooseServer == null){
                        logger.debug("灰色访问未找到灰色服务!");
                    }
                }
            }else{ // 非灰色访问
                if(chooseServer == null || isGrayServer(chooseServer, grayVersion) || isDownServer(chooseServer, downServiceArray)){ // 当前未选中 或选中服务为灰色服务 或已下线,重新选择
                    chooseServer = null;
                    for(int i=0; i<10; i++){
                        Server tmpServer = super.choose(key); // 使用系统随机选择再过滤,否则多个服务起不到负载均衡作用
                        if(!isGrayServer(tmpServer, grayVersion) && !isDownServer(tmpServer, downServiceArray)){
                            chooseServer = tmpServer;
                            logger.debug("非灰色访问选择非灰色服务:{}", getInstanceInfoFromServer(chooseServer));
                            break;
                        }
                    }
                    if(chooseServer == null){
                        logger.debug("非灰色访问未找到非灰色服务!");
                    }
                }
            }

            // 还未选中,最后选择未下线的服务
            if(chooseServer == null && serverList.size()>0){
                for(Server server:serverList){
                    if(!isDownServer(server, downServiceArray)){
                        chooseServer = server;
                        logger.debug("前面规则未匹配,选择非下线服务:{}", getInstanceInfoFromServer(chooseServer));
                        break;
                    }
                }
            }

            GrayVersionHolder.shutdownHystrixRequestContext();
            logger.debug("最终选择了服务:{}", getInstanceInfoFromServer(chooseServer));
        }catch (Exception e){
            chooseServer = super.choose(key);
            logger.error("灰度规则异常!", e);
        }

        return chooseServer;
    }

    /**
     * <一句话功能简述> 判断是否为灰色服务
     * <功能详细描述> 
     * author: zhanggw
     * 创建时间:  2021/1/23
     * @param server 当前服务
     * @param grayVersion 指定灰色服务版本
     * @return boolean
     */
    private boolean isGrayServer(Server server,String grayVersion){
        boolean grayFlag = false;

        try{
            if(server == null){
                return false;
            }

            InstanceInfo instanceInfo = getInstanceInfoFromServer(server);
            if(instanceInfo == null){
                return false;
            }

            Map<String, String> metadata = instanceInfo.getMetadata();
            if(metadata != null && StringUtils.isNotBlank(grayVersion) && grayVersion.equals(metadata.get("service.version"))){
                return true;
            }
            grayFlag = metadata != null && "true".equals(metadata.get("grayFlag"));
        }catch (Exception e){
            logger.error("判断灰度服务异常!", e);
        }

        return grayFlag;
    }

    // 判断服务是否已下线
    private boolean isDownServer(Server server,JSONArray downServiceArray){
        try{
            if(server == null || downServiceArray == null || downServiceArray.size() ==0){
                return false;
            }

            InstanceInfo instanceInfo = getInstanceInfoFromServer(server);
            if(instanceInfo == null){
                return false;
            }
            String serviceIpInfo = instanceInfo.getIPAddr() + "," + instanceInfo.getAppName().toLowerCase();
            String serviceHostInfo = instanceInfo.getHostName().toLowerCase() + "," + instanceInfo.getAppName().toLowerCase();

            for(int i=0; i<downServiceArray.size(); i++){
                String downServiceInfo = downServiceArray.getString(i);
                if(downServiceInfo.contains(serviceIpInfo) || downServiceInfo.contains(serviceHostInfo)){
                    return true;
                }
            }
        }catch (Exception e){
            logger.error("判断服务下线异常!", e);
        }

        return false;
    }

    private InstanceInfo getInstanceInfoFromServer(Server server){
        try{
            return ((DiscoveryEnabledServer) server).getInstanceInfo();
        }catch (Exception e){
            logger.error("获取服务实例异常!", e);
        }

        return null;
    }

}

使用自定义的路由转发GrayRule

import com.netflix.loadbalancer.IRule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * author: zhanggw
 * 创建时间:  2021/1/20
 */
@Configuration
public class RibbonRuleConfig {

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

    @Bean
    public IRule myRule(){
        logger.trace("RibbonRuleConfig.myRule init!");
        return new GrayRule();
    }

}

4.4 微服务在程序处理前拦截并获取请求头信息

import com.alibaba.fastjson.JSON;
import com.ybjdw.base.constant.ConstantData;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * author: zhanggw
 * 创建时间:  2021/1/26
 */
public class GrayInterceptor extends HandlerInterceptorAdapter {

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

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try{
            String gatewayHeader = request.getHeader(ConstantData.gatewayHeader);
            logger.trace("GrayInterceptor gatewayHeader:{}", gatewayHeader);
            if(StringUtils.isNotBlank(gatewayHeader)){
                GrayVersionHolder.setGrayJson(JSON.parseObject(gatewayHeader));
            }
        }catch (Exception e){
            logger.error("灰度拦截器异常!", e);
        }
        return super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        super.afterCompletion(request, response, handler, ex);
    }

}

在微服务中使用自定义灰度拦截器

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * author: zhanggw
 * 创建时间:  2021/1/26
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

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

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new GrayInterceptor());
        logger.debug("成功添加灰度拦截器!");
    }

}

4.5 自定义feign拦截器

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.ybjdw.base.constant.ConstantData;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * author: zhanggw
 * 创建时间:  2021/1/26
 */
@Component
public class FeignInterceptor implements RequestInterceptor {

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

    @Override
    public void apply(RequestTemplate requestTemplate) {
        JSONObject gatewayHeader = null;
        try{
            gatewayHeader = GrayVersionHolder.getGrayJson();
            logger.trace("FeignInterceptor gatewayHeader:{}", gatewayHeader);
            if(gatewayHeader != null){
                // 空数组会导致报错
                JSONArray downServiceArray = gatewayHeader.getJSONArray("downServiceList");
                if(downServiceArray != null && downServiceArray.size() == 0){
                    gatewayHeader.remove("downServiceList");
                }
                requestTemplate.header(ConstantData.gatewayHeader, gatewayHeader.toJSONString());
            }
        }catch (Exception e){
            logger.error("feign拦截器异常! gatewayHeader:" + gatewayHeader, e);
        }
    }

}

4.6 微服务自定义灰度转发规则同4.3网关一样

5.拓展

5.1 平滑重启

        可以在灰度环境的基础,进行微服务节点的平滑重启(一个服务必须有个备用服务),思路如下:

1.服务节点重启前,通过脚本调用接口在redis中将当前节点服务标记为已下线(eureka服务列表不用管,如果调用eureka接口标记节点服务下线,再上线,中间会有延迟)。

2.在网关和单个服务的路由转发策略中,增加对可用服务的检测,如果选择转发的服务节点处于下线状态,再重新选择可用服务节点进行转发(有时会违背灰度规则,但总比不可用好)。

3.服务节点重启完成后,移除在redis中标记为已下线的服务节点。

服务节点重启前标记服务下线的脚步如下(改变eureka服务状态的部分暂时没用)。

#!/bin/bash

if [ ! -n "$1" ]; then
    echo "command error:serviceName empty!"
    echo "please input: $0 serviceName serviceIp status"
    echo "example: $0 user 192.168.0.105 down"
    exit
fi

if [ ! -n "$2" ]; then
    echo "command error:serviceIp empty!"
    echo "please input: $0 serviceName serviceIp status"
    echo "example: $0 user 192.168.0.105 down"
    exit
fi

if [ ! -n "$3" ]; then
    echo "command error:status empty!"
    echo "please input: $0 serviceName serviceIp status"
    echo "example: $0 user 192.168.0.105 down"
    exit
fi

current_time=$(date  "+%Y-%m-%d %H:%M:%S")
typeset -u serviceName
typeset -u status
serviceName=$1
hostName=$2
status=$3

eurekaHost='172.18.22.137:9201'
gatewayHost='172.18.22.137:9302'

# gateway mark service status
curl -X POST -d "data={\"serviceIp\":\"$hostName\",\"serviceName\":\"$serviceName\",\"status\":\"$status\"}" $gatewayHost/tool/markservicestatus >> /dev/null 2>&1
if [ $? -eq 0 ]; then
  echo "$current_time gateway mark service:$serviceName status:$status success!"
else
  echo "$current_time gateway mark service:$serviceName status:$status fail!"
fi

# download eureka service list xml
echo 'start download eureka service list xml!'
curl -X GET http://$eurekaHost/eureka/apps > eureka.xml
if [ $? -eq 0 ]; then
  echo "download eureka service success!"
else
  echo "download eureka service fail!"
fi

# storage serviceNameIp instanceId
declare -A serviceInstanceIdMap=()
for i in {1..80};
do
  xmllint --xpath "//instance[$i]/app/text()" eureka.xml >> /dev/null 2>&1
  if [ $? -eq 0 ]; then
    appName=$(xmllint --xpath "//instance[$i]/app/text()" eureka.xml)
    ipAddr=$(xmllint --xpath "//instance[$i]/ipAddr/text()" eureka.xml)
    instanceId=$(xmllint --xpath "//instance[$i]/instanceId/text()" eureka.xml)
    serviceInstanceIdMap["$appName$ipAddr"]=$instanceId
  fi
done
#echo ${!serviceInstanceIdMap[@]}

# curl eureka update metadata
key=$serviceName$hostName
instanceId=${serviceInstanceIdMap[$key]}
echo $serviceName $hostName instanceId:$instanceId 
# 改变eureka上的服务状态暂时不用
#curl -X PUT $eurekaHost/eureka/apps/$serviceName/$instanceId/status?value=$status >> /dev/null 2>&1

if [ $? -eq 0 ]; then
  echo "$current_time update $serviceName $hostName $status success!"
else
  echo "$current_time update $serviceName $hostName $status fail!"
fi

5.2 开放对外接口

        思路:在网关和微服务处理前进行对第三方调用标识的拦截,并进行权限判断和记录。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kenick

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值