什么是灰度发布

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

灰度期:灰度发布开始到结束期间的这一段时间,称为灰度期。

通过Zuul实现,客户端到网关的灰度

流程设计

1、微服务中通过配置eureka.instance.metadata-map.version来控制新老服务版本号。

2、客户端请求时通过传递token来区分用户。

3、网关过滤器,首先获取灰度相关配置,然后肯定token与规则进行匹配,满足灰度条件的走新服务,不满足的走老服务。

4、如果新服务有问题,则可通过修改灰度配置来使流量全部切回到老服务。

5、如果新服务无问题,则可通过修改灰度配置来使流量全部切到新服务。

Zuul过滤器代码

@Component
public class GrayFilter extends ZuulFilter {

    @Resource
    private GrayRuleMapper grayRuleMapper;

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

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

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

    @Override
    public Object run() throws ZuulException {
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        /**
         * 从数据库中读取灰度配置信息,可优化从缓存中获取
         */
        GrayRuleEntity grayRuleEntity = grayRuleMapper.getGrayRule();
        String oldVersion = grayRuleEntity.getOldVersion();
        String newVersion = grayRuleEntity.getNewVersion();
        String greyUser = grayRuleEntity.getGreyUser();
        String ruleType = grayRuleEntity.getRuleType();

        /**
         * 流程:
         * 1、根据数据库配置规则确定是,当前版本是发布老版本,还是发布新版本,还是发布灰度
         * 2、传递版本号	
         */
        if ("1".equals(ruleType)) {//走老服务
            RibbonFilterContextHolder.getCurrentContext().add("version", oldVersion);
        } else if ("2".equals(ruleType)) {//灰度完成,走新服务
            RibbonFilterContextHolder.getCurrentContext().add("version", newVersion);
        } else {//灰度
            String token = request.getHeader("token");
            //根据token与数据库中greyUser列值的匹配,进行新老版本访问控制
			//演示项目,规则写的比较简单,实际项目中,可以根据详细的规则配置,获取到相应的灰度目标群(比如白名单用户,某个城市范围,某种权限等等),进行服务访问控制
            if (token.equalsIgnoreCase(greyUser)) {
                RibbonFilterContextHolder.getCurrentContext().add("version", newVersion);
            } else {
                RibbonFilterContextHolder.getCurrentContext().add("version", oldVersion);
            }
        }
        return null;
    }
}
public interface GrayRuleMapper {
    /**
     * 获取灰度配置
     * @return
     */
    GrayRuleEntity getGrayRule();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wyl.springcloud.zuul.dao.GrayRuleMapper">

    <select id="getGrayRule" resultType="com.wyl.springcloud.zuul.entity.GrayRuleEntity">
        select * from gray_rule
    </select>

</mapper>
public class GrayRuleEntity {

    /**
     * 版本类别,1:全部老版本,2:全部新版本,3:灰度
     */
    private String ruleType;

    /**
     * 老版本
     */
    private String oldVersion;

    /**
     * 新版本
     */
    private String newVersion;

    /**
     * 灰度用户
     */
    private String greyUser;
}

数据库信息

基于dubbo rpccontext 灰度方案 dubbo如何实现灰度发布_ide

微服务order 1(老版本)

eureka.instance.metadata-map.version=v1
@GetMapping("/getOrderInfo")
public String getOrderInfo() {
    return "order v1";
}

微服务order 2(新版本)

eureka.instance.metadata-map.version=v2
@GetMapping("/getOrderInfo")
public String getOrderInfo() {
    return "order v2";
}

最终效果

1、当数据库ruleType为1时,无论token值是多少,都只会调用v1版本的服务。(新版本出现问题时,二配置为1)

2、当数据库ruleType为2时,无论token值是多少,都只会调用v2版本的服务。(新版本没问题时,配置为2)

3、当数据库ruleType为3时,根据greyUser列值进行判断,如果是灰度用户,则调用v2版本的服务,否则调用v1版本的服务。(灰度发布,配置为3)

通过Ribbon实现,服务到服务的灰度

现在虽然实现了网关到服务请求的灰度控制,但是各个微服务之间还未实现灰度。

流程设计

1、通过网关把版本号传递到各个微服务。

2、微服务配置自定义的Ribbon负载均衡规则,重写choose方法。

3、在choose方法中获取可用服务列表,然后根据网关传过来的版本号进行匹配。

import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
import org.apache.commons.lang.StringUtils;
import org.springframework.context.annotation.Configuration;
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;

@Configuration
public class GrayRibbonRule extends ZoneAvoidanceRule {


