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

背景

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容