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

大佬健發表於2021-08-12

背景

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

相關文章