灰度发布

2021-07-16  本文已影响0人  一生逍遥一生

名词解释

蓝绿发布:
优点:无缝的升级服务

缺点:消耗资源大,2倍的机器

滚动发布:
优点:也能实现无缝的升级服务,同时节约机器

缺点:发布当中,如果出现了问题,不好排查,到底是新系统的BUG还是老系统的BUG

灰度发布:
优点:新功能让一小部分人使用,相当于Beta版,不会影响主业务

如果该新功能反应效果好,再升级为所有人使用,如某信的“拍一拍”功能

实现新功能, a b testing,尽量减少用户使用的时延

节省了服务器,延时,和试错成本

发布方式

现在开发在日常的情况,大部分使用的是Spring Cloud,可以通过Eureka来注册服务,服务提供者和消费者都可以注册到Eureka中,
可以通过API来进行注册、下线、更新配置等操作,Eureka的操作文档地址为:
https://github.com/Netflix/eureka/wiki/Eureka-REST-operations

在日常使用的情况时,在服务提供者和消费者都可以配置Eureka的元数据,也可以自定义元数据。可以通过:
PUT /eureka/v2/apps/appID/instanceID/metadata?key=value
来更新服务的元数据。

Zuul--> 服务

Zuul的依赖中添加下面的依赖:

<dependency>
    <groupId>io.jmnarloch</groupId>
    <artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
    <version>2.1.0</version>
</dependency>

添加相应的filter文件:

package com.edu.cloudzuul.filter;

import com.edu.cloudzuul.dao.CommonGrayRuleDaoCustom;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
@Component
public class GrayFilter extends ZuulFilter {


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

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

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

    @Autowired
    private CommonGrayRuleDaoCustom commonGrayRuleDaoCustom;

    @Override
    public Object run() throws ZuulException {

        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();

        int userId = Integer.parseInt(request.getHeader("userId"));
        // 根据用户id 查 规则  查库 v1,meata
        // 将数据从Eureka的meta-data、redis、数据库、guava 缓存 中获取数据,然后根据规则转发请求
        //  数据库表可以这样设计: id | user_id | service_name | meta_version
        // 这里只是简单实现了转发规则,userId == 1的 转发到 metadata version = v1的服务
        // 金丝雀
        if (userId == 1){
            RibbonFilterContextHolder.getCurrentContext().add("version","v1");
        // 普通用户
        }else if (userId == 2){
            RibbonFilterContextHolder.getCurrentContext().add("version","v2");
        }
        return null;
    }
}

在进行数据库设计的时候:

CREATE TABLE `gray_release_config` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `server_name` varchar(255) DEFAULT NULL, //服务名
   `path` varchar(255) DEFAULT NULL,//需要进行灰度发布的接口路径
     `percent` int(11) DEFAULT NULL,//负载均衡策略,百分之percent的请求转发到forward上
   `forward` int(11) DEFAULT NULL,//自定义元数据值
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8

也可以从guava、redis、apollo、eureka、数据库中获取这些数据。

RibbonFilterContextHolder是基于InheritableThreadLocal来传输数据的工具类,为什么要用InheritableThreadLocal而不是ThreadLocal?

在Spring Cloud中我们用Hystrix来实现断路器,默认是用信号量来进行隔离的,信号量的隔离方式用ThreadLocal在线程中传递数据是没问题的,
当隔离模式为线程时,Hystrix会将请求放入Hystrix 的线程池中执行,这时候某个请求就由A线程变成B线程了,ThreadLocal必然没有效果了,
这时候就用InheritableThreadLocal来传递数据。

服务之间调用:Ribbon Rule

下面是一个自定义rule的例子:

import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import lombok.extern.slf4j.Slf4j;

/**
 * 自定义灰度发布规则 2019-11-19 by david
 */
@Slf4j
@Service
public class GrayRule extends ZoneAvoidanceRule {

