ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-08-12 09:03:17  阅读:202  来源: 互联网

标签:后端 jar 实践 order 环境治理 version 版本 null public


背景

最近有个业务场景,需要做一个新旧数据的兼容。大致可以理解为之前保存到数据库的数据是一个字符串,由于业务调整,该字符串要变为一个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转行生”,回复“多环境”获取

标签:后端,jar,实践,order,环境治理,version,版本,null,public
来源: https://www.cnblogs.com/yeyongjian/p/15131068.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有