• 后端多环境治理的实践(一)


    背景

    最近有个业务场景,需要做一个新旧数据的兼容。大致可以理解为之前保存到数据库的数据是一个字符串,由于业务调整,该字符串要变为一个json。

    新的代码需要判断该字段是否为json,如果是json则序列化为json,如果不是json,则该字符串为json的某个字段。

    逻辑简单,我发布给测试后,测试问我要怎么测试,我说需要用旧的数据才能测试这段逻辑,但是我发布了新的代码后,就不能产生旧的的数据了。

    数据流如下图:

    image

    测试说这样很难测试,能不能像前端同学一样,搞个多版本控制,一键切换版本。

    测试想要的效果目标如下图:

    image

    我在之前的公司也经常遇到这种场景,但是我一般都叫测试修改代码的版本,先发布旧的代码然后生产数据,然后切换到新的版本去验证这种场景。

    这个时候,同事推荐我使用公司的基建服务“多环境治理平台”。

    一、什么是多环境治理

    在公司内部,一般是多个功能一起开发,同一个微服务并行开发是时常发生的事。但是功能的上线时间可能是不同的,所以代码不能合并在同一个分支开发。

    提测的时候,由于测试环境只有一个,要不就是都合并到同一个分支,要不就排队测试。。。

    image

    大伙一起来测试吧

    image

    测试人员在排队使用测试环境

    合并到一起测试的话,代码会冲突,而且会导致测试环境与线上环境不一致(因为测试环境混杂了其他版本的代码)。

    分开测试的话会导致排队现场,阻塞严重。

    多环境治理就是为了解决这个问题****。

    一套测试环境,多个后端版本。

    测试人员可以选择随意切换后端版本,随意测试任意一个版本的后端的功能。

    二、多环境治理的原理

    假设现在有2个featrue功能在开发

    featrue1需要修改user和score微服务。

    featrue2需要修改user和order微服务。

    我们希望最后的流量调度如下图。

    image

    v1的流量优先调用v1版本的微服务,如果找不到v1版本的微服务时,要调用基准版本的微服务。(例如order)

    v2的流量优先调用v2版本的微服务,如果找不到v2版本的微服务时,要调用基准版本的微服务。(例如score)

    要实现以上流量调度,只要做三件事:

    1、**每个微服务注册到注册中心的时候,要带上一个标记,标记自己当前的版本。

    2、**每个请求都要带个版本号,而且这个版本号要由网关开始,一直透穿到下游。

    3、微服务的调用下游时,实例选择策略修改为“优先选择和流量版本相同的实例,如果没有该版本的实例,则选择基准版本的实例”。

    多环境治理还能低成本搭建预发布环境(不需要全部应用都发布一遍pre环境)。

    调整一下策略,

    根据租户ID选择实例,就能实现后端租户ID级别的灰度发布

    根据userID选择实例,就能实现后端userID级别的灰度发布

    三、多环境治理的实践

    上面说的都是公司给我提供的基建服务,而且是用go语言写的。

    文章前面的小伙伴可能不在大公司,没有这样的基建平台,所以这里我根据上面说的原理,自己用java,基于springcloud 做一遍样例给大家。

    大家可以参考我样子,然后基于自己公司的微服务框架增加系统的多环境治理能力。

    下面的代码例子只会贴出最核心的代码,详细的实践可以下载我的代码自己细看。

    一、演示工程目录

    image

    最终的效果如下:

    1、一般的请求会走基准环境的代码。

    2、请求header里面只要带version=v1,则调用v1版本的order和user代码。

    3、请求header里面只要带version=v2,则调用v2版本的order和基准版本的user代码。

    image

    二、工程搭建

    以下代码基于springcloud-2020.03版本。

    (ps:真的感概技术升级太快,之前还在用zuul、ribbon、hystrix,现在基本都升级换代了。所以大家最重要的是懂原理,代码实践这些可能过一段时间就不能直接用了。)

    1、每个微服务注册都注册中心的时候,要带上一个标记,标记自己当前的版本。

    注册到springcloud的eureka时,注册中心允许实例带个一个map的信息。

    在order、user服务加上配置。

    eureka.instance.metadata-map.version=${version}
    

    只要加上这个配置,就表明这个实例的"version"字段是“default”。

    2、每个请求都要带个版本号,而且这个版本号要由网关开始,一直透穿到下游。

    为order和user增加一个过滤器。

    请求来了之后,在request里面找出version标记,把该标记放到ThreadLocal对象中。

    (ps:ThreacLocal对象是线程隔离的,所以多线程的情况下,这个version标记会丢,如果想多线程也不丢这个version标记,则可以使用阿里开源的TransmittableThreadLocal)

    @Slf4j
    @Component
    public class VersionFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            Filter.super.init(filterConfig);
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            String version = httpServletRequest.getHeader(Constont.VERSION);
            Utils.SetVersion(version);
            log.info("set version,{}",version);
            filterChain.doFilter(servletRequest,servletResponse);
            Utils.CleanVersion();
        }
        @Override
        public void destroy() {
            Filter.super.destroy();
        }
    }
    

    调用下游的时候把这个标记传递下去。

    springclud的loadbalancer允许我们调用下游时,对请求做一些自定义的修改。

    @Slf4j
    @Component
    public class VersionLoadBalancerLifecycle implements LoadBalancerLifecycle<RequestDataContext,Object,Object>
    {
        @Override
        public void onStart(Request request) {
            Object context = request.getContext();
            if (context instanceof RequestDataContext) {
                RequestDataContext dataContext = (RequestDataContext) context;
                String version = Utils.GetVersion();
                dataContext.getClientRequest().getHeaders().add(Constont.VERSION,version);
            }
        }
    
        @Override
        public void onStartRequest(Request request, Response lbResponse) {
    
        }
    
        @Override
        public void onComplete(CompletionContext completionContext) {
    
        }
    }
    

    3、微服务的调用下游时,策略修改为“优先选择和流量版本相同的实例,如果没有该版本的实例,则选择基准版本的实例”。

    springcloud内置很多的实例选择策略,有基于zone的区域,有基于健康检查的,也有基于用户暗示的。

    但是都不满足我们的需求,这里我们需要实现自己策略。

    新建类文件

    MulEnvServiceInstanceListSupplier继承

    DelegatingServiceInstanceListSupplier

    然后重写他的方法。

    public class MulEnvServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
    
        public MulEnvServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
            super(delegate);
        }
    
        @Override
        public Flux<List<ServiceInstance>> get() {
            return delegate.get();
        }
    
        @Override
        public Flux<List<ServiceInstance>> get(Request request) {
            return delegate.get(request).map(instances -> filteredByVersion(instances, getVersion(request.getContext())));
        }
    
        private String getVersion(Object requestContext) {
            if (requestContext == null) {
                return null;
            }
            String version = null;
            if (requestContext instanceof RequestDataContext) {
                version = getHintFromHeader((RequestDataContext) requestContext);
            }
            return version;
        }
    
        private String getHintFromHeader(RequestDataContext context) {
            if (context.getClientRequest() != null) {
                HttpHeaders headers = context.getClientRequest().getHeaders();
                if (headers != null) {
                    return headers.getFirst(Constont.VERSION);
                }
            }
            return null;
        }
    
        private List<ServiceInstance> filteredByVersion(List<ServiceInstance> instances, String version) {
            if (!StringUtils.hasText(version)) {
                version = Constont.DEFAULT_VERSION;
            }
            List<ServiceInstance> filteredInstances = new ArrayList<>();
            List<ServiceInstance> defaultVersionInstances = new ArrayList<>();
            for (ServiceInstance serviceInstance : instances) {
                if (serviceInstance.getMetadata().getOrDefault(Constont.VERSION, "").equals(version)) {
                    filteredInstances.add(serviceInstance);
                }
                if (serviceInstance.getMetadata().getOrDefault(Constont.VERSION, "").equals(Constont.DEFAULT_VERSION)) {
                    defaultVersionInstances.add(serviceInstance);
                }
    
            }
            if (filteredInstances.size() > 0) {
                return filteredInstances;
            }
    
            return defaultVersionInstances;
        }
    }
    
    

    其中的filteredByVersion就是我们的选择实例的策略

    image

    新建文件启用这个策略

    @LoadBalancerClients(defaultConfiguration = MulEnvSupportConfiguration.class)
    public class MulEnvSupportConfiguration {
        @Bean
        public ServiceInstanceListSupplier MulEnvServiceInstanceListSupplier(
                ConfigurableApplicationContext context) {
            ServiceInstanceListSupplier base = ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().build(context);
            MulEnvServiceInstanceListSupplier MulEnv = new MulEnvServiceInstanceListSupplier(base);
            return ServiceInstanceListSupplier.builder().withBase(MulEnv).build(context);
        }
    }
    

    三、验证

    我们在user服务写一个测试接口,接口逻辑是返回本实例的“version”。

    @Slf4j
    @RestController
    public class Controller {
        @Autowired
        private Environment environment;
        @Autowired
        private HttpServletRequest httpServletRequest;
        String VERSION = "version";
        @GetMapping("/demo")
        public String demo(){
            String header = httpServletRequest.getHeader(VERSION);
            log.info("headerVersion:{}",header);
            return "user:"+environment.getProperty(VERSION);
        }
    }
    

    然后在order服务写一个demo接口,去调用user接口。同时返回本实例的“version”。

    @RestController
    public class Controller {
        @Autowired
        private UserSerivce userSerivce;
        @Autowired
        private Environment environment;
        @GetMapping("/demo")
        public String Demo(){
    
            String order = "order:" + environment.getProperty(Constont.VERSION);
            return order+"/"+userSerivce.demo();
        }
    }
    

    打包+启动服务

    mvn clean install -DskipTests
    nohup java -jar -Dserver.port=8761 eureka/target/eureka-0.0.1-SNAPSHOT.jar  >null 2>&1 &
    nohup java -jar -Dserver.port=5000 gateway/target/gateway-0.0.1-SNAPSHOT.jar  >null 2>&1 &
    nohup java -jar -Dserver.port=8001 order/target/order-0.0.1-SNAPSHOT.jar  >null 2>&1 &
    nohup java -jar -Dversion=v1 -Dserver.port=8002 order/target/order-0.0.1-SNAPSHOT.jar  >null 2>&1 &
    nohup java -jar -Dversion=v2 -Dserver.port=8003 order/target/order-0.0.1-SNAPSHOT.jar  >null 2>&1 &
    
    nohup java -jar -Dserver.port=9001 user/target/user-0.0.1-SNAPSHOT.jar  >null 2>&1 &
    nohup java -jar -Dversion=v1 -Dserver.port=9002 user/target/user-0.0.1-SNAPSHOT.jar  >null 2>&1 &
    

    image

    正常访问请求

    image

    带上v1的版本号后

    image

    带上v2的版本号后

    image

    而且请求返回结果是固定的,不是轮训default和v1版本的。

    四、多环境治理的MQ问题

    我们可以在微服务调用实例时编写自己的策略,实现后端的多版本控制。

    但是mq消费的时候我们没法编写消费策略,这样多个版本的消息就混杂消费了,做不到版本隔离了。

    下一篇文章会教大家解决多环境治理的mq问题。

    五、代码地址:

    关注“从零开始的it转行生”,回复“多环境”获取

    欢迎关注我的公众号:“从零开始的it转行生”
  • 相关阅读:
    淘宝API学习之道:淘宝TOP之API接口接入教程
    hdu 2952 Counting Sheep
    【Java 虚拟机探索之路系列】:JIT编译器
    Android Afinal框架学习(一) FinalDb 数据库操作
    class、interface、struct的差别
    13个Cat命令管理文件实例汇总
    Linux df 命令用法示例
    九个uname命令获取Linux系统详情的实例
    Linux中查看进程的多线程
    精通Linux的“kill”命令
  • 原文地址:https://www.cnblogs.com/yeyongjian/p/15131068.html
Copyright © 2020-2023  润新知