SpringCloud灰度发布设计和实现

灰度发布

什么是灰度发布呢?要想了解这个问题就要先明白什么是灰度。灰度从字面意思理解就是存在于黑与白之间的一个平滑过渡的区域,所以说对于互联网产品来说,上线和未上线就是黑与白之分,而实现未上线功能平稳过渡的一种方式就叫做灰度发布。

在一般情况下,升级服务器端应用,需要将应用源码或程序包上传到服务器,然后停止掉老版本服务,再启动新版本。但是这种简单的发布方式存在两个问题,一方面,在新版本升级过程中,服务是暂时中断的,另一方面,如果新版本有BUG,升级失败,回滚起来也非常麻烦,容易造成更长时间的服务不可用。

在了解了什么是灰度发布的定义以后,就可以来了解一下灰度发布的具体操作方法了。可以通过抽取一部分用户,比如说选择自己的测试用户,使这些用户的请求全部访问灰度发布的服务,正式用户请求走正常上线的服务,那么就能够将需要灰度发布的版本也连接到正常上线服务中进行测试,没有问题之后将其设置为正常服务即完成版本的上线测试和发布。

实现设计

实现重点主要在:

  1. 利用 ThreadLocal+Feign 实现 http head 中实现信息的传递
  2. 使用Nacos的元数据,定义需要的灰度服务
  3. 自定义Ribbon的路由规则,根据Nacos的元数据选择服务节点

公共配置

ThreadLocal

public class PassParameters {

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

    private static final ThreadLocal localParameters = new ThreadLocal();

    public static PassParametersModel get(){

        PassParametersModel model = (PassParametersModel) localParameters.get();
        log.info("ThreadID:{}, threadLocal {}", Thread.currentThread().getId(), model.toString());
        return model;
    }

    public static void set(PassParametersModel model){

        log.info("ThreadID:{}, threadLocal set {}", Thread.currentThread().getId(), model.toString());
        localParameters.set(model);
    }

}

携带灰度发布相关数据的类

public class PassParametersModel {

    private String token;
    private String grayPublish;

}

请求头常量

public class HttpConstants {
    public static final String AUTHORIZATION_HEADER = "Authorization";

    public static final String GRAY_PUBLISH = "Gray-Publish";
    public static final String GRAY_PUBLISH_MEAT_KEY = "grayPublish";
}

AOP请求拦截处理

@Aspect
@Order(1)
@Component
public class PermissionsValidationAspect {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 权限校验以及一些参数的赋值O
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around(value = "需要拦截的方法")
    public Object check(ProceedingJoinPoint joinPoint) throws Throwable {
        
        // 获取一些参数
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader(HttpConstants.AUTHORIZATION_HEADER);

        // FIXME:
        // token权限校验,不要去查数据库!!!!
        // 来这里的token肯定都是有用的,因为在网关那里就会进行token有效的校验
        // 校验成功之后进行用户身份的赋值,比如uid、username等关键信息

        // 本次请求涉及到的灰度发布
        // FIXME: 通过用户所属的用户组或其他标识信息,专门指定某些用户是灰度发布测试用户,来设置灰度发布的字段
        String grayPublish = request.getHeader(HttpConstants.GRAY_PUBLISH);

        PassParametersModel model = new PassParametersModel();
        model.setToken(token);
        model.setGrayPublish(grayPublish);
        PassParameters.set(model);

        return joinPoint.proceed();
    }
}

Feign配置

Ribbon请求规则

将会从Nacos中获取元服务器的信息,并根据这个信息选择服务器。

public class GrayPublishRibbonRule extends AbstractLoadBalancerRule {

    private ILoadBalancer lb;

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


    private AtomicInteger nextServerCyclicCounter;

    public GrayPublishRibbonRule() {
        nextServerCyclicCounter = new AtomicInteger(0);
    }


    @Override
    public Server choose(Object key) {

        lb = getLoadBalancer();
        List<Server> servers = lb.getReachableServers();  // 获取所有UP的服务
        if (servers.isEmpty()) {
            return null;
        }

        List<Server> normalServerList = new ArrayList<>();
        Map<String, List<Server>> grayPublishMap = new HashMap<>();
        for (Server s : servers){
            NacosServer nacosServer = (NacosServer) s;
            Map<String, String> metadata = nacosServer.getMetadata();
            String grayPublishKey = metadata.get(HttpConstants.GRAY_PUBLISH_MEAT_KEY);
            if (grayPublishKey == null) {
                normalServerList.add(s);
            } else {
                List<Server> grayPublishList = grayPublishMap.computeIfAbsent(grayPublishKey, k -> new ArrayList<>());
                grayPublishList.add(s);
            }
        }

        PassParametersModel model = PassParameters.get();
        // 不是灰度发布请求,走正常的服务
        if (model.getGrayPublish() == null) {
            return defaultLoadBalanceChoose(normalServerList, key);
        }

        List<Server> currentServerList = grayPublishMap.get(model.getGrayPublish());
        // 是灰度发布,但是没有找到对应处理,也走正常的
        if (currentServerList == null || currentServerList.isEmpty()) {
            return defaultLoadBalanceChoose(normalServerList, key);
        }

        // 灰度发布,走对应的服务
        return defaultLoadBalanceChoose(currentServerList, key);
    }


    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }

    // 负载均衡
    public Server defaultLoadBalanceChoose(List<Server> reachableServers, Object key) {

        if (reachableServers == null || reachableServers.isEmpty()) {
            log.warn("No up servers available from load balancer: " + lb);
            return null;
        }

        Server server = null;
        int count = 0;
        while (count++ < 10) {
            int upCount = reachableServers.size();

            int nextServerIndex = incrementAndGetModulo(upCount);
            server = reachableServers.get(nextServerIndex);

            if (server == null) {
                /* Transient. */
                Thread.yield();
                continue;
            }

            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }

            // Next.
            server = null;
        }

        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }

    /**
     * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
     *
     * @param modulo The modulo to bound the value of the counter.
     * @return The next value.
     */
    private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;//求余法
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }
}

Feign拦截器

将需要传递给下个微服务中的数据进行赋值

public class GrayPublishInterceptor implements RequestInterceptor {

    private Logger log = LoggerFactory.getLogger(this.getClass());

    @Override
    public void apply(RequestTemplate requestTemplate) {

        PassParametersModel model = PassParameters.get();

        // token
        if (StringUtils.isNotEmpty(model.getToken())) {
            requestTemplate.header(HttpConstants.AUTHORIZATION_HEADER, model.getToken());
        }

        // grayPublish
        if (StringUtils.isNotEmpty(model.getGrayPublish())) {
            requestTemplate.header(HttpConstants.GRAY_PUBLISH, model.getGrayPublish());
        }

		// FIXME: 补充其他需要传递的内容
    }
}

配置使用

这里是全局配置使用

@Configuration
public class FeignConfig {
    
    // 灰度发布Ribbon规则
    @Bean
    public IRule getRule(){

        return new GrayPublishRibbonRule();
    }

    // header传递
    @Bean
    public GrayPublishInterceptor getInterceptor(){

        return new GrayPublishInterceptor();
    }

}

Nacos元数据配置

通过在application.yml进行配置

# 服务的元数据
spring:
  cloud:
    nacos:
      discovery:
        metadata.grayPublish: HAHAHA_TEST
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值