    /**
     * 在choose方法中,自定义规则,返回的Server就是具体选择出来的服务
     *
     * @param key 服务key
     * @return 可用server
     */
    @Override
    public Server choose(Object key) {
        // 获取负载均衡接口
        ILoadBalancer loadBalancer = this.getLoadBalancer();
        // 获取到所有存活的服务
        List<Server> allServers = loadBalancer.getAllServers();
        // 获取到需要路由的服务
        List<Server> serverList = this.getPredicate().getEligibleServers(allServers, key);
        log.info("[gray choose] key:{}; allServers:{}; serverList:{}", key, allServers, serverList);
        // 如果服务列表为空则返回null          
        if (CollectionUtils.isEmpty(serverList)) {
            log.warn("=====GrayRule choose serverList isEmpty key:{}=====", key);
            return null;
        }
        // 灰度开关,检查是否开启灰度服务开启时扫描灰度列表,避免每次扫描列表增大开销
        String switchValue = JedisClusterUtils.get(GrayConstants.GRAY_SWITCH);
        if (StringUtils.isBlank(switchValue) || "0".equals(switchValue)) {
            return getRandom(serverList);
        }
        // 灰度服务列表
        final Map<String, String> grayAddress = JedisClusterUtils.hgetAll(GrayConstants.GRAY_ADDRESS);
        if (CollectionUtils.isEmpty(grayAddress)) {
            log.info("[choose] : grayAddress isEmpty return serverList:{}", serverList);
            return getRandom(serverList);
        }
        List<String> grayServers = new ArrayList<>(grayAddress.keySet());
        // 查找非灰度服务并返回
        List<Server> noGrayServerList = serverList.stream().filter(x -> !grayServers.contains(x.getHostPort())).collect(Collectors.toList());
        return noGrayServerList.isEmpty() ? null : getRandom(noGrayServerList);
    }

    /**
     * 随机返回一个可用服务
     *
     * @param serverList 服务列表
     * @return 随机获取的服务
     */
    private static Server getRandom(List<Server> serverList) {
        return CollectionUtils.isEmpty(serverList) ? null : serverList.get(ThreadLocalRandom.current().nextInt(serverList.size()));
    }
}

为了针对进来的请求进行灰度发布,需要使用AOP来获取请求中的一些数据,为了使用当前线程的数据,就会使用到ThreadLocal,来获取这个线程的数据,需要
使用方法来获取到:

/**
 * 用于 保存、获取 每个线程中的 request header
 */
@Component
public class RibbonParameters {

    private static final ThreadLocal local = new ThreadLocal();

    public static <T> T get(){
        return (T)local.get();
    }

    public static <T> void set(T t){
        local.set(t);
    }
}
/**
 * 拦截请求,AOP实现,获取request header
 */
@Aspect
@Component
public class RequestAspect {
    /**
     * 定义切入点
     */
    @Pointcut("execution(* com.edu.apipassenge.controller..*Controller*.*(..))")
    private void anyMehtod(){
    }

    /**
     * 在之前切入
     * 此时IDEA中左侧栏能看到被拦截的方法
     * @param joinPoint
     */
    @Before(value = "anyMehtod()")
    public void before(JoinPoint joinPoint){
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String version = request.getHeader("version");
//        Map<String,String> map = new HashMap<>();
//        map.put("version",version);
//        RibbonParameters.set(map);  //写入ThreadLocal

        //灰度规则 匹配的地方 查db, redis
        if (version.trim().equals("v1")) {
            RibbonFilterContextHolder.getCurrentContext().add("version", "v1");
        } else if (version.trim().equals("v2")){
            RibbonFilterContextHolder.getCurrentContext().add("version", "v2");
        }
    }
}

设置配置类:

/**
 * 自定义Ribbon配置,用于启动类
 */
public class GrayRibbonConfiguration {
    @Bean
    public IRule ribbonRule(){
        return new GreyRule();
    }
}

在controller里面需要进行设置:

@SpringBootApplication
@RibbonClient(name = "service-sms" , configuration = GrayRibbonConfiguration.class)
public class ApiPassengeApplication {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
    public static void main(String[] args) {
        SpringApplication.run(ApiPassengeApplication.class, args);
    }
}

参考代码地址为:https://github.com/yishengxiaoyao/gray-publish

参考文献

Eureka REST operations
SpringCloud灰度发布实践(附源码)
Spring Cloud使用Zuul和Ribbon做灰度发布
微服务Zuul网关进行灰度发布
灰度发布的原理及实现
灰度发布落地实战2
灰度发布落地实战1
谈谈微服务平台之灰度发布
SpringCloud-灰度发布

上一篇下一篇

猜你喜欢

热点阅读