后端多環境治理的實踐(一)


背景

最近有個業務場景,需要做一個新舊數據的兼容。大致可以理解為之前保存到數據庫的數據是一個字符串,由於業務調整,該字符串要變為一個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轉行生”,回復“多環境”獲取


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM