欢迎大家关注我公众号“从零开始的it转行生”

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

背景

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

posted @ 2021-08-12 08:45  大佬健  阅读(438)  评论(0编辑  收藏  举报

欢迎大家关注我公众号“从零开始的it转行生”