    @Override
    public Server choose(Object key) {
        HttpServletRequest request =
                ((ServletRequestAttributes)
                        RequestContextHolder.getRequestAttributes()).getRequest();
        //通过网关传过来的
        String version = request.getHeader("zuulVersion");
        // 获取可用服务列表
        List<Server> serverList = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key);
        return getServer(serverList, version);
    }

    /**
     * 根据网关传过来的版本从可用的服务列表中找到匹配的服务。
     * @param serverList
     * @param version
     * @return
     */
    private Server getServer(List<Server> serverList, String version) {
        Server toServer = null;
        for (Server server : serverList) {
            Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
            String metaVersion = metadata.get("version");
            if (!StringUtils.isEmpty(metaVersion)) {
                if (metaVersion.equals(version)) {
                    toServer = server;
                }
            }
        }
        return toServer;
    }
}

网关过滤器,新增版本传递代码

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.wyl.springcloud.zuul.dao.GrayRuleMapper;
import com.wyl.springcloud.zuul.entity.GrayRuleEntity;
import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

@Component
public class GrayFilter extends ZuulFilter {

    @Resource
    private GrayRuleMapper grayRuleMapper;

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

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

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

    @Override
    public Object run() throws ZuulException {
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        /**
         * 可优化从缓存中获取
         */
        GrayRuleEntity grayRuleEntity = grayRuleMapper.getGrayRule();
        String oldVersion = grayRuleEntity.getOldVersion();
        String newVersion = grayRuleEntity.getNewVersion();
        String greyUser = grayRuleEntity.getGreyUser();
        String ruleType = grayRuleEntity.getRuleType();

        /**
         * 流程:
         * 1、根据数据库配置规则确定是,当前版本是发布老版本,还是发布新版本,还是发布灰度
         * 2、传递版本号
         */
        if ("1".equals(ruleType)) {//回滚
            RibbonFilterContextHolder.getCurrentContext().add("version", oldVersion);
			//新增的版本传递
            currentContext.addZuulRequestHeader("zuulVersion",oldVersion);
        } else if ("2".equals(ruleType)) {//正是上限
            RibbonFilterContextHolder.getCurrentContext().add("version", newVersion);
            //新增的版本传递
            currentContext.addZuulRequestHeader("zuulVersion",newVersion);
        } else {//灰度
            String token = request.getHeader("token");
            if (token.equalsIgnoreCase(greyUser)) {
                RibbonFilterContextHolder.getCurrentContext().add("version", newVersion);
                //新增的版本传递
                currentContext.addZuulRequestHeader("zuulVersion",newVersion);
            } else {
                RibbonFilterContextHolder.getCurrentContext().add("version", oldVersion);
                //新增的版本传递
                currentContext.addZuulRequestHeader("zuulVersion",oldVersion);
            }
        }
        return null;
    }
}

其他无需任何配置,只要微服务调用底层是通过ribbon实现负载均衡就会走到我们自定义的规则中(feign自带通过ribbon负载均衡、restTemplate一般都要通过ribbon负载均衡)。

本例中基本已经完成灰度的所有实现,但是还有一个小问题要解决,就是服务之间调用,重写了choose方法,但是并没有实现负载均衡,其实如果要实现一个轮询的方式也很简单,无非就是搞一个递增的数字与服务列表取模而已。