美文网首页
灰度发布

灰度发布

作者: 一生逍遥一生 | 来源:发表于2021-07-16 14:44 被阅读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-灰度发布

    相关文章

      网友评论

          本文标题:灰度发布

          本文链接:https://www.haomeiwen.com/subject/abkbpltx.html